From 94be3545a46aef01f81a5b9c9d6ff7be4679f748 Mon Sep 17 00:00:00 2001 From: Michael Wright Date: Mon, 12 Jul 2021 13:37:21 +0200 Subject: [PATCH 1/3] feat: Parameterized Tests - multiple (#2076) --- docs/index.md | 10 ++++++--- test_runner/flank.yml | 6 ++++- .../kotlin/ftl/args/ValidateAndroidArgs.kt | 11 +++++++++- .../ftl/config/android/AndroidGcloudConfig.kt | 8 ++++++- .../android/CreateAndroidTestContext.kt | 22 +++++++++---------- .../test/kotlin/ftl/args/AndroidArgsTest.kt | 14 ++++++++++++ 6 files changed, 54 insertions(+), 17 deletions(-) diff --git a/docs/index.md b/docs/index.md index d69341f3a4..5fe59bafb8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -588,12 +588,16 @@ gcloud: # - class com.package2.for.shard2.Class ### parameterized-tests - ## Specifies how to handle tests which contain Parameterization. - ## 3 options are available + ## Specifies how to handle tests which contain the parameterization annotation. + ## 4 options are available ## default: treat Parameterized tests as normal and shard accordingly ## ignore-all: Parameterized tests are ignored and not sharded ## shard-into-single: Parameterized tests are collected and put into a single shard - ## Note: if left blank default is used. Default usage may result in significant increase/difference of shard times observed + ## shard-into-multiple: Parameterized tests are collected and sharded into different shards based upon matching names. (Experimental) + ## Note: If left blank default is used. Default usage may result in significant increase/difference of shard times observed + ## Note: If shard-into-single is used, a single additional shard is created that will run the Parameterized tests separately. + ## Note: If shard-into-multiple is used, each parameterized test will be matched by its corresponding name and sharded into a separate shard. + ## This may dramatically increase the amount of expected shards depending upon how many parameterized tests are discovered. # parameterized-tests: default flank: diff --git a/test_runner/flank.yml b/test_runner/flank.yml index 805cfef8a7..671677546c 100644 --- a/test_runner/flank.yml +++ b/test_runner/flank.yml @@ -225,7 +225,11 @@ gcloud: ## default: treat Parameterized tests as normal and shard accordingly ## ignore-all: Parameterized tests are ignored and not sharded ## shard-into-single: Parameterized tests are collected and put into a single shard - ## Note: if left blank default is used. Default usage may result in significant increase/difference of shard times observed + ## shard-into-multiple: Parameterized tests are collected and sharded into different shards based upon matching names. (Experimental) + ## Note: If left blank default is used. Default usage may result in significant increase/difference of shard times observed + ## Note: If shard-into-single is used, a single additional shard is created that will run the Parameterized tests separately. + ## Note: If shard-into-multiple is used, each parameterized test will be matched by its corresponding name and sharded into a separate shard. + ## This may dramatically increase the amount of expected shards depending upon how many parameterized tests are discovered. # parameterized-tests: default flank: diff --git a/test_runner/src/main/kotlin/ftl/args/ValidateAndroidArgs.kt b/test_runner/src/main/kotlin/ftl/args/ValidateAndroidArgs.kt index 4fe81f33f1..bc4afa2390 100644 --- a/test_runner/src/main/kotlin/ftl/args/ValidateAndroidArgs.kt +++ b/test_runner/src/main/kotlin/ftl/args/ValidateAndroidArgs.kt @@ -276,7 +276,16 @@ private fun AndroidArgs.assertParameterizedTests() { if (parameterizedTests.isNullOrEmpty() || parameterizedTests !in listOf( "ignore-all", "default", - "shard-into-single" + "shard-into-single", + "shard-into-multiple" ) ) throw FlankConfigurationError("Parameterized test flag must be one of the following: `default`, `ignore-all`, `shard-into-single`, leaving it blank will result in `default` sharding.") + else checkParameterizedTestFeaturesWarning() +} + +private fun AndroidArgs.checkParameterizedTestFeaturesWarning() { + when (parameterizedTests) { + "shard-into-single" -> logLn("WARNING: All parameterized tests will be collected and sharded separately into a single shard. This may result in a long shard execution times if many parameterized tests are found.") + "shard-into-multiple" -> logLn("WARNING: All parameterized tests will be collected and sharded into different shards. This will result in additional shards created. max-shards is not adhered to for this selection.") + } } diff --git a/test_runner/src/main/kotlin/ftl/config/android/AndroidGcloudConfig.kt b/test_runner/src/main/kotlin/ftl/config/android/AndroidGcloudConfig.kt index b91557e2cf..db6dfa43d3 100644 --- a/test_runner/src/main/kotlin/ftl/config/android/AndroidGcloudConfig.kt +++ b/test_runner/src/main/kotlin/ftl/config/android/AndroidGcloudConfig.kt @@ -250,7 +250,13 @@ data class AndroidGcloudConfig @JsonIgnore constructor( @set:CommandLine.Option( names = ["--parameterized-tests"], - description = ["Specifies how to handle tests which contain the parameterization annotation. Possible values: `default`, `ignore-all`, `shard-into-single`, leaving it blank will result in `default` sharding"] + description = [ + "Specifies how to handle tests which contain the parameterization annotation. Possible values: `default`, `ignore-all`, `shard-into-single`, `shard-into-multiple`.\n" + + "leaving it blank will result in `default` sharding.\n" + + "Note: Making use of shard-into-single` or `shard-into-multiple will result in additional shards being created even if a max number of shards has been specified.\n" + + "Note: If shard-into-single is used, a single additional shard is created that will run the Parameterized tests separately.\n" + + "Note: If shard-into-multiple is used, each parameterized test will be matched by its corresponding name and sharded into a separate shard. This may dramatically increase the amount of expected shards depending upon how many parameterized tests are discovered." + ] ) @set:JsonProperty("parameterized-tests") var parameterizedTests: String? by data diff --git a/test_runner/src/main/kotlin/ftl/run/platform/android/CreateAndroidTestContext.kt b/test_runner/src/main/kotlin/ftl/run/platform/android/CreateAndroidTestContext.kt index 58653cae81..d2f40ba70a 100644 --- a/test_runner/src/main/kotlin/ftl/run/platform/android/CreateAndroidTestContext.kt +++ b/test_runner/src/main/kotlin/ftl/run/platform/android/CreateAndroidTestContext.kt @@ -96,15 +96,15 @@ private fun InstrumentationTestContext.calculateShardsNormally( private fun InstrumentationTestContext.calculateShards( args: AndroidArgs, testFilter: TestFilter = TestFilters.fromTestTargets(args.testTargets, args.testTargetsForShard) -): InstrumentationTestContext = - if (args.parameterizedTests.shouldShardIntoSingle()) { - var flankTestMethods = getFlankTestMethods(testFilter, args.parameterizedTests) - val parameterizedTests = flankTestMethods.filter { it.isParameterizedClass } - flankTestMethods = flankTestMethods.filterNot { it.isParameterizedClass } - val shards = calculateParameterizedShards(flankTestMethods, args) - val parameterizedShard = calculateParameterizedShards(parameterizedTests, args, 1) - shards.copy(shards = shards.shards + parameterizedShard.shards) - } else calculateShardsNormally(args, testFilter) +): InstrumentationTestContext = if (args.parameterizedTests.shouldShardSmartly()) { + var flankTestMethods = getFlankTestMethods(testFilter, args.parameterizedTests) + val parameterizedTests = flankTestMethods.filter { it.isParameterizedClass } + flankTestMethods = flankTestMethods.filterNot { it.isParameterizedClass } + val shards = calculateParameterizedShards(flankTestMethods, args) + val shardCount = if (args.parameterizedTests.isSingleParameterizedShard()) 1 else parameterizedTests.size + val parameterizedShard = calculateParameterizedShards(parameterizedTests, args, shardCount) + shards.copy(shards = shards.shards + parameterizedShard.shards) +} else calculateShardsNormally(args, testFilter) private fun InstrumentationTestContext.calculateParameterizedShards( filteredTests: List, @@ -121,8 +121,6 @@ private fun InstrumentationTestContext.calculateParameterizedShards( ) } -private fun String.shouldShardIntoSingle() = (this == "shard-into-single") - private fun InstrumentationTestContext.calculateDummyShards( args: AndroidArgs, testFilter: TestFilter = TestFilters.fromTestTargets(args.testTargets, args.testTargetsForShard) @@ -177,6 +175,8 @@ private val ignoredAnnotations = listOf( "android.support.test.filters.Suppress" ) +private fun String.shouldShardSmartly() = (this == "shard-into-single" || this == "shard-into-multiple") +private fun String.isSingleParameterizedShard() = (this == "shard-into-single") private fun String.shouldIgnore(): Boolean = (this == "ignore-all") @VisibleForTesting diff --git a/test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt b/test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt index 5edeeee2cc..1bda31327e 100644 --- a/test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt +++ b/test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt @@ -2832,6 +2832,20 @@ AndroidArgs val chunks = runBlocking { parsedYml.runAndroidTests() }.shardChunks assertTrue(chunks.size == 1) } + + @Test + fun `should shard tests into multiple new shards with shard-into-multiple`() { + val yaml = """ + gcloud: + app: $appApk + test: $testExtremeParameterizedOtherApk + parameterized-tests: shard-into-multiple + """.trimIndent() + + val parsedYml = AndroidArgs.load(yaml).validate() + val chunks = runBlocking { parsedYml.runAndroidTests() }.shardChunks + assertTrue(chunks.size == 5) + } } private fun AndroidArgs.Companion.load( From 76d806ea43632915f4496a4a152780d323fd8215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20G=C3=B3ral?= <60390247+jan-gogo@users.noreply.github.com> Date: Tue, 13 Jul 2021 10:01:55 +0200 Subject: [PATCH 2/3] Move legacy execution tool to standalone module. (#2081) --- corellium/domain/build.gradle.kts | 1 + .../flank/corellium/domain/RunTestCorelliumAndroid.kt | 6 +++--- settings.gradle.kts | 2 +- tool/execution/{synchronized => linear}/build.gradle.kts | 1 + .../src/main/kotlin/flank/exection/linear}/Transform.kt | 8 ++++---- 5 files changed, 10 insertions(+), 8 deletions(-) rename tool/execution/{synchronized => linear}/build.gradle.kts (93%) rename {corellium/domain/src/main/kotlin/flank/corellium/domain/util => tool/execution/linear/src/main/kotlin/flank/exection/linear}/Transform.kt (79%) diff --git a/corellium/domain/build.gradle.kts b/corellium/domain/build.gradle.kts index 2a8702c0fc..c1d2021f53 100644 --- a/corellium/domain/build.gradle.kts +++ b/corellium/domain/build.gradle.kts @@ -16,6 +16,7 @@ tasks.withType { kotlinOptions.jvmTarget = "1.8" } dependencies { implementation(Dependencies.KOTLIN_COROUTINES_CORE) api(project(":corellium:api")) + api(project(":tool:execution:linear")) api(project(":tool:apk")) api(project(":tool:filter")) api(project(":tool:shard:calculate")) diff --git a/corellium/domain/src/main/kotlin/flank/corellium/domain/RunTestCorelliumAndroid.kt b/corellium/domain/src/main/kotlin/flank/corellium/domain/RunTestCorelliumAndroid.kt index d1a6f23088..53aab9be09 100644 --- a/corellium/domain/src/main/kotlin/flank/corellium/domain/RunTestCorelliumAndroid.kt +++ b/corellium/domain/src/main/kotlin/flank/corellium/domain/RunTestCorelliumAndroid.kt @@ -23,9 +23,9 @@ import flank.corellium.domain.run.test.android.step.loadPreviousDurations import flank.corellium.domain.run.test.android.step.parseApksInfo import flank.corellium.domain.run.test.android.step.parseTestCasesFromApks import flank.corellium.domain.run.test.android.step.prepareShards -import flank.corellium.domain.util.Transform -import flank.corellium.domain.util.execute -import flank.corellium.domain.util.injectLogger +import flank.exection.linear.Transform +import flank.exection.linear.execute +import flank.exection.linear.injectLogger import flank.instrument.log.Instrument import flank.junit.JUnit import flank.log.Event diff --git a/settings.gradle.kts b/settings.gradle.kts index a50ae08954..c5a75fdd9b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,7 +35,7 @@ include( ":tool:log", ":tool:log:format", ":tool:execution:parallel", - ":tool:execution:synchronized", + ":tool:execution:linear", ) plugins { diff --git a/tool/execution/synchronized/build.gradle.kts b/tool/execution/linear/build.gradle.kts similarity index 93% rename from tool/execution/synchronized/build.gradle.kts rename to tool/execution/linear/build.gradle.kts index 93570d09cd..8a834159ff 100644 --- a/tool/execution/synchronized/build.gradle.kts +++ b/tool/execution/linear/build.gradle.kts @@ -13,6 +13,7 @@ repositories { tasks.withType { kotlinOptions.jvmTarget = "1.8" } dependencies { + api(project(":tool:log")) implementation(Dependencies.KOTLIN_COROUTINES_CORE) testImplementation(Dependencies.JUNIT) } diff --git a/corellium/domain/src/main/kotlin/flank/corellium/domain/util/Transform.kt b/tool/execution/linear/src/main/kotlin/flank/exection/linear/Transform.kt similarity index 79% rename from corellium/domain/src/main/kotlin/flank/corellium/domain/util/Transform.kt rename to tool/execution/linear/src/main/kotlin/flank/exection/linear/Transform.kt index 2649d725ec..89ee7e14b0 100644 --- a/corellium/domain/src/main/kotlin/flank/corellium/domain/util/Transform.kt +++ b/tool/execution/linear/src/main/kotlin/flank/exection/linear/Transform.kt @@ -1,4 +1,4 @@ -package flank.corellium.domain.util +package flank.exection.linear import flank.log.Event import flank.log.Logger @@ -14,13 +14,13 @@ import kotlinx.coroutines.flow.fold * @receiver Value to apply transformations on it. * @return Result of transformations. */ -internal suspend infix fun S.execute(operations: Flow>): S = +suspend infix fun S.execute(operations: Flow>): S = operations.fold(this) { value, transform -> transform(value) } /** * Simple parameterized factory for generating [Transform] instances. */ -internal class CreateTransformation : (Transform) -> Transform by { it } +class CreateTransformation : (Transform) -> Transform by { it } /** * Type-alias for suspendable transforming operation @@ -30,7 +30,7 @@ typealias Transform = suspend S.() -> S /** * Inject log output to the transform function. */ -internal fun Logger.injectLogger( +fun Logger.injectLogger( type: Any = Unit, transform: suspend S.(Output) -> S ): Transform { From 1ca2f77ea25795c09749d3e07cb46cfcaf30d6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20G=C3=B3ral?= <60390247+jan-gogo@users.noreply.github.com> Date: Tue, 13 Jul 2021 10:23:52 +0200 Subject: [PATCH 3/3] feat: Improve :tool:execution:parallel. (#2080) * Integrate :tool:execution:parallel with :tool:log. * Add function for creating Parallel.Type object dynamically. * Improve error message for Parallel.Context.lazyProperty. * Move implementations to internal package. * Add function for checking returned state. * Add selector for context property --- tool/execution/parallel/build.gradle.kts | 1 + .../kotlin/flank/exection/parallel/Create.kt | 15 +++++++- .../kotlin/flank/exection/parallel/Execute.kt | 3 +- .../flank/exection/parallel/Parallel.kt | 29 ++++++--------- .../kotlin/flank/exection/parallel/Reduce.kt | 15 ++++++-- .../kotlin/flank/exection/parallel/Select.kt | 7 ++++ .../flank/exection/parallel/Validate.kt | 36 ++----------------- .../kotlin/flank/exection/parallel/Verify.kt | 8 +++++ .../parallel/internal/ContextProvider.kt | 4 +-- .../exection/parallel/internal/DynamicType.kt | 9 +++++ .../exection/parallel/internal/Execution.kt | 8 +++-- .../exection/parallel/internal/Prepare.kt | 12 ++++++- .../exection/parallel/internal/Property.kt | 13 +++++-- .../exection/parallel/internal/Validate.kt | 36 +++++++++++++++++++ .../exection/parallel/internal/Verify.kt | 6 ++++ .../kotlin/flank/exection/parallel/Example.kt | 2 +- .../flank/exection/parallel/ExecuteKtTest.kt | 2 +- tool/log/src/main/kotlin/flank/log/Event.kt | 7 ++-- tool/log/src/main/kotlin/flank/log/Logger.kt | 2 +- 19 files changed, 145 insertions(+), 70 deletions(-) create mode 100644 tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Select.kt create mode 100644 tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Verify.kt create mode 100644 tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/DynamicType.kt create mode 100644 tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Validate.kt create mode 100644 tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Verify.kt diff --git a/tool/execution/parallel/build.gradle.kts b/tool/execution/parallel/build.gradle.kts index 93570d09cd..8a834159ff 100644 --- a/tool/execution/parallel/build.gradle.kts +++ b/tool/execution/parallel/build.gradle.kts @@ -13,6 +13,7 @@ repositories { tasks.withType { kotlinOptions.jvmTarget = "1.8" } dependencies { + api(project(":tool:log")) implementation(Dependencies.KOTLIN_COROUTINES_CORE) testImplementation(Dependencies.JUNIT) } diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Create.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Create.kt index 5374b6b5c1..36cdcc7dff 100644 --- a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Create.kt +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Create.kt @@ -1,5 +1,6 @@ package flank.exection.parallel +import flank.exection.parallel.internal.DynamicType import flank.exection.parallel.internal.EagerProperties // ======================= Signature ======================= @@ -34,9 +35,21 @@ infix fun Parallel.Type.using( /** * Factory function for creating special task that can validate arguments before execution. - * Typically the [Parallel.Context] with added [EagerProperties] is used to validate initial state. + * The [Parallel.Context] with added [EagerProperties] can validate if state contains required initial values. */ internal fun validator( context: (() -> C) ): Parallel.Task = context() using { context().also { it.state = this }.run { validate() } } + +// ======================= Type ======================= + +/** + * Factory function for creating dynamic [Parallel.Type]. + */ +inline fun type(): Parallel.Type = type(T::class.java) + +/** + * Factory function for creating dynamic [Parallel.Type]. + */ +fun type(type: Class): Parallel.Type = DynamicType(type) diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Execute.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Execute.kt index b1a886679d..e307a55ffb 100644 --- a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Execute.kt +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Execute.kt @@ -2,6 +2,7 @@ package flank.exection.parallel import flank.exection.parallel.internal.Execution import flank.exection.parallel.internal.invoke +import flank.exection.parallel.internal.minusContextValidators import kotlinx.coroutines.flow.Flow /** @@ -12,7 +13,7 @@ import kotlinx.coroutines.flow.Flow infix operator fun Tasks.invoke( args: ParallelState ): Flow = - Execution(this, args).invoke() + Execution(minusContextValidators(), args).invoke() // ======================= Extensions ======================= diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Parallel.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Parallel.kt index 830cb4451f..41d248c4e1 100644 --- a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Parallel.kt +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Parallel.kt @@ -3,7 +3,7 @@ package flank.exection.parallel import flank.exection.parallel.internal.ContextProvider import flank.exection.parallel.internal.EagerProperties import flank.exection.parallel.internal.lazyProperty -import java.lang.System.currentTimeMillis +import flank.log.Output // ======================= Types ======================= @@ -13,7 +13,7 @@ import java.lang.System.currentTimeMillis object Parallel { /** - * Abstraction for execution data provider which is also an context for task execution. + * Abstraction for execution data provider which is also a context for task execution. * For initialization purpose some properties are exposed as variable. */ open class Context : Type { @@ -46,7 +46,12 @@ object Parallel { protected operator fun Type.unaryMinus() = lazyProperty(this) /** - * Internal accessor for initializing (validating) eager properties + * DSL for creating lazy delegate accessor to the state value for a given type with additional property selector. + */ + protected operator fun Type.invoke(select: T.() -> V) = lazyProperty(this, select) + + /** + * Internal accessor for initializing (validating) eager properties. */ internal fun validate() = eager() } @@ -67,8 +72,8 @@ object Parallel { /** * The task signature. * - * @param type A return type of a task - * @param args A set of types for arguments + * @param type A return a type of task. + * @param args A set of argument types. */ data class Signature( val type: Type, @@ -81,15 +86,6 @@ object Parallel { */ class Function(override val context: () -> X) : ContextProvider() - data class Event internal constructor( - val type: Type<*>, - val data: Any, - val timestamp: Long = currentTimeMillis(), - ) { - object Start - object Stop - } - object Logger : Type /** @@ -111,11 +107,6 @@ object Parallel { */ typealias ExecuteTask = suspend ParallelState.() -> R -/** - * Common signature for structural log output. - */ -typealias Output = Any.() -> Unit - /** * Immutable state for parallel execution. */ diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Reduce.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Reduce.kt index f7fc6bf563..91ad2d2faf 100644 --- a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Reduce.kt +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Reduce.kt @@ -1,19 +1,20 @@ package flank.exection.parallel -import flank.exection.parallel.internal.contextValidators +import flank.exection.parallel.internal.contextValidatorTypes import flank.exection.parallel.internal.reduceTo +import flank.exection.parallel.internal.type /** * Reduce given [Tasks] by [expected] types to remove unneeded tasks from the graph. * The returned graph will hold only tasks that are returning selected types, their dependencies and derived dependencies. - * Additionally this is keeping also the validators for initial state. + * Additionally, this is keeping also the validators for initial state. * * @return Reduced [Tasks] */ operator fun Tasks.invoke( expected: Set> ): Tasks = - reduceTo(expected + contextValidators()) + reduceTo(expected + contextValidatorTypes()) /** * Shortcut for tasks reducing. @@ -21,3 +22,11 @@ operator fun Tasks.invoke( operator fun Tasks.invoke( vararg expected: Parallel.Type<*> ): Tasks = invoke(expected.toSet()) + +/** + * Remove the [Tasks] by given [types]. + */ +operator fun Tasks.minus( + types: Set> +): Tasks = + filterNot { task -> task.type in types }.toSet() diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Select.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Select.kt new file mode 100644 index 0000000000..5436ea6c98 --- /dev/null +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Select.kt @@ -0,0 +1,7 @@ +package flank.exection.parallel + +/** + * Select value by type. + */ +@Suppress("UNCHECKED_CAST") +fun ParallelState.select(type: Parallel.Type) = get(type) as T diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Validate.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Validate.kt index 6a5f8a561e..de08d0636a 100644 --- a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Validate.kt +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Validate.kt @@ -1,11 +1,6 @@ package flank.exection.parallel -import flank.exection.parallel.internal.args -import flank.exection.parallel.internal.graph.findCycles -import flank.exection.parallel.internal.graph.findDuplicatedDependencies -import flank.exection.parallel.internal.graph.findMissingDependencies -import flank.exection.parallel.internal.type -import kotlinx.coroutines.runBlocking +import flank.exection.parallel.internal.validateExecutionGraphs /** * Validate the given [Tasks] and [ParallelState] for finding missing dependencies or broken paths. @@ -13,30 +8,5 @@ import kotlinx.coroutines.runBlocking * @param initial The initial arguments for tasks execution. * @return Valid [Tasks] if graph has no broken paths or missing dependencies. */ -fun Tasks.validate(initial: ParallelState = emptyMap()): Tasks = run { - // Separate initial validators from tasks. Validators are important now but not during the execution. - val (validators, tasks) = splitTasks() - - // check if initial state is providing all required values specified in context. - runBlocking { validators.forEach { check -> check.execute(initial) } } - - map(Parallel.Task<*>::type).findDuplicatedDependencies(initial.keys).run { - if (isNotEmpty()) throw Parallel.DependenciesError.Duplicate(this) - } - - val graph = associate { task -> task.type to task.args } - - graph.findMissingDependencies(initial.keys).run { - if (isNotEmpty()) throw Parallel.DependenciesError.Missing(this) - } - - graph.findCycles().run { - if (isNotEmpty()) throw Parallel.DependenciesError.Cycles(this) - } - - tasks.toSet() -} - -private fun Iterable>.splitTasks() = this - .groupBy { task -> task.type is Parallel.Context } - .run { getOrDefault(true, emptyList()) to getOrDefault(false, emptyList()) } +fun Tasks.validate(initial: ParallelState = emptyMap()): Tasks = + validateExecutionGraphs(initial) diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Verify.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Verify.kt new file mode 100644 index 0000000000..b98d23eb93 --- /dev/null +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Verify.kt @@ -0,0 +1,8 @@ +package flank.exection.parallel + +import flank.exection.parallel.internal.checkThrowableValues + +/** + * Verify that given [ParallelState] has no errors. + */ +fun ParallelState.verify(): ParallelState = checkThrowableValues() diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/ContextProvider.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/ContextProvider.kt index 105f3fdcc7..b622c554ed 100644 --- a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/ContextProvider.kt +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/ContextProvider.kt @@ -7,11 +7,11 @@ import flank.exection.parallel.validator /** * Abstract factory for creating task function. */ -abstract class ContextProvider { +abstract class ContextProvider internal constructor() { protected abstract val context: () -> X operator fun invoke(body: suspend X.() -> R): ExecuteTask = { context().also { it.state = this }.body() } - val validator by lazy { validator(context) } + val validate by lazy { validator(context) } } diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/DynamicType.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/DynamicType.kt new file mode 100644 index 0000000000..e4d0a4c957 --- /dev/null +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/DynamicType.kt @@ -0,0 +1,9 @@ +package flank.exection.parallel.internal + +import flank.exection.parallel.Parallel + +internal class DynamicType(val type: Class) : Parallel.Type { + override fun equals(other: Any?): Boolean = (other as? DynamicType<*>)?.type == type + override fun hashCode(): Int = type.hashCode() + javaClass.hashCode() + override fun toString(): String = type.canonicalName +} diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Execution.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Execution.kt index 27157279c3..74657584c4 100644 --- a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Execution.kt +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Execution.kt @@ -1,14 +1,16 @@ package flank.exection.parallel.internal -import flank.exection.parallel.Output import flank.exection.parallel.Parallel -import flank.exection.parallel.Parallel.Event import flank.exection.parallel.Parallel.Logger import flank.exection.parallel.Parallel.Task import flank.exection.parallel.Parallel.Type import flank.exection.parallel.ParallelState import flank.exection.parallel.Property import flank.exection.parallel.Tasks +import flank.log.Event +import flank.log.Output +import flank.log.event +import flank.log.normalize import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -221,7 +223,7 @@ private suspend fun Execution.execute( jobs += type to scope.launch { // Extend root output for adding additional data. - val out: Output = output?.let { { it(Event(type, this)) } } ?: {} + val out: Output = output?.normalize { type event it } ?: {} // Log the task was started Event.Start.out() diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Prepare.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Prepare.kt index 5ab8b3d2a6..c78b7d0bd4 100644 --- a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Prepare.kt +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Prepare.kt @@ -5,7 +5,17 @@ import flank.exection.parallel.Tasks /** * Get initial state validators. + * * This is necessary to perform validations of initial state before the execution. */ -internal fun Tasks.contextValidators(): List = +internal fun Tasks.contextValidatorTypes(): List = mapNotNull { task -> task.type as? Parallel.Context } + +/** + * Return graph without context validation tasks. + * + * Typically, context validation tasks should be used for testing purposes so running them on production is redundant. + * This function can be used to filter them out. + */ +internal fun Tasks.minusContextValidators(): Tasks = + filterNot { task -> task.type is Parallel.Context }.toSet() diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Property.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Property.kt index e39821366f..a6c7dd523d 100644 --- a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Property.kt +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Property.kt @@ -6,11 +6,20 @@ import flank.exection.parallel.ParallelState /** * Factory function for lazy property delegate. */ -internal fun Parallel.Context.lazyProperty(type: Parallel.Type) = lazy { +internal fun Parallel.Context.lazyProperty( + type: Parallel.Type, + select: T.() -> R +): Lazy = lazy { + fun errorMessage() = "Cannot resolve dependency of type: $type. Make sure is specified as argument" @Suppress("UNCHECKED_CAST") - state[type] as T + (state[type] as? T ?: throw IllegalStateException(errorMessage())).select() } +internal fun Parallel.Context.lazyProperty( + type: Parallel.Type +): Lazy = + lazyProperty(type) { this } + /** * Eager properties provider and initializer. */ diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Validate.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Validate.kt new file mode 100644 index 0000000000..f68ecd9e7a --- /dev/null +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Validate.kt @@ -0,0 +1,36 @@ +package flank.exection.parallel.internal + +import flank.exection.parallel.Parallel +import flank.exection.parallel.ParallelState +import flank.exection.parallel.Tasks +import flank.exection.parallel.internal.graph.findCycles +import flank.exection.parallel.internal.graph.findDuplicatedDependencies +import flank.exection.parallel.internal.graph.findMissingDependencies +import kotlinx.coroutines.runBlocking + +// TODO: Check all cases and collect results, instead of throwing the first encountered error. +internal fun Tasks.validateExecutionGraphs(initial: ParallelState = emptyMap()): Tasks = run { + // Separate initial validators from tasks. Validators are important now but not during the execution. + val (validators, tasks) = this + .groupBy { task -> task.type is Parallel.Context } + .run { getOrDefault(true, emptyList()) to getOrDefault(false, emptyList()) } + + // check if initial state is providing all required values specified in context. + runBlocking { validators.forEach { check -> check.execute(initial) } } + + map(Parallel.Task<*>::type).findDuplicatedDependencies(initial.keys).run { + if (isNotEmpty()) throw Parallel.DependenciesError.Duplicate(this) + } + + val graph = associate { task -> task.type to task.args } + + graph.findMissingDependencies(initial.keys).run { + if (isNotEmpty()) throw Parallel.DependenciesError.Missing(this) + } + + graph.findCycles().run { + if (isNotEmpty()) throw Parallel.DependenciesError.Cycles(this) + } + + tasks.toSet() +} diff --git a/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Verify.kt b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Verify.kt new file mode 100644 index 0000000000..f97f57a3c0 --- /dev/null +++ b/tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Verify.kt @@ -0,0 +1,6 @@ +package flank.exection.parallel.internal + +import flank.exection.parallel.ParallelState + +internal fun ParallelState.checkThrowableValues(): ParallelState = + onEach { (_, value) -> if (value is Throwable) throw value } diff --git a/tool/execution/parallel/src/test/kotlin/flank/exection/parallel/Example.kt b/tool/execution/parallel/src/test/kotlin/flank/exection/parallel/Example.kt index 7e710671cc..4ef945f9e9 100644 --- a/tool/execution/parallel/src/test/kotlin/flank/exection/parallel/Example.kt +++ b/tool/execution/parallel/src/test/kotlin/flank/exection/parallel/Example.kt @@ -132,7 +132,7 @@ fun main() { ) Example.run { - execute + context.validator + execute + context.validate }.validate( // Comment line below to simulate error on context.validator args diff --git a/tool/execution/parallel/src/test/kotlin/flank/exection/parallel/ExecuteKtTest.kt b/tool/execution/parallel/src/test/kotlin/flank/exection/parallel/ExecuteKtTest.kt index 2c205dc1f2..e7432bcb72 100644 --- a/tool/execution/parallel/src/test/kotlin/flank/exection/parallel/ExecuteKtTest.kt +++ b/tool/execution/parallel/src/test/kotlin/flank/exection/parallel/ExecuteKtTest.kt @@ -164,7 +164,7 @@ class ExecuteKtTest { ) val actual = runBlocking { execute(args).last() } - assert(actual[A] is NullPointerException) + assert(actual[A] is IllegalStateException) } /** diff --git a/tool/log/src/main/kotlin/flank/log/Event.kt b/tool/log/src/main/kotlin/flank/log/Event.kt index 80c6ec91bb..0e8cc00e64 100644 --- a/tool/log/src/main/kotlin/flank/log/Event.kt +++ b/tool/log/src/main/kotlin/flank/log/Event.kt @@ -3,13 +3,16 @@ package flank.log /** * Structural log representation * + * @property context Any object that can be treated as a context for a group of events. * @property type Unique identifier for value. Could be a KClass of [Event.Type] depending on value type. - * @property value Logged value + * @property value Logged value. + * @property timestamp Time when the event occurs. Unix Timestamp in milliseconds. */ data class Event internal constructor( val context: Any, val type: Any, val value: V, + val timestamp: Long = System.currentTimeMillis(), ) { /** * Interface of event data identified by KClass @@ -41,5 +44,5 @@ infix fun Any.event(any: Any): Event = when (any) { is Pair<*, *> -> Event(this, any.first!!, any.second!!) is Event.Data -> Event(this, any::class.java, any) is Event.Type<*> -> Event(this, any, Unit) - else -> Event(this, any::class.java, any) // Consider to disallow anonymous types + else -> Event(this, any::class.java, any) // Consider disallow unknown types. } diff --git a/tool/log/src/main/kotlin/flank/log/Logger.kt b/tool/log/src/main/kotlin/flank/log/Logger.kt index 33a5aff828..a5043fb03b 100644 --- a/tool/log/src/main/kotlin/flank/log/Logger.kt +++ b/tool/log/src/main/kotlin/flank/log/Logger.kt @@ -10,7 +10,7 @@ interface Logger { /** * Logging function signature. */ -typealias Output = Any.() -> Unit +typealias Output = GenericOutput /** * Generic logging function signature.