Slide 1

Slide 1 text

Автоматизация экспериментов с Kotlin DSL

Slide 2

Slide 2 text

2 Кто я?

Slide 3

Slide 3 text

Мнение докладчика может не совпадать с официальной позицией его работодателя, начальника, коллег или других специалистов. 
 Все представленные в докладе сведения, примеры, выводы и другую информацию вы можете использовать на свой страх и риск. За все ваши действия ответственность несёте только вы сами. 
 Этот доклад не совсем про Kotlin, хотя его в нём более чем достаточно. 3 Disclaimer

Slide 4

Slide 4 text

4 71 млн Посещают нас в месяц 8000 Серверов Одноклассники в цифрах 10 тыс Выполненных экспериментов

Slide 5

Slide 5 text

0| Что такое эксперимент?

Slide 6

Slide 6 text

• Нельзя просто так взять и выкатить фичу на клиента • Технические риски • Продуктовые риски • Запуск новых фич происходит поэтапно • Наполнение кэшей • А/B/…/Z-тестирование • Процесс запуска фичи называется экспериментом 6 Зачем нужны эксперименты?

Slide 7

Slide 7 text

7 Эксперимент в деталях Code Branches if / switch

Slide 8

Slide 8 text

8 Эксперимент в деталях Code Branches if / switch Config by partition

Slide 9

Slide 9 text

9 Эксперимент в деталях Code Branches if / switch Config by partition by server

Slide 10

Slide 10 text

10 Эксперимент в деталях Code Branches if / switch Config by partition by server by geo

Slide 11

Slide 11 text

11 Эксперимент в деталях Code Branches if / switch Config by partition Logs/ Monitoring observe by server by geo

Slide 12

Slide 12 text

12 Пример очень простого эксперимента if (moodWebConf.isWithFriendsEnabled(userId)) { showSelectFriends(); }

Slide 13

Slide 13 text

13 Пример очень простого эксперимента if (moodWebConf.isWithFriendsEnabled(userId)) { showSelectFriends(); }

Slide 14

Slide 14 text

14 Включение фичи one partition //by partition app.mood.withFriendsEnabled: 0

Slide 15

Slide 15 text

15 Включение фичи one partition //by partition app.mood.withFriendsEnabled: 0-63 several partitions

Slide 16

Slide 16 text

16 Включение фичи one partition //by partition app.mood.withFriendsEnabled: 0-255 several partitions all partitions

Slide 17

Slide 17 text

17 Всё очень просто one partition //step#1 app.mood.withFriendsEnabled: 0 //step#2 app.mood.withFriendsEnabled: 0-63 //step#3 app.mood.withFriendsEnabled: 0-255 several partitions all partitions

Slide 18

Slide 18 text

1| Проблемы с экспериментами

Slide 19

Slide 19 text

• Релизный цикл • Нужно вспоминать, где и какие настройки нужно включить/прописать • Хранение экспериментов • Разные нотации обозначения настроек • Отдельный тип задач в Jira • Запуск экспериментов • руками через UI • комментарий в Jira • сообщение в ТамТам 19 Проблемы

Slide 20

Slide 20 text

20 Разные среды - похожие действия t dev test prod

Slide 21

Slide 21 text

• Консистентность настроек между средами • Непонятно в какой стадии эксперимент • Забыли написать комментарий? 21 Проблемы #2

Slide 22

Slide 22 text

2| Концепт решения

Slide 23

Slide 23 text

• Описание эксперимента в виде некоторого DSL • Храним в git-е как код • Автоматизированный накат/откат настроек • Запускаем через CLI • Используем API системы хранения конфигурации • Автоматизация сопутствующих действий • Постим комментарии в Jira, TamTam 23 Чего бы хотелось?

Slide 24

Slide 24 text

3| Прототипирование

Slide 25

Slide 25 text

• Быстро делать прототипы по автоматизации • Три универсальных модуля • sh • uri • template • Готовый DSL и механизмы запуска 25 Ansible - почти идеальный кандидат

Slide 26

