diff --git a/corellium/domain/README.md b/corellium/domain/README.md index 0fb5bbd49b..eccdc62274 100644 --- a/corellium/domain/README.md +++ b/corellium/domain/README.md @@ -9,22 +9,15 @@ This module is specifying public API and internal implementation of Flank-Corell * Public API - files inside [flank.corellium.domain](./src/main/kotlin/flank/corellium/domain) * Internal functions - nested packages inside [flank.corellium.domain](./src/main/kotlin/flank/corellium/domain) -## Design +## Execution -### Stateful execution +Execution can be represented as a graph of tasks relations without cycles. -Following specification is suitable for complicated long-running use-cases, when becomes convenient to split execution into smaller atomic chunks of work. -#### Definition: +#### Version from master branch: -* `Execution` is process which is creating initial `State`, and is running the set of `steps` on it in specific `Context`. -* `Execution` can pass `Context` to `Step` if needed. -* `Step` is a suspendable operation that is receiving `State` as the only argument. -* `Step` must return received or new `State`. -* `Step` can generate side effects. -* `State` is a structure used for sharing data between preceding and following `steps`. -* `Context` is providing arguments and functions for `step`. +![TestAndroid.execute graph](http://www.plantuml.com/plantuml/proxy?cache=no&fmt=svg&src=https://raw.githubusercontent.com/Flank/flank/master/corellium/domain/TestAndroid-execute.puml) -#### Utility: +### New version draft: -* [`Transform.kt`](src/main/kotlin/flank/corellium/domain/util/Transform.kt) +![TestAndroid.execute graph](http://www.plantuml.com/plantuml/proxy?cache=no&fmt=svg&src=https://raw.githubusercontent.com/Flank/flank/2083_Add_module_tool-execution-parallel-plantuml/corellium/domain/TestAndroid-execute.puml) diff --git a/corellium/domain/TestAndroid-execute.puml b/corellium/domain/TestAndroid-execute.puml new file mode 100644 index 0000000000..872c132e14 --- /dev/null +++ b/corellium/domain/TestAndroid-execute.puml @@ -0,0 +1,44 @@ +@startuml + +skinparam componentStyle rectangle + +note as N #ffffff +* Brighter tasks are required by the darker tasks. +* The brightness means how fast the task will start. +* White tasks are starting first. +end note + +[Authorize] #fbfbfb +[OutputDir] #fbfbfb +[ParseApkInfo] #fbfbfb +[ParseTestCases] #fbfbfb +[LoadPreviousDurations] #dfdfdf +[PrepareShards] #c3c3c3 +[DumpShards] #a7a7a7 +[InvokeDevices] #a7a7a7 +[InstallApks] #8b8b8b +[ExecuteTests] #6f6f6f +[GenerateReport] #535353 +[CompleteTests] #373737 + +[DumpShards] --> [PrepareShards] +[DumpShards] --> [OutputDir] +[ExecuteTests] --> [PrepareShards] +[ExecuteTests] --> [ParseApkInfo] +[ExecuteTests] --> [Authorize] +[ExecuteTests] --> [InvokeDevices] +[ExecuteTests] --> [InstallApks] +[CompleteTests] --> [GenerateReport] +[CompleteTests] --> [DumpShards] +[GenerateReport] --> [ExecuteTests] +[GenerateReport] --> [OutputDir] +[InstallApks] --> [Authorize] +[InstallApks] --> [PrepareShards] +[InstallApks] --> [InvokeDevices] +[InvokeDevices] --> [Authorize] +[InvokeDevices] --> [PrepareShards] +[LoadPreviousDurations] --> [ParseTestCases] +[PrepareShards] --> [ParseTestCases] +[PrepareShards] --> [LoadPreviousDurations] + +@enduml diff --git a/corellium/domain/build.gradle.kts b/corellium/domain/build.gradle.kts index e04c7f98b3..8e8bbd7d8b 100644 --- a/corellium/domain/build.gradle.kts +++ b/corellium/domain/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { testImplementation(Dependencies.JUNIT) testImplementation(project(":corellium:adapter")) + testImplementation(project(":tool:execution:parallel:plantuml")) } tasks.test { diff --git a/corellium/domain/src/test/kotlin/flank/corellium/domain/AndroidTestDiagram.kt b/corellium/domain/src/test/kotlin/flank/corellium/domain/AndroidTestDiagram.kt new file mode 100644 index 0000000000..8d610317d2 --- /dev/null +++ b/corellium/domain/src/test/kotlin/flank/corellium/domain/AndroidTestDiagram.kt @@ -0,0 +1,12 @@ +package flank.corellium.domain + +import flank.exection.parallel.plantuml.generatePlanUml +import org.junit.Test + +class AndroidTestDiagram { + + @Test + fun generate() { + TestAndroid.run { generatePlanUml(execute - context.validate) } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 53ff47465d..2f7589d4a4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,7 @@ include( ":tool:log", ":tool:log:format", ":tool:execution:parallel", + ":tool:execution:parallel:plantuml", ":tool:execution:synchronized", ":tool:analytics", ":tool:analytics:mixpanel", diff --git a/tool/execution/parallel/plantuml/build.gradle.kts b/tool/execution/parallel/plantuml/build.gradle.kts new file mode 100644 index 0000000000..cd61feb4d4 --- /dev/null +++ b/tool/execution/parallel/plantuml/build.gradle.kts @@ -0,0 +1,17 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin(Plugins.Kotlin.PLUGIN_JVM) +} + +repositories { + mavenCentral() +} + +tasks.withType { kotlinOptions.jvmTarget = "1.8" } + +dependencies { + api(project(":tool:execution:parallel")) + implementation(Dependencies.KOTLIN_COROUTINES_CORE) + testImplementation(Dependencies.JUNIT) +} diff --git a/tool/execution/parallel/plantuml/src/main/kotlin/flank/exection/parallel/plantuml/PlantUml.kt b/tool/execution/parallel/plantuml/src/main/kotlin/flank/exection/parallel/plantuml/PlantUml.kt new file mode 100644 index 0000000000..561c79df14 --- /dev/null +++ b/tool/execution/parallel/plantuml/src/main/kotlin/flank/exection/parallel/plantuml/PlantUml.kt @@ -0,0 +1,66 @@ +package flank.exection.parallel.plantuml + +import flank.exection.parallel.Tasks +import flank.exection.parallel.plantuml.internal.generatePlanUmlFile +import flank.exection.parallel.plantuml.internal.generatePlantUmlString +import java.io.File + +/** + * Generates plantuml file and saves on drive. + * + * @receiver Source object of execution. Used for generating filename and reducing tasks names. + * @param tasks Graph of tasks required to generate diagram. + * @param dir Optional path to directory for generated file. + */ +fun Any.generatePlanUml( + tasks: Tasks, + dir: String = "" +): File = generatePlanUmlFile( + tasks = tasks, + path = File(dir).resolve(javaClass.simpleName).absolutePath + "-execute.puml", + prefixToRemove = javaClass.name +) + +/** + * Generates plantuml file and saves on drive. + * + * @param tasks Graph of tasks required to generate diagram. + * @param path Path to generated file. + * @param prefixToRemove Optional prefix to remove from each task name. + */ +fun generatePlanUml( + tasks: Tasks, + path: String, + prefixToRemove: String = "", +): File = generatePlanUmlFile( + tasks = tasks, + path = path, + prefixToRemove = prefixToRemove +) + +/** + * Generates plantuml string. + * + * @receiver Source object of execution. Used for reducing tasks names. + * @param tasks Graph of tasks required to generate diagram. + */ +fun Any.generatePlantUml( + tasks: Tasks +): String = generatePlantUmlString( + tasks = tasks, + prefixToRemove = javaClass.name +) + +/** + * Generates plantuml string. + * + * @param tasks Graph of tasks required to generate diagram. + * @param prefixToRemove Optional prefix to remove from each task name. + */ +fun generatePlantUml( + tasks: Tasks, + prefixToRemove: String = "", +): String = generatePlantUmlString( + tasks = tasks, + prefixToRemove = prefixToRemove +) diff --git a/tool/execution/parallel/plantuml/src/main/kotlin/flank/exection/parallel/plantuml/internal/GeneratePlantUmlFile.kt b/tool/execution/parallel/plantuml/src/main/kotlin/flank/exection/parallel/plantuml/internal/GeneratePlantUmlFile.kt new file mode 100644 index 0000000000..0d4d0eff62 --- /dev/null +++ b/tool/execution/parallel/plantuml/src/main/kotlin/flank/exection/parallel/plantuml/internal/GeneratePlantUmlFile.kt @@ -0,0 +1,14 @@ +package flank.exection.parallel.plantuml.internal + +import flank.exection.parallel.Tasks +import java.io.File + +internal fun generatePlanUmlFile( + tasks: Tasks, + path: String, + prefixToRemove: String, +): File { + val plant = generatePlantUmlString(tasks, prefixToRemove) + println(plant) + return File(path).apply { writeText(plant) } +} diff --git a/tool/execution/parallel/plantuml/src/main/kotlin/flank/exection/parallel/plantuml/internal/GeneratePlantUmlString.kt b/tool/execution/parallel/plantuml/src/main/kotlin/flank/exection/parallel/plantuml/internal/GeneratePlantUmlString.kt new file mode 100644 index 0000000000..0a932ce5b7 --- /dev/null +++ b/tool/execution/parallel/plantuml/src/main/kotlin/flank/exection/parallel/plantuml/internal/GeneratePlantUmlString.kt @@ -0,0 +1,85 @@ +package flank.exection.parallel.plantuml.internal + +import flank.exection.parallel.Parallel +import flank.exection.parallel.Tasks +import java.awt.Color + +// =================== Internal API =================== + +internal fun generatePlantUmlString( + tasks: Tasks, + prefixToRemove: String = "", +): String { + val graph: Graph = tasks.associate { task -> task.signature.type to task.signature.args } + val depth: Map = graph.calculateDepth() + val maxDepth: Int = depth.values.maxOrNull() ?: 0 + val colors: Colors = depth.mapValues { (_, value) -> calculateColor(maxDepth, value) } + val name = fun Any.() = javaClass.name.removePrefix("$prefixToRemove$").replace('$', '.').let { "[$it]" } + + return """ +@startuml + +skinparam componentStyle rectangle + +note as N #ffffff +* Brighter tasks are required by the darker tasks. +* The brightness means how fast the task will start. +* White tasks are starting first. +end note + +${colors.printColors(name)} + +${graph.printRelations(name)} + +@enduml + + """.trimIndent() +} + +// =================== Private implementation =================== + +private typealias Graph = Map> +private typealias Node = Parallel.Type<*> +private typealias Colors = Map + +private fun calculateColor(maxDepth: Int, value: Int): String { + val c = (COLOR_MAX / maxDepth) * (maxDepth - value) + COLOR_OFFSET + return Integer.toHexString(Color(c, c, c).rgb).drop(2) // drop alpha +} + +private const val COLOR_MAX = 200 +private const val COLOR_OFFSET = 55 + +private fun Map>.calculateDepth(): Map { + var jump = 0 + val state = (values.flatten() - keys).toMutableList() + val remaining: MutableMap> = toMutableMap() + val depth = mutableMapOf() + + while (depth.size < size) { + val current = remaining + .filterValues { state.containsAll(it) }.keys + .onEach { depth[it] = jump } + state += current + remaining -= current + jump++ + } + + return depth +} + +private fun Colors.printColors( + name: Node.() -> String +) = toList().joinToString("\n") { (node, value) -> + "${node.name()} #$value" +} + +private fun Graph.printRelations( + name: Node.() -> String +) = filterValues { dependencies -> + dependencies.isNotEmpty() +}.toList().joinToString("\n") { (node, dependencies) -> + dependencies.joinToString("\n") { dep -> + node.name() + " --> " + dep.name() + } +}