From c52f74e2bdae2854a7fb9f4cf63d4838e32f2e22 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Fri, 20 Sep 2024 19:50:01 +0100 Subject: [PATCH 01/18] Add `suspend` to various methods to prepare for refactor to kontinuity --- build.gradle.kts | 37 +---- src/commonMain/kotlin/Parameterize.kt | 24 ++-- .../kotlin/ParameterizeConfiguration.kt | 1 - .../kotlin/ParameterizeConfigurationSpec.kt | 22 +-- ...ParameterizeConfigurationSpec_decorator.kt | 14 +- ...arameterizeConfigurationSpec_onComplete.kt | 32 ++--- ...ParameterizeConfigurationSpec_onFailure.kt | 118 ++++++++-------- .../kotlin/ParameterizeExceptionSpec.kt | 20 +-- .../kotlin/ParameterizeScopeSpec.kt | 127 ++++++++++-------- src/commonTest/kotlin/ParameterizeSpec.kt | 38 +++--- src/commonTest/kotlin/TestUtils.kt | 25 +++- src/commonTest/kotlin/test/EdgeCases.kt | 2 +- 12 files changed, 241 insertions(+), 219 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index ab3f3e3..3e7e229 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,6 +32,7 @@ plugins { repositories { mavenCentral() + mavenLocal() } apiValidation { @@ -59,35 +60,6 @@ kotlin { nodejs() } - linuxX64() - linuxArm64() - androidNativeArm32() - androidNativeArm64() - androidNativeX86() - androidNativeX64() - macosX64() - macosArm64() - iosSimulatorArm64() - iosX64() - watchosSimulatorArm64() - watchosX64() - watchosArm32() - watchosArm64() - tvosSimulatorArm64() - tvosX64() - tvosArm64() - iosArm64() - watchosDeviceArm64() - mingwX64() - - wasmJs { - browser() - nodejs() - } - wasmWasi { - nodejs() - } - @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") @@ -98,10 +70,15 @@ kotlin { languageSettings.optIn("kotlin.contracts.ExperimentalContracts") } - val commonMain by getting + val commonMain by getting { + dependencies { + implementation("io.github.kyay10:kontinuity:0.0.1") + } + } val commonTest by getting { dependencies { implementation(kotlin("test")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") } } val jvmMain by getting { diff --git a/src/commonMain/kotlin/Parameterize.kt b/src/commonMain/kotlin/Parameterize.kt index 791df9f..2a5cab1 100644 --- a/src/commonMain/kotlin/Parameterize.kt +++ b/src/commonMain/kotlin/Parameterize.kt @@ -20,6 +20,10 @@ package com.benwoodworth.parameterize import com.benwoodworth.parameterize.ParameterizeConfiguration.* +import effekt.Handler +import effekt.HandlerPrompt +import effekt.handle +import effekt.use import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.experimental.ExperimentalTypeInference @@ -79,10 +83,10 @@ import kotlin.reflect.KProperty * * @throws ParameterizeException if the DSL is used incorrectly. (See restrictions) */ -public inline fun parameterize( +public suspend inline fun parameterize( configuration: ParameterizeConfiguration = ParameterizeConfiguration.default, - block: ParameterizeScope.() -> Unit -) { + crossinline block: suspend ParameterizeScope.() -> Unit +): Unit = handle { // Exercise extreme caution modifying this code, since the iterator is sensitive to the behavior of this function. // Code inlined from a previous version could have subtly different semantics when interacting with the runtime // iterator of a later release, and would be major breaking change that's difficult to detect. @@ -113,12 +117,12 @@ public inline fun parameterize( // False positive: onComplete is called in place exactly once through the configuration by the end parameterize call "LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND" ) -public inline fun parameterize( +public suspend inline fun parameterize( configuration: ParameterizeConfiguration = ParameterizeConfiguration.default, noinline decorator: suspend DecoratorScope.(iteration: suspend DecoratorScope.() -> Unit) -> Unit = configuration.decorator, noinline onFailure: OnFailureScope.(failure: Throwable) -> Unit = configuration.onFailure, noinline onComplete: OnCompleteScope.() -> Unit = configuration.onComplete, - block: ParameterizeScope.() -> Unit + crossinline block: suspend ParameterizeScope.() -> Unit ) { contract { callsInPlace(onComplete, InvocationKind.EXACTLY_ONCE) @@ -206,7 +210,7 @@ public class ParameterizeScope internal constructor( * ``` */ @Suppress("UnusedReceiverParameter") // Should only be accessible within parameterize scopes -public fun ParameterizeScope.parameter(arguments: Sequence): ParameterizeScope.Parameter = +public suspend fun ParameterizeScope.parameter(arguments: Sequence): ParameterizeScope.Parameter = @OptIn(ExperimentalParameterizeApi::class) ParameterizeScope.Parameter(arguments) @@ -217,7 +221,7 @@ public fun ParameterizeScope.parameter(arguments: Sequence): Parameterize * val letter by parameter('a'..'z') * ``` */ -public fun ParameterizeScope.parameter(arguments: Iterable): ParameterizeScope.Parameter = +public suspend fun ParameterizeScope.parameter(arguments: Iterable): ParameterizeScope.Parameter = parameter(arguments.asSequence()) /** @@ -227,7 +231,7 @@ public fun ParameterizeScope.parameter(arguments: Iterable): Parameterize * val primeUnder20 by parameterOf(2, 3, 5, 7, 11, 13, 17, 19) * ``` */ -public fun ParameterizeScope.parameterOf(vararg arguments: T): ParameterizeScope.Parameter = +public suspend fun ParameterizeScope.parameterOf(vararg arguments: T): ParameterizeScope.Parameter = parameter(arguments.asSequence()) /** @@ -253,7 +257,7 @@ public fun ParameterizeScope.parameterOf(vararg arguments: T): ParameterizeS @OptIn(ExperimentalTypeInference::class) @OverloadResolutionByLambdaReturnType @JvmName("parameterLazySequence") -public inline fun ParameterizeScope.parameter( +public suspend inline fun ParameterizeScope.parameter( crossinline lazyArguments: LazyParameterScope.() -> Sequence ): ParameterizeScope.Parameter = parameter(object : Sequence { @@ -294,7 +298,7 @@ public inline fun ParameterizeScope.parameter( @OptIn(ExperimentalTypeInference::class) @OverloadResolutionByLambdaReturnType @JvmName("parameterLazyIterable") -public inline fun ParameterizeScope.parameter( +public suspend inline fun ParameterizeScope.parameter( crossinline lazyArguments: LazyParameterScope.() -> Iterable ): ParameterizeScope.Parameter = parameter { diff --git a/src/commonMain/kotlin/ParameterizeConfiguration.kt b/src/commonMain/kotlin/ParameterizeConfiguration.kt index 6b0c0d5..bd7ad5c 100644 --- a/src/commonMain/kotlin/ParameterizeConfiguration.kt +++ b/src/commonMain/kotlin/ParameterizeConfiguration.kt @@ -136,7 +136,6 @@ public class ParameterizeConfiguration internal constructor( } /** @see Builder.decorator */ - @RestrictsSuspension public class DecoratorScope internal constructor( private val parameterizeState: ParameterizeState ) { diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec.kt index 4e9a04e..66cb5fe 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec.kt @@ -60,7 +60,7 @@ class ParameterizeConfigurationSpec { val property: KProperty1, val builderProperty: KMutableProperty1, val distinctValue: T, - val parameterizeWithConfigurationAndOptionPassed: ( + val parameterizeWithConfigurationAndOptionPassed: suspend ( configuration: ParameterizeConfiguration, block: ParameterizeScope.() -> Unit ) -> Unit ) { @@ -144,7 +144,7 @@ class ParameterizeConfigurationSpec { * straightforward. */ @Test - fun options_should_be_executed_in_the_correct_order() { + fun options_should_be_executed_in_the_correct_order() = runTestCC { val order = mutableListOf() // Non-builder constructor so all options must be specified @@ -175,10 +175,10 @@ class ParameterizeConfigurationSpec { } private fun interface ConfiguredParameterize { - fun configuredParameterize(configure: Builder.() -> Unit, block: ParameterizeScope.() -> Unit) + suspend fun configuredParameterize(configure: Builder.() -> Unit, block: ParameterizeScope.() -> Unit) } - private fun testConfiguredParameterize(test: ConfiguredParameterize.() -> Unit) = testAll( + private fun testConfiguredParameterize(test: suspend ConfiguredParameterize.() -> Unit) = testAll( "configuration-only overload" to { test { configure, block -> val configuration = ParameterizeConfiguration { configure() } @@ -203,7 +203,7 @@ class ParameterizeConfigurationSpec { // not possible to resolve to it without passing at least one of the options. So instead, add multiple cases // such that each option will eventually have a case where its default argument is used. // (It is possible to achieve with reflection using `KFunction.callBy()`, but only on the JVM at the moment) - *configurationOptions.map<_, Pair Unit>> { (_, option) -> + *configurationOptions.map<_, Pair Unit>> { (_, option) -> "options overload with default arguments taken from the `configuration` (except `$option`)" to { test { configure, block -> val configuration = ParameterizeConfiguration { configure() } @@ -211,11 +211,11 @@ class ParameterizeConfigurationSpec { } } - }.toTypedArray Unit>>() + }.toTypedArray Unit>>() ) private fun interface ParameterizeWithOptionDefault { - fun parameterizeWithOptionDefault(block: ParameterizeScope.() -> Unit) + suspend fun parameterizeWithOptionDefault(block: suspend ParameterizeScope.() -> Unit) } /** @@ -233,11 +233,11 @@ class ParameterizeConfigurationSpec { */ private fun testParameterizeWithOptionDefault( configure: Builder.() -> Unit, - parameterizeWithDifferentOptionPassed: ( + parameterizeWithDifferentOptionPassed: suspend ( configuration: ParameterizeConfiguration, - block: ParameterizeScope.() -> Unit + block: suspend ParameterizeScope.() -> Unit ) -> Unit, - test: ParameterizeWithOptionDefault.() -> Unit + test: suspend ParameterizeWithOptionDefault.() -> Unit ) = testAll( "with default from builder" to { val configuration = ParameterizeConfiguration { configure() } @@ -342,7 +342,7 @@ class ParameterizeConfigurationSpec { } @Test - fun on_complete_should_be_marked_as_being_called_in_place_exactly_once() { + fun on_complete_should_be_marked_as_being_called_in_place_exactly_once() = runTestCC { // Must be assigned exactly once val lateAssignedValue: Any diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt index f1b4a10..ef56610 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt @@ -25,14 +25,14 @@ import kotlin.test.* @Suppress("ClassName") class ParameterizeConfigurationSpec_decorator { - private inline fun testParameterize( + private suspend inline fun testParameterize( noinline decorator: suspend DecoratorScope.(iteration: suspend DecoratorScope.() -> Unit) -> Unit, noinline onFailure: OnFailureScope.(failure: Throwable) -> Unit = { recordFailure = true breakEarly = true }, noinline onComplete: OnCompleteScope.() -> Unit = ParameterizeConfiguration.default.onComplete, - block: ParameterizeScope.() -> Unit + crossinline block: suspend ParameterizeScope.() -> Unit ): Unit = parameterize( decorator = decorator, @@ -42,7 +42,7 @@ class ParameterizeConfigurationSpec_decorator { ) @Test - fun should_be_invoked_once_per_iteration() { + fun should_be_invoked_once_per_iteration() = runTestCC { var iterationCount = 0 var timesInvoked = 0 @@ -148,7 +148,7 @@ class ParameterizeConfigurationSpec_decorator { } @Test - fun should_throw_if_iteration_function_is_not_invoked() { + fun should_throw_if_iteration_function_is_not_invoked() = runTestCC { val exception = assertFailsWith { testParameterize( decorator = { @@ -165,7 +165,7 @@ class ParameterizeConfigurationSpec_decorator { } @Test - fun should_throw_if_iteration_function_is_invoked_more_than_once() { + fun should_throw_if_iteration_function_is_invoked_more_than_once() = runTestCC { val exception = assertFailsWith { testParameterize( decorator = { iteration -> @@ -259,9 +259,9 @@ class ParameterizeConfigurationSpec_decorator { } @Test - fun declaring_parameter_after_iteration_function_should_fail() { + fun declaring_parameter_after_iteration_function_should_fail() = runTestCC { assertFailsWith { - lateinit var declareParameter: () -> Unit + lateinit var declareParameter: suspend () -> Unit testParameterize( decorator = { iteration -> diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt index 3597d54..dae4af9 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt @@ -22,10 +22,10 @@ import kotlin.test.* @Suppress("ClassName") class ParameterizeConfigurationSpec_onComplete { - private inline fun testParameterize( + private suspend inline fun testParameterize( noinline onFailure: OnFailureScope.(failure: Throwable) -> Unit = {}, // Continue on failure noinline onComplete: OnCompleteScope.() -> Unit, - block: ParameterizeScope.() -> Unit + crossinline block: suspend ParameterizeScope.() -> Unit ): Unit = parameterize( onFailure = onFailure, @@ -34,7 +34,7 @@ class ParameterizeConfigurationSpec_onComplete { ) @Test - fun should_be_invoked_once_after_all_iterations() { + fun should_be_invoked_once_after_all_iterations() = runTestCC { var timesInvoked = 0 var invokedBeforeLastIteration = false @@ -59,7 +59,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun should_be_invoked_once_after_all_iterations_with_break() { + fun should_be_invoked_once_after_all_iterations_with_break() = runTestCC { var timesInvoked = 0 var invokedBeforeLastIteration = false var failureCount = 0 @@ -89,7 +89,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun failures_within_on_complete_should_propagate_out_uncaught() { + fun failures_within_on_complete_should_propagate_out_uncaught() = runTestCC { class FailureWithinOnComplete : Throwable() assertFailsWith { @@ -103,7 +103,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun iteration_count_should_be_correct() { + fun iteration_count_should_be_correct() = runTestCC { var expectedIterationCount = 0L testParameterize( @@ -118,7 +118,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun iteration_count_should_be_correct_with_break() { + fun iteration_count_should_be_correct_with_break() = runTestCC { var expectedIterationCount = 0L testParameterize( @@ -140,7 +140,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun iteration_count_should_be_correct_with_skips() { + fun iteration_count_should_be_correct_with_skips() = runTestCC { var expectedIterationCount = 0L testParameterize( @@ -159,7 +159,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun skip_count_should_be_correct() { + fun skip_count_should_be_correct() = runTestCC { var expectedSkipCount = 0L testParameterize( @@ -177,7 +177,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun skip_count_should_be_correct_with_break() { + fun skip_count_should_be_correct_with_break() = runTestCC { var expectedSkipCount = 0L testParameterize( @@ -202,7 +202,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun success_count_should_be_correct_with_skips_and_failures() { + fun success_count_should_be_correct_with_skips_and_failures() = runTestCC { var expectedSuccessCount = 0L testParameterize( @@ -225,7 +225,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun failure_count_should_be_correct() { + fun failure_count_should_be_correct() = runTestCC { var expectedFailureCount = 0L testParameterize( @@ -243,7 +243,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun completed_early_without_breaking_should_be_false() { + fun completed_early_without_breaking_should_be_false() = runTestCC { testParameterize( onComplete = { assertFalse(completedEarly) @@ -254,7 +254,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun completed_early_with_break_on_last_iteration_should_be_false() { + fun completed_early_with_break_on_last_iteration_should_be_false() = runTestCC { testParameterize( onFailure = { breakEarly = true @@ -273,7 +273,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun completed_early_with_break_before_last_iteration_should_be_true() { + fun completed_early_with_break_before_last_iteration_should_be_true() = runTestCC { testParameterize( onFailure = { breakEarly = true @@ -292,7 +292,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun recorded_failures_should_be_correct() { + fun recorded_failures_should_be_correct() = runTestCC { val expectedRecordedFailures = mutableListOf>>>() var lastIteration = -1 diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onFailure.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onFailure.kt index 8c75694..f8d26d1 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onFailure.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onFailure.kt @@ -21,9 +21,9 @@ import kotlin.test.* @Suppress("ClassName") class ParameterizeConfigurationSpec_onFailure { - private inline fun testParameterize( + private suspend inline fun testParameterize( noinline onFailure: OnFailureScope.(failure: Throwable) -> Unit, - block: ParameterizeScope.() -> Unit + crossinline block: suspend ParameterizeScope.() -> Unit ): Unit = parameterize( onFailure = onFailure, @@ -32,7 +32,7 @@ class ParameterizeConfigurationSpec_onFailure { ) @Test - fun should_be_invoked_once_per_failure() { + fun should_be_invoked_once_per_failure() = runTestCC { val failureIterations = listOf(1, 3, 4, 7, 9, 10) var currentIteration = -1 @@ -55,7 +55,7 @@ class ParameterizeConfigurationSpec_onFailure { } @Test - fun should_be_invoked_with_the_failure() { + fun should_be_invoked_with_the_failure() = runTestCC { val failures = List(10) { Throwable(it.toString()) } val invokedWithFailures = mutableListOf() @@ -74,7 +74,7 @@ class ParameterizeConfigurationSpec_onFailure { } @Test - fun should_not_continue_if_should_break_is_true() { + fun should_not_continue_if_should_break_is_true() = runTestCC { val failureIterations = listOf(1, 3, 4, 7) val breakIteration = failureIterations.last() @@ -97,7 +97,7 @@ class ParameterizeConfigurationSpec_onFailure { } @Test - fun failures_within_on_failure_should_propagate_out_uncaught() { + fun failures_within_on_failure_should_propagate_out_uncaught() = runTestCC { class FailureWithinOnFailure : Throwable() assertFailsWith { @@ -112,7 +112,7 @@ class ParameterizeConfigurationSpec_onFailure { } @Test - fun iteration_count_should_be_correct() { + fun iteration_count_should_be_correct() = runTestCC { var expectedIterationCount = 0L testParameterize( @@ -128,7 +128,7 @@ class ParameterizeConfigurationSpec_onFailure { } @Test - fun failure_count_should_be_correct() { + fun failure_count_should_be_correct() = runTestCC { var expectedFailureCount = 0L testParameterize( @@ -146,7 +146,7 @@ class ParameterizeConfigurationSpec_onFailure { } @Test - fun failure_arguments_should_be_those_from_the_last_iteration() { + fun failure_arguments_should_be_those_from_the_last_iteration() = runTestCC { val lastParameterArguments = mutableListOf>() testParameterize( @@ -177,68 +177,74 @@ class ParameterizeConfigurationSpec_onFailure { } @Test - fun failure_arguments_should_only_include_used_parameters() = testParameterize( - onFailure = { - val actualUsedParameters = arguments.map { it.parameter.name } - assertEquals(listOf("used1", "used2"), actualUsedParameters) - } - ) { - val used1 by parameterOf(Unit) - val unused1 by parameterOf(Unit) - val used2 by parameterOf(Unit) - val unused2 by parameterOf(Unit) + fun failure_arguments_should_only_include_used_parameters() = runTestCC { + testParameterize( + onFailure = { + val actualUsedParameters = arguments.map { it.parameter.name } + assertEquals(listOf("used1", "used2"), actualUsedParameters) + } + ) { + val used1 by parameterOf(Unit) + val unused1 by parameterOf(Unit) + val used2 by parameterOf(Unit) + val unused2 by parameterOf(Unit) - useParameter(used1) - useParameter(used2) + useParameter(used1) + useParameter(used2) - fail() + fail() + } } @Test - fun failure_arguments_should_include_lazily_used_parameters_that_were_unused_this_iteration() = testParameterize( - onFailure = { - val actualUsedParameters = arguments.map { it.parameter.name } - assertContains(actualUsedParameters, "letter") - } - ) { - val letter by parameterOf('a', 'b') + fun failure_arguments_should_include_lazily_used_parameters_that_were_unused_this_iteration() = runTestCC { + testParameterize( + onFailure = { + val actualUsedParameters = arguments.map { it.parameter.name } + assertContains(actualUsedParameters, "letter") + } + ) { + val letter by parameterOf('a', 'b') - var letterUsedThisIteration = false + var letterUsedThisIteration = false - val letterNumber by parameter { - letterUsedThisIteration = true - (1..2).map { "$letter$it" } - } + val letterNumber by parameter { + letterUsedThisIteration = true + (1..2).map { "$letter$it" } + } - // Letter contributes to the failure, even though it wasn't used this iteration - if (letterNumber == "b2") { - check(!letterUsedThisIteration) { "Letter was actually used this iteration, so test is invalid" } - fail() + // Letter contributes to the failure, even though it wasn't used this iteration + if (letterNumber == "b2") { + check(!letterUsedThisIteration) { "Letter was actually used this iteration, so test is invalid" } + fail() + } } } @Test - fun failure_arguments_should_not_include_captured_parameters_from_previous_iterations() = testParameterize( - onFailure = { - val parameters = arguments.map { it.parameter.name } + fun failure_arguments_should_not_include_captured_parameters_from_previous_iterations() = runTestCC { + testParameterize( + onFailure = { + val parameters = arguments.map { it.parameter.name } - assertFalse( - "neverUsedDuringTheCurrentIteration" in parameters, - "neverUsedDuringTheCurrentIteration in $parameters" - ) - } - ) { - val neverUsedDuringTheCurrentIteration by parameterOf(Unit) + assertFalse( + "neverUsedDuringTheCurrentIteration" in parameters, + "neverUsedDuringTheCurrentIteration in $parameters" + ) + } + ) { + val neverUsedDuringTheCurrentIteration by parameterOf(Unit) - @Suppress("UNUSED_EXPRESSION") - val usePreviousIterationParameter by parameterOf( - { }, // Don't use it the first iteration - { neverUsedDuringTheCurrentIteration } - ) + @Suppress("UNUSED_EXPRESSION") + val usePreviousIterationParameter by parameterOf( + { }, // Don't use it the first iteration + { neverUsedDuringTheCurrentIteration } + ) - // On the 2nd iteration, use the parameter captured from the 1st iteration - usePreviousIterationParameter() + // On the 2nd iteration, use the parameter captured from the 1st iteration + usePreviousIterationParameter() - fail() + fail() + } } } diff --git a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt index a90e5d4..49ca53b 100644 --- a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt +++ b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt @@ -24,7 +24,7 @@ class ParameterizeExceptionSpec { * its state and parameter tracking are invalid. */ @Test - fun should_cause_parameterize_to_immediately_fail_without_or_triggering_handlers() { + fun should_cause_parameterize_to_immediately_fail_without_or_triggering_handlers() = runTestCC { lateinit var exception: ParameterizeException val actualException = assertFailsWith { @@ -45,7 +45,7 @@ class ParameterizeExceptionSpec { * fail, as the *inner* [parameterize] being invalid does not make the *outer* one invalid. */ @Test - fun when_thrown_from_a_different_parameterize_call_it_should_be_handled_like_any_other_failure() { + fun when_thrown_from_a_different_parameterize_call_it_should_be_handled_like_any_other_failure() = runTestCC { lateinit var exceptionFromDifferentParameterize: ParameterizeException var onFailureInvoked = false @@ -83,7 +83,7 @@ class ParameterizeExceptionSpec { } @Test - fun parameter_disappears_on_second_iteration_due_to_external_condition() { + fun parameter_disappears_on_second_iteration_due_to_external_condition() = runTestCC { val exception = assertFailsWith { var shouldDeclareA = true @@ -102,7 +102,7 @@ class ParameterizeExceptionSpec { } @Test - fun parameter_appears_on_second_iteration_due_to_external_condition() { + fun parameter_appears_on_second_iteration_due_to_external_condition() = runTestCC { val exception = assertFailsWith { var shouldDeclareA = false @@ -119,9 +119,10 @@ class ParameterizeExceptionSpec { assertEquals("Expected to be declaring `b`, but got `a`", exception.message) } +/* @Test - fun nested_parameter_declaration_within_arguments_iterator_function() { + fun nested_parameter_declaration_within_arguments_iterator_function() = runTestCC { fun ParameterizeScope.testArguments() = object : Sequence { override fun iterator(): Iterator { val inner by parameterOf(Unit) @@ -145,7 +146,7 @@ class ParameterizeExceptionSpec { } @Test - fun nested_parameter_declaration_within_arguments_iterator_next_function() { + fun nested_parameter_declaration_within_arguments_iterator_next_function() = runTestCC { fun ParameterizeScope.testArgumentsIterator() = object : Iterator { private var index = 0 @@ -176,7 +177,7 @@ class ParameterizeExceptionSpec { } @Test - fun nested_parameter_declaration_with_another_valid_intermediate_parameter_usage() { + fun nested_parameter_declaration_with_another_valid_intermediate_parameter_usage() = runTestCC { val exception = assertFailsWith { parameterize { val trackedNestingInterference by parameterOf(Unit) @@ -200,7 +201,7 @@ class ParameterizeExceptionSpec { } @Test - fun declaring_parameter_after_iteration_completed() { + fun declaring_parameter_after_iteration_completed() = runTestCC { var declareParameter = {} parameterize { @@ -215,9 +216,10 @@ class ParameterizeExceptionSpec { assertEquals("Cannot declare parameter `parameter` after its iteration has completed", failure.message) } +*/ @Test - fun failing_earlier_than_the_previous_iteration() { + fun failing_earlier_than_the_previous_iteration() = runTestCC { val nondeterministicFailure = Throwable("Unexpected failure") val failure = assertFailsWith { diff --git a/src/commonTest/kotlin/ParameterizeScopeSpec.kt b/src/commonTest/kotlin/ParameterizeScopeSpec.kt index c2bb472..eb89f1e 100644 --- a/src/commonTest/kotlin/ParameterizeScopeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeScopeSpec.kt @@ -36,33 +36,39 @@ class ParameterizeScopeSpec { } @Test - fun parameter_from_sequence_should_be_constructed_with_the_same_arguments_instance() = parameterize { - val sequence = sequenceOf() - val parameter = parameter(sequence) + fun parameter_from_sequence_should_be_constructed_with_the_same_arguments_instance() = runTestCC { + parameterize { + val sequence = sequenceOf() + val parameter = parameter(sequence) - assertSame(sequence, parameter.arguments) + assertSame(sequence, parameter.arguments) + } } @Test - fun parameter_from_iterable_should_have_the_correct_arguments() = parameterize { - val parameter = parameter(Iterable { ArgumentIterator }) + fun parameter_from_iterable_should_have_the_correct_arguments() = runTestCC { + parameterize { + val parameter = parameter(Iterable { ArgumentIterator }) - assertSame(ArgumentIterator, parameter.arguments.iterator()) + assertSame(ArgumentIterator, parameter.arguments.iterator()) + } } @Test - fun parameter_of_listed_arguments_should_have_the_correct_arguments() = parameterize { - data class UniqueArgument(val argument: String) + fun parameter_of_listed_arguments_should_have_the_correct_arguments() = runTestCC { + parameterize { + data class UniqueArgument(val argument: String) - val listedArguments = listOf( - UniqueArgument("A"), - UniqueArgument("B"), - UniqueArgument("C") - ) + val listedArguments = listOf( + UniqueArgument("A"), + UniqueArgument("B"), + UniqueArgument("C") + ) - val parameter = parameterOf(*listedArguments.toTypedArray()) + val parameter = parameterOf(*listedArguments.toTypedArray()) - assertContentEquals(listedArguments.asSequence(), parameter.arguments) + assertContentEquals(listedArguments.asSequence(), parameter.arguments) + } } /** @@ -70,14 +76,14 @@ class ParameterizeScopeSpec { * use to specify for all the lazy overloads parametrically. */ private interface LazyParameterFunction { - operator fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): Parameter + suspend operator fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): Parameter class LazyArguments(val createIterator: () -> Iterator) } private val lazyParameterFunctions = listOf( "from sequence" to object : LazyParameterFunction { - override fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): Parameter = + override suspend fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): Parameter = with(scope) { parameter { val arguments = lazyArguments() @@ -86,7 +92,7 @@ class ParameterizeScopeSpec { } }, "from iterable" to object : LazyParameterFunction { - override fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): Parameter = + override suspend fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): Parameter = with(scope) { parameter { val arguments = lazyArguments() @@ -97,69 +103,78 @@ class ParameterizeScopeSpec { ) @Test - fun parameter_from_lazy_arguments_should_have_the_correct_arguments() = parameterize { - testAll(lazyParameterFunctions) { lazyParameterFunction -> - val lazyParameter = lazyParameterFunction(this@parameterize) { - LazyArguments { ArgumentIterator } - } + fun parameter_from_lazy_arguments_should_have_the_correct_arguments() = runTestCC { + parameterize { + testAll(lazyParameterFunctions) { lazyParameterFunction -> + val lazyParameter = lazyParameterFunction(this@parameterize) { + LazyArguments { ArgumentIterator } + } - assertSame(ArgumentIterator, lazyParameter.arguments.iterator()) + assertSame(ArgumentIterator, lazyParameter.arguments.iterator()) + } } } @Test - fun parameter_from_lazy_arguments_should_not_be_computed_before_declaring() = parameterize { - testAll(lazyParameterFunctions) { lazyParameterFunction -> - /*val undeclared by*/ lazyParameterFunction(this@parameterize) { fail("computed") } + fun parameter_from_lazy_arguments_should_not_be_computed_before_declaring() = runTestCC { + parameterize { + testAll(lazyParameterFunctions) { lazyParameterFunction -> + /*val undeclared by*/ lazyParameterFunction(this@parameterize) { fail("computed") } + } } } @Test - fun parameter_from_lazy_argument_iterable_should_only_be_computed_once() = parameterize { - testAll(lazyParameterFunctions) { lazyParameterFunction -> - var evaluationCount = 0 + fun parameter_from_lazy_argument_iterable_should_only_be_computed_once() = runTestCC { + parameterize { + testAll(lazyParameterFunctions) { lazyParameterFunction -> + var evaluationCount = 0 - val lazyParameter = lazyParameterFunction(this@parameterize) { - evaluationCount++ - LazyArguments { (1..10).iterator() } - } + val lazyParameter = lazyParameterFunction(this@parameterize) { + evaluationCount++ + LazyArguments { (1..10).iterator() } + } - repeat(5) { i -> - val arguments = lazyParameter.arguments.toList() - assertEquals((1..10).toList(), arguments, "Iteration #$i") - } + repeat(5) { i -> + val arguments = lazyParameter.arguments.toList() + assertEquals((1..10).toList(), arguments, "Iteration #$i") + } - assertEquals(1, evaluationCount) + assertEquals(1, evaluationCount) + } } } @Test - fun string_representation_should_show_used_parameter_arguments_in_declaration_order() = parameterize { - val a by parameterOf(1) - val unused1 by parameterOf(Unit) - val b by parameterOf(2) - val unused2 by parameterOf(Unit) - val c by parameterOf(3) - - // Used in a different order - useParameter(c) - useParameter(b) - useParameter(a) - - assertEquals("${ParameterizeScope::class.simpleName}(a = $a, b = $b, c = $c)", this.toString()) + fun string_representation_should_show_used_parameter_arguments_in_declaration_order() = runTestCC { + parameterize { + val a by parameterOf(1) + val unused1 by parameterOf(Unit) + val b by parameterOf(2) + val unused2 by parameterOf(Unit) + val c by parameterOf(3) + + // Used in a different order + useParameter(c) + useParameter(b) + useParameter(a) + + assertEquals("${ParameterizeScope::class.simpleName}(a = $a, b = $b, c = $c)", this.toString()) + } } @Test - fun parameter_delegate_string_representation_when_declared_should_equal_that_of_the_current_argument() = + fun parameter_delegate_string_representation_when_declared_should_equal_that_of_the_current_argument() = runTestCC { parameterize { lateinit var delegate: ParameterDelegate - + val argument = parameterOf("argument") val parameter by PropertyDelegateProvider { thisRef: Nothing?, property -> - parameterOf("argument") + argument .provideDelegate(thisRef, property) .also { delegate = it } // intercept delegate } assertSame(parameter, delegate.toString()) } + } } diff --git a/src/commonTest/kotlin/ParameterizeSpec.kt b/src/commonTest/kotlin/ParameterizeSpec.kt index 83e401d..4152955 100644 --- a/src/commonTest/kotlin/ParameterizeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeSpec.kt @@ -32,8 +32,8 @@ class ParameterizeSpec { */ private fun testParameterize( expectedIterations: Iterable, - block: ParameterizeScope.() -> T - ) { + block: suspend ParameterizeScope.() -> T + ) = runTestCC { val iterations = mutableListOf() parameterize { @@ -57,19 +57,21 @@ class ParameterizeSpec { } @Test - fun parameter_arguments_iterator_should_be_computed_when_declared() = parameterize { - var computed = false + fun parameter_arguments_iterator_should_be_computed_when_declared() = runTestCC { + parameterize { + var computed = false - val parameter by parameter(Sequence { - computed = true - listOf(Unit).iterator() - }) + val parameter by parameter(Sequence { + computed = true + listOf(Unit).iterator() + }) - assertTrue(computed, "computed") + assertTrue(computed, "computed") + } } @Test - fun second_parameter_argument_should_not_be_computed_until_the_next_iteration() { + fun second_parameter_argument_should_not_be_computed_until_the_next_iteration() = runTestCC { var finishedFirstIteration = false class AssertingIterator : Iterator { @@ -94,7 +96,7 @@ class ParameterizeSpec { } @Test - fun parameter_should_iterate_to_the_next_argument_while_declaring() { + fun parameter_should_iterate_to_the_next_argument_while_declaring() = runTestCC { var state: String parameterize { @@ -232,7 +234,7 @@ class ParameterizeSpec { fun custom_lazy_arguments_implementation() = testParameterize( listOf("a1", "a2", "a3", "b1", "b2", "b3", "c1", "c2", "c3") ) { - fun ParameterizeScope.customLazyParameter( + suspend fun ParameterizeScope.customLazyParameter( lazyArguments: () -> Iterable ): ParameterizeScope.Parameter { val arguments by lazy(lazyArguments) @@ -255,7 +257,7 @@ class ParameterizeSpec { } @Test - fun captured_parameters_should_be_usable_after_the_iteration_completes() { + fun captured_parameters_should_be_usable_after_the_iteration_completes() = runTestCC { val capturedParameters = mutableListOf<() -> Int>() parameterize { @@ -271,17 +273,17 @@ class ParameterizeSpec { } } - @Test - fun should_be_able_to_return_from_an_outer_function_from_within_the_block() { +/* @Test + fun should_be_able_to_return_from_an_outer_function_from_within_the_block() = runTestCC { parameterize { return@should_be_able_to_return_from_an_outer_function_from_within_the_block } - } + }*/ /** * The motivating use case here is decorating a Kotest test group, in which the test declarations suspend. */ - @Test +/* @Test fun should_be_able_to_decorate_a_suspend_block() { val coordinates = sequence { parameterize { @@ -296,5 +298,5 @@ class ParameterizeSpec { listOf("a1", "a2", "a3", "b1", "b2", "b3", "c1", "c2", "c3"), coordinates.toList() ) - } + }*/ } diff --git a/src/commonTest/kotlin/TestUtils.kt b/src/commonTest/kotlin/TestUtils.kt index c4677a8..2feae1f 100644 --- a/src/commonTest/kotlin/TestUtils.kt +++ b/src/commonTest/kotlin/TestUtils.kt @@ -16,7 +16,14 @@ package com.benwoodworth.parameterize +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import runCC +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext import kotlin.test.Ignore +import kotlin.time.Duration /** * [Ignore] on native targets. @@ -56,11 +63,11 @@ object TestAllScope { fun testAll( testCases: Iterable>, - test: TestAllScope.(testCase: T) -> Unit + test: suspend TestAllScope.(testCase: T) -> Unit ) { val results = testCases .map { (description, testCase) -> - description to runCatching { TestAllScope.test(testCase) } + description to runCatching { runTestCC { TestAllScope.test(testCase) } } } val passed = results.count { (_, result) -> result.isSuccess } @@ -100,11 +107,21 @@ fun testAll( fun testAll( vararg testCases: Pair, - test: TestAllScope.(testCase: T) -> Unit + test: suspend TestAllScope.(testCase: T) -> Unit ): Unit = testAll(testCases.toList(), test) -fun testAll(vararg testCases: Pair Unit>): Unit = +fun testAll(vararg testCases: Pair Unit>): Unit = testAll(testCases.toList()) { testCase -> testCase() } + +inline fun runTestCC( + context: CoroutineContext = EmptyCoroutineContext, + timeout: Duration? = null, + crossinline testBody: suspend TestScope.() -> Unit +): TestResult = if (timeout == null) runTest(context) { + runCC { testBody() } +} else runTest(context, timeout) { + runCC { testBody() } +} \ No newline at end of file diff --git a/src/commonTest/kotlin/test/EdgeCases.kt b/src/commonTest/kotlin/test/EdgeCases.kt index 7f3e24f..7767884 100644 --- a/src/commonTest/kotlin/test/EdgeCases.kt +++ b/src/commonTest/kotlin/test/EdgeCases.kt @@ -22,7 +22,7 @@ import com.benwoodworth.parameterize.ParameterizeState import com.benwoodworth.parameterize.parameterize internal object EdgeCases { - val iterationFailures = listOf Throwable>>( + val iterationFailures = listOf Throwable>>( "ParameterizeContinue" to { ParameterizeContinue }, From 67d76396fb0cff85f7d9e67f81c28fba06d00871 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Fri, 20 Sep 2024 19:55:01 +0100 Subject: [PATCH 02/18] Remove ParameterizeScope.iterationCompleted and ignore its tests --- src/commonMain/kotlin/Parameterize.kt | 10 +--------- src/commonMain/kotlin/ParameterizeIterator.kt | 2 -- .../kotlin/ParameterizeConfigurationSpec_decorator.kt | 1 + .../kotlin/ParameterizeConfigurationSpec_onFailure.kt | 1 + 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/commonMain/kotlin/Parameterize.kt b/src/commonMain/kotlin/Parameterize.kt index 2a5cab1..d1cb4b8 100644 --- a/src/commonMain/kotlin/Parameterize.kt +++ b/src/commonMain/kotlin/Parameterize.kt @@ -20,10 +20,7 @@ package com.benwoodworth.parameterize import com.benwoodworth.parameterize.ParameterizeConfiguration.* -import effekt.Handler -import effekt.HandlerPrompt import effekt.handle -import effekt.use import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.experimental.ExperimentalTypeInference @@ -142,7 +139,6 @@ public suspend inline fun parameterize( public class ParameterizeScope internal constructor( internal val parameterizeState: ParameterizeState, ) { - internal var iterationCompleted: Boolean = false /** @suppress */ override fun toString(): String = @@ -156,17 +152,13 @@ public class ParameterizeScope internal constructor( /** @suppress */ public operator fun Parameter.provideDelegate(thisRef: Any?, property: KProperty<*>): ParameterDelegate { - parameterizeState.checkState(!iterationCompleted) { - "Cannot declare parameter `${property.name}` after its iteration has completed" - } - @Suppress("UNCHECKED_CAST") return parameterizeState.declareParameter(property as KProperty, arguments) } /** @suppress */ public operator fun ParameterDelegate.getValue(thisRef: Any?, property: KProperty<*>): T { - if (!iterationCompleted) parameterState.useArgument() + parameterState.useArgument() return argument } diff --git a/src/commonMain/kotlin/ParameterizeIterator.kt b/src/commonMain/kotlin/ParameterizeIterator.kt index 8936e7a..4c9d056 100644 --- a/src/commonMain/kotlin/ParameterizeIterator.kt +++ b/src/commonMain/kotlin/ParameterizeIterator.kt @@ -76,10 +76,8 @@ internal class ParameterizeIterator( } private fun afterEach() { - val currentIterationScope = checkNotNull(currentIterationScope) { "${::currentIterationScope.name} was null" } val decoratorCoroutine = checkNotNull(decoratorCoroutine) { "${::decoratorCoroutine.name} was null" } - currentIterationScope.iterationCompleted = true decoratorCoroutine.afterIteration() this.currentIterationScope = null diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt index ef56610..d526bb5 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt @@ -258,6 +258,7 @@ class ParameterizeConfigurationSpec_decorator { ) } + @Ignore @Test fun declaring_parameter_after_iteration_function_should_fail() = runTestCC { assertFailsWith { diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onFailure.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onFailure.kt index f8d26d1..e73939c 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onFailure.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onFailure.kt @@ -221,6 +221,7 @@ class ParameterizeConfigurationSpec_onFailure { } } + @Ignore @Test fun failure_arguments_should_not_include_captured_parameters_from_previous_iterations() = runTestCC { testParameterize( From c171a830aba5ef292ebc9432c6ae8d02bf2562f7 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Fri, 20 Sep 2024 20:17:06 +0100 Subject: [PATCH 03/18] Remove ParameterState.arguments and ignore its tests --- src/commonMain/kotlin/ParameterState.kt | 11 ++--------- src/commonTest/kotlin/ParameterStateSpec.kt | 2 ++ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/commonMain/kotlin/ParameterState.kt b/src/commonMain/kotlin/ParameterState.kt index d277df5..c835072 100644 --- a/src/commonMain/kotlin/ParameterState.kt +++ b/src/commonMain/kotlin/ParameterState.kt @@ -32,7 +32,7 @@ import kotlin.reflect.KProperty * also validates that the parameter is in fact being used with the expected * property. * - * When first declared, the parameter [property] and the [arguments] it was + * When first declared, the parameter [property] it was * declared with will be stored, along with a new argument iterator and the * first argument from it. The arguments are lazily read in from the iterator as * they're needed, and will seamlessly continue with the start again after the @@ -53,7 +53,6 @@ internal class ParameterState( private val parameterizeState: ParameterizeState ) { private var property: KProperty<*>? = null - private var arguments: Sequence<*>? = null private var argument: Any? = null // T private var argumentIterator: Iterator<*>? = null @@ -62,7 +61,6 @@ internal class ParameterState( internal fun reset() { property = null - arguments = null argument = null argumentIterator = null hasBeenUsed = false @@ -113,7 +111,6 @@ internal class ParameterState( } this.property = property - this.arguments = arguments this.argument = iterator.next() this.argumentIterator = iterator.takeIf { it.hasNext() } } @@ -147,11 +144,7 @@ internal class ParameterState( * @throws IllegalStateException if the argument has not been declared yet. */ fun nextArgument() { - val arguments = checkNotNull(arguments) { - "Cannot iterate arguments before parameter has been declared" - } - - val iterator = argumentIterator ?: arguments.iterator() + val iterator = argumentIterator ?: error("Cannot iterate arguments before parameter has been declared") argument = iterator.next() argumentIterator = iterator.takeIf { it.hasNext() } diff --git a/src/commonTest/kotlin/ParameterStateSpec.kt b/src/commonTest/kotlin/ParameterStateSpec.kt index f170feb..4f93cc7 100644 --- a/src/commonTest/kotlin/ParameterStateSpec.kt +++ b/src/commonTest/kotlin/ParameterStateSpec.kt @@ -220,6 +220,7 @@ class ParameterStateSpec { assertTrue(parameter.isLastArgument) } + @Ignore @Test fun next_after_the_last_argument_should_loop_back_to_the_first() { parameter.declare(::property, sequenceOf("first", "second")) @@ -230,6 +231,7 @@ class ParameterStateSpec { assertEquals("first", parameter.getArgument(::property)) } + @Ignore @Test fun next_after_the_last_argument_should_set_is_last_argument_to_false() { parameter.declare(::property, sequenceOf("first", "second")) From 9b0059da74477fbb457120af43730fdf858f07ce Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sat, 21 Sep 2024 17:29:12 +0100 Subject: [PATCH 04/18] Remove ParameterState.property usages and ignore its tests --- src/commonMain/kotlin/ParameterState.kt | 39 ++++----- src/commonMain/kotlin/Parameterize.kt | 4 +- src/commonMain/kotlin/ParameterizeState.kt | 7 +- src/commonTest/kotlin/ParameterStateSpec.kt | 85 ++++++++++--------- .../kotlin/ParameterizeExceptionSpec.kt | 2 + 5 files changed, 67 insertions(+), 70 deletions(-) diff --git a/src/commonMain/kotlin/ParameterState.kt b/src/commonMain/kotlin/ParameterState.kt index c835072..e4cc012 100644 --- a/src/commonMain/kotlin/ParameterState.kt +++ b/src/commonMain/kotlin/ParameterState.kt @@ -49,17 +49,17 @@ import kotlin.reflect.KProperty * ignored since they're assumed to be the same, and the state remains unchanged * in favor of continuing through the current iterator where it left off. */ -internal class ParameterState( - private val parameterizeState: ParameterizeState -) { - private var property: KProperty<*>? = null +internal class ParameterState { + private var isDeclared: Boolean = false private var argument: Any? = null // T + var property: KProperty<*>? = null private var argumentIterator: Iterator<*>? = null var hasBeenUsed: Boolean = false private set internal fun reset() { + isDeclared = false property = null argument = null argumentIterator = null @@ -71,7 +71,7 @@ internal class ParameterState( */ val isLastArgument: Boolean get() { - checkNotNull(property) { "Parameter has not been declared" } + check(isDeclared) { "Parameter has not been declared" } return argumentIterator == null } @@ -80,37 +80,32 @@ internal class ParameterState( * Returns a string representation of the current argument, or a "not declared" message. */ override fun toString(): String = - if (property == null) { + if (!isDeclared) { "Parameter not declared yet." } else { argument.toString() } /** - * Set up the delegate for a parameter [property] with the given [arguments]. + * Set up the delegate with the given [arguments]. * - * If this delegate is already [declare]d, [property] and [arguments] should be equal to those that were originally passed in. - * The [property] will be checked to make sure it's the same, and the current argument will remain the same. + * If this delegate is already [declare]d, [arguments] should be equal to that that were originally passed in. + * The current argument will remain the same. * The new [arguments] will be ignored in favor of reusing the existing arguments, under the assumption that they're equal. * * @throws ParameterizeException if already declared for a different [property]. * @throws ParameterizeContinue if [arguments] is empty. */ - fun declare(property: KProperty, arguments: Sequence) { - // Nothing to do if already declared (besides validating the property) - this.property?.let { declaredProperty -> - parameterizeState.checkState(property.equalsProperty(declaredProperty)) { - "Expected to be declaring `${declaredProperty.name}`, but got `${property.name}`" - } - return - } + fun declare(arguments: Sequence<*>) { + // Nothing to do if already declared + if (isDeclared) return val iterator = arguments.iterator() if (!iterator.hasNext()) { throw ParameterizeContinue // Before changing any state } - this.property = property + isDeclared = true this.argument = iterator.next() this.argumentIterator = iterator.takeIf { it.hasNext() } } @@ -121,15 +116,11 @@ internal class ParameterState( * @throws ParameterizeException if already declared for a different [property]. * @throws IllegalStateException if the argument has not been declared yet. */ - fun getArgument(property: KProperty): T { - val declaredProperty = checkNotNull(this.property) { + fun getArgument(): T { + check(isDeclared) { "Cannot get argument before parameter has been declared" } - parameterizeState.checkState(property.equalsProperty(declaredProperty)) { - "Cannot use parameter delegate with `${property.name}`, since it was declared with `${declaredProperty.name}`." - } - @Suppress("UNCHECKED_CAST") // Argument is declared with property's arguments, so must be T return argument as T } diff --git a/src/commonMain/kotlin/Parameterize.kt b/src/commonMain/kotlin/Parameterize.kt index d1cb4b8..edae7de 100644 --- a/src/commonMain/kotlin/Parameterize.kt +++ b/src/commonMain/kotlin/Parameterize.kt @@ -153,7 +153,9 @@ public class ParameterizeScope internal constructor( /** @suppress */ public operator fun Parameter.provideDelegate(thisRef: Any?, property: KProperty<*>): ParameterDelegate { @Suppress("UNCHECKED_CAST") - return parameterizeState.declareParameter(property as KProperty, arguments) + return parameterizeState.declareParameter(property as KProperty, arguments).apply { + parameterState.property = property + } } /** @suppress */ diff --git a/src/commonMain/kotlin/ParameterizeState.kt b/src/commonMain/kotlin/ParameterizeState.kt index b7ab782..84faa37 100644 --- a/src/commonMain/kotlin/ParameterizeState.kt +++ b/src/commonMain/kotlin/ParameterizeState.kt @@ -79,11 +79,10 @@ internal class ParameterizeState { if (parameterToIterate == null) reset() } } else { - ParameterState(this) - .also { parameters += it } + ParameterState().also { parameters += it } } - parameter.declare(property, arguments) + parameter.declare(arguments) parameterCount++ // After declaring, since the parameter shouldn't count if declare throws if (parameter === parameterToIterate) { @@ -95,7 +94,7 @@ internal class ParameterizeState { lastParameterWithNextArgument = parameter } - return ParameterDelegate(parameter, parameter.getArgument(property)) + return ParameterDelegate(parameter, parameter.getArgument()) } private inline fun trackNestedDeclaration(property: KProperty<*>, block: () -> T): T { diff --git a/src/commonTest/kotlin/ParameterStateSpec.kt b/src/commonTest/kotlin/ParameterStateSpec.kt index 4f93cc7..f984873 100644 --- a/src/commonTest/kotlin/ParameterStateSpec.kt +++ b/src/commonTest/kotlin/ParameterStateSpec.kt @@ -30,13 +30,13 @@ class ParameterStateSpec { @BeforeTest fun beforeTest() { - parameter = ParameterState(ParameterizeState()) + parameter = ParameterState() } private fun assertUndeclared(parameter: ParameterState) { val failure = assertFailsWith { - parameter.getArgument(::property) + parameter.getArgument() } assertEquals(getArgumentBeforeDeclaredMessage, failure.message, "message") @@ -66,7 +66,7 @@ class ParameterStateSpec { fun string_representation_when_initialized_should_equal_that_of_the_current_argument() { val argument = "argument" - parameter.declare(::property, sequenceOf(argument)) + parameter.declare(sequenceOf(argument)) assertSame(argument, parameter.toString()) } @@ -79,14 +79,14 @@ class ParameterStateSpec { @Test fun declaring_with_no_arguments_should_throw_ParameterizeContinue() { assertFailsWith { - parameter.declare(::property, emptySequence()) + parameter.declare(emptySequence()) } } @Test fun declaring_with_no_arguments_should_leave_parameter_undeclared() { runCatching { - parameter.declare(::property, emptySequence()) + parameter.declare(emptySequence()) } assertUndeclared(parameter) @@ -101,7 +101,7 @@ class ParameterStateSpec { listOf(Unit).iterator() } - parameter.declare(::property, arguments) + parameter.declare(arguments) assertTrue(gotFirstArgument, "gotFirstArgument") } @@ -121,19 +121,19 @@ class ParameterStateSpec { } } - parameter.declare(::property, Sequence(::AssertingIterator)) + parameter.declare(Sequence(::AssertingIterator)) } @Test fun declare_with_one_argument_should_set_is_last_argument_to_true() { - parameter.declare(::property, sequenceOf("first")) + parameter.declare(sequenceOf("first")) assertTrue(parameter.isLastArgument) } @Test fun declare_with_more_than_one_argument_should_set_is_last_argument_to_false() { - parameter.declare(::property, sequenceOf("first", "second")) + parameter.declare(sequenceOf("first", "second")) assertFalse(parameter.isLastArgument) } @@ -141,18 +141,19 @@ class ParameterStateSpec { @Test fun getting_argument_before_declared_should_throw_IllegalStateException() { val failure = assertFailsWith { - parameter.getArgument(::property) + parameter.getArgument() } assertEquals(getArgumentBeforeDeclaredMessage, failure.message, "message") } @Test + @Ignore fun getting_argument_with_the_wrong_property_should_throw_ParameterizeException() { - parameter.declare(::property, sequenceOf(Unit)) + parameter.declare(sequenceOf(Unit)) val exception = assertFailsWith { - parameter.getArgument(::differentProperty) + parameter.getArgument() } assertEquals( @@ -163,14 +164,14 @@ class ParameterStateSpec { @Test fun getting_argument_should_initially_return_the_first_argument() { - parameter.declare(::property, sequenceOf("first", "second")) + parameter.declare(sequenceOf("first", "second")) - assertEquals("first", parameter.getArgument(::property)) + assertEquals("first", parameter.getArgument()) } @Test fun use_argument_should_set_has_been_used_to_true() { - parameter.declare(::property, sequenceOf("first", "second")) + parameter.declare(sequenceOf("first", "second")) parameter.useArgument() assertTrue(parameter.hasBeenUsed) @@ -187,20 +188,20 @@ class ParameterStateSpec { @Test fun next_should_move_to_the_next_argument() { - parameter.declare(::property, sequenceOf("first", "second", "third")) - parameter.getArgument(::property) + parameter.declare(sequenceOf("first", "second", "third")) + parameter.getArgument() parameter.nextArgument() - assertEquals("second", parameter.getArgument(::property)) + assertEquals("second", parameter.getArgument()) parameter.nextArgument() - assertEquals("third", parameter.getArgument(::property)) + assertEquals("third", parameter.getArgument()) } @Test fun next_to_a_middle_argument_should_leave_is_last_argument_as_false() { - parameter.declare(::property, sequenceOf("first", "second", "third", "fourth")) - parameter.getArgument(::property) + parameter.declare(sequenceOf("first", "second", "third", "fourth")) + parameter.getArgument() parameter.nextArgument() assertFalse(parameter.isLastArgument, "second") @@ -211,8 +212,8 @@ class ParameterStateSpec { @Test fun next_to_the_last_argument_should_set_is_last_argument_to_true() { - parameter.declare(::property, sequenceOf("first", "second", "third", "fourth")) - parameter.getArgument(::property) + parameter.declare(sequenceOf("first", "second", "third", "fourth")) + parameter.getArgument() parameter.nextArgument() // second parameter.nextArgument() // third parameter.nextArgument() // forth @@ -223,19 +224,19 @@ class ParameterStateSpec { @Ignore @Test fun next_after_the_last_argument_should_loop_back_to_the_first() { - parameter.declare(::property, sequenceOf("first", "second")) - parameter.getArgument(::property) + parameter.declare(sequenceOf("first", "second")) + parameter.getArgument() parameter.nextArgument() // second parameter.nextArgument() // first - assertEquals("first", parameter.getArgument(::property)) + assertEquals("first", parameter.getArgument()) } @Ignore @Test fun next_after_the_last_argument_should_set_is_last_argument_to_false() { - parameter.declare(::property, sequenceOf("first", "second")) - parameter.getArgument(::property) + parameter.declare(sequenceOf("first", "second")) + parameter.getArgument() parameter.nextArgument() // second parameter.nextArgument() // first @@ -244,42 +245,43 @@ class ParameterStateSpec { @Test fun redeclare_should_not_change_current_argument() { - parameter.declare(::property, sequenceOf("a", "b")) + parameter.declare(sequenceOf("a", "b")) val newArguments = Sequence { fail("Re-declaring should keep the old arguments") } - parameter.declare(::property, newArguments) + parameter.declare(newArguments) - assertEquals("a", parameter.getArgument(::property)) + assertEquals("a", parameter.getArgument()) } @Test fun redeclare_arguments_should_keep_using_the_original_arguments() { - parameter.declare(::property, sequenceOf("a")) + parameter.declare(sequenceOf("a")) val newArguments = Sequence { fail("Re-declaring should keep the old arguments") } - parameter.declare(::property, newArguments) + parameter.declare(newArguments) } @Test + @Ignore fun redeclare_with_different_parameter_should_throw_ParameterizeException() { - parameter.declare(::property, sequenceOf(Unit)) + parameter.declare(sequenceOf(Unit)) assertFailsWith { - parameter.declare(::differentProperty, sequenceOf(Unit)) + parameter.declare(sequenceOf(Unit)) } } @Test fun redeclare_with_different_parameter_should_not_change_has_been_used() { - parameter.declare(::property, sequenceOf("a")) + parameter.declare(sequenceOf("a")) parameter.useArgument() runCatching { - parameter.declare(::differentProperty, sequenceOf("a")) + parameter.declare(sequenceOf("a")) } assertTrue(parameter.hasBeenUsed) @@ -287,8 +289,8 @@ class ParameterStateSpec { @Test fun reset_should_set_has_been_used_to_false() { - parameter.declare(::property, sequenceOf("a", "b")) - parameter.getArgument(::property) + parameter.declare(sequenceOf("a", "b")) + parameter.getArgument() parameter.reset() assertFalse(parameter.hasBeenUsed) @@ -314,8 +316,9 @@ class ParameterStateSpec { @Test fun get_failure_argument_when_declared_should_have_correct_property_and_argument() { val expectedArgument = "a" - parameter.declare(::property, sequenceOf(expectedArgument)) - parameter.getArgument(::property) + parameter.declare(sequenceOf(expectedArgument)) + parameter.property = ::property + parameter.getArgument() val (property, argument) = parameter.getFailureArgument() assertTrue(property.equalsProperty(::property)) diff --git a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt index 49ca53b..3d320ef 100644 --- a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt +++ b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt @@ -83,6 +83,7 @@ class ParameterizeExceptionSpec { } @Test + @Ignore fun parameter_disappears_on_second_iteration_due_to_external_condition() = runTestCC { val exception = assertFailsWith { var shouldDeclareA = true @@ -101,6 +102,7 @@ class ParameterizeExceptionSpec { assertEquals("Expected to be declaring `a`, but got `b`", exception.message) } + @Ignore @Test fun parameter_appears_on_second_iteration_due_to_external_condition() = runTestCC { val exception = assertFailsWith { From ca08fcaa92891979a0be1553f9622be6a597e81c Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sat, 21 Sep 2024 17:41:20 +0100 Subject: [PATCH 05/18] Remove ParameterizeState.declaringParameter and ignore its tests Declare parameter upon `parameter` call --- src/commonMain/kotlin/Parameterize.kt | 20 +++++++++---------- src/commonMain/kotlin/ParameterizeState.kt | 19 +----------------- .../kotlin/ParameterizeScopeSpec.kt | 19 +++++++++--------- src/commonTest/kotlin/ParameterizeSpec.kt | 4 +++- 4 files changed, 23 insertions(+), 39 deletions(-) diff --git a/src/commonMain/kotlin/Parameterize.kt b/src/commonMain/kotlin/Parameterize.kt index edae7de..3f6f487 100644 --- a/src/commonMain/kotlin/Parameterize.kt +++ b/src/commonMain/kotlin/Parameterize.kt @@ -151,11 +151,9 @@ public class ParameterizeScope internal constructor( } /** @suppress */ - public operator fun Parameter.provideDelegate(thisRef: Any?, property: KProperty<*>): ParameterDelegate { - @Suppress("UNCHECKED_CAST") - return parameterizeState.declareParameter(property as KProperty, arguments).apply { - parameterState.property = property - } + public operator fun ParameterDelegate.provideDelegate(thisRef: Any?, property: KProperty<*>): ParameterDelegate { + parameterState.property = property + return this } /** @suppress */ @@ -204,9 +202,9 @@ public class ParameterizeScope internal constructor( * ``` */ @Suppress("UnusedReceiverParameter") // Should only be accessible within parameterize scopes -public suspend fun ParameterizeScope.parameter(arguments: Sequence): ParameterizeScope.Parameter = +public suspend fun ParameterizeScope.parameter(arguments: Sequence): ParameterizeScope.ParameterDelegate = @OptIn(ExperimentalParameterizeApi::class) - ParameterizeScope.Parameter(arguments) + parameterizeState.declareParameter(arguments) /** * Declare a parameter with the given [arguments]. @@ -215,7 +213,7 @@ public suspend fun ParameterizeScope.parameter(arguments: Sequence): Para * val letter by parameter('a'..'z') * ``` */ -public suspend fun ParameterizeScope.parameter(arguments: Iterable): ParameterizeScope.Parameter = +public suspend fun ParameterizeScope.parameter(arguments: Iterable): ParameterizeScope.ParameterDelegate = parameter(arguments.asSequence()) /** @@ -225,7 +223,7 @@ public suspend fun ParameterizeScope.parameter(arguments: Iterable): Para * val primeUnder20 by parameterOf(2, 3, 5, 7, 11, 13, 17, 19) * ``` */ -public suspend fun ParameterizeScope.parameterOf(vararg arguments: T): ParameterizeScope.Parameter = +public suspend fun ParameterizeScope.parameterOf(vararg arguments: T): ParameterizeScope.ParameterDelegate = parameter(arguments.asSequence()) /** @@ -253,7 +251,7 @@ public suspend fun ParameterizeScope.parameterOf(vararg arguments: T): Param @JvmName("parameterLazySequence") public suspend inline fun ParameterizeScope.parameter( crossinline lazyArguments: LazyParameterScope.() -> Sequence -): ParameterizeScope.Parameter = +): ParameterizeScope.ParameterDelegate = parameter(object : Sequence { private var arguments: Sequence? = null @@ -294,7 +292,7 @@ public suspend inline fun ParameterizeScope.parameter( @JvmName("parameterLazyIterable") public suspend inline fun ParameterizeScope.parameter( crossinline lazyArguments: LazyParameterScope.() -> Iterable -): ParameterizeScope.Parameter = +): ParameterizeScope.ParameterDelegate = parameter { lazyArguments().asSequence() } diff --git a/src/commonMain/kotlin/ParameterizeState.kt b/src/commonMain/kotlin/ParameterizeState.kt index 84faa37..4e8e66e 100644 --- a/src/commonMain/kotlin/ParameterizeState.kt +++ b/src/commonMain/kotlin/ParameterizeState.kt @@ -32,7 +32,6 @@ internal class ParameterizeState { * The true number of parameters in the current iteration is maintained in [parameterCount]. */ private val parameters = ArrayList() - private var declaringParameter: KProperty<*>? = null private var parameterCount = 0 /** @@ -67,9 +66,8 @@ internal class ParameterizeState { } fun declareParameter( - property: KProperty, arguments: Sequence - ): ParameterDelegate = trackNestedDeclaration(property) { + ): ParameterDelegate { val parameterIndex = parameterCount val parameter = if (parameterIndex in parameters.indices) { @@ -97,24 +95,9 @@ internal class ParameterizeState { return ParameterDelegate(parameter, parameter.getArgument()) } - private inline fun trackNestedDeclaration(property: KProperty<*>, block: () -> T): T { - val outerParameter = declaringParameter - checkState(outerParameter == null) { - "Nesting parameters is not currently supported: `${property.name}` was declared within `${outerParameter!!.name}`'s arguments" - } - - try { - declaringParameter = property - return block() - } finally { - declaringParameter = outerParameter - } - } - fun handleContinue() { skipCount++ } - /** * Get a list of used arguments for reporting a failure. */ diff --git a/src/commonTest/kotlin/ParameterizeScopeSpec.kt b/src/commonTest/kotlin/ParameterizeScopeSpec.kt index eb89f1e..c4af94b 100644 --- a/src/commonTest/kotlin/ParameterizeScopeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeScopeSpec.kt @@ -35,7 +35,7 @@ class ParameterizeScopeSpec { override fun next(): Nothing = throw NoSuchElementException() } - @Test +/* @Test fun parameter_from_sequence_should_be_constructed_with_the_same_arguments_instance() = runTestCC { parameterize { val sequence = sequenceOf() @@ -69,21 +69,21 @@ class ParameterizeScopeSpec { assertContentEquals(listedArguments.asSequence(), parameter.arguments) } - } + }*/ /** * The lazy `parameter {}` functions should have the same behavior, so this provides an abstraction that a test can * use to specify for all the lazy overloads parametrically. */ private interface LazyParameterFunction { - suspend operator fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): Parameter + suspend operator fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): ParameterDelegate class LazyArguments(val createIterator: () -> Iterator) } private val lazyParameterFunctions = listOf( "from sequence" to object : LazyParameterFunction { - override suspend fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): Parameter = + override suspend fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): ParameterDelegate = with(scope) { parameter { val arguments = lazyArguments() @@ -92,7 +92,7 @@ class ParameterizeScopeSpec { } }, "from iterable" to object : LazyParameterFunction { - override suspend fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): Parameter = + override suspend fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): ParameterDelegate = with(scope) { parameter { val arguments = lazyArguments() @@ -102,7 +102,7 @@ class ParameterizeScopeSpec { } ) - @Test + /* @Test fun parameter_from_lazy_arguments_should_have_the_correct_arguments() = runTestCC { parameterize { testAll(lazyParameterFunctions) { lazyParameterFunction -> @@ -113,8 +113,9 @@ class ParameterizeScopeSpec { assertSame(ArgumentIterator, lazyParameter.arguments.iterator()) } } - } + }*/ + @Ignore @Test fun parameter_from_lazy_arguments_should_not_be_computed_before_declaring() = runTestCC { parameterize { @@ -124,7 +125,7 @@ class ParameterizeScopeSpec { } } - @Test +/* @Test fun parameter_from_lazy_argument_iterable_should_only_be_computed_once() = runTestCC { parameterize { testAll(lazyParameterFunctions) { lazyParameterFunction -> @@ -143,7 +144,7 @@ class ParameterizeScopeSpec { assertEquals(1, evaluationCount) } } - } + }*/ @Test fun string_representation_should_show_used_parameter_arguments_in_declaration_order() = runTestCC { diff --git a/src/commonTest/kotlin/ParameterizeSpec.kt b/src/commonTest/kotlin/ParameterizeSpec.kt index 4152955..1252d99 100644 --- a/src/commonTest/kotlin/ParameterizeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeSpec.kt @@ -16,6 +16,7 @@ package com.benwoodworth.parameterize +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -95,6 +96,7 @@ class ParameterizeSpec { } } + @Ignore @Test fun parameter_should_iterate_to_the_next_argument_while_declaring() = runTestCC { var state: String @@ -236,7 +238,7 @@ class ParameterizeSpec { ) { suspend fun ParameterizeScope.customLazyParameter( lazyArguments: () -> Iterable - ): ParameterizeScope.Parameter { + ): ParameterizeScope.ParameterDelegate { val arguments by lazy(lazyArguments) class CustomLazyArguments : Iterable { From bc628db8a4c18a54bdf72953e6413b19ccd89bb5 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sat, 21 Sep 2024 19:47:47 +0100 Subject: [PATCH 06/18] Add type param to ParameterState --- src/commonMain/kotlin/ParameterState.kt | 12 ++++++------ src/commonMain/kotlin/Parameterize.kt | 4 ++-- src/commonMain/kotlin/ParameterizeState.kt | 11 +++++------ src/commonTest/kotlin/ParameterStateSpec.kt | 18 +++++++++--------- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/commonMain/kotlin/ParameterState.kt b/src/commonMain/kotlin/ParameterState.kt index e4cc012..4b1c390 100644 --- a/src/commonMain/kotlin/ParameterState.kt +++ b/src/commonMain/kotlin/ParameterState.kt @@ -49,11 +49,11 @@ import kotlin.reflect.KProperty * ignored since they're assumed to be the same, and the state remains unchanged * in favor of continuing through the current iterator where it left off. */ -internal class ParameterState { +internal class ParameterState { private var isDeclared: Boolean = false - private var argument: Any? = null // T - var property: KProperty<*>? = null - private var argumentIterator: Iterator<*>? = null + private var argument: T? = null // T + var property: KProperty? = null + private var argumentIterator: Iterator? = null var hasBeenUsed: Boolean = false private set @@ -96,7 +96,7 @@ internal class ParameterState { * @throws ParameterizeException if already declared for a different [property]. * @throws ParameterizeContinue if [arguments] is empty. */ - fun declare(arguments: Sequence<*>) { + fun declare(arguments: Sequence) { // Nothing to do if already declared if (isDeclared) return @@ -116,7 +116,7 @@ internal class ParameterState { * @throws ParameterizeException if already declared for a different [property]. * @throws IllegalStateException if the argument has not been declared yet. */ - fun getArgument(): T { + fun getArgument(): T { check(isDeclared) { "Cannot get argument before parameter has been declared" } diff --git a/src/commonMain/kotlin/Parameterize.kt b/src/commonMain/kotlin/Parameterize.kt index 3f6f487..9429dcd 100644 --- a/src/commonMain/kotlin/Parameterize.kt +++ b/src/commonMain/kotlin/Parameterize.kt @@ -152,7 +152,7 @@ public class ParameterizeScope internal constructor( /** @suppress */ public operator fun ParameterDelegate.provideDelegate(thisRef: Any?, property: KProperty<*>): ParameterDelegate { - parameterState.property = property + parameterState.property = property as KProperty return this } @@ -178,7 +178,7 @@ public class ParameterizeScope internal constructor( /** @suppress */ public class ParameterDelegate internal constructor( - internal val parameterState: ParameterState, + internal val parameterState: ParameterState, internal val argument: T ) { /** diff --git a/src/commonMain/kotlin/ParameterizeState.kt b/src/commonMain/kotlin/ParameterizeState.kt index 4e8e66e..f24df14 100644 --- a/src/commonMain/kotlin/ParameterizeState.kt +++ b/src/commonMain/kotlin/ParameterizeState.kt @@ -22,7 +22,6 @@ import com.benwoodworth.parameterize.ParameterizeScope.ParameterDelegate import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.jvm.JvmInline -import kotlin.reflect.KProperty internal class ParameterizeState { /** @@ -31,7 +30,7 @@ internal class ParameterizeState { * Parameter instances are re-used between iterations, so will never be removed. * The true number of parameters in the current iteration is maintained in [parameterCount]. */ - private val parameters = ArrayList() + private val parameters = ArrayList>() private var parameterCount = 0 /** @@ -39,12 +38,12 @@ internal class ParameterizeState { * * Set to `null` once the parameter is iterated. */ - private var parameterToIterate: ParameterState? = null + private var parameterToIterate: ParameterState<*>? = null /** * The last parameter this iteration that has another argument after declaring, or `null` if there hasn't been one yet. */ - private var lastParameterWithNextArgument: ParameterState? = null + private var lastParameterWithNextArgument: ParameterState<*>? = null private var iterationCount = 0L private var skipCount = 0L @@ -75,9 +74,9 @@ internal class ParameterizeState { // If null, then a previous parameter's argument has already been iterated, // so all subsequent parameters should be discarded in case they depended on it if (parameterToIterate == null) reset() - } + } as ParameterState } else { - ParameterState().also { parameters += it } + ParameterState().also { parameters += it } } parameter.declare(arguments) diff --git a/src/commonTest/kotlin/ParameterStateSpec.kt b/src/commonTest/kotlin/ParameterStateSpec.kt index f984873..e3ab7fa 100644 --- a/src/commonTest/kotlin/ParameterStateSpec.kt +++ b/src/commonTest/kotlin/ParameterStateSpec.kt @@ -26,7 +26,7 @@ class ParameterStateSpec { private val property: String get() = error("${::property.name} is not meant to be used") private val differentProperty: String get() = error("${::differentProperty.name} is not meant to be used") - private lateinit var parameter: ParameterState + private lateinit var parameter: ParameterState @BeforeTest fun beforeTest() { @@ -34,7 +34,7 @@ class ParameterStateSpec { } - private fun assertUndeclared(parameter: ParameterState) { + private fun assertUndeclared(parameter: ParameterState<*>) { val failure = assertFailsWith { parameter.getArgument() } @@ -189,7 +189,7 @@ class ParameterStateSpec { @Test fun next_should_move_to_the_next_argument() { parameter.declare(sequenceOf("first", "second", "third")) - parameter.getArgument() + parameter.getArgument() parameter.nextArgument() assertEquals("second", parameter.getArgument()) @@ -201,7 +201,7 @@ class ParameterStateSpec { @Test fun next_to_a_middle_argument_should_leave_is_last_argument_as_false() { parameter.declare(sequenceOf("first", "second", "third", "fourth")) - parameter.getArgument() + parameter.getArgument() parameter.nextArgument() assertFalse(parameter.isLastArgument, "second") @@ -213,7 +213,7 @@ class ParameterStateSpec { @Test fun next_to_the_last_argument_should_set_is_last_argument_to_true() { parameter.declare(sequenceOf("first", "second", "third", "fourth")) - parameter.getArgument() + parameter.getArgument() parameter.nextArgument() // second parameter.nextArgument() // third parameter.nextArgument() // forth @@ -225,7 +225,7 @@ class ParameterStateSpec { @Test fun next_after_the_last_argument_should_loop_back_to_the_first() { parameter.declare(sequenceOf("first", "second")) - parameter.getArgument() + parameter.getArgument() parameter.nextArgument() // second parameter.nextArgument() // first @@ -236,7 +236,7 @@ class ParameterStateSpec { @Test fun next_after_the_last_argument_should_set_is_last_argument_to_false() { parameter.declare(sequenceOf("first", "second")) - parameter.getArgument() + parameter.getArgument() parameter.nextArgument() // second parameter.nextArgument() // first @@ -290,7 +290,7 @@ class ParameterStateSpec { @Test fun reset_should_set_has_been_used_to_false() { parameter.declare(sequenceOf("a", "b")) - parameter.getArgument() + parameter.getArgument() parameter.reset() assertFalse(parameter.hasBeenUsed) @@ -318,7 +318,7 @@ class ParameterStateSpec { val expectedArgument = "a" parameter.declare(sequenceOf(expectedArgument)) parameter.property = ::property - parameter.getArgument() + parameter.getArgument() val (property, argument) = parameter.getFailureArgument() assertTrue(property.equalsProperty(::property)) From 83b1b0a594ed4f99b39943a998e70ed541190e4b Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Tue, 24 Sep 2024 18:56:40 +0100 Subject: [PATCH 07/18] Remove ParameterState.reset Pass HandlerPrompt through to ParameterizeState --- src/commonMain/kotlin/ParameterState.kt | 10 +--------- src/commonMain/kotlin/Parameterize.kt | 2 +- src/commonMain/kotlin/ParameterizeIterator.kt | 6 ++++-- src/commonMain/kotlin/ParameterizeState.kt | 17 +++++++++++------ src/commonTest/kotlin/ParameterStateSpec.kt | 9 --------- 5 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/commonMain/kotlin/ParameterState.kt b/src/commonMain/kotlin/ParameterState.kt index 4b1c390..a82e006 100644 --- a/src/commonMain/kotlin/ParameterState.kt +++ b/src/commonMain/kotlin/ParameterState.kt @@ -50,7 +50,7 @@ import kotlin.reflect.KProperty * in favor of continuing through the current iterator where it left off. */ internal class ParameterState { - private var isDeclared: Boolean = false + internal var isDeclared: Boolean = false private var argument: T? = null // T var property: KProperty? = null private var argumentIterator: Iterator? = null @@ -58,14 +58,6 @@ internal class ParameterState { var hasBeenUsed: Boolean = false private set - internal fun reset() { - isDeclared = false - property = null - argument = null - argumentIterator = null - hasBeenUsed = false - } - /** * @throws IllegalStateException if used before the argument has been declared. */ diff --git a/src/commonMain/kotlin/Parameterize.kt b/src/commonMain/kotlin/Parameterize.kt index 9429dcd..3e2177c 100644 --- a/src/commonMain/kotlin/Parameterize.kt +++ b/src/commonMain/kotlin/Parameterize.kt @@ -88,7 +88,7 @@ public suspend inline fun parameterize( // Code inlined from a previous version could have subtly different semantics when interacting with the runtime // iterator of a later release, and would be major breaking change that's difficult to detect. - val iterator = ParameterizeIterator(configuration) + val iterator = ParameterizeIterator(configuration, this) while (true) { val scope = iterator.nextIteration() ?: break diff --git a/src/commonMain/kotlin/ParameterizeIterator.kt b/src/commonMain/kotlin/ParameterizeIterator.kt index 4c9d056..d39d9d5 100644 --- a/src/commonMain/kotlin/ParameterizeIterator.kt +++ b/src/commonMain/kotlin/ParameterizeIterator.kt @@ -17,6 +17,7 @@ package com.benwoodworth.parameterize import com.benwoodworth.parameterize.ParameterizeConfiguration.DecoratorScope +import effekt.HandlerPrompt import kotlin.coroutines.Continuation import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.intrinsics.createCoroutineUnintercepted @@ -26,9 +27,10 @@ internal data object ParameterizeContinue : Throwable() @PublishedApi internal class ParameterizeIterator( - private val configuration: ParameterizeConfiguration + private val configuration: ParameterizeConfiguration, + p: HandlerPrompt ) { - private val parameterizeState = ParameterizeState() + private val parameterizeState = ParameterizeState(p) private var breakEarly = false private var currentIterationScope: ParameterizeScope? = null // Non-null if afterEach still needs to be called diff --git a/src/commonMain/kotlin/ParameterizeState.kt b/src/commonMain/kotlin/ParameterizeState.kt index f24df14..d6145d9 100644 --- a/src/commonMain/kotlin/ParameterizeState.kt +++ b/src/commonMain/kotlin/ParameterizeState.kt @@ -19,11 +19,13 @@ package com.benwoodworth.parameterize import com.benwoodworth.parameterize.ParameterizeConfiguration.OnCompleteScope import com.benwoodworth.parameterize.ParameterizeConfiguration.OnFailureScope import com.benwoodworth.parameterize.ParameterizeScope.ParameterDelegate +import effekt.Handler +import effekt.HandlerPrompt import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.jvm.JvmInline -internal class ParameterizeState { +internal class ParameterizeState(p: HandlerPrompt) : Handler by p { /** * The parameters created for [parameterize]. * @@ -70,11 +72,14 @@ internal class ParameterizeState { val parameterIndex = parameterCount val parameter = if (parameterIndex in parameters.indices) { - parameters[parameterIndex].apply { - // If null, then a previous parameter's argument has already been iterated, - // so all subsequent parameters should be discarded in case they depended on it - if (parameterToIterate == null) reset() - } as ParameterState + // If null, then a previous parameter's argument has already been iterated, + // so all subsequent parameters should be discarded in case they depended on it + if (parameterToIterate != null) { + check(parameters[parameterIndex].isDeclared) + parameters[parameterIndex] as ParameterState + } else { + ParameterState().also { parameters[parameterIndex] = it } + } } else { ParameterState().also { parameters += it } } diff --git a/src/commonTest/kotlin/ParameterStateSpec.kt b/src/commonTest/kotlin/ParameterStateSpec.kt index e3ab7fa..d8896aa 100644 --- a/src/commonTest/kotlin/ParameterStateSpec.kt +++ b/src/commonTest/kotlin/ParameterStateSpec.kt @@ -287,15 +287,6 @@ class ParameterStateSpec { assertTrue(parameter.hasBeenUsed) } - @Test - fun reset_should_set_has_been_used_to_false() { - parameter.declare(sequenceOf("a", "b")) - parameter.getArgument() - parameter.reset() - - assertFalse(parameter.hasBeenUsed) - } - @Test fun is_last_argument_before_declared_should_throw() { val failure = assertFailsWith { From ea6e73a37d3caa7bd726287c09664f4e659eaeae Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Tue, 24 Sep 2024 19:18:11 +0100 Subject: [PATCH 08/18] Replace ParameterState.declare with a constructor Remove ParameterState.isDeclared and corresponding tests Remove ParameterState.getArgument in favour of the property argument --- src/commonMain/kotlin/ParameterState.kt | 92 ++-------- src/commonMain/kotlin/ParameterizeState.kt | 10 +- src/commonTest/kotlin/ParameterStateSpec.kt | 185 +++----------------- 3 files changed, 47 insertions(+), 240 deletions(-) diff --git a/src/commonMain/kotlin/ParameterState.kt b/src/commonMain/kotlin/ParameterState.kt index a82e006..83e084e 100644 --- a/src/commonMain/kotlin/ParameterState.kt +++ b/src/commonMain/kotlin/ParameterState.kt @@ -23,37 +23,30 @@ import kotlin.reflect.KProperty * [parameterize] DSL, maintaining an argument, and lazily loading the next ones * in as needed. * - * The state can also be reset for reuse later with another parameter, allowing - * the same instance to be shared, saving on unnecessary instantiations. Since - * this means the underlying argument type can change, this class doesn't have a - * generic type for it, and instead has each function pull a generic type from a - * provided property, and validates it against the property this parameter was - * declared with. This ensures that the argument type is correct at runtime, and - * also validates that the parameter is in fact being used with the expected - * property. - * * When first declared, the parameter [property] it was * declared with will be stored, along with a new argument iterator and the * first argument from it. The arguments are lazily read in from the iterator as - * they're needed, and will seamlessly continue with the start again after the - * last argument, using [isLastArgument] as an indicator. The stored iterator + * they're needed, using [isLastArgument] as an indicator. The stored iterator * will always have a next argument available, and will be set to null when its - * last argument is read in to release its reference until the next iterator is - * created to begin from the start again. + * last argument is read in to release its reference * - * Since each [parameterize] iteration should declare the same parameters, - * in the same order with the same arguments, declared with the same - * already-declared state instance as the previous iteration. Calling [declare] - * again will leave the state unchanged, only serving to validate that the - * parameter was in fact declared the same as before. The new arguments are - * ignored since they're assumed to be the same, and the state remains unchanged - * in favor of continuing through the current iterator where it left off. */ -internal class ParameterState { - internal var isDeclared: Boolean = false - private var argument: T? = null // T +internal class ParameterState(argumentIterator: Iterator) { + /** + * Set up the delegate with the given [arguments]. + * + * @throws ParameterizeContinue if [arguments] is empty. + */ + constructor(arguments: Sequence) : this(arguments.iterator()) + + init { + if (!argumentIterator.hasNext()) throw ParameterizeContinue // Before changing any state + } + + var argument: T = argumentIterator.next() + private set var property: KProperty? = null - private var argumentIterator: Iterator? = null + private var argumentIterator: Iterator? = argumentIterator.takeIf { it.hasNext() } var hasBeenUsed: Boolean = false private set @@ -62,60 +55,13 @@ internal class ParameterState { * @throws IllegalStateException if used before the argument has been declared. */ val isLastArgument: Boolean - get() { - check(isDeclared) { "Parameter has not been declared" } - return argumentIterator == null - } + get() = argumentIterator == null /** * Returns a string representation of the current argument, or a "not declared" message. */ - override fun toString(): String = - if (!isDeclared) { - "Parameter not declared yet." - } else { - argument.toString() - } - - /** - * Set up the delegate with the given [arguments]. - * - * If this delegate is already [declare]d, [arguments] should be equal to that that were originally passed in. - * The current argument will remain the same. - * The new [arguments] will be ignored in favor of reusing the existing arguments, under the assumption that they're equal. - * - * @throws ParameterizeException if already declared for a different [property]. - * @throws ParameterizeContinue if [arguments] is empty. - */ - fun declare(arguments: Sequence) { - // Nothing to do if already declared - if (isDeclared) return - - val iterator = arguments.iterator() - if (!iterator.hasNext()) { - throw ParameterizeContinue // Before changing any state - } - - isDeclared = true - this.argument = iterator.next() - this.argumentIterator = iterator.takeIf { it.hasNext() } - } - - /** - * Get the current argument, and set [hasBeenUsed] to true. - * - * @throws ParameterizeException if already declared for a different [property]. - * @throws IllegalStateException if the argument has not been declared yet. - */ - fun getArgument(): T { - check(isDeclared) { - "Cannot get argument before parameter has been declared" - } - - @Suppress("UNCHECKED_CAST") // Argument is declared with property's arguments, so must be T - return argument as T - } + override fun toString(): String = argument.toString() fun useArgument() { hasBeenUsed = true diff --git a/src/commonMain/kotlin/ParameterizeState.kt b/src/commonMain/kotlin/ParameterizeState.kt index d6145d9..4f12c5b 100644 --- a/src/commonMain/kotlin/ParameterizeState.kt +++ b/src/commonMain/kotlin/ParameterizeState.kt @@ -75,17 +75,15 @@ internal class ParameterizeState(p: HandlerPrompt) : Handler by p { // If null, then a previous parameter's argument has already been iterated, // so all subsequent parameters should be discarded in case they depended on it if (parameterToIterate != null) { - check(parameters[parameterIndex].isDeclared) parameters[parameterIndex] as ParameterState } else { - ParameterState().also { parameters[parameterIndex] = it } + ParameterState(arguments).also { parameters[parameterIndex] = it } } } else { - ParameterState().also { parameters += it } + ParameterState(arguments).also { parameters += it } } - parameter.declare(arguments) - parameterCount++ // After declaring, since the parameter shouldn't count if declare throws + parameterCount++ // After declaring, since the parameter shouldn't count if ParameterState's constructor throws if (parameter === parameterToIterate) { parameter.nextArgument() @@ -96,7 +94,7 @@ internal class ParameterizeState(p: HandlerPrompt) : Handler by p { lastParameterWithNextArgument = parameter } - return ParameterDelegate(parameter, parameter.getArgument()) + return ParameterDelegate(parameter, parameter.argument) } fun handleContinue() { diff --git a/src/commonTest/kotlin/ParameterStateSpec.kt b/src/commonTest/kotlin/ParameterStateSpec.kt index d8896aa..6581975 100644 --- a/src/commonTest/kotlin/ParameterStateSpec.kt +++ b/src/commonTest/kotlin/ParameterStateSpec.kt @@ -19,79 +19,23 @@ package com.benwoodworth.parameterize import kotlin.test.* class ParameterStateSpec { - private val getArgumentBeforeDeclaredMessage = "Cannot get argument before parameter has been declared" - private val getFailureArgumentBeforeDeclaredMessage = - "Cannot get failure argument before parameter has been declared" - private val property: String get() = error("${::property.name} is not meant to be used") - private val differentProperty: String get() = error("${::differentProperty.name} is not meant to be used") - - private lateinit var parameter: ParameterState - - @BeforeTest - fun beforeTest() { - parameter = ParameterState() - } - - - private fun assertUndeclared(parameter: ParameterState<*>) { - val failure = assertFailsWith { - parameter.getArgument() - } - - assertEquals(getArgumentBeforeDeclaredMessage, failure.message, "message") - } - - @Test - fun string_representation_when_not_declared_should_match_message_from_lazy() { - val messageFromLazy = lazy { error("unused") }.toString() - - val replacements = listOf( - "Lazy value" to "Parameter", - "initialized" to "declared" - ) - - val expected = replacements - .onEach { (old) -> - check(old in messageFromLazy) { "'$old' in '$messageFromLazy'" } - } - .fold(messageFromLazy) { result, (old, new) -> - result.replace(old, new) - } - - assertEquals(expected, parameter.toString()) - } @Test fun string_representation_when_initialized_should_equal_that_of_the_current_argument() { val argument = "argument" - - parameter.declare(sequenceOf(argument)) + val parameter = ParameterState(sequenceOf(argument)) assertSame(argument, parameter.toString()) } - @Test - fun has_been_used_should_initially_be_false() { - assertFalse(parameter.hasBeenUsed) - } - @Test fun declaring_with_no_arguments_should_throw_ParameterizeContinue() { assertFailsWith { - parameter.declare(emptySequence()) + ParameterState(emptySequence()) } } - @Test - fun declaring_with_no_arguments_should_leave_parameter_undeclared() { - runCatching { - parameter.declare(emptySequence()) - } - - assertUndeclared(parameter) - } - @Test fun declare_should_immediately_get_the_first_argument() { var gotFirstArgument = false @@ -101,7 +45,7 @@ class ParameterStateSpec { listOf(Unit).iterator() } - parameter.declare(arguments) + ParameterState(arguments) assertTrue(gotFirstArgument, "gotFirstArgument") } @@ -121,39 +65,30 @@ class ParameterStateSpec { } } - parameter.declare(Sequence(::AssertingIterator)) + val parameter = ParameterState(Sequence(::AssertingIterator)) } @Test fun declare_with_one_argument_should_set_is_last_argument_to_true() { - parameter.declare(sequenceOf("first")) + val parameter = ParameterState(sequenceOf("first")) assertTrue(parameter.isLastArgument) } @Test fun declare_with_more_than_one_argument_should_set_is_last_argument_to_false() { - parameter.declare(sequenceOf("first", "second")) + val parameter = ParameterState(sequenceOf("first", "second")) assertFalse(parameter.isLastArgument) } - @Test - fun getting_argument_before_declared_should_throw_IllegalStateException() { - val failure = assertFailsWith { - parameter.getArgument() - } - - assertEquals(getArgumentBeforeDeclaredMessage, failure.message, "message") - } - @Test @Ignore fun getting_argument_with_the_wrong_property_should_throw_ParameterizeException() { - parameter.declare(sequenceOf(Unit)) + val parameter = ParameterState(sequenceOf(Unit)) val exception = assertFailsWith { - parameter.getArgument() + parameter.argument } assertEquals( @@ -164,44 +99,34 @@ class ParameterStateSpec { @Test fun getting_argument_should_initially_return_the_first_argument() { - parameter.declare(sequenceOf("first", "second")) + val parameter = ParameterState(sequenceOf("first", "second")) - assertEquals("first", parameter.getArgument()) + assertEquals("first", parameter.argument) } @Test fun use_argument_should_set_has_been_used_to_true() { - parameter.declare(sequenceOf("first", "second")) + val parameter = ParameterState(sequenceOf("first", "second")) parameter.useArgument() assertTrue(parameter.hasBeenUsed) } - @Test - fun next_before_declare_should_throw_IllegalStateException() { - val failure = assertFailsWith { - parameter.nextArgument() - } - - assertEquals("Cannot iterate arguments before parameter has been declared", failure.message) - } - @Test fun next_should_move_to_the_next_argument() { - parameter.declare(sequenceOf("first", "second", "third")) - parameter.getArgument() + val parameter = ParameterState(sequenceOf("first", "second", "third")) + parameter.argument parameter.nextArgument() - assertEquals("second", parameter.getArgument()) + assertEquals("second", parameter.argument) parameter.nextArgument() - assertEquals("third", parameter.getArgument()) + assertEquals("third", parameter.argument) } @Test fun next_to_a_middle_argument_should_leave_is_last_argument_as_false() { - parameter.declare(sequenceOf("first", "second", "third", "fourth")) - parameter.getArgument() + val parameter = ParameterState(sequenceOf("first", "second", "third", "fourth")) parameter.nextArgument() assertFalse(parameter.isLastArgument, "second") @@ -212,8 +137,7 @@ class ParameterStateSpec { @Test fun next_to_the_last_argument_should_set_is_last_argument_to_true() { - parameter.declare(sequenceOf("first", "second", "third", "fourth")) - parameter.getArgument() + val parameter = ParameterState(sequenceOf("first", "second", "third", "fourth")) parameter.nextArgument() // second parameter.nextArgument() // third parameter.nextArgument() // forth @@ -224,92 +148,31 @@ class ParameterStateSpec { @Ignore @Test fun next_after_the_last_argument_should_loop_back_to_the_first() { - parameter.declare(sequenceOf("first", "second")) - parameter.getArgument() + val parameter = ParameterState(sequenceOf("first", "second")) + parameter.argument parameter.nextArgument() // second parameter.nextArgument() // first - assertEquals("first", parameter.getArgument()) + assertEquals("first", parameter.argument) } @Ignore @Test fun next_after_the_last_argument_should_set_is_last_argument_to_false() { - parameter.declare(sequenceOf("first", "second")) - parameter.getArgument() + val parameter = ParameterState(sequenceOf("first", "second")) + parameter.argument parameter.nextArgument() // second parameter.nextArgument() // first assertFalse(parameter.isLastArgument) } - @Test - fun redeclare_should_not_change_current_argument() { - parameter.declare(sequenceOf("a", "b")) - - val newArguments = Sequence { - fail("Re-declaring should keep the old arguments") - } - parameter.declare(newArguments) - - assertEquals("a", parameter.getArgument()) - } - - @Test - fun redeclare_arguments_should_keep_using_the_original_arguments() { - parameter.declare(sequenceOf("a")) - - val newArguments = Sequence { - fail("Re-declaring should keep the old arguments") - } - parameter.declare(newArguments) - } - - @Test - @Ignore - fun redeclare_with_different_parameter_should_throw_ParameterizeException() { - parameter.declare(sequenceOf(Unit)) - - assertFailsWith { - parameter.declare(sequenceOf(Unit)) - } - } - - @Test - fun redeclare_with_different_parameter_should_not_change_has_been_used() { - parameter.declare(sequenceOf("a")) - parameter.useArgument() - - runCatching { - parameter.declare(sequenceOf("a")) - } - - assertTrue(parameter.hasBeenUsed) - } - - @Test - fun is_last_argument_before_declared_should_throw() { - val failure = assertFailsWith { - parameter.isLastArgument - } - assertEquals("Parameter has not been declared", failure.message) - } - - @Test - fun get_failure_argument_when_not_declared_should_throw_IllegalStateException() { - val failure = assertFailsWith { - parameter.getFailureArgument() - } - - assertEquals(getFailureArgumentBeforeDeclaredMessage, failure.message, "message") - } - @Test fun get_failure_argument_when_declared_should_have_correct_property_and_argument() { val expectedArgument = "a" - parameter.declare(sequenceOf(expectedArgument)) + val parameter = ParameterState(sequenceOf(expectedArgument)) parameter.property = ::property - parameter.getArgument() + parameter.argument val (property, argument) = parameter.getFailureArgument() assertTrue(property.equalsProperty(::property)) From 587b56d0fade79c023c9c4c579868b2915dafcf5 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Tue, 24 Sep 2024 19:23:14 +0100 Subject: [PATCH 09/18] Remove KPropertyUtils and equalsProperty --- src/commonMain/kotlin/KPropertyUtils.kt | 28 ---------- src/commonTest/kotlin/KPropertyUtilsSpec.kt | 54 ------------------- src/commonTest/kotlin/ParameterStateSpec.kt | 5 +- src/jsMain/kotlin/KPropertyUtils.js.kt | 23 -------- src/jvmMain/kotlin/KPropertyUtils.jvm.kt | 23 -------- .../kotlin/KPropertyUtils.native.kt | 23 -------- .../kotlin/KPropertyUtils.wasmJs.kt | 23 -------- .../kotlin/KPropertyUtils.wasmWasi.kt | 23 -------- 8 files changed, 3 insertions(+), 199 deletions(-) delete mode 100644 src/commonMain/kotlin/KPropertyUtils.kt delete mode 100644 src/commonTest/kotlin/KPropertyUtilsSpec.kt delete mode 100644 src/jsMain/kotlin/KPropertyUtils.js.kt delete mode 100644 src/jvmMain/kotlin/KPropertyUtils.jvm.kt delete mode 100644 src/nativeMain/kotlin/KPropertyUtils.native.kt delete mode 100644 src/wasmJsMain/kotlin/KPropertyUtils.wasmJs.kt delete mode 100644 src/wasmWasiMain/kotlin/KPropertyUtils.wasmWasi.kt diff --git a/src/commonMain/kotlin/KPropertyUtils.kt b/src/commonMain/kotlin/KPropertyUtils.kt deleted file mode 100644 index 9923920..0000000 --- a/src/commonMain/kotlin/KPropertyUtils.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2024 Ben Woodworth - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.benwoodworth.parameterize - -import kotlin.reflect.KProperty - -/** - * Indicates whether some [other] property is equal to [this] one. - * - * Necessary since not all platforms support checking that two property references are from the same property in the - * source code. In those cases, this function compares their names so that there's some confidence that they're not - * unequal. - */ -internal expect inline fun KProperty<*>.equalsProperty(other: KProperty<*>): Boolean diff --git a/src/commonTest/kotlin/KPropertyUtilsSpec.kt b/src/commonTest/kotlin/KPropertyUtilsSpec.kt deleted file mode 100644 index d1d5f64..0000000 --- a/src/commonTest/kotlin/KPropertyUtilsSpec.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2024 Ben Woodworth - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.benwoodworth.parameterize - -import kotlin.properties.ReadOnlyProperty -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class KPropertyUtilsSpec { - @Test - fun property_should_equal_the_same_instance() { - val property by ReadOnlyProperty { _, property -> property } - - val reference = property - - assertTrue(reference.equalsProperty(reference)) - } - - @Test - fun property_should_equal_the_same_property_through_different_delegate_calls() { - val property by ReadOnlyProperty { _, property -> property } - - val reference1 = property - val reference2 = property - - assertTrue(reference1.equalsProperty(reference2)) - } - - @Test - fun property_should_not_equal_a_different_property() { - val property1 by ReadOnlyProperty { _, property -> property } - val property2 by ReadOnlyProperty { _, property -> property } - - val reference1 = property1 - val reference2 = property2 - - assertFalse(reference1.equalsProperty(reference2)) - } -} diff --git a/src/commonTest/kotlin/ParameterStateSpec.kt b/src/commonTest/kotlin/ParameterStateSpec.kt index 6581975..026f690 100644 --- a/src/commonTest/kotlin/ParameterStateSpec.kt +++ b/src/commonTest/kotlin/ParameterStateSpec.kt @@ -171,11 +171,12 @@ class ParameterStateSpec { fun get_failure_argument_when_declared_should_have_correct_property_and_argument() { val expectedArgument = "a" val parameter = ParameterState(sequenceOf(expectedArgument)) - parameter.property = ::property + val propReference = ::property + parameter.property = propReference parameter.argument val (property, argument) = parameter.getFailureArgument() - assertTrue(property.equalsProperty(::property)) + assertEquals(propReference, property) assertSame(expectedArgument, argument) } } diff --git a/src/jsMain/kotlin/KPropertyUtils.js.kt b/src/jsMain/kotlin/KPropertyUtils.js.kt deleted file mode 100644 index 3714019..0000000 --- a/src/jsMain/kotlin/KPropertyUtils.js.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2024 Ben Woodworth - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.benwoodworth.parameterize - -import kotlin.reflect.KProperty - -@Suppress("NOTHING_TO_INLINE") -internal actual inline fun KProperty<*>.equalsProperty(other: KProperty<*>): Boolean = - this.name == other.name diff --git a/src/jvmMain/kotlin/KPropertyUtils.jvm.kt b/src/jvmMain/kotlin/KPropertyUtils.jvm.kt deleted file mode 100644 index d1e5ec9..0000000 --- a/src/jvmMain/kotlin/KPropertyUtils.jvm.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2024 Ben Woodworth - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.benwoodworth.parameterize - -import kotlin.reflect.KProperty - -@Suppress("NOTHING_TO_INLINE") -internal actual inline fun KProperty<*>.equalsProperty(other: KProperty<*>): Boolean = - this == other diff --git a/src/nativeMain/kotlin/KPropertyUtils.native.kt b/src/nativeMain/kotlin/KPropertyUtils.native.kt deleted file mode 100644 index d1e5ec9..0000000 --- a/src/nativeMain/kotlin/KPropertyUtils.native.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2024 Ben Woodworth - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.benwoodworth.parameterize - -import kotlin.reflect.KProperty - -@Suppress("NOTHING_TO_INLINE") -internal actual inline fun KProperty<*>.equalsProperty(other: KProperty<*>): Boolean = - this == other diff --git a/src/wasmJsMain/kotlin/KPropertyUtils.wasmJs.kt b/src/wasmJsMain/kotlin/KPropertyUtils.wasmJs.kt deleted file mode 100644 index 3714019..0000000 --- a/src/wasmJsMain/kotlin/KPropertyUtils.wasmJs.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2024 Ben Woodworth - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.benwoodworth.parameterize - -import kotlin.reflect.KProperty - -@Suppress("NOTHING_TO_INLINE") -internal actual inline fun KProperty<*>.equalsProperty(other: KProperty<*>): Boolean = - this.name == other.name diff --git a/src/wasmWasiMain/kotlin/KPropertyUtils.wasmWasi.kt b/src/wasmWasiMain/kotlin/KPropertyUtils.wasmWasi.kt deleted file mode 100644 index 3714019..0000000 --- a/src/wasmWasiMain/kotlin/KPropertyUtils.wasmWasi.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2024 Ben Woodworth - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.benwoodworth.parameterize - -import kotlin.reflect.KProperty - -@Suppress("NOTHING_TO_INLINE") -internal actual inline fun KProperty<*>.equalsProperty(other: KProperty<*>): Boolean = - this.name == other.name From 812ad33f36d20e41e0177ca4977e0d4666082915 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Fri, 27 Sep 2024 12:13:03 +0100 Subject: [PATCH 10/18] Use kontinuity for declareParameter Delete irrelevant tests Ensure that only one single `runTest` is triggered per test, and that its result is returned --- src/commonMain/kotlin/ParameterState.kt | 8 +- src/commonMain/kotlin/Parameterize.kt | 46 +++--- src/commonMain/kotlin/ParameterizeIterator.kt | 152 ------------------ src/commonMain/kotlin/ParameterizeState.kt | 79 +++------ src/commonTest/kotlin/ParameterStateSpec.kt | 44 ----- .../kotlin/ParameterizeConfigurationSpec.kt | 11 +- ...ParameterizeConfigurationSpec_decorator.kt | 15 +- ...arameterizeConfigurationSpec_onComplete.kt | 4 +- ...ParameterizeConfigurationSpec_onFailure.kt | 66 +++++--- .../kotlin/ParameterizeExceptionSpec.kt | 67 ++++---- .../kotlin/ParameterizeFailureSpec.kt | 2 +- .../kotlin/ParameterizeScopeSpec.kt | 11 +- src/commonTest/kotlin/ParameterizeSpec.kt | 23 ++- src/commonTest/kotlin/TestUtils.kt | 21 ++- src/commonTest/kotlin/test/EdgeCases.kt | 4 - .../kotlin/ParameterizeFailedErrorSpec.jvm.kt | 4 +- 16 files changed, 174 insertions(+), 383 deletions(-) diff --git a/src/commonMain/kotlin/ParameterState.kt b/src/commonMain/kotlin/ParameterState.kt index 83e084e..d5daa89 100644 --- a/src/commonMain/kotlin/ParameterState.kt +++ b/src/commonMain/kotlin/ParameterState.kt @@ -31,16 +31,14 @@ import kotlin.reflect.KProperty * last argument is read in to release its reference * */ -internal class ParameterState(argumentIterator: Iterator) { +internal class ParameterState(argumentIterator: Iterator, val isLast: Boolean = false) { /** * Set up the delegate with the given [arguments]. - * - * @throws ParameterizeContinue if [arguments] is empty. */ - constructor(arguments: Sequence) : this(arguments.iterator()) + constructor(arguments: Sequence, isLast: Boolean = false) : this(arguments.iterator(), isLast) init { - if (!argumentIterator.hasNext()) throw ParameterizeContinue // Before changing any state + if (!argumentIterator.hasNext()) TODO() // Before changing any state } var argument: T = argumentIterator.next() diff --git a/src/commonMain/kotlin/Parameterize.kt b/src/commonMain/kotlin/Parameterize.kt index 3e2177c..866497b 100644 --- a/src/commonMain/kotlin/Parameterize.kt +++ b/src/commonMain/kotlin/Parameterize.kt @@ -19,7 +19,11 @@ package com.benwoodworth.parameterize -import com.benwoodworth.parameterize.ParameterizeConfiguration.* +import com.benwoodworth.parameterize.ParameterizeConfiguration.DecoratorScope +import com.benwoodworth.parameterize.ParameterizeConfiguration.OnCompleteScope +import com.benwoodworth.parameterize.ParameterizeConfiguration.OnFailureScope +import effekt.HandlerPrompt +import effekt.discardWithFast import effekt.handle import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -80,25 +84,24 @@ import kotlin.reflect.KProperty * * @throws ParameterizeException if the DSL is used incorrectly. (See restrictions) */ -public suspend inline fun parameterize( +public suspend fun parameterize( configuration: ParameterizeConfiguration = ParameterizeConfiguration.default, - crossinline block: suspend ParameterizeScope.() -> Unit -): Unit = handle { + block: suspend ParameterizeScope.() -> Unit +): Unit = with(ParameterizeScope(ParameterizeState(HandlerPrompt()))) { // Exercise extreme caution modifying this code, since the iterator is sensitive to the behavior of this function. // Code inlined from a previous version could have subtly different semantics when interacting with the runtime // iterator of a later release, and would be major breaking change that's difficult to detect. - - val iterator = ParameterizeIterator(configuration, this) - - while (true) { - val scope = iterator.nextIteration() ?: break - - try { - scope.block() - } catch (failure: Throwable) { - iterator.handleFailure(failure) + handle breakEarly@{ + parameterizeState.handle { + try { + block() + } catch (failure: Throwable) { + val result = parameterizeState.handleFailure(configuration.onFailure, failure) + if (result.breakEarly) this@breakEarly.discardWithFast(Result.success(Unit)) + } } } + parameterizeState.handleComplete(configuration.onComplete) } /** @@ -114,12 +117,12 @@ public suspend inline fun parameterize( // False positive: onComplete is called in place exactly once through the configuration by the end parameterize call "LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND" ) -public suspend inline fun parameterize( +public suspend fun parameterize( configuration: ParameterizeConfiguration = ParameterizeConfiguration.default, - noinline decorator: suspend DecoratorScope.(iteration: suspend DecoratorScope.() -> Unit) -> Unit = configuration.decorator, - noinline onFailure: OnFailureScope.(failure: Throwable) -> Unit = configuration.onFailure, - noinline onComplete: OnCompleteScope.() -> Unit = configuration.onComplete, - crossinline block: suspend ParameterizeScope.() -> Unit + decorator: suspend DecoratorScope.(iteration: suspend DecoratorScope.() -> Unit) -> Unit = configuration.decorator, + onFailure: OnFailureScope.(failure: Throwable) -> Unit = configuration.onFailure, + onComplete: OnCompleteScope.() -> Unit = configuration.onComplete, + block: suspend ParameterizeScope.() -> Unit ) { contract { callsInPlace(onComplete, InvocationKind.EXACTLY_ONCE) @@ -151,7 +154,10 @@ public class ParameterizeScope internal constructor( } /** @suppress */ - public operator fun ParameterDelegate.provideDelegate(thisRef: Any?, property: KProperty<*>): ParameterDelegate { + public operator fun ParameterDelegate.provideDelegate( + thisRef: Any?, + property: KProperty<*> + ): ParameterDelegate { parameterState.property = property as KProperty return this } diff --git a/src/commonMain/kotlin/ParameterizeIterator.kt b/src/commonMain/kotlin/ParameterizeIterator.kt index d39d9d5..e69de29 100644 --- a/src/commonMain/kotlin/ParameterizeIterator.kt +++ b/src/commonMain/kotlin/ParameterizeIterator.kt @@ -1,152 +0,0 @@ -/* - * Copyright 2024 Ben Woodworth - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.benwoodworth.parameterize - -import com.benwoodworth.parameterize.ParameterizeConfiguration.DecoratorScope -import effekt.HandlerPrompt -import kotlin.coroutines.Continuation -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.intrinsics.createCoroutineUnintercepted -import kotlin.coroutines.resume - -internal data object ParameterizeContinue : Throwable() - -@PublishedApi -internal class ParameterizeIterator( - private val configuration: ParameterizeConfiguration, - p: HandlerPrompt -) { - private val parameterizeState = ParameterizeState(p) - - private var breakEarly = false - private var currentIterationScope: ParameterizeScope? = null // Non-null if afterEach still needs to be called - private var decoratorCoroutine: DecoratorCoroutine? = null - - /** - * Signals the start of a new [parameterize] iteration, and returns its scope if there is one. - */ - @PublishedApi - internal fun nextIteration(): ParameterizeScope? { - if (currentIterationScope != null) afterEach() - - if (breakEarly || !parameterizeState.hasNextArgumentCombination) { - handleComplete() - return null - } - - parameterizeState.startNextIteration() - return ParameterizeScope(parameterizeState).also { - currentIterationScope = it - beforeEach() - } - } - - @PublishedApi - internal fun handleFailure(failure: Throwable): Unit = when { - failure is ParameterizeContinue -> parameterizeState.handleContinue() - - failure is ParameterizeException && failure.parameterizeState === parameterizeState -> { - afterEach() // Since nextIteration() won't be called again to finalize the iteration - throw failure - } - - else -> { - afterEach() // Since the decorator should complete before onFailure is invoked - - val result = parameterizeState.handleFailure(configuration.onFailure, failure) - breakEarly = result.breakEarly - } - } - - private fun beforeEach() { - decoratorCoroutine = DecoratorCoroutine(parameterizeState, configuration) - .also { it.beforeIteration() } - } - - private fun afterEach() { - val decoratorCoroutine = checkNotNull(decoratorCoroutine) { "${::decoratorCoroutine.name} was null" } - - decoratorCoroutine.afterIteration() - - this.currentIterationScope = null - this.decoratorCoroutine = null - } - - private fun handleComplete() { - parameterizeState.handleComplete(configuration.onComplete) - } -} - -/** - * The [decorator][ParameterizeConfiguration.decorator] suspends for the iteration so that the one lambda can be run as - * two separate parts, without needing to wrap the (inlined) [parameterize] block. - */ -private class DecoratorCoroutine( - private val parameterizeState: ParameterizeState, - private val configuration: ParameterizeConfiguration -) { - private val scope = DecoratorScope(parameterizeState) - - private var continueAfterIteration: Continuation? = null - private var completed = false - - private val iteration: suspend DecoratorScope.() -> Unit = { - parameterizeState.checkState(continueAfterIteration == null) { - "Decorator must invoke the iteration function exactly once, but was invoked twice" - } - - suspendDecorator { continueAfterIteration = it } - isLastIteration = !parameterizeState.hasNextArgumentCombination - } - - fun beforeIteration() { - check(!completed) { "Decorator already completed" } - - val invokeDecorator: suspend DecoratorScope.() -> Unit = { - configuration.decorator(this, iteration) - } - - invokeDecorator - .createCoroutineUnintercepted( - receiver = scope, - completion = Continuation(EmptyCoroutineContext) { - completed = true - it.getOrThrow() - } - ) - .resume(Unit) - - parameterizeState.checkState(continueAfterIteration != null) { - if (completed) { - "Decorator must invoke the iteration function exactly once, but was not invoked" - } else { - "Decorator suspended unexpectedly" - } - } - } - - fun afterIteration() { - check(!completed) { "Decorator already completed" } - - continueAfterIteration?.resume(Unit) - ?: error("Iteration not invoked") - - parameterizeState.checkState(completed) { - "Decorator suspended unexpectedly" - } - } -} diff --git a/src/commonMain/kotlin/ParameterizeState.kt b/src/commonMain/kotlin/ParameterizeState.kt index 4f12c5b..a06184c 100644 --- a/src/commonMain/kotlin/ParameterizeState.kt +++ b/src/commonMain/kotlin/ParameterizeState.kt @@ -21,6 +21,7 @@ import com.benwoodworth.parameterize.ParameterizeConfiguration.OnFailureScope import com.benwoodworth.parameterize.ParameterizeScope.ParameterDelegate import effekt.Handler import effekt.HandlerPrompt +import effekt.use import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.jvm.JvmInline @@ -28,73 +29,34 @@ import kotlin.jvm.JvmInline internal class ParameterizeState(p: HandlerPrompt) : Handler by p { /** * The parameters created for [parameterize]. - * - * Parameter instances are re-used between iterations, so will never be removed. - * The true number of parameters in the current iteration is maintained in [parameterCount]. */ private val parameters = ArrayList>() - private var parameterCount = 0 - - /** - * The parameter that will be iterated to the next argument during this iteration. - * - * Set to `null` once the parameter is iterated. - */ - private var parameterToIterate: ParameterState<*>? = null - - /** - * The last parameter this iteration that has another argument after declaring, or `null` if there hasn't been one yet. - */ - private var lastParameterWithNextArgument: ParameterState<*>? = null private var iterationCount = 0L private var skipCount = 0L private var failureCount = 0L private val recordedFailures = mutableListOf() - val hasNextArgumentCombination: Boolean - get() = lastParameterWithNextArgument != null || iterationCount == 0L - val isFirstIteration: Boolean get() = iterationCount == 1L - fun startNextIteration() { - iterationCount++ - parameterCount = 0 - - parameterToIterate = lastParameterWithNextArgument - lastParameterWithNextArgument = null - } - - fun declareParameter( + suspend fun declareParameter( arguments: Sequence - ): ParameterDelegate { - val parameterIndex = parameterCount - - val parameter = if (parameterIndex in parameters.indices) { - // If null, then a previous parameter's argument has already been iterated, - // so all subsequent parameters should be discarded in case they depended on it - if (parameterToIterate != null) { - parameters[parameterIndex] as ParameterState - } else { - ParameterState(arguments).also { parameters[parameterIndex] = it } - } - } else { - ParameterState(arguments).also { parameters += it } + ): ParameterDelegate = use { resume -> + // TODO skip calling decorator on first iteration, but call it on the rest + // and also call it for the top-most iteration. + val iterator = arguments.iterator() + if (!iterator.hasNext()) return@use + while (true) { + iterationCount++ + val argument = iterator.next() + val isLast = !iterator.hasNext() + val parameter = ParameterState(sequenceOf(argument), isLast) + parameters.add(parameter) + resume(ParameterDelegate(parameter, argument)) + check(parameters.removeLast() == parameter) { "Unexpected last parameter" } + if (isLast) break } - - parameterCount++ // After declaring, since the parameter shouldn't count if ParameterState's constructor throws - - if (parameter === parameterToIterate) { - parameter.nextArgument() - parameterToIterate = null - } - - if (!parameter.isLastArgument) { - lastParameterWithNextArgument = parameter - } - - return ParameterDelegate(parameter, parameter.argument) } fun handleContinue() { @@ -104,7 +66,7 @@ internal class ParameterizeState(p: HandlerPrompt) : Handler by p { * Get a list of used arguments for reporting a failure. */ fun getFailureArguments(): List> = - parameters.take(parameterCount) + parameters .filter { it.hasBeenUsed } .map { it.getFailureArgument() } @@ -112,10 +74,7 @@ internal class ParameterizeState(p: HandlerPrompt) : Handler by p { value class HandleFailureResult(val breakEarly: Boolean) fun handleFailure(onFailure: OnFailureScope.(Throwable) -> Unit, failure: Throwable): HandleFailureResult { - checkState(parameterToIterate == null, failure) { - "Previous iteration executed to this point successfully, but now failed with the same arguments" - } - + if(failure is ParameterizeException && failure.parameterizeState === this) throw failure failureCount++ val scope = OnFailureScope( @@ -144,7 +103,7 @@ internal class ParameterizeState(p: HandlerPrompt) : Handler by p { iterationCount, skipCount, failureCount, - completedEarly = hasNextArgumentCombination, + completedEarly = parameters.any { !it.isLast }, recordedFailures, ) diff --git a/src/commonTest/kotlin/ParameterStateSpec.kt b/src/commonTest/kotlin/ParameterStateSpec.kt index 026f690..dbb4631 100644 --- a/src/commonTest/kotlin/ParameterStateSpec.kt +++ b/src/commonTest/kotlin/ParameterStateSpec.kt @@ -29,13 +29,6 @@ class ParameterStateSpec { assertSame(argument, parameter.toString()) } - @Test - fun declaring_with_no_arguments_should_throw_ParameterizeContinue() { - assertFailsWith { - ParameterState(emptySequence()) - } - } - @Test fun declare_should_immediately_get_the_first_argument() { var gotFirstArgument = false @@ -82,21 +75,6 @@ class ParameterStateSpec { assertFalse(parameter.isLastArgument) } - @Test - @Ignore - fun getting_argument_with_the_wrong_property_should_throw_ParameterizeException() { - val parameter = ParameterState(sequenceOf(Unit)) - - val exception = assertFailsWith { - parameter.argument - } - - assertEquals( - "Cannot use parameter delegate with `differentProperty`, since it was declared with `property`.", - exception.message - ) - } - @Test fun getting_argument_should_initially_return_the_first_argument() { val parameter = ParameterState(sequenceOf("first", "second")) @@ -145,28 +123,6 @@ class ParameterStateSpec { assertTrue(parameter.isLastArgument) } - @Ignore - @Test - fun next_after_the_last_argument_should_loop_back_to_the_first() { - val parameter = ParameterState(sequenceOf("first", "second")) - parameter.argument - parameter.nextArgument() // second - parameter.nextArgument() // first - - assertEquals("first", parameter.argument) - } - - @Ignore - @Test - fun next_after_the_last_argument_should_set_is_last_argument_to_false() { - val parameter = ParameterState(sequenceOf("first", "second")) - parameter.argument - parameter.nextArgument() // second - parameter.nextArgument() // first - - assertFalse(parameter.isLastArgument) - } - @Test fun get_failure_argument_when_declared_should_have_correct_property_and_argument() { val expectedArgument = "a" diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec.kt index 66cb5fe..04ff66e 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec.kt @@ -17,7 +17,6 @@ package com.benwoodworth.parameterize import com.benwoodworth.parameterize.ParameterizeConfiguration.Builder -import com.benwoodworth.parameterize.ParameterizeConfigurationSpec.ParameterizeWithOptionDefault import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KProperty1 import kotlin.test.* @@ -94,7 +93,7 @@ class ParameterizeConfigurationSpec { @Test - fun builder_should_apply_options_correctly() = testAll(configurationOptions) { option -> + fun builder_should_apply_options_correctly() = testAllCC(configurationOptions) { option -> val configuration = ParameterizeConfiguration { option.setDistinctValue(this) @@ -105,7 +104,7 @@ class ParameterizeConfigurationSpec { } @Test - fun builder_should_copy_from_another_configuration_correctly() = testAll(configurationOptions) { option -> + fun builder_should_copy_from_another_configuration_correctly() = testAllCC(configurationOptions) { option -> val copyFrom = ParameterizeConfiguration { option.setDistinctValue(this) } @@ -116,7 +115,7 @@ class ParameterizeConfigurationSpec { } @Test - fun string_representation_should_be_class_name_with_options_listed() = testAll(configurationOptions) { testOption -> + fun string_representation_should_be_class_name_with_options_listed() = testAllCC(configurationOptions) { testOption -> val configuration = ParameterizeConfiguration { testOption.setDistinctValue(this) } @@ -178,7 +177,7 @@ class ParameterizeConfigurationSpec { suspend fun configuredParameterize(configure: Builder.() -> Unit, block: ParameterizeScope.() -> Unit) } - private fun testConfiguredParameterize(test: suspend ConfiguredParameterize.() -> Unit) = testAll( + private fun testConfiguredParameterize(test: suspend ConfiguredParameterize.() -> Unit) = testAllCC( "configuration-only overload" to { test { configure, block -> val configuration = ParameterizeConfiguration { configure() } @@ -238,7 +237,7 @@ class ParameterizeConfigurationSpec { block: suspend ParameterizeScope.() -> Unit ) -> Unit, test: suspend ParameterizeWithOptionDefault.() -> Unit - ) = testAll( + ) = testAllCC( "with default from builder" to { val configuration = ParameterizeConfiguration { configure() } diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt index d526bb5..113c146 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt @@ -32,7 +32,7 @@ class ParameterizeConfigurationSpec_decorator { breakEarly = true }, noinline onComplete: OnCompleteScope.() -> Unit = ParameterizeConfiguration.default.onComplete, - crossinline block: suspend ParameterizeScope.() -> Unit + noinline block: suspend ParameterizeScope.() -> Unit ): Unit = parameterize( decorator = decorator, @@ -60,7 +60,7 @@ class ParameterizeConfigurationSpec_decorator { } @Test - fun failures_within_decorator_should_immediately_terminate_parameterize() { + fun failures_within_decorator_should_immediately_terminate_parameterize() = runTestCC { class FailureWithinDecorator : Throwable() testAll Unit) -> Unit>( @@ -100,7 +100,7 @@ class ParameterizeConfigurationSpec_decorator { * without hacking around the type system like this. But a nice error should be provided just in case. */ @Test - fun suspending_unexpectedly_should_fail() { + fun suspending_unexpectedly_should_fail() = runTestCC { val suspendWithoutResuming: suspend Any.() -> Unit = { suspendCoroutineUninterceptedOrReturn { COROUTINE_SUSPENDED } } @@ -128,7 +128,7 @@ class ParameterizeConfigurationSpec_decorator { } @Test - fun iteration_function_should_return_regardless_of_how_parameterize_block_fails() = testAll( + fun iteration_function_should_return_regardless_of_how_parameterize_block_fails() = testAllCC( EdgeCases.iterationFailures ) { getFailure -> var returned = false @@ -183,7 +183,7 @@ class ParameterizeConfigurationSpec_decorator { } @Test - fun is_first_iteration_should_be_correct() = testAll( + fun is_first_iteration_should_be_correct() = testAllCC( (1..3) .flatMap { listOf(it to "before", it to "after") } .map { "in iteration ${it.first}, ${it.second}" to it } @@ -209,7 +209,7 @@ class ParameterizeConfigurationSpec_decorator { } @Test - fun is_last_iteration_should_be_correct() = testAll( + fun is_last_iteration_should_be_correct() = testAllCC( (1..3).map { "in iteration $it" to it } ) { inIteration -> var currentIteration = 1 @@ -230,7 +230,7 @@ class ParameterizeConfigurationSpec_decorator { } @Test - fun is_last_iteration_when_accessed_before_invoking_iteration_should_throw() = testAll( + fun is_last_iteration_when_accessed_before_invoking_iteration_should_throw() = testAllCC( (1..3).map { "in iteration $it" to it } ) { inIteration -> var iterationNumber = 1 @@ -258,7 +258,6 @@ class ParameterizeConfigurationSpec_decorator { ) } - @Ignore @Test fun declaring_parameter_after_iteration_function_should_fail() = runTestCC { assertFailsWith { diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt index dae4af9..5f9919e 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt @@ -25,7 +25,7 @@ class ParameterizeConfigurationSpec_onComplete { private suspend inline fun testParameterize( noinline onFailure: OnFailureScope.(failure: Throwable) -> Unit = {}, // Continue on failure noinline onComplete: OnCompleteScope.() -> Unit, - crossinline block: suspend ParameterizeScope.() -> Unit + noinline block: suspend ParameterizeScope.() -> Unit ): Unit = parameterize( onFailure = onFailure, @@ -321,7 +321,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun error_constructor_should_build_error_with_correct_values() = testAll( + fun error_constructor_should_build_error_with_correct_values() = testAllCC( "base values" to OnCompleteScope( recordedFailures = emptyList(), failureCount = 1, diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onFailure.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onFailure.kt index e73939c..7b96954 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onFailure.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onFailure.kt @@ -23,7 +23,7 @@ import kotlin.test.* class ParameterizeConfigurationSpec_onFailure { private suspend inline fun testParameterize( noinline onFailure: OnFailureScope.(failure: Throwable) -> Unit, - crossinline block: suspend ParameterizeScope.() -> Unit + noinline block: suspend ParameterizeScope.() -> Unit ): Unit = parameterize( onFailure = onFailure, @@ -145,34 +145,33 @@ class ParameterizeConfigurationSpec_onFailure { } } + data class FailureParameterArgumentsException(val parameterArguments: List>): Exception() + @Test fun failure_arguments_should_be_those_from_the_last_iteration() = runTestCC { - val lastParameterArguments = mutableListOf>() - testParameterize( onFailure = { + assertIs(it) val actualParameterArguments = arguments .map { (parameter, argument) -> parameter.name to argument } - assertEquals(lastParameterArguments, actualParameterArguments) + assertEquals(it.parameterArguments, actualParameterArguments) } ) { - lastParameterArguments.clear() - val iteration by parameter(0..10) - lastParameterArguments += "iteration" to iteration + val iterationPair = "iteration" to iteration - if (iteration % 2 == 0) { + val evenIterationPair = if (iteration % 2 == 0) { val evenIteration by parameterOf(iteration) - lastParameterArguments += "evenIteration" to evenIteration - } + "evenIteration" to evenIteration + } else null - if (iteration % 3 == 0) { + val threevenIterationPair = if (iteration % 3 == 0) { val threevenIteration by parameterOf(iteration) - lastParameterArguments += "threevenIteration" to threevenIteration - } + "threevenIteration" to threevenIteration + } else null - fail() + throw FailureParameterArgumentsException(listOfNotNull(iterationPair, evenIterationPair, threevenIterationPair)) } } @@ -221,17 +220,18 @@ class ParameterizeConfigurationSpec_onFailure { } } - @Ignore @Test - fun failure_arguments_should_not_include_captured_parameters_from_previous_iterations() = runTestCC { + fun failure_arguments_should_include_captured_parameters_from_previous_iterations() = runTestCC { + var isFirstIteration = true testParameterize( onFailure = { val parameters = arguments.map { it.parameter.name } - assertFalse( - "neverUsedDuringTheCurrentIteration" in parameters, - "neverUsedDuringTheCurrentIteration in $parameters" + assertTrue( + "neverUsedDuringTheCurrentIteration" !in parameters == isFirstIteration, + "neverUsedDuringTheCurrentIteration !in $parameters != $isFirstIteration" ) + isFirstIteration = false } ) { val neverUsedDuringTheCurrentIteration by parameterOf(Unit) @@ -248,4 +248,32 @@ class ParameterizeConfigurationSpec_onFailure { fail() } } + + @Test + fun failure_arguments_should_not_include_parameters_only_used_in_previous_iterations() = runTestCC { + var isFirstIteration = true + testParameterize( + onFailure = { + val parameters = arguments.map { it.parameter.name } + + assertTrue( + "neverUsedDuringTheCurrentIteration" in parameters == isFirstIteration, + "neverUsedDuringTheCurrentIteration in $parameters != $isFirstIteration" + ) + isFirstIteration = false + } + ) { + val neverUsedDuringTheCurrentIteration by parameterOf(Unit) + + @Suppress("UNUSED_EXPRESSION") + val useParameter by parameterOf( + { neverUsedDuringTheCurrentIteration }, + { }, // Don't use it the second iteration + ) + + useParameter() + + fail() + } + } } diff --git a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt index 3d320ef..df41db2 100644 --- a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt +++ b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt @@ -16,7 +16,12 @@ package com.benwoodworth.parameterize -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail class ParameterizeExceptionSpec { /** @@ -83,43 +88,35 @@ class ParameterizeExceptionSpec { } @Test - @Ignore fun parameter_disappears_on_second_iteration_due_to_external_condition() = runTestCC { - val exception = assertFailsWith { - var shouldDeclareA = true + var shouldDeclareA = true - parameterize { - if (shouldDeclareA) { - val a by parameterOf(1) - } + parameterize { + if (shouldDeclareA) { + val a by parameterOf(1) + } - val b by parameterOf(1, 2) + val b by parameterOf(1, 2) - shouldDeclareA = false - } + shouldDeclareA = false } - - assertEquals("Expected to be declaring `a`, but got `b`", exception.message) + assertEquals(shouldDeclareA, false) } - @Ignore @Test fun parameter_appears_on_second_iteration_due_to_external_condition() = runTestCC { - val exception = assertFailsWith { - var shouldDeclareA = false + var shouldDeclareA = false - parameterize { - if (shouldDeclareA) { - val a by parameterOf(2) - } + parameterize { + if (shouldDeclareA) { + val a by parameterOf(2) + } - val b by parameterOf(1, 2) + val b by parameterOf(1, 2) - shouldDeclareA = true - } + shouldDeclareA = true } - - assertEquals("Expected to be declaring `b`, but got `a`", exception.message) + assertEquals(shouldDeclareA, true) } /* @@ -224,23 +221,15 @@ class ParameterizeExceptionSpec { fun failing_earlier_than_the_previous_iteration() = runTestCC { val nondeterministicFailure = Throwable("Unexpected failure") - val failure = assertFailsWith { - var shouldFail = false + var shouldFail = false - parameterize { - if (shouldFail) throw nondeterministicFailure + parameterize { + if (shouldFail) throw nondeterministicFailure - val iteration by parameter(1..2) + val iteration by parameter(1..2) - shouldFail = true - } + shouldFail = true } - - assertEquals( - "Previous iteration executed to this point successfully, but now failed with the same arguments", - failure.message, - "message" - ) - assertSame(nondeterministicFailure, failure.cause, "cause") + assertTrue(shouldFail) } } diff --git a/src/commonTest/kotlin/ParameterizeFailureSpec.kt b/src/commonTest/kotlin/ParameterizeFailureSpec.kt index 65c2360..c35d811 100644 --- a/src/commonTest/kotlin/ParameterizeFailureSpec.kt +++ b/src/commonTest/kotlin/ParameterizeFailureSpec.kt @@ -30,7 +30,7 @@ class ParameterizeFailureSpec { @Test fun string_representation_should_list_properties_with_the_failure_matching_the_stdlib_result_representation() = - testAll( + testAllCC( "with message" to Throwable("failure"), "without message" to Throwable() ) { failure -> diff --git a/src/commonTest/kotlin/ParameterizeScopeSpec.kt b/src/commonTest/kotlin/ParameterizeScopeSpec.kt index c4af94b..f8d3327 100644 --- a/src/commonTest/kotlin/ParameterizeScopeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeScopeSpec.kt @@ -115,12 +115,13 @@ class ParameterizeScopeSpec { } }*/ - @Ignore @Test - fun parameter_from_lazy_arguments_should_not_be_computed_before_declaring() = runTestCC { - parameterize { - testAll(lazyParameterFunctions) { lazyParameterFunction -> - /*val undeclared by*/ lazyParameterFunction(this@parameterize) { fail("computed") } + fun parameter_from_lazy_arguments_should_be_computed_before_delegation() = runTestCC { + testAll(lazyParameterFunctions) { lazyParameterFunction -> + assertFailsWith("computed") { + parameterize { + lazyParameterFunction(this@parameterize) { throw IllegalStateException("computed") } + } } } } diff --git a/src/commonTest/kotlin/ParameterizeSpec.kt b/src/commonTest/kotlin/ParameterizeSpec.kt index 1252d99..90128f4 100644 --- a/src/commonTest/kotlin/ParameterizeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeSpec.kt @@ -16,7 +16,6 @@ package com.benwoodworth.parameterize -import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -96,13 +95,10 @@ class ParameterizeSpec { } } - @Ignore @Test - fun parameter_should_iterate_to_the_next_argument_while_declaring() = runTestCC { - var state: String - + fun parameter_should_restore_local_state_on_each_iteration() = runTestCC { parameterize { - state = "creating arguments" + var state = "creating arguments" val iterationArguments = Sequence { object : Iterator { var nextArgument = 0 @@ -110,17 +106,16 @@ class ParameterizeSpec { override fun hasNext(): Boolean = nextArgument <= 5 override fun next(): Int { - assertEquals("declaring parameter", state, "state (iteration $nextArgument)") return nextArgument++ } } } - state = "creating parameter" + state = "declaring parameter" val iterationParameter = parameter(iterationArguments) - state = "declaring parameter" val iteration by iterationParameter + assertEquals("declaring parameter", state, "state (iteration $iteration)") state = "using parameter" useParameter(iteration) @@ -182,9 +177,13 @@ class ParameterizeSpec { ) { var string = "" - repeat(3) { + // repeat doesn't work on JS because JS broke its for-loop over IntRange + // optimization. TODO find relevant YouTrack issue + var i = 0 + while(i < 3) { val letter by parameterOf('a', 'b', 'c') string += letter + i++ } string @@ -192,7 +191,7 @@ class ParameterizeSpec { @Test fun parameter_with_no_arguments_should_finish_iteration_early() = testParameterize( - listOf("123", "124", "125", "134", "135", "145", null, "234", "235", "245", null, "345", null, null, null) + listOf("123", "124", "125", "134", "135", "145", "234", "235", "245", "345") ) { // increasing digits val digit1 by parameter(1..5) @@ -205,7 +204,7 @@ class ParameterizeSpec { @Test @Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION") fun unused_parameter_with_no_arguments_should_finish_iteration_early() = testParameterize( - listOf(null) + listOf() ) { val unused by parameterOf() diff --git a/src/commonTest/kotlin/TestUtils.kt b/src/commonTest/kotlin/TestUtils.kt index 2feae1f..f90f011 100644 --- a/src/commonTest/kotlin/TestUtils.kt +++ b/src/commonTest/kotlin/TestUtils.kt @@ -61,13 +61,13 @@ object TestAllScope { throw TestAllSkip(message) } -fun testAll( +suspend fun testAll( testCases: Iterable>, test: suspend TestAllScope.(testCase: T) -> Unit ) { val results = testCases .map { (description, testCase) -> - description to runCatching { runTestCC { TestAllScope.test(testCase) } } + description to runCatching { TestAllScope.test(testCase) } } val passed = results.count { (_, result) -> result.isSuccess } @@ -105,17 +105,30 @@ fun testAll( } } -fun testAll( +fun testAllCC( + testCases: Iterable>, + test: suspend TestAllScope.(testCase: T) -> Unit +) = runTestCC { testAll(testCases, test) } + +suspend fun testAll( vararg testCases: Pair, test: suspend TestAllScope.(testCase: T) -> Unit ): Unit = testAll(testCases.toList(), test) -fun testAll(vararg testCases: Pair Unit>): Unit = +suspend fun testAll(vararg testCases: Pair Unit>): Unit = testAll(testCases.toList()) { testCase -> testCase() } +fun testAllCC( + vararg testCases: Pair, + test: suspend TestAllScope.(testCase: T) -> Unit +): TestResult = runTestCC { testAll(testCases = testCases, test) } + +fun testAllCC(vararg testCases: Pair Unit>): TestResult = + runTestCC { testAll(testCases = testCases) } + inline fun runTestCC( context: CoroutineContext = EmptyCoroutineContext, timeout: Duration? = null, diff --git a/src/commonTest/kotlin/test/EdgeCases.kt b/src/commonTest/kotlin/test/EdgeCases.kt index 7767884..cae29e1 100644 --- a/src/commonTest/kotlin/test/EdgeCases.kt +++ b/src/commonTest/kotlin/test/EdgeCases.kt @@ -16,16 +16,12 @@ package com.benwoodworth.parameterize.test -import com.benwoodworth.parameterize.ParameterizeContinue import com.benwoodworth.parameterize.ParameterizeException import com.benwoodworth.parameterize.ParameterizeState import com.benwoodworth.parameterize.parameterize internal object EdgeCases { val iterationFailures = listOf Throwable>>( - "ParameterizeContinue" to { - ParameterizeContinue - }, "ParameterizeException for same parameterize" to { parameterizeState -> ParameterizeException(parameterizeState, "same parameterize") }, diff --git a/src/jvmTest/kotlin/ParameterizeFailedErrorSpec.jvm.kt b/src/jvmTest/kotlin/ParameterizeFailedErrorSpec.jvm.kt index 9743f3e..5b14e66 100644 --- a/src/jvmTest/kotlin/ParameterizeFailedErrorSpec.jvm.kt +++ b/src/jvmTest/kotlin/ParameterizeFailedErrorSpec.jvm.kt @@ -43,7 +43,7 @@ class ParameterizeFailedErrorSpecJvm { } @Test - fun has_failures_should_be_correct() = testAll( + fun has_failures_should_be_correct() = testAllCC( "empty" to emptyList(), "non-empty" to listOf(Throwable("Failure")) ) { failures -> @@ -66,7 +66,7 @@ class ParameterizeFailedErrorSpecJvm { * it should be suppressed. */ @Test - fun methods_inherited_from_MultipleFailuresError_should_be_hidden_from_the_API() { + fun methods_inherited_from_MultipleFailuresError_should_be_hidden_from_the_API() = runTestCC { fun KClass<*>.inheritableMethods(): List = java.methods .filter { Modifier.isPublic(it.modifiers) } From d10e630f4ec6fba5120a3341edea26a94736f823 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sat, 28 Sep 2024 13:32:50 +0100 Subject: [PATCH 11/18] Implement decorator functionality Restore hasBeenUsed status on each iteration Make DecoratorScope @RestrictsSuspension Remove unused ParameterState iteration functionality --- src/commonMain/kotlin/DecoratorCoroutine.kt | 68 +++++++++ src/commonMain/kotlin/ParameterState.kt | 43 +----- src/commonMain/kotlin/Parameterize.kt | 46 +++--- .../kotlin/ParameterizeConfiguration.kt | 1 + src/commonMain/kotlin/ParameterizeState.kt | 45 +++++- src/commonTest/kotlin/ParameterStateSpec.kt | 138 ------------------ ...ParameterizeConfigurationSpec_decorator.kt | 18 --- .../kotlin/ParameterizeExceptionSpec.kt | 8 +- .../kotlin/ParameterizeScopeSpec.kt | 94 ++++-------- src/commonTest/kotlin/ParameterizeSpec.kt | 4 +- 10 files changed, 163 insertions(+), 302 deletions(-) create mode 100644 src/commonMain/kotlin/DecoratorCoroutine.kt delete mode 100644 src/commonTest/kotlin/ParameterStateSpec.kt diff --git a/src/commonMain/kotlin/DecoratorCoroutine.kt b/src/commonMain/kotlin/DecoratorCoroutine.kt new file mode 100644 index 0000000..f815b37 --- /dev/null +++ b/src/commonMain/kotlin/DecoratorCoroutine.kt @@ -0,0 +1,68 @@ +package com.benwoodworth.parameterize + +import com.benwoodworth.parameterize.ParameterizeConfiguration.DecoratorScope +import kotlin.coroutines.Continuation +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.intrinsics.createCoroutineUnintercepted +import kotlin.coroutines.resume + + +/** + * The [decorator][ParameterizeConfiguration.decorator] suspends for the iteration so that the one lambda can be run as + * two separate parts, without needing to wrap the (inlined) [parameterize] block. + */ +internal class DecoratorCoroutine( + private val parameterizeState: ParameterizeState, + private val configuration: ParameterizeConfiguration +) { + private val scope = DecoratorScope(parameterizeState) + + private var continueAfterIteration: Continuation? = null + private var completed = false + + private val iteration: suspend DecoratorScope.() -> Unit = { + parameterizeState.checkState(continueAfterIteration == null) { + "Decorator must invoke the iteration function exactly once, but was invoked twice" + } + + suspendDecorator { continueAfterIteration = it } + isLastIteration = !parameterizeState.hasNextArgumentCombination + } + + fun beforeIteration() { + check(!completed) { "Decorator already completed" } + + val invokeDecorator: suspend DecoratorScope.() -> Unit = { + configuration.decorator(this, iteration) + } + + invokeDecorator + .createCoroutineUnintercepted( + receiver = scope, + completion = Continuation(EmptyCoroutineContext) { + completed = true + it.getOrThrow() + } + ) + .resume(Unit) + + parameterizeState.checkState(continueAfterIteration != null) { + if (completed) { + "Decorator must invoke the iteration function exactly once, but was not invoked" + } else { + "Decorator suspended unexpectedly" + } + } + } + + fun afterIteration() { + check(!completed) { "Decorator already completed" } + + continueAfterIteration?.resume(Unit) + ?: error("Iteration not invoked") + + parameterizeState.checkState(completed) { + "Decorator suspended unexpectedly" + } + } +} diff --git a/src/commonMain/kotlin/ParameterState.kt b/src/commonMain/kotlin/ParameterState.kt index d5daa89..5d741fc 100644 --- a/src/commonMain/kotlin/ParameterState.kt +++ b/src/commonMain/kotlin/ParameterState.kt @@ -20,41 +20,16 @@ import kotlin.reflect.KProperty /** * The parameter state is responsible for managing a parameter in the - * [parameterize] DSL, maintaining an argument, and lazily loading the next ones - * in as needed. + * [parameterize] DSL and maintaining an argument. * * When first declared, the parameter [property] it was - * declared with will be stored, along with a new argument iterator and the - * first argument from it. The arguments are lazily read in from the iterator as - * they're needed, using [isLastArgument] as an indicator. The stored iterator - * will always have a next argument available, and will be set to null when its - * last argument is read in to release its reference + * declared with will be stored, along with the argument. * */ -internal class ParameterState(argumentIterator: Iterator, val isLast: Boolean = false) { - /** - * Set up the delegate with the given [arguments]. - */ - constructor(arguments: Sequence, isLast: Boolean = false) : this(arguments.iterator(), isLast) - - init { - if (!argumentIterator.hasNext()) TODO() // Before changing any state - } - - var argument: T = argumentIterator.next() - private set +internal class ParameterState(val argument: T, val isLast: Boolean = false) { var property: KProperty? = null - private var argumentIterator: Iterator? = argumentIterator.takeIf { it.hasNext() } var hasBeenUsed: Boolean = false - private set - - /** - * @throws IllegalStateException if used before the argument has been declared. - */ - val isLastArgument: Boolean - get() = argumentIterator == null - /** * Returns a string representation of the current argument, or a "not declared" message. @@ -65,18 +40,6 @@ internal class ParameterState(argumentIterator: Iterator, val isLast: Bool hasBeenUsed = true } - /** - * Iterates the parameter argument. - * - * @throws IllegalStateException if the argument has not been declared yet. - */ - fun nextArgument() { - val iterator = argumentIterator ?: error("Cannot iterate arguments before parameter has been declared") - - argument = iterator.next() - argumentIterator = iterator.takeIf { it.hasNext() } - } - /** * Returns the property and argument. * diff --git a/src/commonMain/kotlin/Parameterize.kt b/src/commonMain/kotlin/Parameterize.kt index 866497b..bd988a5 100644 --- a/src/commonMain/kotlin/Parameterize.kt +++ b/src/commonMain/kotlin/Parameterize.kt @@ -87,23 +87,35 @@ import kotlin.reflect.KProperty public suspend fun parameterize( configuration: ParameterizeConfiguration = ParameterizeConfiguration.default, block: suspend ParameterizeScope.() -> Unit -): Unit = with(ParameterizeScope(ParameterizeState(HandlerPrompt()))) { +): Unit = with(ParameterizeScope(ParameterizeState(HandlerPrompt(), configuration))) { // Exercise extreme caution modifying this code, since the iterator is sensitive to the behavior of this function. // Code inlined from a previous version could have subtly different semantics when interacting with the runtime // iterator of a later release, and would be major breaking change that's difficult to detect. - handle breakEarly@{ + breakEarly { parameterizeState.handle { - try { - block() - } catch (failure: Throwable) { + parameterizeState.beforeEach() + val result = runCatching { block() } + parameterizeState.afterEach() + + result.onFailure { failure -> val result = parameterizeState.handleFailure(configuration.onFailure, failure) - if (result.breakEarly) this@breakEarly.discardWithFast(Result.success(Unit)) + if (result.breakEarly) breakEarly() } } } parameterizeState.handleComplete(configuration.onComplete) } +internal fun interface Breakable { + suspend fun breakEarly(): Nothing +} + +internal suspend inline fun breakEarly(crossinline block: suspend Breakable.() -> Unit) = handle { + block { + discardWithFast(Result.success(Unit)) + } +} + /** * Calls [parameterize] with a copy of the [configuration] that has options overridden. * @@ -165,27 +177,13 @@ public class ParameterizeScope internal constructor( /** @suppress */ public operator fun ParameterDelegate.getValue(thisRef: Any?, property: KProperty<*>): T { parameterState.useArgument() - return argument + return parameterState.argument } - - /** - * @constructor - * **Experimental:** Prefer using the scope-limited [parameter] function, if possible. - * The constructor will be made `@PublishedApi internal` once - * [context parameters](https://github.com/Kotlin/KEEP/issues/367) are introduced to the language. - * - * @suppress - */ - @JvmInline - public value class Parameter @ExperimentalParameterizeApi constructor( - public val arguments: Sequence - ) - /** @suppress */ - public class ParameterDelegate internal constructor( + @JvmInline + public value class ParameterDelegate internal constructor( internal val parameterState: ParameterState, - internal val argument: T ) { /** * Returns a string representation of the current argument. @@ -196,7 +194,7 @@ public class ParameterizeScope internal constructor( * ``` */ override fun toString(): String = - argument.toString() + parameterState.argument.toString() } } diff --git a/src/commonMain/kotlin/ParameterizeConfiguration.kt b/src/commonMain/kotlin/ParameterizeConfiguration.kt index bd7ad5c..6b0c0d5 100644 --- a/src/commonMain/kotlin/ParameterizeConfiguration.kt +++ b/src/commonMain/kotlin/ParameterizeConfiguration.kt @@ -136,6 +136,7 @@ public class ParameterizeConfiguration internal constructor( } /** @see Builder.decorator */ + @RestrictsSuspension public class DecoratorScope internal constructor( private val parameterizeState: ParameterizeState ) { diff --git a/src/commonMain/kotlin/ParameterizeState.kt b/src/commonMain/kotlin/ParameterizeState.kt index a06184c..9efdd86 100644 --- a/src/commonMain/kotlin/ParameterizeState.kt +++ b/src/commonMain/kotlin/ParameterizeState.kt @@ -26,7 +26,7 @@ import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.jvm.JvmInline -internal class ParameterizeState(p: HandlerPrompt) : Handler by p { +internal class ParameterizeState(p: HandlerPrompt, val configuration: ParameterizeConfiguration) : Handler by p { /** * The parameters created for [parameterize]. */ @@ -36,25 +36,41 @@ internal class ParameterizeState(p: HandlerPrompt) : Handler by p { private var skipCount = 0L private var failureCount = 0L private val recordedFailures = mutableListOf() + private var decoratorCoroutine: DecoratorCoroutine? = null val isFirstIteration: Boolean - get() = iterationCount == 1L + get() = iterationCount == 0L + + val hasNextArgumentCombination get() = parameters.any { !it.isLast } suspend fun declareParameter( arguments: Sequence ): ParameterDelegate = use { resume -> - // TODO skip calling decorator on first iteration, but call it on the rest - // and also call it for the top-most iteration. val iterator = arguments.iterator() - if (!iterator.hasNext()) return@use + if (!iterator.hasNext()) { + afterEach() + return@use + } + var isFirstIteration = true while (true) { iterationCount++ val argument = iterator.next() val isLast = !iterator.hasNext() - val parameter = ParameterState(sequenceOf(argument), isLast) + val parameter = ParameterState(argument, isLast) + if(isFirstIteration) { + isFirstIteration = false + } else { + beforeEach() + } + val hasBeenUsed = BooleanArray(parameters.size) { + parameters[it].hasBeenUsed + } parameters.add(parameter) - resume(ParameterDelegate(parameter, argument)) + resume(ParameterDelegate(parameter)) check(parameters.removeLast() == parameter) { "Unexpected last parameter" } + parameters.forEachIndexed { i, parameter -> + parameter.hasBeenUsed = hasBeenUsed[i] + } if (isLast) break } } @@ -103,7 +119,7 @@ internal class ParameterizeState(p: HandlerPrompt) : Handler by p { iterationCount, skipCount, failureCount, - completedEarly = parameters.any { !it.isLast }, + completedEarly = hasNextArgumentCombination, recordedFailures, ) @@ -111,4 +127,17 @@ internal class ParameterizeState(p: HandlerPrompt) : Handler by p { onComplete() } } + + internal fun beforeEach() { + decoratorCoroutine = DecoratorCoroutine(this, configuration) + .also { it.beforeIteration() } + } + + internal fun afterEach() { + val decoratorCoroutine = checkNotNull(decoratorCoroutine) { "${::decoratorCoroutine.name} was null" } + + decoratorCoroutine.afterIteration() + + this.decoratorCoroutine = null + } } diff --git a/src/commonTest/kotlin/ParameterStateSpec.kt b/src/commonTest/kotlin/ParameterStateSpec.kt deleted file mode 100644 index dbb4631..0000000 --- a/src/commonTest/kotlin/ParameterStateSpec.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2024 Ben Woodworth - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.benwoodworth.parameterize - -import kotlin.test.* - -class ParameterStateSpec { - private val property: String get() = error("${::property.name} is not meant to be used") - - @Test - fun string_representation_when_initialized_should_equal_that_of_the_current_argument() { - val argument = "argument" - val parameter = ParameterState(sequenceOf(argument)) - - assertSame(argument, parameter.toString()) - } - - @Test - fun declare_should_immediately_get_the_first_argument() { - var gotFirstArgument = false - - val arguments = Sequence { - gotFirstArgument = true - listOf(Unit).iterator() - } - - ParameterState(arguments) - assertTrue(gotFirstArgument, "gotFirstArgument") - } - - @Test - fun declare_should_not_immediately_get_the_second_argument() { - class AssertingIterator : Iterator { - var nextArgument = 1 - - override fun hasNext(): Boolean = - nextArgument <= 2 - - override fun next(): String { - assertNotEquals(2, nextArgument, "should not get argument 2") - - return "argument $nextArgument" - .also { nextArgument++ } - } - } - - val parameter = ParameterState(Sequence(::AssertingIterator)) - } - - @Test - fun declare_with_one_argument_should_set_is_last_argument_to_true() { - val parameter = ParameterState(sequenceOf("first")) - - assertTrue(parameter.isLastArgument) - } - - @Test - fun declare_with_more_than_one_argument_should_set_is_last_argument_to_false() { - val parameter = ParameterState(sequenceOf("first", "second")) - - assertFalse(parameter.isLastArgument) - } - - @Test - fun getting_argument_should_initially_return_the_first_argument() { - val parameter = ParameterState(sequenceOf("first", "second")) - - assertEquals("first", parameter.argument) - } - - @Test - fun use_argument_should_set_has_been_used_to_true() { - val parameter = ParameterState(sequenceOf("first", "second")) - parameter.useArgument() - - assertTrue(parameter.hasBeenUsed) - } - - @Test - fun next_should_move_to_the_next_argument() { - val parameter = ParameterState(sequenceOf("first", "second", "third")) - parameter.argument - - parameter.nextArgument() - assertEquals("second", parameter.argument) - - parameter.nextArgument() - assertEquals("third", parameter.argument) - } - - @Test - fun next_to_a_middle_argument_should_leave_is_last_argument_as_false() { - val parameter = ParameterState(sequenceOf("first", "second", "third", "fourth")) - - parameter.nextArgument() - assertFalse(parameter.isLastArgument, "second") - - parameter.nextArgument() - assertFalse(parameter.isLastArgument, "third") - } - - @Test - fun next_to_the_last_argument_should_set_is_last_argument_to_true() { - val parameter = ParameterState(sequenceOf("first", "second", "third", "fourth")) - parameter.nextArgument() // second - parameter.nextArgument() // third - parameter.nextArgument() // forth - - assertTrue(parameter.isLastArgument) - } - - @Test - fun get_failure_argument_when_declared_should_have_correct_property_and_argument() { - val expectedArgument = "a" - val parameter = ParameterState(sequenceOf(expectedArgument)) - val propReference = ::property - parameter.property = propReference - parameter.argument - - val (property, argument) = parameter.getFailureArgument() - assertEquals(propReference, property) - assertSame(expectedArgument, argument) - } -} diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt index 113c146..af377b2 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec_decorator.kt @@ -257,22 +257,4 @@ class ParameterizeConfigurationSpec_decorator { "message" ) } - - @Test - fun declaring_parameter_after_iteration_function_should_fail() = runTestCC { - assertFailsWith { - lateinit var declareParameter: suspend () -> Unit - - testParameterize( - decorator = { iteration -> - iteration() - declareParameter() - } - ) { - declareParameter = { - val parameter by parameterOf(Unit) - } - } - } - } } diff --git a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt index df41db2..49785a4 100644 --- a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt +++ b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt @@ -198,10 +198,11 @@ class ParameterizeExceptionSpec { exception.message ) } +*/ @Test fun declaring_parameter_after_iteration_completed() = runTestCC { - var declareParameter = {} + var declareParameter = suspend {} parameterize { declareParameter = { @@ -209,13 +210,10 @@ class ParameterizeExceptionSpec { } } - val failure = assertFailsWith { + val failure = assertFailsWith { declareParameter() } - - assertEquals("Cannot declare parameter `parameter` after its iteration has completed", failure.message) } -*/ @Test fun failing_earlier_than_the_previous_iteration() = runTestCC { diff --git a/src/commonTest/kotlin/ParameterizeScopeSpec.kt b/src/commonTest/kotlin/ParameterizeScopeSpec.kt index f8d3327..a7699c4 100644 --- a/src/commonTest/kotlin/ParameterizeScopeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeScopeSpec.kt @@ -16,11 +16,13 @@ package com.benwoodworth.parameterize -import com.benwoodworth.parameterize.ParameterizeScope.Parameter import com.benwoodworth.parameterize.ParameterizeScope.ParameterDelegate import com.benwoodworth.parameterize.ParameterizeScopeSpec.LazyParameterFunction.LazyArguments import kotlin.properties.PropertyDelegateProvider -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertSame /** * Specifies the [parameterize] DSL and its syntax. @@ -35,55 +37,25 @@ class ParameterizeScopeSpec { override fun next(): Nothing = throw NoSuchElementException() } -/* @Test - fun parameter_from_sequence_should_be_constructed_with_the_same_arguments_instance() = runTestCC { - parameterize { - val sequence = sequenceOf() - val parameter = parameter(sequence) - - assertSame(sequence, parameter.arguments) - } - } - - @Test - fun parameter_from_iterable_should_have_the_correct_arguments() = runTestCC { - parameterize { - val parameter = parameter(Iterable { ArgumentIterator }) - - assertSame(ArgumentIterator, parameter.arguments.iterator()) - } - } - - @Test - fun parameter_of_listed_arguments_should_have_the_correct_arguments() = runTestCC { - parameterize { - data class UniqueArgument(val argument: String) - - val listedArguments = listOf( - UniqueArgument("A"), - UniqueArgument("B"), - UniqueArgument("C") - ) - - val parameter = parameterOf(*listedArguments.toTypedArray()) - - assertContentEquals(listedArguments.asSequence(), parameter.arguments) - } - }*/ - /** * The lazy `parameter {}` functions should have the same behavior, so this provides an abstraction that a test can * use to specify for all the lazy overloads parametrically. */ private interface LazyParameterFunction { - suspend operator fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): ParameterDelegate + suspend operator fun invoke( + scope: ParameterizeScope, + lazyArguments: () -> LazyArguments + ): ParameterDelegate class LazyArguments(val createIterator: () -> Iterator) } private val lazyParameterFunctions = listOf( "from sequence" to object : LazyParameterFunction { - override suspend fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): ParameterDelegate = + override suspend fun invoke( + scope: ParameterizeScope, + lazyArguments: () -> LazyArguments + ): ParameterDelegate = with(scope) { parameter { val arguments = lazyArguments() @@ -92,7 +64,10 @@ class ParameterizeScopeSpec { } }, "from iterable" to object : LazyParameterFunction { - override suspend fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): ParameterDelegate = + override suspend fun invoke( + scope: ParameterizeScope, + lazyArguments: () -> LazyArguments + ): ParameterDelegate = with(scope) { parameter { val arguments = lazyArguments() @@ -102,19 +77,6 @@ class ParameterizeScopeSpec { } ) - /* @Test - fun parameter_from_lazy_arguments_should_have_the_correct_arguments() = runTestCC { - parameterize { - testAll(lazyParameterFunctions) { lazyParameterFunction -> - val lazyParameter = lazyParameterFunction(this@parameterize) { - LazyArguments { ArgumentIterator } - } - - assertSame(ArgumentIterator, lazyParameter.arguments.iterator()) - } - } - }*/ - @Test fun parameter_from_lazy_arguments_should_be_computed_before_delegation() = runTestCC { testAll(lazyParameterFunctions) { lazyParameterFunction -> @@ -126,26 +88,24 @@ class ParameterizeScopeSpec { } } -/* @Test + @Test fun parameter_from_lazy_argument_iterable_should_only_be_computed_once() = runTestCC { - parameterize { - testAll(lazyParameterFunctions) { lazyParameterFunction -> - var evaluationCount = 0 - - val lazyParameter = lazyParameterFunction(this@parameterize) { + testAll(lazyParameterFunctions) { lazyParameterFunction -> + var currentIteration = 0 + var evaluationCount = 0 + parameterize { + val lazyParameter by lazyParameterFunction(this@parameterize) { evaluationCount++ LazyArguments { (1..10).iterator() } } - repeat(5) { i -> - val arguments = lazyParameter.arguments.toList() - assertEquals((1..10).toList(), arguments, "Iteration #$i") - } + assertEquals(currentIteration + 1, lazyParameter, "Iteration #$currentIteration") assertEquals(1, evaluationCount) + currentIteration++ } } - }*/ + } @Test fun string_representation_should_show_used_parameter_arguments_in_declaration_order() = runTestCC { @@ -168,7 +128,7 @@ class ParameterizeScopeSpec { @Test fun parameter_delegate_string_representation_when_declared_should_equal_that_of_the_current_argument() = runTestCC { parameterize { - lateinit var delegate: ParameterDelegate + var delegate: ParameterDelegate? = null val argument = parameterOf("argument") val parameter by PropertyDelegateProvider { thisRef: Nothing?, property -> argument @@ -176,7 +136,7 @@ class ParameterizeScopeSpec { .also { delegate = it } // intercept delegate } - assertSame(parameter, delegate.toString()) + assertSame(parameter, delegate!!.toString()) } } } diff --git a/src/commonTest/kotlin/ParameterizeSpec.kt b/src/commonTest/kotlin/ParameterizeSpec.kt index 90128f4..a2aa10f 100644 --- a/src/commonTest/kotlin/ParameterizeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeSpec.kt @@ -274,7 +274,7 @@ class ParameterizeSpec { } } -/* @Test +/* @Test TODO fun should_be_able_to_return_from_an_outer_function_from_within_the_block() = runTestCC { parameterize { return@should_be_able_to_return_from_an_outer_function_from_within_the_block @@ -284,7 +284,7 @@ class ParameterizeSpec { /** * The motivating use case here is decorating a Kotest test group, in which the test declarations suspend. */ -/* @Test +/* @Test TODO fun should_be_able_to_decorate_a_suspend_block() { val coordinates = sequence { parameterize { From a445557fbd46bfad8834119f7d7075e832ca17d1 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sat, 28 Sep 2024 15:45:11 +0100 Subject: [PATCH 12/18] Refactor parameterize Add ParameterizeDecorator Fix testing for empty iterations --- src/commonMain/kotlin/DecoratorCoroutine.kt | 68 --------- src/commonMain/kotlin/Parameterize.kt | 30 ++-- .../kotlin/ParameterizeDecorator.kt | 143 ++++++++++++++++++ src/commonMain/kotlin/ParameterizeState.kt | 81 ++++------ ...arameterizeConfigurationSpec_onComplete.kt | 16 ++ src/commonTest/kotlin/ParameterizeSpec.kt | 20 +-- 6 files changed, 205 insertions(+), 153 deletions(-) delete mode 100644 src/commonMain/kotlin/DecoratorCoroutine.kt create mode 100644 src/commonMain/kotlin/ParameterizeDecorator.kt diff --git a/src/commonMain/kotlin/DecoratorCoroutine.kt b/src/commonMain/kotlin/DecoratorCoroutine.kt deleted file mode 100644 index f815b37..0000000 --- a/src/commonMain/kotlin/DecoratorCoroutine.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.benwoodworth.parameterize - -import com.benwoodworth.parameterize.ParameterizeConfiguration.DecoratorScope -import kotlin.coroutines.Continuation -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.intrinsics.createCoroutineUnintercepted -import kotlin.coroutines.resume - - -/** - * The [decorator][ParameterizeConfiguration.decorator] suspends for the iteration so that the one lambda can be run as - * two separate parts, without needing to wrap the (inlined) [parameterize] block. - */ -internal class DecoratorCoroutine( - private val parameterizeState: ParameterizeState, - private val configuration: ParameterizeConfiguration -) { - private val scope = DecoratorScope(parameterizeState) - - private var continueAfterIteration: Continuation? = null - private var completed = false - - private val iteration: suspend DecoratorScope.() -> Unit = { - parameterizeState.checkState(continueAfterIteration == null) { - "Decorator must invoke the iteration function exactly once, but was invoked twice" - } - - suspendDecorator { continueAfterIteration = it } - isLastIteration = !parameterizeState.hasNextArgumentCombination - } - - fun beforeIteration() { - check(!completed) { "Decorator already completed" } - - val invokeDecorator: suspend DecoratorScope.() -> Unit = { - configuration.decorator(this, iteration) - } - - invokeDecorator - .createCoroutineUnintercepted( - receiver = scope, - completion = Continuation(EmptyCoroutineContext) { - completed = true - it.getOrThrow() - } - ) - .resume(Unit) - - parameterizeState.checkState(continueAfterIteration != null) { - if (completed) { - "Decorator must invoke the iteration function exactly once, but was not invoked" - } else { - "Decorator suspended unexpectedly" - } - } - } - - fun afterIteration() { - check(!completed) { "Decorator already completed" } - - continueAfterIteration?.resume(Unit) - ?: error("Iteration not invoked") - - parameterizeState.checkState(completed) { - "Decorator suspended unexpectedly" - } - } -} diff --git a/src/commonMain/kotlin/Parameterize.kt b/src/commonMain/kotlin/Parameterize.kt index bd988a5..bf3b45b 100644 --- a/src/commonMain/kotlin/Parameterize.kt +++ b/src/commonMain/kotlin/Parameterize.kt @@ -22,7 +22,6 @@ package com.benwoodworth.parameterize import com.benwoodworth.parameterize.ParameterizeConfiguration.DecoratorScope import com.benwoodworth.parameterize.ParameterizeConfiguration.OnCompleteScope import com.benwoodworth.parameterize.ParameterizeConfiguration.OnFailureScope -import effekt.HandlerPrompt import effekt.discardWithFast import effekt.handle import kotlin.contracts.InvocationKind @@ -87,30 +86,24 @@ import kotlin.reflect.KProperty public suspend fun parameterize( configuration: ParameterizeConfiguration = ParameterizeConfiguration.default, block: suspend ParameterizeScope.() -> Unit -): Unit = with(ParameterizeScope(ParameterizeState(HandlerPrompt(), configuration))) { +): Unit = with(ParameterizeState()) { // Exercise extreme caution modifying this code, since the iterator is sensitive to the behavior of this function. // Code inlined from a previous version could have subtly different semantics when interacting with the runtime // iterator of a later release, and would be major breaking change that's difficult to detect. breakEarly { - parameterizeState.handle { - parameterizeState.beforeEach() - val result = runCatching { block() } - parameterizeState.afterEach() - - result.onFailure { failure -> - val result = parameterizeState.handleFailure(configuration.onFailure, failure) - if (result.breakEarly) breakEarly() - } - } + withDecorator(configuration.decorator, onFailure = { failure -> + val result = handleFailure(configuration.onFailure, failure) + if (result.breakEarly) breakEarly() + }, block) } - parameterizeState.handleComplete(configuration.onComplete) + handleComplete(configuration.onComplete) } -internal fun interface Breakable { +private fun interface Breakable { suspend fun breakEarly(): Nothing } -internal suspend inline fun breakEarly(crossinline block: suspend Breakable.() -> Unit) = handle { +private suspend inline fun breakEarly(crossinline block: suspend Breakable.() -> Unit) = handle { block { discardWithFast(Result.success(Unit)) } @@ -151,9 +144,10 @@ public suspend fun parameterize( /** @see parameterize */ @ParameterizeDsl -public class ParameterizeScope internal constructor( - internal val parameterizeState: ParameterizeState, +public class ParameterizeScope @PublishedApi internal constructor( + internal val parameterizeIterator: ParameterizeDecorator, ) { + internal val parameterizeState get() = parameterizeIterator.parameterizeState /** @suppress */ override fun toString(): String = @@ -208,7 +202,7 @@ public class ParameterizeScope internal constructor( @Suppress("UnusedReceiverParameter") // Should only be accessible within parameterize scopes public suspend fun ParameterizeScope.parameter(arguments: Sequence): ParameterizeScope.ParameterDelegate = @OptIn(ExperimentalParameterizeApi::class) - parameterizeState.declareParameter(arguments) + parameterizeIterator.declareParameter(arguments) /** * Declare a parameter with the given [arguments]. diff --git a/src/commonMain/kotlin/ParameterizeDecorator.kt b/src/commonMain/kotlin/ParameterizeDecorator.kt new file mode 100644 index 0000000..c3a3445 --- /dev/null +++ b/src/commonMain/kotlin/ParameterizeDecorator.kt @@ -0,0 +1,143 @@ +package com.benwoodworth.parameterize + +import com.benwoodworth.parameterize.ParameterizeConfiguration.DecoratorScope +import com.benwoodworth.parameterize.ParameterizeScope.ParameterDelegate +import effekt.Handler +import effekt.HandlerPrompt +import effekt.handle +import effekt.use +import kotlin.coroutines.Continuation +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.intrinsics.createCoroutineUnintercepted +import kotlin.coroutines.resume + +internal class ParameterizeDecorator( + internal val parameterizeState: ParameterizeState, + private val decorator: suspend DecoratorScope.(iteration: suspend DecoratorScope.() -> Unit) -> Unit, + p: HandlerPrompt +) : Handler by p { + private var decoratorCoroutine: DecoratorCoroutine? = null + + internal fun beforeEach() { + check(decoratorCoroutine == null) { "${::decoratorCoroutine.name} was improperly finished" } + decoratorCoroutine = DecoratorCoroutine(parameterizeState, decorator) + .also { it.beforeIteration() } + } + + internal fun afterEach() { + decoratorCoroutine?.afterIteration() ?: error("${::decoratorCoroutine.name} was null") + decoratorCoroutine = null + } + + suspend fun declareParameter( + arguments: Sequence + ): ParameterDelegate = use { resume -> + arguments.forEachWithIterations(onEmpty = { + afterEach() + return@use + }) { isFirst, isLast, argument -> + parameterizeState.newIteration() + if (!isFirst) beforeEach() + + val parameter = ParameterState(argument, isLast) + parameterizeState.preservingHasBeenUsed { + parameterizeState.withParameter(parameter) { + resume(ParameterDelegate(parameter)) + } + } + } + } +} + +private inline fun Sequence.forEachWithIterations( + onEmpty: () -> Unit, + block: (isFirst: Boolean, isLast: Boolean, T) -> Unit +) { + val iterator = iterator() + if (!iterator.hasNext()) { + onEmpty() + return + } + var isFirstIteration = true + var isLastIteration: Boolean + do { + val element = iterator.next() + isLastIteration = !iterator.hasNext() + block(isFirstIteration, isLastIteration, element) + if (isFirstIteration) { + isFirstIteration = false + } + } while (!isLastIteration) +} + +internal suspend fun ParameterizeState.withDecorator( + decorator: suspend DecoratorScope.(iteration: suspend DecoratorScope.() -> Unit) -> Unit, + onFailure: suspend (Throwable) -> Unit, + block: suspend ParameterizeScope.() -> Unit, +): Unit = handle { + val decorator = ParameterizeDecorator(this@withDecorator, decorator, this) + decorator.beforeEach() + val result = runCatching { block(ParameterizeScope(decorator)) } + decorator.afterEach() + result.onFailure { onFailure(it) } +} + +/** + * The [decorator][ParameterizeConfiguration.decorator] suspends for the iteration so that the one lambda can be run as + * two separate parts, without needing to wrap the (inlined) [parameterize] block. + */ +private class DecoratorCoroutine( + private val parameterizeState: ParameterizeState, + private val decorator: suspend DecoratorScope.(iteration: suspend DecoratorScope.() -> Unit) -> Unit +) { + private val scope = DecoratorScope(parameterizeState) + + private var continueAfterIteration: Continuation? = null + private var completed = false + + private val iteration: suspend DecoratorScope.() -> Unit = { + parameterizeState.checkState(continueAfterIteration == null) { + "Decorator must invoke the iteration function exactly once, but was invoked twice" + } + + suspendDecorator { continueAfterIteration = it } + isLastIteration = !parameterizeState.hasNextArgumentCombination + } + + fun beforeIteration() { + check(!completed) { "Decorator already completed" } + + val invokeDecorator: suspend DecoratorScope.() -> Unit = { + decorator(this, iteration) + } + + invokeDecorator + .createCoroutineUnintercepted( + receiver = scope, + completion = Continuation(EmptyCoroutineContext) { + completed = true + it.getOrThrow() + } + ) + .resume(Unit) + + parameterizeState.checkState(continueAfterIteration != null) { + if (completed) { + "Decorator must invoke the iteration function exactly once, but was not invoked" + } else { + "Decorator suspended unexpectedly" + } + } + } + + fun afterIteration() { + check(!completed) { "Decorator already completed" } + + continueAfterIteration?.resume(Unit) + ?: error("Iteration not invoked") + + parameterizeState.checkState(completed) { + "Decorator suspended unexpectedly" + } + } +} diff --git a/src/commonMain/kotlin/ParameterizeState.kt b/src/commonMain/kotlin/ParameterizeState.kt index 9efdd86..5dbf8bc 100644 --- a/src/commonMain/kotlin/ParameterizeState.kt +++ b/src/commonMain/kotlin/ParameterizeState.kt @@ -18,15 +18,11 @@ package com.benwoodworth.parameterize import com.benwoodworth.parameterize.ParameterizeConfiguration.OnCompleteScope import com.benwoodworth.parameterize.ParameterizeConfiguration.OnFailureScope -import com.benwoodworth.parameterize.ParameterizeScope.ParameterDelegate -import effekt.Handler -import effekt.HandlerPrompt -import effekt.use import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.jvm.JvmInline -internal class ParameterizeState(p: HandlerPrompt, val configuration: ParameterizeConfiguration) : Handler by p { +internal class ParameterizeState { /** * The parameters created for [parameterize]. */ @@ -36,48 +32,12 @@ internal class ParameterizeState(p: HandlerPrompt, val configuration: Para private var skipCount = 0L private var failureCount = 0L private val recordedFailures = mutableListOf() - private var decoratorCoroutine: DecoratorCoroutine? = null val isFirstIteration: Boolean get() = iterationCount == 0L val hasNextArgumentCombination get() = parameters.any { !it.isLast } - suspend fun declareParameter( - arguments: Sequence - ): ParameterDelegate = use { resume -> - val iterator = arguments.iterator() - if (!iterator.hasNext()) { - afterEach() - return@use - } - var isFirstIteration = true - while (true) { - iterationCount++ - val argument = iterator.next() - val isLast = !iterator.hasNext() - val parameter = ParameterState(argument, isLast) - if(isFirstIteration) { - isFirstIteration = false - } else { - beforeEach() - } - val hasBeenUsed = BooleanArray(parameters.size) { - parameters[it].hasBeenUsed - } - parameters.add(parameter) - resume(ParameterDelegate(parameter)) - check(parameters.removeLast() == parameter) { "Unexpected last parameter" } - parameters.forEachIndexed { i, parameter -> - parameter.hasBeenUsed = hasBeenUsed[i] - } - if (isLast) break - } - } - - fun handleContinue() { - skipCount++ - } /** * Get a list of used arguments for reporting a failure. */ @@ -89,8 +49,32 @@ internal class ParameterizeState(p: HandlerPrompt, val configuration: Para @JvmInline value class HandleFailureResult(val breakEarly: Boolean) + fun newIteration() { + iterationCount++ + } + + fun handleContinue() { + skipCount++ + } + + inline fun withParameter(parameter: ParameterState, block: () -> Unit) { + parameters.add(parameter) + block() + check(parameters.removeLast() == parameter) { "Unexpected last parameter" } + } + + inline fun preservingHasBeenUsed(block: () -> Unit) { + val hasBeenUsed = BooleanArray(parameters.size) { + parameters[it].hasBeenUsed + } + block() + parameters.forEachIndexed { index, parameter -> + parameter.hasBeenUsed = hasBeenUsed[index] + } + } + fun handleFailure(onFailure: OnFailureScope.(Throwable) -> Unit, failure: Throwable): HandleFailureResult { - if(failure is ParameterizeException && failure.parameterizeState === this) throw failure + if (failure is ParameterizeException && failure.parameterizeState === this) throw failure failureCount++ val scope = OnFailureScope( @@ -127,17 +111,4 @@ internal class ParameterizeState(p: HandlerPrompt, val configuration: Para onComplete() } } - - internal fun beforeEach() { - decoratorCoroutine = DecoratorCoroutine(this, configuration) - .also { it.beforeIteration() } - } - - internal fun afterEach() { - val decoratorCoroutine = checkNotNull(decoratorCoroutine) { "${::decoratorCoroutine.name} was null" } - - decoratorCoroutine.afterIteration() - - this.decoratorCoroutine = null - } } diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt index 5f9919e..92a07d0 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt @@ -117,6 +117,22 @@ class ParameterizeConfigurationSpec_onComplete { } } + @Test + fun iteration_count_should_be_correct_with_empty() = runTestCC { + var expectedIterationCount = 0L + + testParameterize( + onComplete = { + assertEquals(expectedIterationCount, iterationCount) + } + ) { + val iteration by parameter(0..100) + + expectedIterationCount++ + val empty by parameterOf() + } + } + @Test fun iteration_count_should_be_correct_with_break() = runTestCC { var expectedIterationCount = 0L diff --git a/src/commonTest/kotlin/ParameterizeSpec.kt b/src/commonTest/kotlin/ParameterizeSpec.kt index a2aa10f..2514dbf 100644 --- a/src/commonTest/kotlin/ParameterizeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeSpec.kt @@ -36,13 +36,11 @@ class ParameterizeSpec { ) = runTestCC { val iterations = mutableListOf() - parameterize { - try { - iterations += block() - } catch (caught: Throwable) { - iterations += null - throw caught - } + parameterize(decorator = { + iterations += null + it() + }) { + block().also { iterations[iterations.lastIndex] = it } } assertEquals(expectedIterations.toList(), iterations, "Incorrect iterations") @@ -191,7 +189,7 @@ class ParameterizeSpec { @Test fun parameter_with_no_arguments_should_finish_iteration_early() = testParameterize( - listOf("123", "124", "125", "134", "135", "145", "234", "235", "245", "345") + listOf("123", "124", "125", "134", "135", "145", null, "234", "235", "245", null, "345", null, null, null) ) { // increasing digits val digit1 by parameter(1..5) @@ -202,13 +200,11 @@ class ParameterizeSpec { } @Test - @Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION") fun unused_parameter_with_no_arguments_should_finish_iteration_early() = testParameterize( - listOf() + listOf(null) ) { val unused by parameterOf() - - "finished" + unused } @Test From 569d6a45b30871d8e8b6854c57a3566461864ddf Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sat, 28 Sep 2024 15:52:30 +0100 Subject: [PATCH 13/18] Move parameterize implementation to the overload with lambda arguments --- src/commonMain/kotlin/Parameterize.kt | 41 ++++++++----------- .../kotlin/ParameterizeConfiguration.kt | 1 - .../kotlin/ParameterizeScopeSpec.kt | 9 ---- 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/src/commonMain/kotlin/Parameterize.kt b/src/commonMain/kotlin/Parameterize.kt index bf3b45b..c4ff6d1 100644 --- a/src/commonMain/kotlin/Parameterize.kt +++ b/src/commonMain/kotlin/Parameterize.kt @@ -86,18 +86,12 @@ import kotlin.reflect.KProperty public suspend fun parameterize( configuration: ParameterizeConfiguration = ParameterizeConfiguration.default, block: suspend ParameterizeScope.() -> Unit -): Unit = with(ParameterizeState()) { - // Exercise extreme caution modifying this code, since the iterator is sensitive to the behavior of this function. - // Code inlined from a previous version could have subtly different semantics when interacting with the runtime - // iterator of a later release, and would be major breaking change that's difficult to detect. - breakEarly { - withDecorator(configuration.decorator, onFailure = { failure -> - val result = handleFailure(configuration.onFailure, failure) - if (result.breakEarly) breakEarly() - }, block) - } - handleComplete(configuration.onComplete) -} +): Unit = parameterize( + decorator = configuration.decorator, + onFailure = configuration.onFailure, + onComplete = configuration.onComplete, + block = block +) private fun interface Breakable { suspend fun breakEarly(): Nothing @@ -118,10 +112,6 @@ private suspend inline fun breakEarly(crossinline block: suspend Breakable.() -> * * @see parameterize */ -@Suppress( - // False positive: onComplete is called in place exactly once through the configuration by the end parameterize call - "LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND" -) public suspend fun parameterize( configuration: ParameterizeConfiguration = ParameterizeConfiguration.default, decorator: suspend DecoratorScope.(iteration: suspend DecoratorScope.() -> Unit) -> Unit = configuration.decorator, @@ -132,14 +122,18 @@ public suspend fun parameterize( contract { callsInPlace(onComplete, InvocationKind.EXACTLY_ONCE) } - - val newConfiguration = ParameterizeConfiguration(configuration) { - this.decorator = decorator - this.onFailure = onFailure - this.onComplete = onComplete + with(ParameterizeState()) { + // Exercise extreme caution modifying this code, since the iterator is sensitive to the behavior of this function. + // Code inlined from a previous version could have subtly different semantics when interacting with the runtime + // iterator of a later release, and would be major breaking change that's difficult to detect. + breakEarly { + withDecorator(decorator, onFailure = { failure -> + val result = handleFailure(onFailure, failure) + if (result.breakEarly) breakEarly() + }, block) + } + handleComplete(onComplete) } - - parameterize(newConfiguration, block) } /** @see parameterize */ @@ -164,6 +158,7 @@ public class ParameterizeScope @PublishedApi internal constructor( thisRef: Any?, property: KProperty<*> ): ParameterDelegate { + @Suppress("UNCHECKED_CAST") parameterState.property = property as KProperty return this } diff --git a/src/commonMain/kotlin/ParameterizeConfiguration.kt b/src/commonMain/kotlin/ParameterizeConfiguration.kt index 6b0c0d5..ed0d45e 100644 --- a/src/commonMain/kotlin/ParameterizeConfiguration.kt +++ b/src/commonMain/kotlin/ParameterizeConfiguration.kt @@ -16,7 +16,6 @@ package com.benwoodworth.parameterize -import com.benwoodworth.parameterize.ParameterizeConfiguration.Builder import kotlin.coroutines.Continuation import kotlin.coroutines.RestrictsSuspension import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED diff --git a/src/commonTest/kotlin/ParameterizeScopeSpec.kt b/src/commonTest/kotlin/ParameterizeScopeSpec.kt index a7699c4..929667b 100644 --- a/src/commonTest/kotlin/ParameterizeScopeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeScopeSpec.kt @@ -28,15 +28,6 @@ import kotlin.test.assertSame * Specifies the [parameterize] DSL and its syntax. */ class ParameterizeScopeSpec { - /** - * A unique iterator that the tests can use to verify that a constructed [Parameter] has the correct - * [arguments][Parameter.arguments]. - */ - private data object ArgumentIterator : Iterator { - override fun hasNext(): Boolean = false - override fun next(): Nothing = throw NoSuchElementException() - } - /** * The lazy `parameter {}` functions should have the same behavior, so this provides an abstraction that a test can * use to specify for all the lazy overloads parametrically. From 0d73775760fd26e9dbfba99714b91f024844e00d Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sat, 28 Sep 2024 19:01:52 +0100 Subject: [PATCH 14/18] Refactor withDecorator to give access to the result Fix iteration count miscounting and add a test for it --- src/commonMain/kotlin/Parameterize.kt | 6 +++--- src/commonMain/kotlin/ParameterizeDecorator.kt | 10 +++++----- src/commonMain/kotlin/ParameterizeState.kt | 2 +- .../ParameterizeConfigurationSpec_onComplete.kt | 16 ++++++++++++++++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/commonMain/kotlin/Parameterize.kt b/src/commonMain/kotlin/Parameterize.kt index c4ff6d1..925cb98 100644 --- a/src/commonMain/kotlin/Parameterize.kt +++ b/src/commonMain/kotlin/Parameterize.kt @@ -127,10 +127,10 @@ public suspend fun parameterize( // Code inlined from a previous version could have subtly different semantics when interacting with the runtime // iterator of a later release, and would be major breaking change that's difficult to detect. breakEarly { - withDecorator(decorator, onFailure = { failure -> - val result = handleFailure(onFailure, failure) + withDecorator(decorator, block) { + val result = handleFailure(onFailure, it.exceptionOrNull() ?: return@withDecorator) if (result.breakEarly) breakEarly() - }, block) + } } handleComplete(onComplete) } diff --git a/src/commonMain/kotlin/ParameterizeDecorator.kt b/src/commonMain/kotlin/ParameterizeDecorator.kt index c3a3445..1faecf8 100644 --- a/src/commonMain/kotlin/ParameterizeDecorator.kt +++ b/src/commonMain/kotlin/ParameterizeDecorator.kt @@ -19,6 +19,7 @@ internal class ParameterizeDecorator( private var decoratorCoroutine: DecoratorCoroutine? = null internal fun beforeEach() { + parameterizeState.newIteration() check(decoratorCoroutine == null) { "${::decoratorCoroutine.name} was improperly finished" } decoratorCoroutine = DecoratorCoroutine(parameterizeState, decorator) .also { it.beforeIteration() } @@ -36,7 +37,6 @@ internal class ParameterizeDecorator( afterEach() return@use }) { isFirst, isLast, argument -> - parameterizeState.newIteration() if (!isFirst) beforeEach() val parameter = ParameterState(argument, isLast) @@ -70,16 +70,16 @@ private inline fun Sequence.forEachWithIterations( } while (!isLastIteration) } -internal suspend fun ParameterizeState.withDecorator( +internal suspend fun ParameterizeState.withDecorator( decorator: suspend DecoratorScope.(iteration: suspend DecoratorScope.() -> Unit) -> Unit, - onFailure: suspend (Throwable) -> Unit, - block: suspend ParameterizeScope.() -> Unit, + block: suspend ParameterizeScope.() -> T, + action: suspend (Result) -> Unit ): Unit = handle { val decorator = ParameterizeDecorator(this@withDecorator, decorator, this) decorator.beforeEach() val result = runCatching { block(ParameterizeScope(decorator)) } decorator.afterEach() - result.onFailure { onFailure(it) } + action(result) } /** diff --git a/src/commonMain/kotlin/ParameterizeState.kt b/src/commonMain/kotlin/ParameterizeState.kt index 5dbf8bc..4ea43be 100644 --- a/src/commonMain/kotlin/ParameterizeState.kt +++ b/src/commonMain/kotlin/ParameterizeState.kt @@ -34,7 +34,7 @@ internal class ParameterizeState { private val recordedFailures = mutableListOf() val isFirstIteration: Boolean - get() = iterationCount == 0L + get() = iterationCount == 1L val hasNextArgumentCombination get() = parameters.any { !it.isLast } diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt index 92a07d0..9b4bae1 100644 --- a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt +++ b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt @@ -117,6 +117,22 @@ class ParameterizeConfigurationSpec_onComplete { } } + @Test + fun iteration_count_should_be_correct_with_two_params() = runTestCC { + var expectedIterationCount = 0L + + testParameterize( + onComplete = { + assertEquals(expectedIterationCount, iterationCount) + } + ) { + val iteration by parameter(0..100) + val iteration2 by parameter(0..10) + + expectedIterationCount++ + } + } + @Test fun iteration_count_should_be_correct_with_empty() = runTestCC { var expectedIterationCount = 0L From 6a93df07f9988050d76c63803827dcf958e602e7 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sat, 28 Sep 2024 19:07:42 +0100 Subject: [PATCH 15/18] Implement commented-out tests by changing non-local returns to `discard` and sequences to `channelFlow` --- src/commonTest/kotlin/ParameterizeSpec.kt | 45 ++++++++++++++++------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/commonTest/kotlin/ParameterizeSpec.kt b/src/commonTest/kotlin/ParameterizeSpec.kt index 2514dbf..0e179de 100644 --- a/src/commonTest/kotlin/ParameterizeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeSpec.kt @@ -16,6 +16,13 @@ package com.benwoodworth.parameterize +import effekt.discardWithFast +import effekt.handle +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import runCC import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -270,24 +277,34 @@ class ParameterizeSpec { } } -/* @Test TODO - fun should_be_able_to_return_from_an_outer_function_from_within_the_block() = runTestCC { - parameterize { - return@should_be_able_to_return_from_an_outer_function_from_within_the_block + @Test + fun should_be_able_to_discard_to_an_outer_function_from_within_the_block() = runTestCC { + handle { + parameterize { + discardWithFast(Result.success(Unit)) + } } - }*/ + } /** * The motivating use case here is decorating a Kotest test group, in which the test declarations suspend. */ -/* @Test TODO - fun should_be_able_to_decorate_a_suspend_block() { - val coordinates = sequence { - parameterize { - val letter by parameter('a'..'c') - val number by parameter(1..3) - - yield("$letter$number") + @Test + fun should_be_able_to_decorate_a_suspend_block() = runTest { + // This works as well with a normal flow, but this could + // change in future versions of kontinuity (because + // currently, we wrap the `coroutineContext` to add + // extra data, but that data could simply be added to the context. + // if we do that though, `flow` complains that the context, + // and hence the coroutine, changed) + val coordinates = channelFlow { + runCC { + parameterize { + val letter by parameter('a'..'c') + val number by parameter(1..3) + + send("$letter$number") + } } } @@ -295,5 +312,5 @@ class ParameterizeSpec { listOf("a1", "a2", "a3", "b1", "b2", "b3", "c1", "c2", "c3"), coordinates.toList() ) - }*/ + } } From 144c0f07b1453c64990a3ecc0a8e528bb1bff1ae Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sun, 29 Sep 2024 20:27:27 +0100 Subject: [PATCH 16/18] Add TODO for commented out tests in ParameterizeExceptionSpec --- src/commonTest/kotlin/ParameterizeExceptionSpec.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt index 49785a4..6225258 100644 --- a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt +++ b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt @@ -118,7 +118,7 @@ class ParameterizeExceptionSpec { } assertEquals(shouldDeclareA, true) } -/* +/* TODO @Test fun nested_parameter_declaration_within_arguments_iterator_function() = runTestCC { From 0d39738d214db586d3de9995e8c75bc5ed9085fe Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Sun, 29 Sep 2024 20:32:19 +0100 Subject: [PATCH 17/18] Minor cleanup --- src/commonTest/kotlin/ParameterizeExceptionSpec.kt | 1 + src/commonTest/kotlin/ParameterizeSpec.kt | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt index 6225258..fc945fc 100644 --- a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt +++ b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt @@ -210,6 +210,7 @@ class ParameterizeExceptionSpec { } } + // TODO intercept missing prompt exception and change it to a ParameterizeException val failure = assertFailsWith { declareParameter() } diff --git a/src/commonTest/kotlin/ParameterizeSpec.kt b/src/commonTest/kotlin/ParameterizeSpec.kt index 0e179de..b622346 100644 --- a/src/commonTest/kotlin/ParameterizeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeSpec.kt @@ -19,7 +19,6 @@ package com.benwoodworth.parameterize import effekt.discardWithFast import effekt.handle import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import runCC @@ -43,9 +42,9 @@ class ParameterizeSpec { ) = runTestCC { val iterations = mutableListOf() - parameterize(decorator = { + parameterize(decorator = { iteration -> iterations += null - it() + iteration() }) { block().also { iterations[iterations.lastIndex] = it } } From b53fe37e5851fdf70dbe1d4050a408de23ec26c1 Mon Sep 17 00:00:00 2001 From: Youssef Shoaib Date: Tue, 1 Oct 2024 12:20:20 +0100 Subject: [PATCH 18/18] Implement skip counting --- src/commonMain/kotlin/ParameterizeDecorator.kt | 1 + src/commonMain/kotlin/ParameterizeIterator.kt | 0 2 files changed, 1 insertion(+) delete mode 100644 src/commonMain/kotlin/ParameterizeIterator.kt diff --git a/src/commonMain/kotlin/ParameterizeDecorator.kt b/src/commonMain/kotlin/ParameterizeDecorator.kt index 1faecf8..1fc9fcf 100644 --- a/src/commonMain/kotlin/ParameterizeDecorator.kt +++ b/src/commonMain/kotlin/ParameterizeDecorator.kt @@ -34,6 +34,7 @@ internal class ParameterizeDecorator( arguments: Sequence ): ParameterDelegate = use { resume -> arguments.forEachWithIterations(onEmpty = { + parameterizeState.handleContinue() afterEach() return@use }) { isFirst, isLast, argument -> diff --git a/src/commonMain/kotlin/ParameterizeIterator.kt b/src/commonMain/kotlin/ParameterizeIterator.kt deleted file mode 100644 index e69de29..0000000