Slide 26 text

26 Описание эксперимента --- - include: step0.yml tags: - step0 - include: step1.yml tags: - step1 … - include: step5.yml tags: - step5

Slide 27

Slide 27 text

27 Каждый шаг имеет своё описание --- - include: step0.yml tags: - step0 - include: step1.yml tags: - step1 … - include: step5.yml tags: - step5 --- - hosts: local gather_facts: no tasks: - name: Cleanup all include_role: name: pms vars: step_definition: "проинициализирую настройки" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: "" anotherProperty: name: "app.mood.anotherProperty" value: "" with_items: - "{{ host_common }}" loop_control: loop_var: host_name

Slide 28

Slide 28 text

28 Сокращённый вариант - hosts: local tasks: - name: Cleanup all include_role: name: pms vars: step_definition: "проинициализирую настройки" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: ""

Slide 29

Slide 29 text

29 Общая часть всех экспериментов - hosts: local tasks: - name: Cleanup all include_role: name: pms vars: step_definition: "проинициализирую настройки" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: ""

Slide 30

Slide 30 text

30 Ansible Role - name: update properties uri: url: "{{ pms_host }}/api/property" method: POST user: "{{ pms_username }}" password: "{{ pms_password }}" body: applicationName: "{{ application_name }}" hostName: "{{ host_name }}" propertyName: "{{ item.value.name }}" propertyValue: "{{ item.value.value }}" body_format: json status_code: 200 headers: Content-Type: "application/json" with_dict: "{{ properties }}"

Slide 31

Slide 31 text

31 Куда - name: update properties uri: url: "{{ pms_host }}/api/property" method: POST user: "{{ pms_username }}" password: "{{ pms_password }}" body: applicationName: "{{ application_name }}" hostName: "{{ host_name }}" propertyName: "{{ item.value.name }}" propertyValue: "{{ item.value.value }}" body_format: json status_code: 200 headers: Content-Type: "application/json" with_dict: "{{ properties }}"

Slide 32

Slide 32 text

32 Что - name: update properties uri: url: "{{ pms_host }}/api/property" method: POST user: "{{ pms_username }}" password: "{{ pms_password }}" body: applicationName: "{{ application_name }}" hostName: "{{ host_name }}" propertyName: "{{ item.value.name }}" propertyValue: "{{ item.value.value }}" body_format: json status_code: 200 headers: Content-Type: "application/json" with_dict: "{{ properties }}"

Slide 33

Slide 33 text

33 Properties - hosts: local tasks: - name: Cleanup all include_role: name: pms vars: step_definition: "проинициализирую настройки" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: ""

Slide 34

Slide 34 text

34 Запуск эксперимента одной строкой ansible-playbook -i dev-inv —extra-vars="..." exp.yml --tags step0

Slide 35

Slide 35 text

35 Интерпретатор ansible-playbook -i dev-inv —extra-vars="..." exp.yml --tags step0

Slide 36

Slide 36 text

36 Конфигурация ansible-playbook -i dev-inv —extra-vars="..." exp.yml --tags step0

Slide 37

Slide 37 text

37 Описание эксперимента ansible-playbook -i dev-inv —extra-vars="..." exp.yml --tags step0

Slide 38

Slide 38 text

38 Текущий шаг исполнения ansible-playbook -i dev-inv —extra-vars="..." exp.yml --tags step0

Slide 39

Slide 39 text

Почему бы так и не оставить?

Slide 40

Slide 40 text

40 Проблема копипаста. Step#1 --- - hosts: local gather_facts: no tasks: - name: Cleanup all include_role: name: pms vars: step_definition: "проинициализирую настройки" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: "" anotherProperty: name: "app.mood.anotherProperty" value: "" with_items: - "{{ host_common }}" loop_control: loop_var: host_name

Slide 41

Slide 41 text

41 Проблема копипаста. Step#2 --- - hosts: local gather_facts: no tasks: - name: Cleanup all include_role: name: pms vars: step_definition: "на одну партицию" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: "63" anotherProperty: name: "app.mood.anotherProperty" value: "63" with_items: - "{{ host_common }}" loop_control: loop_var: host_name

