Skip to content

Commit

Permalink
WIP Extract configuration logic
Browse files Browse the repository at this point in the history
TODO
- Readme module.
- iterationFailures unused?
- remove check failure in next commit?
- check if PException was caught on next iteration
- configured shouldn't catch PException
- vanilla `parameterize` docs, and other configuration docs
- merge iterator -> state (vanilla & configured)

Improves performance of unconfigured parameterize, and greatly simplifies core logic
configuration may be dropped later
  • Loading branch information
BenWoodworth committed Oct 14, 2024
1 parent 38c6cdb commit 87c0a83
Show file tree
Hide file tree
Showing 27 changed files with 470 additions and 458 deletions.
63 changes: 63 additions & 0 deletions parameterize-configuration/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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.
*/

import org.jetbrains.dokka.gradle.DokkaTask

plugins {
id("kotlin-multiplatform-conventions")
id("dokka-conventions")
id("binary-compatibility-validator-conventions")
id("publishing-conventions")
id("ci-conventions")
}

repositories {
mavenCentral()
}

kotlin {
sourceSets {
configureEach {
languageSettings {
optIn("com.benwoodworth.parameterize.internal.ParameterizeApiFriendModuleApi")
}
}

val commonMain by getting {
dependencies {
api(project(":parameterize-core"))
}
}
val jvmMain by getting {
dependencies {
implementation(libs.opentest4j)
}
}
}
}

tasks.withType<DokkaTask>().configureEach {
doLast {
layout.buildDirectory.asFileTree.asSequence()
.filter { it.isFile && it.extension == "html" }
.forEach { file ->
file.readText()
// Remove "ParameterizeConfiguration." prefix from link text for *Scope classes
.replace(Regex("""(?<=>)ParameterizeConfiguration\.(?=\w+Scope</a>)"""), "")
.let { file.writeText(it) }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.*
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

public inline fun parameterize(
configuration: ParameterizeConfiguration,
block: ParameterizeScope.() -> Unit
) {
val state = ConfiguredParameterizeState(configuration)

parameterize {
val scope = state.nextIteration() ?: return

try {
scope.block()
} catch (failure: Throwable) {
state.handleFailure(failure)
}
}

state.handleComplete()
}

/**
* Calls [parameterize] with a copy of the [configuration] that has options overridden.
*
* @param decorator See [ParameterizeConfiguration.Builder.decorator]
* @param onFailure See [ParameterizeConfiguration.Builder.onFailure]
* @param onComplete See [ParameterizeConfiguration.Builder.onComplete]
*
* @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(
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
) {
contract {
callsInPlace(onComplete, InvocationKind.EXACTLY_ONCE)
}

val newConfiguration = ParameterizeConfiguration(configuration) {
this.decorator = decorator
this.onFailure = onFailure
this.onComplete = onComplete
}

parameterize(newConfiguration, block)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* 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.*
import com.benwoodworth.parameterize.ParameterizeScope.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.intrinsics.createCoroutineUnintercepted
import kotlin.coroutines.resume
import kotlin.jvm.JvmInline
import kotlin.reflect.KProperty

@PublishedApi
internal class ConfiguredParameterizeState(
private val configuration: ParameterizeConfiguration
) {
private var iterationCount = 0L
private var skipCount = 0L
private var failureCount = 0L
private val recordedFailures = mutableListOf<ParameterizeFailure>()

private var breakEarly = false
private var currentIterationScope: ConfiguredParameterizeScope? = 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: ParameterizeScope): ParameterizeScope? {
if (breakEarly) {
return null
}

if (currentIterationScope != null) afterEach()

return ConfiguredParameterizeScope(parameterizeScope).also {
currentIterationScope = it
iterationCount++
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 = 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
}

fun handleContinue() {
skipCount++
}

@JvmInline
value class HandleFailureResult(val breakEarly: Boolean)

fun handleFailure(onFailure: OnFailureScope.(Throwable) -> Unit, failure: Throwable): HandleFailureResult {
failureCount++

val scope = OnFailureScope(
state = this,
iterationCount,
failureCount,
)

with(scope) {
onFailure(failure)

if (recordFailure) {
recordedFailures += ParameterizeFailure(failure, parameters)
}

return HandleFailureResult(breakEarly)
}
}

private fun handleComplete() {
afterEach()

val scope = OnCompleteScope(
iterationCount,
skipCount,
failureCount,
completedEarly = hasNextArgumentCombination,
recordedFailures,
)

configuration.onComplete(scope)
}
}

private class ConfiguredParameterizeScope(
private val baseScope: ParameterizeScope
) : ParameterizeScope {
val declaredParameters = mutableListOf<DeclaredParameter<*>>()

override fun <T> Parameter<T>.provideDelegate(thisRef: Any?, property: KProperty<*>): DeclaredParameter<T> =
with(baseScope) {
provideDelegate(thisRef, property)
.also { declaredParameters += 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 configuration: ParameterizeConfiguration
) {
private val scope = DecoratorScope(parameterizeState)

private var continueAfterIteration: Continuation<Unit>? = 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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ public class ParameterizeConfiguration internal constructor(

/** @see Builder.onFailure */
public class OnFailureScope internal constructor(
private val state: ParameterizeState,
private val state: ConfiguredParameterizeState,

/**
* The number of iterations that have been executed, including this failing iteration.
Expand Down
Loading

0 comments on commit 87c0a83

Please sign in to comment.