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/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/commonMain/kotlin/ParameterState.kt b/src/commonMain/kotlin/ParameterState.kt index d277df5..5d741fc 100644 --- a/src/commonMain/kotlin/ParameterState.kt +++ b/src/commonMain/kotlin/ParameterState.kt @@ -20,143 +20,26 @@ 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. * - * 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 the argument. * - * When first declared, the parameter [property] and the [arguments] 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 - * 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. - * - * 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( - 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 +internal class ParameterState(val argument: T, val isLast: Boolean = false) { + var property: KProperty? = null var hasBeenUsed: Boolean = false - private set - - internal fun reset() { - property = null - arguments = null - argument = null - argumentIterator = null - hasBeenUsed = false - } - - /** - * @throws IllegalStateException if used before the argument has been declared. - */ - val isLastArgument: Boolean - get() { - checkNotNull(property) { "Parameter has not been declared" } - return argumentIterator == null - } - /** * Returns a string representation of the current argument, or a "not declared" message. */ - override fun toString(): String = - if (property == null) { - "Parameter not declared yet." - } else { - argument.toString() - } - - /** - * Set up the delegate for a parameter [property] 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. - * 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 - } - - val iterator = arguments.iterator() - if (!iterator.hasNext()) { - throw ParameterizeContinue // Before changing any state - } - - this.property = property - this.arguments = arguments - 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(property: KProperty): T { - val declaredProperty = checkNotNull(this.property) { - "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 - } + override fun toString(): String = argument.toString() fun useArgument() { hasBeenUsed = true } - /** - * Iterates the parameter argument. - * - * @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() - - 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 791df9f..925cb98 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.discardWithFast +import effekt.handle import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.experimental.ExperimentalTypeInference @@ -79,24 +83,23 @@ import kotlin.reflect.KProperty * * @throws ParameterizeException if the DSL is used incorrectly. (See restrictions) */ -public inline fun parameterize( +public suspend fun parameterize( configuration: ParameterizeConfiguration = ParameterizeConfiguration.default, - block: ParameterizeScope.() -> Unit -) { - // 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) + block: suspend ParameterizeScope.() -> Unit +): Unit = parameterize( + decorator = configuration.decorator, + onFailure = configuration.onFailure, + onComplete = configuration.onComplete, + block = block +) - while (true) { - val scope = iterator.nextIteration() ?: break +private fun interface Breakable { + suspend fun breakEarly(): Nothing +} - try { - scope.block() - } catch (failure: Throwable) { - iterator.handleFailure(failure) - } +private suspend inline fun breakEarly(crossinline block: suspend Breakable.() -> Unit) = handle { + block { + discardWithFast(Result.success(Unit)) } } @@ -109,36 +112,36 @@ public inline fun parameterize( * * @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 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, - block: 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) } - - 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, block) { + val result = handleFailure(onFailure, it.exceptionOrNull() ?: return@withDecorator) + if (result.breakEarly) breakEarly() + } + } + handleComplete(onComplete) } - - parameterize(newConfiguration, block) } /** @see parameterize */ @ParameterizeDsl -public class ParameterizeScope internal constructor( - internal val parameterizeState: ParameterizeState, +public class ParameterizeScope @PublishedApi internal constructor( + internal val parameterizeIterator: ParameterizeDecorator, ) { - internal var iterationCompleted: Boolean = false + internal val parameterizeState get() = parameterizeIterator.parameterizeState /** @suppress */ override fun toString(): String = @@ -151,39 +154,25 @@ 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" - } - + public operator fun ParameterDelegate.provideDelegate( + thisRef: Any?, + property: KProperty<*> + ): ParameterDelegate { @Suppress("UNCHECKED_CAST") - return parameterizeState.declareParameter(property as KProperty, arguments) + parameterState.property = property as KProperty + return this } /** @suppress */ public operator fun ParameterDelegate.getValue(thisRef: Any?, property: KProperty<*>): T { - if (!iterationCompleted) parameterState.useArgument() - return argument + parameterState.useArgument() + 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( - internal val parameterState: ParameterState, - internal val argument: T + @JvmInline + public value class ParameterDelegate internal constructor( + internal val parameterState: ParameterState, ) { /** * Returns a string representation of the current argument. @@ -194,7 +183,7 @@ public class ParameterizeScope internal constructor( * ``` */ override fun toString(): String = - argument.toString() + parameterState.argument.toString() } } @@ -206,9 +195,9 @@ 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.ParameterDelegate = @OptIn(ExperimentalParameterizeApi::class) - ParameterizeScope.Parameter(arguments) + parameterizeIterator.declareParameter(arguments) /** * Declare a parameter with the given [arguments]. @@ -217,7 +206,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.ParameterDelegate = parameter(arguments.asSequence()) /** @@ -227,7 +216,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.ParameterDelegate = parameter(arguments.asSequence()) /** @@ -253,9 +242,9 @@ 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 = +): ParameterizeScope.ParameterDelegate = parameter(object : Sequence { private var arguments: Sequence? = null @@ -294,9 +283,9 @@ 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 = +): ParameterizeScope.ParameterDelegate = parameter { lazyArguments().asSequence() } diff --git a/src/commonMain/kotlin/ParameterizeConfiguration.kt b/src/commonMain/kotlin/ParameterizeConfiguration.kt index 779f0bc..367abe5 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/commonMain/kotlin/ParameterizeDecorator.kt b/src/commonMain/kotlin/ParameterizeDecorator.kt new file mode 100644 index 0000000..1faecf8 --- /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() { + parameterizeState.newIteration() + 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 -> + 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, + 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() + action(result) +} + +/** + * 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/ParameterizeIterator.kt b/src/commonMain/kotlin/ParameterizeIterator.kt deleted file mode 100644 index 1c79680..0000000 --- a/src/commonMain/kotlin/ParameterizeIterator.kt +++ /dev/null @@ -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 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 -) { - private val parameterizeState = ParameterizeState() - - 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 -> {} - - 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 currentIterationScope = checkNotNull(currentIterationScope) { "${::currentIterationScope.name} was null" } - val decoratorCoroutine = checkNotNull(decoratorCoroutine) { "${::decoratorCoroutine.name} was null" } - - currentIterationScope.iterationCompleted = true - 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 74fd04e..a93dc0c 100644 --- a/src/commonMain/kotlin/ParameterizeState.kt +++ b/src/commonMain/kotlin/ParameterizeState.kt @@ -18,115 +18,58 @@ package com.benwoodworth.parameterize import com.benwoodworth.parameterize.ParameterizeConfiguration.OnCompleteScope import com.benwoodworth.parameterize.ParameterizeConfiguration.OnFailureScope -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 { /** * 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 declaringParameter: KProperty<*>? = null - 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 val parameters = ArrayList>() private var iterationCount = 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( - property: KProperty, - arguments: Sequence - ): ParameterDelegate = trackNestedDeclaration(property) { - 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() - } - } else { - ParameterState(this) - .also { parameters += it } - } - - parameter.declare(property, arguments) - parameterCount++ // After declaring, since the parameter shouldn't count if declare throws - - if (parameter === parameterToIterate) { - parameter.nextArgument() - parameterToIterate = null - } - - if (!parameter.isLastArgument) { - lastParameterWithNextArgument = parameter - } - - return ParameterDelegate(parameter, parameter.getArgument(property)) - } - - 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 - } - } + val hasNextArgumentCombination get() = parameters.any { !it.isLast } /** * Get a list of used arguments for reporting a failure. */ fun getFailureArguments(): List> = - parameters.take(parameterCount) + parameters .filter { it.hasBeenUsed } .map { it.getFailureArgument() } @JvmInline 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" + fun newIteration() { + iterationCount++ + } + + 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 failureCount++ val scope = OnFailureScope( 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 deleted file mode 100644 index f170feb..0000000 --- a/src/commonTest/kotlin/ParameterStateSpec.kt +++ /dev/null @@ -1,322 +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 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(ParameterizeState()) - } - - - private fun assertUndeclared(parameter: ParameterState) { - val failure = assertFailsWith { - parameter.getArgument(::property) - } - - 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(::property, 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(::property, emptySequence()) - } - } - - @Test - fun declaring_with_no_arguments_should_leave_parameter_undeclared() { - runCatching { - parameter.declare(::property, emptySequence()) - } - - assertUndeclared(parameter) - } - - @Test - fun declare_should_immediately_get_the_first_argument() { - var gotFirstArgument = false - - val arguments = Sequence { - gotFirstArgument = true - listOf(Unit).iterator() - } - - parameter.declare(::property, 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++ } - } - } - - parameter.declare(::property, Sequence(::AssertingIterator)) - } - - @Test - fun declare_with_one_argument_should_set_is_last_argument_to_true() { - parameter.declare(::property, 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")) - - assertFalse(parameter.isLastArgument) - } - - @Test - fun getting_argument_before_declared_should_throw_IllegalStateException() { - val failure = assertFailsWith { - parameter.getArgument(::property) - } - - assertEquals(getArgumentBeforeDeclaredMessage, failure.message, "message") - } - - @Test - fun getting_argument_with_the_wrong_property_should_throw_ParameterizeException() { - parameter.declare(::property, sequenceOf(Unit)) - - val exception = assertFailsWith { - parameter.getArgument(::differentProperty) - } - - 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() { - parameter.declare(::property, sequenceOf("first", "second")) - - assertEquals("first", parameter.getArgument(::property)) - } - - @Test - fun use_argument_should_set_has_been_used_to_true() { - parameter.declare(::property, 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(::property, sequenceOf("first", "second", "third")) - parameter.getArgument(::property) - - parameter.nextArgument() - assertEquals("second", parameter.getArgument(::property)) - - parameter.nextArgument() - assertEquals("third", parameter.getArgument(::property)) - } - - @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.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() { - parameter.declare(::property, sequenceOf("first", "second", "third", "fourth")) - parameter.getArgument(::property) - parameter.nextArgument() // second - parameter.nextArgument() // third - parameter.nextArgument() // forth - - assertTrue(parameter.isLastArgument) - } - - @Test - fun next_after_the_last_argument_should_loop_back_to_the_first() { - parameter.declare(::property, sequenceOf("first", "second")) - parameter.getArgument(::property) - parameter.nextArgument() // second - parameter.nextArgument() // first - - assertEquals("first", parameter.getArgument(::property)) - } - - @Test - fun next_after_the_last_argument_should_set_is_last_argument_to_false() { - parameter.declare(::property, sequenceOf("first", "second")) - parameter.getArgument(::property) - parameter.nextArgument() // second - parameter.nextArgument() // first - - assertFalse(parameter.isLastArgument) - } - - @Test - fun redeclare_should_not_change_current_argument() { - parameter.declare(::property, sequenceOf("a", "b")) - - val newArguments = Sequence { - fail("Re-declaring should keep the old arguments") - } - parameter.declare(::property, newArguments) - - assertEquals("a", parameter.getArgument(::property)) - } - - @Test - fun redeclare_arguments_should_keep_using_the_original_arguments() { - parameter.declare(::property, sequenceOf("a")) - - val newArguments = Sequence { - fail("Re-declaring should keep the old arguments") - } - parameter.declare(::property, newArguments) - } - - @Test - fun redeclare_with_different_parameter_should_throw_ParameterizeException() { - parameter.declare(::property, sequenceOf(Unit)) - - assertFailsWith { - parameter.declare(::differentProperty, sequenceOf(Unit)) - } - } - - @Test - fun redeclare_with_different_parameter_should_not_change_has_been_used() { - parameter.declare(::property, sequenceOf("a")) - parameter.useArgument() - - runCatching { - parameter.declare(::differentProperty, sequenceOf("a")) - } - - assertTrue(parameter.hasBeenUsed) - } - - @Test - fun reset_should_set_has_been_used_to_false() { - parameter.declare(::property, sequenceOf("a", "b")) - parameter.getArgument(::property) - parameter.reset() - - assertFalse(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(::property, sequenceOf(expectedArgument)) - parameter.getArgument(::property) - - val (property, argument) = parameter.getFailureArgument() - assertTrue(property.equalsProperty(::property)) - assertSame(expectedArgument, argument) - } -} diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec.kt index 4e9a04e..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.* @@ -60,7 +59,7 @@ class ParameterizeConfigurationSpec { val property: KProperty1, val builderProperty: KMutableProperty1, val distinctValue: T, - val parameterizeWithConfigurationAndOptionPassed: ( + val parameterizeWithConfigurationAndOptionPassed: suspend ( configuration: ParameterizeConfiguration, block: ParameterizeScope.() -> Unit ) -> Unit ) { @@ -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) } @@ -144,7 +143,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 +174,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) = testAllCC( "configuration-only overload" to { test { configure, block -> val configuration = ParameterizeConfiguration { configure() } @@ -203,7 +202,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 +210,11 @@ class ParameterizeConfigurationSpec { } } - }.toTypedArray Unit>>() + }.toTypedArray Unit>>() ) private fun interface ParameterizeWithOptionDefault { - fun parameterizeWithOptionDefault(block: ParameterizeScope.() -> Unit) + suspend fun parameterizeWithOptionDefault(block: suspend ParameterizeScope.() -> Unit) } /** @@ -233,12 +232,12 @@ class ParameterizeConfigurationSpec { */ private fun testParameterizeWithOptionDefault( configure: Builder.() -> Unit, - parameterizeWithDifferentOptionPassed: ( + parameterizeWithDifferentOptionPassed: suspend ( configuration: ParameterizeConfiguration, - block: ParameterizeScope.() -> Unit + block: suspend ParameterizeScope.() -> Unit ) -> Unit, - test: ParameterizeWithOptionDefault.() -> Unit - ) = testAll( + test: suspend ParameterizeWithOptionDefault.() -> Unit + ) = testAllCC( "with default from builder" to { val configuration = ParameterizeConfiguration { configure() } @@ -342,7 +341,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..af377b2 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 + noinline 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 @@ -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 @@ -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 -> @@ -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 @@ -257,22 +257,4 @@ class ParameterizeConfigurationSpec_decorator { "message" ) } - - @Test - fun declaring_parameter_after_iteration_function_should_fail() { - assertFailsWith { - lateinit var declareParameter: () -> Unit - - testParameterize( - decorator = { iteration -> - iteration() - declareParameter() - } - ) { - declareParameter = { - val parameter by parameterOf(Unit) - } - } - } - } } diff --git a/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt b/src/commonTest/kotlin/ParameterizeConfigurationSpec_onComplete.kt index d243d8a..097a430 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 + noinline 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,39 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun iteration_count_should_be_correct_with_break() { + 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 + + 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 testParameterize( @@ -140,7 +172,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun failure_count_should_be_correct() { + fun failure_count_should_be_correct() = runTestCC { var expectedFailureCount = 0L testParameterize( @@ -158,7 +190,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) @@ -169,7 +201,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 @@ -188,7 +220,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 @@ -207,7 +239,7 @@ class ParameterizeConfigurationSpec_onComplete { } @Test - fun recorded_failures_should_be_correct() { + fun recorded_failures_should_be_correct() = runTestCC { val expectedRecordedFailures = mutableListOf>>>() var lastIteration = -1 @@ -236,7 +268,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 8c75694..7b96954 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 + noinline 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( @@ -145,100 +145,135 @@ class ParameterizeConfigurationSpec_onFailure { } } - @Test - fun failure_arguments_should_be_those_from_the_last_iteration() { - val lastParameterArguments = mutableListOf>() + data class FailureParameterArgumentsException(val parameterArguments: List>): Exception() + @Test + fun failure_arguments_should_be_those_from_the_last_iteration() = runTestCC { 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 + + throw FailureParameterArgumentsException(listOfNotNull(iterationPair, evenIterationPair, threevenIterationPair)) + } + } + + @Test + 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) fail() } } @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_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') - useParameter(used1) - useParameter(used2) + var letterUsedThisIteration = false - fail() + 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() + } + } } @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_captured_parameters_from_previous_iterations() = runTestCC { + var isFirstIteration = true + testParameterize( + onFailure = { + val parameters = arguments.map { it.parameter.name } - var letterUsedThisIteration = false + assertTrue( + "neverUsedDuringTheCurrentIteration" !in parameters == isFirstIteration, + "neverUsedDuringTheCurrentIteration !in $parameters != $isFirstIteration" + ) + isFirstIteration = false + } + ) { + val neverUsedDuringTheCurrentIteration by parameterOf(Unit) - val letterNumber by parameter { - letterUsedThisIteration = true - (1..2).map { "$letter$it" } - } + @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() - // 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_parameters_only_used_in_previous_iterations() = runTestCC { + var isFirstIteration = true + testParameterize( + onFailure = { + val parameters = arguments.map { it.parameter.name } - assertFalse( - "neverUsedDuringTheCurrentIteration" in parameters, - "neverUsedDuringTheCurrentIteration in $parameters" - ) - } - ) { - val neverUsedDuringTheCurrentIteration by parameterOf(Unit) + assertTrue( + "neverUsedDuringTheCurrentIteration" in parameters == isFirstIteration, + "neverUsedDuringTheCurrentIteration in $parameters != $isFirstIteration" + ) + isFirstIteration = false + } + ) { + val neverUsedDuringTheCurrentIteration by parameterOf(Unit) - @Suppress("UNUSED_EXPRESSION") - val usePreviousIterationParameter by parameterOf( - { }, // Don't use it the first iteration - { neverUsedDuringTheCurrentIteration } - ) + @Suppress("UNUSED_EXPRESSION") + val useParameter by parameterOf( + { neverUsedDuringTheCurrentIteration }, + { }, // Don't use it the second iteration + ) - // On the 2nd iteration, use the parameter captured from the 1st iteration - usePreviousIterationParameter() + useParameter() - fail() + fail() + } } } diff --git a/src/commonTest/kotlin/ParameterizeExceptionSpec.kt b/src/commonTest/kotlin/ParameterizeExceptionSpec.kt index a90e5d4..fc945fc 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 { /** @@ -24,7 +29,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 +50,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,45 +88,40 @@ class ParameterizeExceptionSpec { } @Test - fun parameter_disappears_on_second_iteration_due_to_external_condition() { - val exception = assertFailsWith { - var shouldDeclareA = true + fun parameter_disappears_on_second_iteration_due_to_external_condition() = runTestCC { + 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) } @Test - fun parameter_appears_on_second_iteration_due_to_external_condition() { - val exception = assertFailsWith { - var shouldDeclareA = false + fun parameter_appears_on_second_iteration_due_to_external_condition() = runTestCC { + 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) } +/* TODO @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 +145,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 +176,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) @@ -198,10 +198,11 @@ class ParameterizeExceptionSpec { exception.message ) } +*/ @Test - fun declaring_parameter_after_iteration_completed() { - var declareParameter = {} + fun declaring_parameter_after_iteration_completed() = runTestCC { + var declareParameter = suspend {} parameterize { declareParameter = { @@ -209,34 +210,25 @@ class ParameterizeExceptionSpec { } } - val failure = assertFailsWith { + // TODO intercept missing prompt exception and change it to a ParameterizeException + val failure = assertFailsWith { declareParameter() } - - 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 { - 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 c2bb472..929667b 100644 --- a/src/commonTest/kotlin/ParameterizeScopeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeScopeSpec.kt @@ -16,68 +16,37 @@ 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. */ 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() - } - - @Test - fun parameter_from_sequence_should_be_constructed_with_the_same_arguments_instance() = parameterize { - val sequence = sequenceOf() - val parameter = parameter(sequence) - - assertSame(sequence, parameter.arguments) - } - - @Test - fun parameter_from_iterable_should_have_the_correct_arguments() = parameterize { - val parameter = parameter(Iterable { ArgumentIterator }) - - assertSame(ArgumentIterator, parameter.arguments.iterator()) - } - - @Test - fun parameter_of_listed_arguments_should_have_the_correct_arguments() = 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 { - 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 fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): Parameter = + override suspend fun invoke( + scope: ParameterizeScope, + lazyArguments: () -> LazyArguments + ): ParameterDelegate = with(scope) { parameter { val arguments = lazyArguments() @@ -86,7 +55,10 @@ class ParameterizeScopeSpec { } }, "from iterable" to object : LazyParameterFunction { - override fun invoke(scope: ParameterizeScope, lazyArguments: () -> LazyArguments): Parameter = + override suspend fun invoke( + scope: ParameterizeScope, + lazyArguments: () -> LazyArguments + ): ParameterDelegate = with(scope) { parameter { val arguments = lazyArguments() @@ -97,69 +69,65 @@ class ParameterizeScopeSpec { ) @Test - fun parameter_from_lazy_arguments_should_have_the_correct_arguments() = parameterize { + fun parameter_from_lazy_arguments_should_be_computed_before_delegation() = runTestCC { testAll(lazyParameterFunctions) { lazyParameterFunction -> - val lazyParameter = lazyParameterFunction(this@parameterize) { - LazyArguments { ArgumentIterator } + assertFailsWith("computed") { + parameterize { + lazyParameterFunction(this@parameterize) { throw IllegalStateException("computed") } + } } - - 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") } - } - } - - @Test - fun parameter_from_lazy_argument_iterable_should_only_be_computed_once() = parameterize { + fun parameter_from_lazy_argument_iterable_should_only_be_computed_once() = runTestCC { testAll(lazyParameterFunctions) { lazyParameterFunction -> + var currentIteration = 0 var evaluationCount = 0 + parameterize { + val lazyParameter by lazyParameterFunction(this@parameterize) { + evaluationCount++ + LazyArguments { (1..10).iterator() } + } - val lazyParameter = lazyParameterFunction(this@parameterize) { - evaluationCount++ - LazyArguments { (1..10).iterator() } - } + assertEquals(currentIteration + 1, lazyParameter, "Iteration #$currentIteration") - repeat(5) { i -> - val arguments = lazyParameter.arguments.toList() - assertEquals((1..10).toList(), arguments, "Iteration #$i") + assertEquals(1, evaluationCount) + currentIteration++ } - - 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 - + var delegate: ParameterDelegate? = null + 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()) + assertSame(parameter, delegate!!.toString()) } + } } diff --git a/src/commonTest/kotlin/ParameterizeSpec.kt b/src/commonTest/kotlin/ParameterizeSpec.kt index 83e401d..b622346 100644 --- a/src/commonTest/kotlin/ParameterizeSpec.kt +++ b/src/commonTest/kotlin/ParameterizeSpec.kt @@ -16,6 +16,12 @@ package com.benwoodworth.parameterize +import effekt.discardWithFast +import effekt.handle +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import runCC import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -32,17 +38,15 @@ class ParameterizeSpec { */ private fun testParameterize( expectedIterations: Iterable, - block: ParameterizeScope.() -> T - ) { + block: suspend ParameterizeScope.() -> T + ) = runTestCC { val iterations = mutableListOf() - parameterize { - try { - iterations += block() - } catch (caught: Throwable) { - iterations += null - throw caught - } + parameterize(decorator = { iteration -> + iterations += null + iteration() + }) { + block().also { iterations[iterations.lastIndex] = it } } assertEquals(expectedIterations.toList(), iterations, "Incorrect iterations") @@ -57,19 +61,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,11 +100,9 @@ class ParameterizeSpec { } @Test - fun parameter_should_iterate_to_the_next_argument_while_declaring() { - 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 @@ -106,17 +110,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) @@ -178,9 +181,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 @@ -199,13 +206,11 @@ class ParameterizeSpec { } @Test - @Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION") fun unused_parameter_with_no_arguments_should_finish_iteration_early() = testParameterize( listOf(null) ) { val unused by parameterOf() - - "finished" + unused } @Test @@ -232,9 +237,9 @@ 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 { + ): ParameterizeScope.ParameterDelegate { val arguments by lazy(lazyArguments) class CustomLazyArguments : Iterable { @@ -255,7 +260,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 { @@ -272,9 +277,11 @@ class ParameterizeSpec { } @Test - fun should_be_able_to_return_from_an_outer_function_from_within_the_block() { - parameterize { - return@should_be_able_to_return_from_an_outer_function_from_within_the_block + fun should_be_able_to_discard_to_an_outer_function_from_within_the_block() = runTestCC { + handle { + parameterize { + discardWithFast(Result.success(Unit)) + } } } @@ -282,13 +289,21 @@ class ParameterizeSpec { * The motivating use case here is decorating a Kotest test group, in which the test declarations suspend. */ @Test - 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") + 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") + } } } diff --git a/src/commonTest/kotlin/TestUtils.kt b/src/commonTest/kotlin/TestUtils.kt index c4677a8..f90f011 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. @@ -54,9 +61,9 @@ object TestAllScope { throw TestAllSkip(message) } -fun testAll( +suspend fun testAll( testCases: Iterable>, - test: TestAllScope.(testCase: T) -> Unit + test: suspend TestAllScope.(testCase: T) -> Unit ) { val results = testCases .map { (description, testCase) -> @@ -98,13 +105,36 @@ 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: TestAllScope.(testCase: T) -> Unit + 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, + 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..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 - }, + val iterationFailures = listOf Throwable>>( "ParameterizeException for same parameterize" to { parameterizeState -> ParameterizeException(parameterizeState, "same parameterize") }, 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/jvmTest/kotlin/ParameterizeFailedErrorSpec.jvm.kt b/src/jvmTest/kotlin/ParameterizeFailedErrorSpec.jvm.kt index d54a5cf..0ee0642 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) } 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