Slide 42

Slide 42 text

42 Проблема копипаста. Step#N --- - hosts: local gather_facts: no tasks: - name: Cleanup all include_role: name: pms vars: step_definition: "на всех" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: "0-255" anotherProperty: name: "app.mood.anotherProperty" value: "0-255" with_items: - "{{ host_common }}" loop_control: loop_var: host_name

Slide 43

Slide 43 text

• Нужно писать модули • на питоне • я не знаю питон :) • Обобщённый DSL это хорошо для решения общих задач • Частное решение позволяет писать меньше кода 43 Проблемы использования Ansible-а

Slide 44

Slide 44 text

Начнём с модели

Slide 45

Slide 45 text

45 DSL Design. Список шагов эксперимента steps { empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }

Slide 46

Slide 46 text

46 Инициализация/Отключка steps { empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }

Slide 47

Slide 47 text

47 Открываем на бота steps { empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }

Slide 48

Slide 48 text

48 Открытие по партициям steps { empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }

Slide 49

Slide 49 text

49 На всех steps { empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }

Slide 50

Slide 50 text

50 Настройки. Отделяем мух от котлет experiment { steps { empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }

Slide 51

Slide 51 text

51 Step + Property experiment { steps { empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }

Slide 52

Slide 52 text

52 Компактно и понятно experiment { steps { empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }

Slide 53

Slide 53 text

4| Почти на Java

Slide 54

Slide 54 text

• Java - знаем, любим, но вербозно • кто-то пишет скриптинг на Java? • JVM - очень здорово • интероп с уже написанными библиотеками • знакомая платформа 54 Что же выбрать для реализации?

Slide 55

Slide 55 text

• Groovy, JRuby, Javascript, … • синтаксический сахар • JSR223 • Но мы хотим: • в идеале писать скрипты и их обработку на одном языке • иметь статическую типизацию, • проверки на этапе компиляции • близкую к идеальной поддержку в IDE 55 А что вообще есть то?

Slide 56

Slide 56 text

56 JSR223. Как заиспользовать Java-класс //javascript var exp = Java.type("one.exps.Experiment"); //JRuby require "java" java_import "one.exps.Experiment" exp = Experiment.new //Groovy def exp = new Experiment()

Slide 57

Slide 57 text

• Kotlin потому что: • синтаксический сахар • JSR223 • компиляция • статическая типизация • хороший code completion • хотелось попробовать новое и интересное ;) 57 Почему Kotlin?

Slide 58

Slide 58 text

5| Реализуем DSL

Slide 59

Slide 59 text

59 Как реализовать такой DSL? experiment { steps { empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }

Slide 60

Slide 60 text

60 Древовидная структура @DslMarker annotation class ElementMarker @ElementMarker abstract class Element { val children = arrayListOf() fun make(element: T, init: T.() -> Unit): T { element.init() children.add(element) return element } }

Slide 61

Slide 61 text

61 Init function experiment { steps { // call init here partition("half"){ // call init here } } ... }

Slide 62

Slide 62 text

62 Init function @DslMarker annotation class ElementMarker @ElementMarker abstract class Element { val children = arrayListOf() fun make(element: T, init: T.() -> Unit): T { element.init() children.add(element) return element } }

Slide 63

Slide 63 text

63 Safe-Type experiment { steps { // call init here partition("half"){ // call init here } } ... } fun partition(name: String, init: PartitionStep.() -> Unit) = make(PartitionStep(name), init)

Slide 64

Slide 64 text

64 Lateinit vars experiment { description = "very cool experiment” steps { // call init here partition("half"){ // call init here } } … }

Slide 65

Slide 65 text

• Кто знает зачем он нужен? • Валидация скоупа внутри DSL • вложенность элементов 65 @DSLMarker

Slide 66

Slide 66 text

66 @DSLMarker @DslMarker annotation class ElementMarker @ElementMarker abstract class Element { val children = arrayListOf() fun make(element: T, init: T.() -> Unit): T { element.init() children.add(element) return element } }

Slide 67

Slide 67 text

67 Без @DSLMarker experiment { steps { steps { } } ... } Шаги могут быть вложенными (?)

Slide 68

Slide 68 text

68 С @DSLMarker experiment { steps { steps { } } ... } Шаги не могут быть вложенными

Slide 69

Slide 69 text

• Переопределение операторов • steps += step • Infix-функции • key to value 69 DSL useful features

Slide 70

Slide 70 text

Оптимизация DSL

Slide 71

Slide 71 text

71 Оптимизация DSL experiment { steps { empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }

Slide 72

Slide 72 text

72 Засахарим немного experiment { steps { openByPartitionSteps() } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }

Slide 73

Slide 73 text

73 Extension Function - это очень удобно //extension function fun Steps.openByPartitionSteps() { empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }

Slide 74

Slide 74 text

74 Кастомные свойства experiment { steps { custom("messages", "сообщения для настроения 'Хочу общаться'") { properties("odnoklassniki-web", prop("hosts")) { property("app.property.custom", "i_want_comm=SEND_MESSAGE") } } openByPartitionSteps() } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }

Slide 75

Slide 75 text

75 Это всё-таки язык программирования custom("42everywhere", "всем по 42") { properties("odnoklassniki-web", prop("hosts")) { calcProperty("app.complex.property", { PMS.property("odnoklassniki-web", "app.complex.property") .lines() .map { Converter.LIST_STRING.convert(it) .minus("42").plus("42") .joinToString(",") } .joinToString("\n") }) } }

Slide 76

Slide 76 text

Kotlin Script

Slide 77

Slide 77 text

• Файл с расширением .kts • Имплицитные аргументы • args[0] • Запускается через command line • kotlinc -script -classpath <> • Запускается с помощью IDE 77 Kotlin Script

Slide 78

Slide 78 text

78 Kotlin Script. Ожидание import runner.* runner.run { exp { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } } }

Slide 79

Slide 79 text

• Нужно как-то подключить классы проекта • Нужно как-то подключить все зависимости проекта • Gradle не умеет (по крайней мере из коробки) • IDEA не умеет* 79 Kotlin Script. CMD kotlinc -script -classpath <>

Slide 80

Slide 80 text

80 Kotlin Script. IDEA

Slide 81

Slide 81 text

81 Kotlin Script. IDEA java -Dfile.encoding=UTF-8 \ -classpath "kotlin-compiler.jar:kotlin-reflect.jar: kotlin-stdlib.jar:kotlin-script-runtime.jar" org.jetbrains.kotlin.cli.jvm.K2JVMCompiler -script ~/experiments/src/main/kotlin/one/exps/scripts/XPRM-10222.kts XPRM-10222.kts:1:8: error: unresolved reference: one import one.exps.*

Slide 82

Slide 82 text

82 Kotlin Script. IDEA java -Dfile.encoding=UTF-8 \ -classpath "kotlin-compiler.jar:kotlin-reflect.jar: kotlin-stdlib.jar:kotlin-script-runtime.jar" org.jetbrains.kotlin.cli.jvm.K2JVMCompiler -script ~/experiments/src/main/kotlin/one/exps/scripts/XPRM-10222.kts XPRM-10222.kts:1:8: error: unresolved reference: one import one.exps.*

Slide 83

Slide 83 text

83 https://github.com/andrewoma/kotlin-script #!/bin/sh exec kotlins -cp `mvncp ` !# import runner.* runner.run { exp { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } }

Slide 84

Slide 84 text

84 https://github.com/andrewoma/kotlin-scripting-kickstarter #!/usr/bin/env kotlin-script.sh import runner.* fun main(args: Array) { runner.run { exp { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } } }

Slide 85

Slide 85 text

85 https://github.com/holgerbrandl/kscript #!/usr/bin/env kscript //DEPS one:experiments:1.0.0 import runner.* runner.run { exp { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } }

Slide 86

Slide 86 text

• Необходимо писать обвязки • Нельзя запустить из IDE (ну кроме как console) • Непонятно как подебажить скрипт, если очень нужно 86 Проблемы Kotlin Script

Slide 87

Slide 87 text

87 https://github.com/aatarasoff/kotlin-script-starter //JSR223 //ConsoleExecutor.kt fun main(args : Array) { ScriptEngineManager().getEngineByExtension("kts")!! .eval(getResource("${args[0]}.kts").readText()) }

Slide 88

Slide 88 text

88 Когда нету Engine-a //JSR223 //ConsoleExecutor.kt fun main(args : Array) { ScriptEngineManager().getEngineByExtension("kts")!! .eval(getResource("${args[0]}.kts").readText()) } Exception in thread "main" kotlin.KotlinNullPointerException

Slide 89

Slide 89 text

89 JSR223 support //build.gradle compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") //META-INF/services/javax.script.ScriptEngineFactory org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory

Slide 90

Slide 90 text

90 А что если в зависимостях Guava? //build.gradle compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") compile("com.google.guava:guava:$guava_version") //META-INF/services/javax.script.ScriptEngineFactory org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory

Slide 91

Slide 91 text

91 Метод есть, но нам говорят что нет //build.gradle compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") compile("com.google.guava:guava:$guava_version") //META-INF/services/javax.script.ScriptEngineFactory org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkState(...)

Slide 92

Slide 92 text

92 Всё дело в компиляторе //build.gradle compile("org.jetbrains.kotlin:kotlin-compiler:$kotlin_version") compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") compile("com.google.guava:guava:$guava_version") Компилятор содержит классы с теми же пакетами и с теми же именами, что и Guava

Slide 93

Slide 93 text

93 В интернетах советуют Embeddable-компилятор //build.gradle compile "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version" compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") { exclude module: 'kotlin-compiler' }

Slide 94

Slide 94 text

94 Теперь уже не хватает класса //build.gradle compile "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version" compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") { exclude module: 'kotlin-compiler' } compile("com.google.guava:guava:$guava_version") java.lang.NoClassDefFoundError: com/intellij/openapi/util/Disposer

Slide 95

Slide 95 text

95 Класс есть, а пакет другой //build.gradle compile "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version" compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") { exclude module: 'kotlin-compiler' } compile("com.google.guava:guava:$guava_version") org.jetbrains.kotlin.com.intellij.openapi.util.Disposer

Slide 96

Slide 96 text

96 “Скопируем” классы фабрики JSR223 //KotlinJsr223JvmLocalScriptEngine.kt //KotlinJsr223JvmLocalScriptEngineFactory.kt

Slide 97

Slide 97 text

97 Ctrl+R //KotlinJsr223JvmLocalScriptEngine.kt //KotlinJsr223JvmLocalScriptEngineFactory.kt replace com.intellij.openapi.util.Disposer to org.jetbrains.kotlin.com.intellij.openapi.util.Disposer

Slide 98

Slide 98 text

98 Заиспользуем модификацию //KotlinJsr223JvmLocalScriptEngine.kt //KotlinJsr223JvmLocalScriptEngineFactory.kt replace com.intellij.openapi.util.Disposer to org.jetbrains.kotlin.com.intellij.openapi.util.Disposer //META-INF/services/javax.script.ScriptEngineFactory your.package.here.KotlinJsr223JvmLocalScriptEngineFactory

Slide 99

Slide 99 text

99 Апгрейдим Console Executor fun main(args : Array) { val parsedArgs = Args(ArgParser(args)) val experimentName = parsedArgs.experiment val factory = ScriptEngineManager().getEngineByExtension("kts")!! factory.eval(getResource("$experimentName.kts").readText()) val executionPlan = ExecutionPlan(expRegistry.find(experimentName)) ConsoleExecutor(executionPlan).execute(parsedArgs.stage) }

Slide 100

Slide 100 text

100 Парсим аргументы fun main(args : Array) { val parsedArgs = Args(ArgParser(args)) val experimentName = parsedArgs.experiment val factory = ScriptEngineManager().getEngineByExtension("kts")!! factory.eval(getResource("$experimentName.kts").readText()) val executionPlan = ExecutionPlan(expRegistry.find(experimentName)) ConsoleExecutor(executionPlan).execute(parsedArgs.stage) }

Slide 101

Slide 101 text

101 Знакомая фабрика fun main(args : Array) { val parsedArgs = Args(ArgParser(args)) val experimentName = parsedArgs.experiment val factory = ScriptEngineManager().getEngineByExtension("kts")!! factory.eval(getResource("$experimentName.kts").readText()) val executionPlan = ExecutionPlan(expRegistry.find(experimentName)) ConsoleExecutor(executionPlan).execute(parsedArgs.stage) }

Slide 102

Slide 102 text

102 Выполнение эксперимента fun main(args : Array) { val parsedArgs = Args(ArgParser(args)) val experimentName = parsedArgs.experiment val factory = ScriptEngineManager().getEngineByExtension("kts")!! factory.eval(getResource("$experimentName.kts").readText()) val executionPlan = ExecutionPlan(expRegistry.find(experimentName)) ConsoleExecutor(executionPlan).execute(parsedArgs.stage) }

Slide 103

Slide 103 text

103 Процессинг скриптов в ресурсы //build.gradle processResources { from 'src/main/kotlin/one/exps/scripts' }

Slide 104

Slide 104 text

104 Определим основной исполняемый класс //build.gradle processResources { from 'src/main/kotlin/one/exps/scripts' } mainClassName = ‘one.exps.execution.ConsoleExecutorKt'

Slide 105

Slide 105 text

105 Передаём аргументы через Gradle //build.gradle processResources { from ‘src/main/kotlin/one/exps/scripts' } mainClassName = 'one.exps.execution.ConsoleExecutorKt' run { standardInput = System.in args System.getProperty("exec.args", "").split() }

Slide 106

Slide 106 text

Launch it

Slide 107

Slide 107 text

107 IDEA

Slide 108

Slide 108 text

108 CMD + Gradle ./gradlew run -Dexec.args="-e dev -s all -x XPRM-9756"

Slide 109

Slide 109 text

109 CMD + Docker docker run --rm -it one.experiment/experiments:1.0 -e dev -s all -x XPRM-9756 https://github.com/bmuschko/gradle-docker-plugin

Slide 110

Slide 110 text

110 Консольный лог

Slide 111

Slide 111 text

111 Отчётность

Slide 112

Slide 112 text

6| Демо

Slide 113

Slide 113 text

7| Разделяй и властвуй

Slide 114

Slide 114 text

114 Разделение ответственностей user model experiment { steps { openByPartitionSteps() } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }

Slide 115

Slide 115 text

115 Переход одной модели в другую user model execution model Трансформация

Slide 116

Slide 116 text

116 “Плоская” модель исполнения execution model execution { steps { init { description='на никого' property { "applicationName": "odnoklassniki-web", "hostName": "host-odnoklassniki-web", "propertyName": "app.mood.withFriendsEnabled", "propertyValue": "" } property { ... } } ... all { ... } } definition='возможность отмечать друзей в настроении' }

Slide 117

Slide 117 text

117 Модель исполнения. Один шаг init { description='на никого' property { "applicationName": "odnoklassniki-web", "hostName": "host-odnoklassniki-web", "propertyName": "app.mood.withFriendsEnabled", "propertyValue": "" } property { "applicationName": "odnoklassniki-web", "hostName": "host-odnoklassniki-web", "propertyName": "app.mood.anotherProperty", "propertyValue": "" } }

Slide 118

Slide 118 text

118 Переход модели в ещё одну модель user model validation model Трансформация

Slide 119

Slide 119 text

119 Модель пользователя. Шаг 4 user model 1 2 3 4

Slide 120

Slide 120 text

120 Модель валидации. Проверка предыдущих шагов validation model 1 2 3 C

Slide 121

Slide 121 text

121 Сверка реального и ожидаемого состояния 1 2 3 C validation model app.mood.withFriendsEnabled: 0-63 app.mood.anotherProperty: 0-63 check state C

Slide 122

Slide 122 text

122 Упс. Кто-то пытается зачитерить

Slide 123

Slide 123 text

123 Основная модель - дополнительные модели • Модель исполнения • Модель валидации • Модель планирования • создание задач в Jira • визуализация плана • ??? • любые хотелки

Slide 124

Slide 124 text

• JUnit • KotlinTest 124 Тестируем трансформации

Slide 125

Slide 125 text

• JUnit • KotlinTest • Spek 125 Тестируем трансформации https://github.com/JetBrains/spek https://github.com/spekframework/spek

Slide 126

Slide 126 text

126 Spek. Пиши как пользователь exp { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } }

Slide 127

Slide 127 text

127 Тестовый эксперимент val experiment = exp { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } }

Slide 128

Slide 128 text

128 Spek. Новая спецификация object ExperimentToExecutionPlanSpek : Spek ({ describe("Experiment") { val experiment = exp { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } } } })

Slide 129

Slide 129 text

129 Добавляем действия и проверки object ExperimentToExecutionPlanSpek : Spek ({ describe("Experiment") { ... } on("Make execution plan") { val execPlan = ExecutionPlan(experiment) it("should has execution steps") { assertTrue(execPlan.steps.isNotEmpty()) } it("something else") { ... } } })

Slide 130

Slide 130 text

130 Spek + build.gradle buildscript { ext.kotlin_version = '1.1.51' ext.spek_version = '1.1.5' ext.junit_runner_version = '1.0.1' repositories { ... } dependencies { classpath "org.junit.platform:junit-platform-gradle-plugin:$junit_runner_version" } }

Slide 131

Slide 131 text

131 Нужно согласовать версии buildscript { ext.kotlin_version = '1.1.51' ext.spek_version = '1.1.5' ext.junit_runner_version = '1.0.1' repositories { ... } dependencies { classpath "org.junit.platform:junit-platform-gradle-plugin:$junit_runner_version" } }

Slide 132

Slide 132 text

132 Добавляем поддержку запуска Spek-тестов buildscript { ... } junitPlatform { platformVersion '1.0.1' filters { engines { include 'spek' } } }

Slide 133

Slide 133 text

133 Spek. Launch ./gradlew junitPlatformTest https://github.com/raniejade/spek-idea-plugin

Slide 134

Slide 134 text

>| Выводы

Slide 135

Slide 135 text

• Скрипты можно хранить в SCM • относимся к экспериментам как к коду • Унифицированный DSL • Автоматизированный запуск • из IDE или командной строки • меньше ручных действий • Приятный язык для создания и поддержки DSL 135 Что мы получили?

Slide 136

Slide 136 text

• Kotlin Script пока что “сыроват” • есть способы преодоления • Инструменты не идеальны • а так хотелось • Не всё и не сразу можно покрыть базовыми сценариями • постоянные доработки • свой синтаксический сахар • баланс между сложностью освоения и гибкостью инструмента 136 Trade-Offs

Slide 137

Slide 137 text

137 Мы ещё в начале пути ;) t MVP CLI Jira, TamTam Auto Launch Jira custom fields and charts Auto Feedback Charts Smart monitoring

Slide 138

Slide 138 text

Links

Slide 139

Slide 139 text

139 Стартер для скриптов на Kotlin-е https://github.com/aatarasoff/kotlin-script-starter

Slide 140

Slide 140 text

140 Напочитать http://developerblog.info/2017/10/02/kotlin-dsl-i-vsie-vsie-vsie http://developerblog.info/2017/10/26/kotlin-dsl-i-nosuchmethod http://developerblog.info/2017/10/24/nieochievidnyie-zamietki-1

Slide 141

Slide 141 text

141 Q&A

Slide 142

Slide 142 text

No content