From 0f46c9727e7ff34867741c6f2235c18c7e615e2a Mon Sep 17 00:00:00 2001 From: sksamuel Date: Sun, 31 Jan 2021 20:21:09 -0600 Subject: [PATCH 1/5] Added Descriptor abstraction; updated scripts to use name of containing file as parent spec. --- .../kotlin/io/kotest/mpp/Reflection.kt | 2 + .../core/extensions/IsActiveExtension.kt | 11 +- .../kotlin/io/kotest/core/internal/active.kt | 10 +- .../kotlin/io/kotest/core/plan/TestPlan.kt | 4 +- .../kotlin/io/kotest/core/plan/descriptors.kt | 292 ++++++++++++ .../kotlin/io/kotest/core/plan/names.kt | 423 +++++++++--------- .../kotlin/io/kotest/core/plan/nodes.kt | 184 -------- .../kotlin/io/kotest/core/plan/source.kt | 9 + .../io/kotest/core/script/ScriptRuntime.kt | 38 +- .../kotlin/io/kotest/core/script/styles.kt | 70 ++- .../kotlin/io/kotest/core/test/NestedTest.kt | 7 +- .../kotlin/io/kotest/core/test/TestCase.kt | 7 +- .../core/test/TestCaseExecutionListener.kt | 3 - .../kotlin/io/kotest/core/test/TestContext.kt | 6 +- .../kotlin/io/kotest/engine/KotestEngine.kt | 5 +- .../io/kotest/engine/NotificationManager.kt | 10 +- .../listener/CompositeTestEngineListener.kt | 24 +- .../listener/IsolationTestEngineListener.kt | 89 ++-- .../SynchronizedTestEngineListener.kt | 34 +- .../engine/listener/TestEngineListener.kt | 37 +- .../io/kotest/engine/script/ScriptExecutor.kt | 66 +-- .../io/kotest/engine/script/instantiate.kt | 3 +- .../kotest/engine/active/IsActiveTest.kt | 16 +- .../engine/plan/ScriptDescriptorTest.kt | 19 + .../kotest/engine/plan/SpecDescriptorTest.kt | 24 + .../kotest/engine/plan/SpecNameTest.kt | 24 - .../junit/platform/JUnitTestEngineListener.kt | 72 ++- .../runner/junit/platform/descriptors.kt | 48 +- 28 files changed, 947 insertions(+), 590 deletions(-) create mode 100644 kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/descriptors.kt delete mode 100644 kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/nodes.kt create mode 100644 kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/source.kt create mode 100644 kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/plan/ScriptDescriptorTest.kt create mode 100644 kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/plan/SpecDescriptorTest.kt delete mode 100644 kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/plan/SpecNameTest.kt diff --git a/kotest-common/src/commonMain/kotlin/io/kotest/mpp/Reflection.kt b/kotest-common/src/commonMain/kotlin/io/kotest/mpp/Reflection.kt index b8012a67dc8..ac821e05f85 100644 --- a/kotest-common/src/commonMain/kotlin/io/kotest/mpp/Reflection.kt +++ b/kotest-common/src/commonMain/kotlin/io/kotest/mpp/Reflection.kt @@ -60,6 +60,8 @@ object BasicReflection : Reflection { */ fun KClass<*>.bestName(): String = reflection.fqn(this) ?: simpleName ?: this.toString() +fun KClass<*>.qualifiedNameOrNull(): String? = reflection.fqn(this) + /** * Finds the first annotation of type T on this class, or returns null if annotations * are not supported on this platform or the annotation is missing. diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/extensions/IsActiveExtension.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/extensions/IsActiveExtension.kt index 11ec99ec276..42156de4674 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/extensions/IsActiveExtension.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/extensions/IsActiveExtension.kt @@ -1,12 +1,12 @@ package io.kotest.core.extensions import io.kotest.core.config.ExperimentalKotest -import io.kotest.core.plan.TestPlanNode +import io.kotest.core.plan.Descriptor /** - * An extension point that is used to override if a [TestPlanNode] is inactive or active. + * An extension point that is used to override if a [Descriptor] is inactive or active. * - * If multiple instances of this extension are defined then all will be executed, but the order is not specified. + * If multiple instances of this extension are defined then all will be executed and all must respond active. */ @ExperimentalKotest interface IsActiveExtension : Extension { @@ -14,12 +14,9 @@ interface IsActiveExtension : Extension { /** * Invoked to override if a test or spec is active or inactive. * - * The provided node will have it's active status set depending on the default rules or - * as modified by any previously executed extensions. - * * This method can choose to override that status by returning: * - true if this test or spec should be active regardless of the input active status * - false if this test or spec should be inactive regardless of the input active status */ - suspend fun isActive(node: TestPlanNode): Boolean + suspend fun isActive(descriptor: Descriptor): Boolean } diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/internal/active.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/internal/active.kt index 6104ed7a141..ac38641ef94 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/internal/active.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/internal/active.kt @@ -12,7 +12,7 @@ import io.kotest.core.internal.tags.activeTags import io.kotest.core.internal.tags.allTags import io.kotest.core.internal.tags.isActive import io.kotest.core.internal.tags.parse -import io.kotest.core.plan.toNode +import io.kotest.core.plan.toDescriptor import io.kotest.core.test.TestCaseSeverityLevel import io.kotest.mpp.log import io.kotest.mpp.sysprop @@ -22,11 +22,9 @@ import io.kotest.mpp.sysprop * or any registered [IsActiveExtension]s. */ suspend fun TestCase.isActive(): Boolean { - val defaultActive = isActiveInternal() - val node = this.toNode(defaultActive) - return configuration.extensions().filterIsInstance().fold(node) { acc, op -> - acc.copy(active = op.isActive(acc)) - }.active + val descriptor = this.descriptor ?: this.description.toDescriptor(this.source) + return isActiveInternal() && configuration.extensions().filterIsInstance() + .all { it.isActive(descriptor) } } /** diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/TestPlan.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/TestPlan.kt index adeaed5dd70..07fad0acde1 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/TestPlan.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/TestPlan.kt @@ -4,7 +4,7 @@ package io.kotest.core.plan * A test plan describes the tests for a project. * * It consists of a tree describing tests, where each node in the tree is - * an instance of [TestPlanNode], and configuration. + * an instance of [Descriptor], and configuration. * */ -data class TestPlan(val root: TestPlanNode, val config: Map) +data class TestPlan(val root: Descriptor, val config: Map) diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/descriptors.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/descriptors.kt new file mode 100644 index 00000000000..0d9a72d10a2 --- /dev/null +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/descriptors.kt @@ -0,0 +1,292 @@ +@file:Suppress("MemberVisibilityCanBePrivate", "unused") + +package io.kotest.core.plan + +import io.kotest.core.SourceRef +import io.kotest.core.config.ExperimentalKotest +import io.kotest.core.plan.Descriptor.EngineDescriptor +import io.kotest.core.plan.Descriptor.SpecDescriptor +import io.kotest.core.script.ScriptSpec +import io.kotest.core.spec.DisplayName as DisplayNameAnnotation +import io.kotest.core.test.Description +import io.kotest.core.test.TestId +import io.kotest.core.test.TestPath +import io.kotest.core.test.TestType +import io.kotest.mpp.annotation +import io.kotest.mpp.bestName +import io.kotest.mpp.qualifiedNameOrNull +import kotlin.reflect.KClass + +/** + * A [Descriptor] is an ADT that represents nodes in the [TestPlan] tree. + * + * There are four types of descriptors - a single [EngineDescriptor] at the root, + * with [SpecDescriptor]s as direct children of the engine. + * + * Then [TestDescriptor]s are children of the spec descriptors and comprise leaf nodes. + * + * This class is intended as a long term replacement for [Description]. + * + * For example: + * + * - kotest + * - spec + * - container + * - test + * - test + * - spec + * - test + * - container + * - container + * - test + */ +@ExperimentalKotest +sealed class Descriptor { + + companion object { + + /** + * Returns a [SpecDescriptor] for a spec class. + * + * If the spec has been annotated with [DisplayName] (on supported platforms), then that will be used + * for the display name, otherwise the default is to use the fully qualified class name. + * + * Note: The display name must be globally unique. Two specs, even in different packages, + * cannot share the same names, so if [DisplayName] is used, developers must ensure it does not + * clash with another spec. + */ + fun fromSpecClass(kclass: KClass<*>): SpecDescriptor { + val name = kclass.bestName() + val display = kclass.annotation()?.name ?: kclass.simpleName ?: this.toString() + return SpecDescriptor( + name = Name(name), + displayName = DisplayName(display), + classname = kclass.qualifiedNameOrNull(), + script = false, + source = Source.ClassSource(kclass.bestName() + ".kt") + ) + } + + /** + * Returns a [SpecDescriptor] for a script file. + */ + fun fromScriptClass(kclass: KClass<*>): SpecDescriptor { + val name = kclass.bestName() + val display = kclass.simpleName ?: this.toString() + val fqn = kclass.qualifiedNameOrNull() + return SpecDescriptor( + name = Name(name), + displayName = DisplayName(display), + classname = fqn, + script = true, + source = Source.ClassSource(kclass.bestName() + ".kt") + ) + } + } + + /** + * Returns an ephemeral id that can be used to uniquely refer to this test during a test run. + * In 4.4 this will be the test name without special characters, but to allow for proper unicode + * tests, this will change to a numeric id. Do not rely on the format + */ + @ExperimentalKotest + fun id(): TestId = TestId(name.value.replace("[^a-zA-Z0-9]".toRegex(), "_")) + + /** + * Returns a parsable name for this descriptor. + */ + abstract val name: Name + + /** + * Returns a human readable name used for reports and displays. + */ + abstract val displayName: DisplayName + + /** + * Returns true if this descriptor is for a class based test file. + */ + fun isSpec() = this is SpecDescriptor + + /** + * Returns true if this descriptor is the root engine node. + */ + fun isEngine() = this is EngineDescriptor + + /** + * Returns true if this descriptor is for a root test case. + */ + fun isTestCase() = this is TestDescriptor + + /** + * Returns true if this descriptor represents a root test case. + */ + fun isRootTest() = this is TestDescriptor && this.parent.isSpec() + + /** + * Returns the depth of this node, where the [EngineDescriptor] is depth 0, a [SpecDescriptor] is depth 1, and so on. + */ + fun depth() = parents().size + + /** + * Returns true if this descriptor is a container of other descriptors. + */ + fun isContainer() = when (this) { + EngineDescriptor -> true + is SpecDescriptor -> true + is TestDescriptor -> this.type == TestType.Container + } + + /** + * Recursively returns any parent descriptors. + */ + fun parents(): List = when (this) { + EngineDescriptor -> emptyList() + is SpecDescriptor -> listOf(EngineDescriptor) + is TestDescriptor -> parent.parents() + parent + } + + /** + * Returns the [SpecDescriptor] parent for this descriptor, if any. + * + * If this descriptor is itself a spec, then this function will return itself. + * + * If this descriptor is the [EngineDescriptor], a [ScriptDescriptor], or a [TestDescriptor] located + * inside a script, then this function returns null. + */ + fun spec(): SpecDescriptor? = when (this) { + EngineDescriptor -> null + is SpecDescriptor -> this + is TestDescriptor -> parent.spec() + } + + /** + * Returns true if this descriptor is the immediate parent of the given [descriptor]. + */ + fun isParentOf(descriptor: Descriptor): Boolean = when (descriptor) { + // nothing can be the parent of the engine + EngineDescriptor -> false + // only the engine can be the parent of a spec + is SpecDescriptor -> this is EngineDescriptor + is TestDescriptor -> this.id() == descriptor.parent.id() + } + + /** + * Returns true if this descriptor is ancestor (1..nth-parent) of the given [descriptor]. + */ + fun isAncestorOf(descriptor: Descriptor): Boolean = when (descriptor) { + // nothing can be the ancestor of the engine + EngineDescriptor -> false + // only the engine can be a ancestor of a spec or script + is SpecDescriptor -> this is EngineDescriptor + is TestDescriptor -> isParentOf(descriptor) || isAncestorOf(descriptor.parent) + } + + /** + * Returns true if this descriptor is the immediate child of the given [descriptor]. + */ + fun isChildOf(descriptor: Descriptor): Boolean = when (this) { + EngineDescriptor -> false + is SpecDescriptor -> descriptor is EngineDescriptor + is TestDescriptor -> parent.id() == descriptor.id() + } + + /** + * Returns true if this node is a child, grandchild, etc of the given [descriptor]. + */ + fun isDescendentOf(descriptor: Descriptor): Boolean = when (this) { + // the engine cannot be a descendant of any other node + EngineDescriptor -> false + // a spec can only be a descendant of the engine + is SpecDescriptor -> this is EngineDescriptor + is TestDescriptor -> isChildOf(descriptor) || descriptor.isDescendentOf(descriptor) + } + + /** + * Returns true if this node is part of the path to the given [descriptor]. That is, if this + * instance is either an ancestor of, of the same as, the given node. + */ + fun contains(descriptor: Descriptor): Boolean = this.id() == descriptor.id() || this.isAncestorOf(descriptor) + + /** + * Returns a parseable path to the test. + * Includes the engine, spec or script, and all parent tests. + */ + fun testPath(): TestPath = when (this) { + EngineDescriptor -> TestPath(this.name.value) + is SpecDescriptor -> EngineDescriptor.testPath().append(this.name.value) + is TestDescriptor -> parent.testPath().append(this.name.value) + } + + abstract fun append(name: Name, displayName: DisplayName, type: TestType, source: Source.TestSource): TestDescriptor + + object EngineDescriptor : Descriptor() { + override val name: Name = Name("kotest") + override val displayName: DisplayName = DisplayName("kotest") + override fun append(name: Name, displayName: DisplayName, type: TestType, source: Source.TestSource) = + error("Cannot register a test on the engine") + } + + /** + * A [Descriptor] for a spec class or a script file. + * + * @param name the fully qualified class name if available, otherwise the simple class name + * @param displayName the simple class name unless overriden by a @DisplayName annotation. Note, only spec + * classes are able to specify a display name annotation. + * @param classname the fully qualified class name if available, or null. + * @param script true if this spec is a kotlin script. + */ + data class SpecDescriptor( + override val name: Name, + override val displayName: DisplayName, + val classname: String?, + val script: Boolean, + val source: Source.ClassSource, + ) : Descriptor() { + override fun append(name: Name, displayName: DisplayName, type: TestType, source: Source.TestSource) = + TestDescriptor(this, name, displayName, type, source) + } + + data class TestDescriptor( + val parent: Descriptor, + override val name: Name, + override val displayName: DisplayName, + val type: TestType, + val source: Source.TestSource, + ) : Descriptor() { + override fun append( + name: Name, + displayName: DisplayName, + type: TestType, + source: Source.TestSource + ): TestDescriptor { + if (this@TestDescriptor.type == TestType.Test) error("Cannot register test on TestType.Test") + return TestDescriptor(this, name, displayName, type, source) + } + } +} + +/** + * Creates a [Descriptor] from the deprecated descriptions. + */ +fun Description.toDescriptor(sourceRef: SourceRef): Descriptor { + return when (this) { + is Description.Spec -> + if (this.kclass.bestName() == ScriptSpec::class.bestName()) + Descriptor.fromScriptClass(this.kclass) + else + Descriptor.fromSpecClass(this.kclass) + is Description.Test -> Descriptor.TestDescriptor( + parent = this.parent.toDescriptor(sourceRef), + name = Name(this.name.name), + displayName = DisplayName(this.name.displayName), + type = this.type, + source = Source.TestSource(sourceRef.fileName, sourceRef.lineNumber) + ) + } +} + +data class Name(val value: String) +data class DisplayName(val value: String) +data class TestPath(val value: String) + +fun TestPath.append(component: String) = TestPath(listOf(this.value, component).joinToString("/")) diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/names.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/names.kt index 95f71014274..73bd15a57e6 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/names.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/names.kt @@ -1,213 +1,210 @@ -package io.kotest.core.plan - -import io.kotest.core.config.ExperimentalKotest -import io.kotest.core.config.configuration -import io.kotest.core.spec.DisplayName -import io.kotest.core.spec.Spec -import io.kotest.core.test.TestNameCase -import io.kotest.mpp.annotation -import io.kotest.mpp.bestName -import kotlin.reflect.KClass - -/** - * Models the name of a [TestPlanNode] in the test plan. - * - * Each member of this ADT has a [name] for that component only, and a [fullName] which models - * the name including parent names. - * - * This class is a long term replacement for [Description]. - */ -@ExperimentalKotest -sealed class NodeName { - - companion object { - const val NodeNameSeparator = "/" - - /** - * Returns a [NodeName] for this spec class. - * - * If the spec has been annotated with [DisplayName] (on supported platforms), then that will be used, - * otherwise the default is to use the fully qualified class name. - * - * Note: This name must be globally unique. Two specs, even in different packages, - * cannot share the same name, so if [DisplayName] is used, developers must ensure it does not - * clash with another spec. - */ - fun fromSpecClass(kclass: KClass): SpecName { - val name = kclass.bestName() - val displayName = kclass.annotation()?.name ?: name - val fullName = listOf(EngineName.name, name).joinToString(NodeNameSeparator) - return SpecName(name, displayName, fullName, name) - } - } - - /** - * The internal name for this node. - * Does not include any parents. For that, see [fullName]. - */ - abstract val name: String - - /** - * The name of this node as used for display purposes. - * This may differ from [name]. - */ - abstract val displayName: String - - /** - * The full internal name for a node. For example, for a test, this would be the full test path, - * including any parents, the spec name, and the engine name. - */ - abstract val fullName: String - - object EngineName : NodeName() { - override val name: String = "kotest" - override val displayName: String = "kotest" - override val fullName: String = "kotest" - } - - /** - * Models the name of a spec. - * - * The [name] for a spec is usually the class name. - * The [displayName] is also the class name, unless overriden by [DisplayName]. - * - */ - data class SpecName( - override val name: String, - override val displayName: String, - override val fullName: String, - val fqn: String, - ) : NodeName() { - - /** - * Returns a new [TestName] by appending the given [name] to this spec name. - * The name will be parsed for focus, bang. - */ - fun append(name: String): TestName = parseTestName(fullName, name) - } - - /** - * Models the name of a test case. - * - * A test name can sometimes have a affixes set automatically from the framework. - * For example, when using BehaviorSpec, tests can have "given", "when", "then" prepended. - * - * The [name] is the internal name, with focus, bang removed and without affixes. - * The [displayName] is the name as entered by the user, including affixes added by the framework. - */ - data class TestName( - override val name: String, - override val displayName: String, - override val fullName: String, - val prefix: String? = null, - val suffix: String? = null, - val focus: Boolean = false, - val bang: Boolean = false, - ) : NodeName() { - - init { - require(name.isNotBlank() && name.isNotEmpty()) { "Cannot create test with blank or empty name" } - require(fullName.isNotBlank() && fullName.isNotEmpty()) { "Cannot create test with blank or empty fqn" } - require(!focus || !bang) { "Bang and focus cannot both be true" } - } - - /** - * Returns a new [TestName] by appending the given [name] to this name. - * The name will be parsed for focus, bang. - */ - fun append(name: String): TestName = parseTestName(fullName, name) - } -} - -/** - * Creates a new test name from the given name, parsing for focus and bang. - * Test name case and affixes will use settings from config. - */ -@ExperimentalKotest -fun parseTestName(parent: String, name: String, specAffixDefault: Boolean = false): NodeName.TestName = - parseTestName( - parent, - null, - name, - null, - configuration.testNameCase, - configuration.removeTestNameWhitespace, - configuration.includeTestScopeAffixes ?: specAffixDefault - ) - -/** - * Parses a [NodeName.TestName] correctly handling focus, bang, prefix and suffix. - * If a prefix is specified the focus/bang is moved to before the prefix in the display name. - * - * This means the user writes when("!disable") and the platform invokes ("when", "!disable") - * and ends up with !when disable, so that it is correctly parsed by the test runtime. - * - * @param parent the parent full name. - * @param prefix optional prefix that some specs may specify, such as "Given:" - * @param name the user supplied name for the test - * @param suffix optional suffix that some specs may specify such as "should" - - * @param testNameCase a [TestNameCase] parameter to adjust the capitalisation of the display name - * @param includeAffixesInDisplayName if true then the prefix and/or suffix will be included in the display name. - * if false, they will not be included in the display name. If null, the default from config will be used. - */ -@ExperimentalKotest -fun parseTestName( - parent: String, - prefix: String?, - name: String, - suffix: String?, - testNameCase: TestNameCase, - removeTestNameWhiteSpec: Boolean, - includeAffixesInDisplayName: Boolean, -): NodeName.TestName { - - val trimmedName = if (removeTestNameWhiteSpec) { - name.removeAllExtraWhitespaces() - } else { - name.removeNewLineCharacter() - } - - val (focus, bang, croppedName) = when { - trimmedName.startsWith("!") -> Triple(first = false, second = true, third = trimmedName.drop(1).trim()) - trimmedName.startsWith("f:") -> Triple(first = true, second = false, third = trimmedName.drop(2).trim()) - else -> Triple(first = false, second = false, third = trimmedName) - } - - val withPrefix = when (includeAffixesInDisplayName) { - true -> prefix ?: "" - false -> "" - } - - val displayName = if (withPrefix.isBlank()) { - when (testNameCase) { - TestNameCase.Sentence -> croppedName.capitalize() - TestNameCase.InitialLowercase -> croppedName.uncapitalize() - TestNameCase.Lowercase -> croppedName.toLowerCase() - else -> croppedName - } - } else { - when (testNameCase) { - TestNameCase.Sentence -> "${withPrefix.capitalize()}${croppedName.uncapitalize()}" - TestNameCase.InitialLowercase -> "${withPrefix.uncapitalize()}${croppedName.uncapitalize()}" - TestNameCase.Lowercase -> "${withPrefix.toLowerCase()}${croppedName.toLowerCase()}" - else -> "$withPrefix$croppedName" - } - } - - return NodeName.TestName( - name, - displayName, - listOf(parent, name).joinToString(NodeName.NodeNameSeparator), - prefix, - suffix, - focus, - bang - ) -} - -private fun String.uncapitalize() = - this[0].toLowerCase() + substring(1 until this.length) - -private fun String.removeAllExtraWhitespaces() = this.split(Regex("\\s")).filterNot { it == "" }.joinToString(" ") -private fun String.removeNewLineCharacter() = this.replace("\n", "").trim() +//package io.kotest.core.plan +// +//import io.kotest.core.config.ExperimentalKotest +//import io.kotest.core.config.configuration +//import io.kotest.core.spec.DisplayName +//import io.kotest.core.test.DescriptionName +//import io.kotest.core.test.TestNameCase +//import io.kotest.mpp.annotation +//import io.kotest.mpp.bestName +//import kotlin.reflect.KClass +// +///** +// * Models the name of a [Descriptor] in the test plan. +// * +// * Each member of this ADT has a name that can be used for parsing purposes, and a display name +// * that may have been modified by the user for reporting purposes. +// * +// * This class is a long term replacement for [DescriptionName]. +// */ +//@ExperimentalKotest +//sealed class Name { +// +// companion object { +// +// /** +// * Returns a [SpecName] for this spec class. +// * +// * If the spec has been annotated with [DisplayName] (on supported platforms), then that will be used, +// * otherwise the default is to use the fully qualified class name. +// * +// * Note: This name must be globally unique. Two specs, even in different packages, +// * cannot share the same names, so if [DisplayName] is used, developers must ensure it does not +// * clash with another spec. +// */ +// fun fromSpecClass(kclass: KClass<*>): SpecName { +// val name = kclass.bestName() +// val displayName = kclass.annotation()?.name ?: kclass.simpleName ?: this.toString() +// return SpecName(name, displayName) +// } +// +// /** +// * Returns a [ScriptName] for a script class. +// * +// * If the spec has been annotated with [DisplayName] (on supported platforms), then that will be used, +// * otherwise the default is to use the fully qualified class name. +// * +// * Note: This name must be globally unique. Two specs, even in different packages, +// * cannot share the same names, so if [DisplayName] is used, developers must ensure it does not +// * clash with another spec. +// */ +// fun fromScriptClass(kclass: KClass<*>): ScriptName { +// val name = kclass.bestName() +// val displayName = kclass.simpleName ?: this.toString() +// return ScriptName(name, displayName) +// } +// } +// +// /** +// * A parsable name for this node. +// */ +// abstract val name: String +// +// /** +// * The name of this node as used for display purposes. +// * This may differ from [name]. +// */ +// abstract val displayName: String +// +// object EngineName : Name() { +// override val name: String = "kotest" +// override val displayName: String = "kotest" +// } +// +// /** +// * Models the name of a script. +// * +// * @param name the fully qualified class name. +// * @param displayName the simple class name. +// * +// */ +// data class ScriptName( +// override val name: String, +// override val displayName: String, +// ) : Name() +// +// /** +// * Models the name of a spec. +// * +// * @param name the fully qualified class name. +// * @param displayName short class name, unless overriden by the [DisplayName] annotation. +// * +// */ +// data class SpecName( +// override val name: String, +// override val displayName: String, +// ) : Name() +// +// /** +// * Models the name of a test case. +// * +// * A test name can sometimes have a affixes set automatically from the framework. +// * For example, when using BehaviorSpec, tests can have "given", "when", "then" prepended. +// * +// * The [name] is the internal name, with focus, bang removed and without affixes. +// * The [displayName] is the name as entered by the user, including affixes added by the framework. +// */ +// data class TestName( +// override val name: String, +// override val displayName: String, +// val prefix: String? = null, +// val suffix: String? = null, +// val focus: Boolean = false, +// val bang: Boolean = false, +// ) : Name() { +// +// init { +// require(name.isNotBlank() && name.isNotEmpty()) { "Cannot create test with blank or empty name" } +// require(!focus || !bang) { "Bang and focus cannot both be true" } +// } +// } +//} +// +///** +// * Creates a new test name from the given name, parsing for focus and bang. +// * Test name case and affixes will use settings from config. +// */ +//@ExperimentalKotest +//fun parseTestName(name: String, specAffixDefault: Boolean = false): Name.TestName = +// parseTestName( +// null, +// name, +// null, +// configuration.testNameCase, +// configuration.removeTestNameWhitespace, +// configuration.includeTestScopeAffixes ?: specAffixDefault +// ) +// +///** +// * Parses a [Name.TestName] correctly handling focus, bang, prefix and suffix. +// * If a prefix is specified the focus/bang is moved to before the prefix in the display name. +// * +// * This means the user writes when("!disable") and the platform invokes ("when", "!disable") +// * and ends up with !when disable, so that it is correctly parsed by the test runtime. +// * +// * @param prefix optional prefix that some specs may specify, such as "Given:" +// * @param name the user supplied name for the test +// * @param suffix optional suffix that some specs may specify such as "should" +// +// * @param testNameCase a [TestNameCase] parameter to adjust the capitalisation of the display name +// * @param includeAffixesInDisplayName if true then the prefix and/or suffix will be included in the display name. +// * if false, they will not be included in the display name. If null, the default from config will be used. +// */ +//@ExperimentalKotest +//fun parseTestName( +// prefix: String?, +// name: String, +// suffix: String?, +// testNameCase: TestNameCase, +// removeTestNameWhiteSpec: Boolean, +// includeAffixesInDisplayName: Boolean, +//): Name.TestName { +// +// val trimmedName = if (removeTestNameWhiteSpec) { +// name.removeAllExtraWhitespaces() +// } else { +// name.removeNewLineCharacter() +// } +// +// val (focus, bang, croppedName) = when { +// trimmedName.startsWith("!") -> Triple(first = false, second = true, third = trimmedName.drop(1).trim()) +// trimmedName.startsWith("f:") -> Triple(first = true, second = false, third = trimmedName.drop(2).trim()) +// else -> Triple(first = false, second = false, third = trimmedName) +// } +// +// val withPrefix = when (includeAffixesInDisplayName) { +// true -> prefix ?: "" +// false -> "" +// } +// +// val displayName = if (withPrefix.isBlank()) { +// when (testNameCase) { +// TestNameCase.Sentence -> croppedName.capitalize() +// TestNameCase.InitialLowercase -> croppedName.uncapitalize() +// TestNameCase.Lowercase -> croppedName.toLowerCase() +// else -> croppedName +// } +// } else { +// when (testNameCase) { +// TestNameCase.Sentence -> "${withPrefix.capitalize()}${croppedName.uncapitalize()}" +// TestNameCase.InitialLowercase -> "${withPrefix.uncapitalize()}${croppedName.uncapitalize()}" +// TestNameCase.Lowercase -> "${withPrefix.toLowerCase()}${croppedName.toLowerCase()}" +// else -> "$withPrefix$croppedName" +// } +// } +// +// return Name.TestName( +// name, +// displayName, +// prefix, +// suffix, +// focus, +// bang +// ) +//} +// +//private fun String.uncapitalize() = +// this[0].toLowerCase() + substring(1 until this.length) +// +//private fun String.removeAllExtraWhitespaces() = this.split(Regex("\\s")).filterNot { it == "" }.joinToString(" ") +//private fun String.removeNewLineCharacter() = this.replace("\n", "").trim() diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/nodes.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/nodes.kt deleted file mode 100644 index c8ee353b266..00000000000 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/nodes.kt +++ /dev/null @@ -1,184 +0,0 @@ -@file:Suppress("MemberVisibilityCanBePrivate", "unused") - -package io.kotest.core.plan - -import io.kotest.core.SourceRef -import io.kotest.core.Tag -import io.kotest.core.config.ExperimentalKotest -import io.kotest.core.plan.TestPlanNode.EngineNode -import io.kotest.core.plan.TestPlanNode.SpecNode -import io.kotest.core.plan.TestPlanNode.TestCaseNode -import io.kotest.core.sourceRef -import io.kotest.core.spec.Spec -import io.kotest.core.test.Description -import io.kotest.core.test.TestCase -import io.kotest.core.test.TestId -import io.kotest.core.test.TestType -import kotlin.random.Random -import kotlin.reflect.KClass - -/** - * A [TestPlanNode] is a lightweight descriptor for a node in the [TestPlan] tree. - * - * The test plan tree consists of a single root [EngineNode], with children of either [SpecNode]s, - * or [TestCaseNode]s. - * - * Only the [EngineNode] can contain [SpecNode]s and leaf nodes must always be [TestCaseNode]s. - * - * This class is intended as a long term replacement for [Description]. - * - * For example: - * - * - kotest - * - spec - * - container - * - test - * - test - * - spec - * - test - * - container - * - container - * - test - */ -@ExperimentalKotest -sealed class TestPlanNode { - - abstract val id: TestId - abstract val name: NodeName - - fun isSpec() = this is SpecNode - fun isEngine() = this is EngineNode - fun isTestCase() = this is TestCaseNode - fun isRootTest() = this is TestCaseNode && this.parent.isSpec() - - /** - * Returns the depth of this node, where the [EngineNode] is depth 0, a [SpecNode] is depth 1, and so on. - */ - fun depth() = parents().size - - /** - * Returns all parent nodes. - */ - fun parents(): List = when (this) { - EngineNode -> emptyList() - is SpecNode -> listOf(EngineNode) - is TestCaseNode -> parent.parents() + parent - } - - /** - * Returns the [SpecNode] parent for this node. - * - * If this node is itself a spec, then this function will return itself. - * - * If this node is the [EngineNode], then this function returns null. - */ - fun spec(): TestPlanNode? = when (this) { - EngineNode -> null - is SpecNode -> this - is TestCaseNode -> parent.spec() - } - - /** - * Returns true if this node is the immediate parent of the given [node]. - */ - fun isParentOf(node: TestPlanNode): Boolean = when (node) { - // nothing can be the parent of the engine - EngineNode -> false - // only the engine can be a parent of a spec - is SpecNode -> this is EngineNode - is TestCaseNode -> this.id == node.parent.id - } - - /** - * Returns true if this node is ancestor (1..nth-parent) of the given [node]. - */ - fun isAncestorOf(node: TestPlanNode): Boolean = when (node) { - // nothing can be the ancestor of the engine - EngineNode -> false - // only the engine can be a ancestor of a spec - is SpecNode -> this is EngineNode - is TestCaseNode -> isParentOf(node) || isAncestorOf(node.parent) - } - - /** - * Returns true if this node is the immediate child given [node]. - */ - fun isChildOf(node: TestPlanNode): Boolean = when (this) { - EngineNode -> false - is SpecNode -> node is EngineNode - is TestCaseNode -> parent.id == node.id - } - - /** - * Returns true if this node is a child, grandchild, etc of the given [node]. - */ - fun isDescendentOf(node: TestPlanNode): Boolean = when (this) { - // the engine cannot be a descendant of any other node - EngineNode -> false - // a spec can only be a descendant of the engine - is SpecNode -> this is EngineNode - is TestCaseNode -> isChildOf(node) || parent.isDescendentOf(node) - } - - /** - * Returns true if this node is part of the path to the given [node]. That is, if this - * instance is either an ancestor of, of the same as, the given node. - */ - fun contains(node: TestPlanNode): Boolean = this.id == node.id || this.isAncestorOf(node) - - object EngineNode : TestPlanNode() { - override val id: TestId = TestId("kotest") - override val name: NodeName = NodeName.EngineName - } - - data class SpecNode( - override val name: NodeName.SpecName, - val specClass: KClass, - // the runtime tags applied to this spec - val tags: Set, - // true if this spec is active - val active: Boolean, - ) : TestPlanNode() { - override val id: TestId = TestId(Random.nextLong().toString()) - } - - data class TestCaseNode( - val parent: TestPlanNode, - override val name: NodeName.TestName, - val type: TestType, - // the runtime tags applied to this test - val tags: Set, - // link to the source ref where this test is defined - val source: SourceRef, - // true if this test case is active - val active: Boolean, - ) : TestPlanNode() { - override val id: TestId = TestId(Random.nextLong().toString()) - } -} - -fun Description.toNode(spec: Spec): TestPlanNode { - return when (this) { - is Description.Spec -> SpecNode(NodeName.fromSpecClass(spec::class), spec::class, spec.tags(), false) - - is Description.Test -> { - val parent = this.parent.toNode(spec) - val name = when (parent) { - EngineNode -> TODO() - is SpecNode -> parent.name.append(this.displayName()) - is TestCaseNode -> parent.name.append(this.displayName()) - } - TestCaseNode( - parent, - name, - type, - emptySet(), - sourceRef(), - false - ) - } - } -} - -fun TestCase.toNode(active: Boolean): TestCaseNode = - (description.toNode(spec) as TestCaseNode).copy(active = active) diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/source.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/source.kt new file mode 100644 index 00000000000..cb088f39d07 --- /dev/null +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/source.kt @@ -0,0 +1,9 @@ +package io.kotest.core.plan + +sealed class Source { + + abstract val filename: String + + data class ClassSource(override val filename: String) : Source() + data class TestSource(override val filename: String, val lineNumber: Int) : Source() +} diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/script/ScriptRuntime.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/script/ScriptRuntime.kt index ce532d4aeed..8152a24ba91 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/script/ScriptRuntime.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/script/ScriptRuntime.kt @@ -1,5 +1,10 @@ package io.kotest.core.script +import io.kotest.core.config.ExperimentalKotest +import io.kotest.core.plan.Descriptor +import io.kotest.core.plan.DisplayName +import io.kotest.core.plan.Name +import io.kotest.core.plan.Source import io.kotest.core.sourceRef import io.kotest.core.spec.style.FunSpec import io.kotest.core.test.DescriptionName @@ -9,11 +14,13 @@ import io.kotest.core.test.TestContext import io.kotest.core.test.TestType import io.kotest.mpp.log +@ExperimentalKotest class ScriptSpec : FunSpec() /** * This is a global that scripts can use to gain access to configuration at runtime. */ +@ExperimentalKotest object ScriptRuntime { private var spec = ScriptSpec() @@ -37,16 +44,23 @@ object ScriptRuntime { ) { log("ScriptRuntime: registerRootTest $name") val config = if (xdisabled) TestCaseConfig().copy(enabled = false) else TestCaseConfig() + val description = spec.description().append(name, type) rootTests.add( TestCase( - spec.description().append(name, type), - spec, - test, - sourceRef(), - type, - config, - null, - null + description = description, + spec = spec, + test = test, + source = sourceRef(), + type = type, + config = config, + factoryId = null, + assertionMode = null, + descriptor = Descriptor.fromScriptClass(ScriptSpec::class).append( + Name(description.name.name), + DisplayName(description.name.displayName), + TestType.Test, + Source.TestSource(sourceRef().fileName, sourceRef().lineNumber), + ), ) ) } @@ -56,7 +70,11 @@ object ScriptRuntime { spec = ScriptSpec() } - fun materializeRootTests(): List { - return rootTests.toList() + fun materializeRootTests(parent: Descriptor.SpecDescriptor): List { + // the test cases will have been registered with a placeholder spec description, since we don't know + // what that is until runtime. So now we must replace that. + return rootTests.toList().map { + it.copy(descriptor = it.descriptor!!.copy(parent = parent)) + } } } diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/script/styles.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/script/styles.kt index eed47f54359..57716186afe 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/script/styles.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/script/styles.kt @@ -1,5 +1,11 @@ package io.kotest.core.script +import io.kotest.core.plan.Descriptor +import io.kotest.core.plan.DisplayName +import io.kotest.core.plan.Name +import io.kotest.core.plan.Source +import io.kotest.core.plan.toDescriptor +import io.kotest.core.sourceRef import io.kotest.core.test.Description import io.kotest.core.test.DescriptionName import io.kotest.core.test.TestCaseConfig @@ -20,12 +26,25 @@ fun test(name: String, test: suspend TestContext.() -> Unit) { fun context(name: String, test: suspend ContextScope.() -> Unit) { val testName = createTestName(name) val description = ScriptSpec().description().append(testName, TestType.Container) - ScriptRuntime.registerRootTest(testName, false, TestType.Container) { ContextScope(description, it).test() } + val d = description.toDescriptor(sourceRef()).append( + Name(testName.name), + DisplayName(testName.displayName), + TestType.Container, + Source.TestSource(sourceRef().fileName, sourceRef().lineNumber), + ) + ScriptRuntime.registerRootTest(testName, false, TestType.Container) { + ContextScope( + description, + it, + d + ).test() + } } class ContextScope( val description: Description, val testContext: TestContext, + val descriptor: Descriptor, ) { suspend fun test(name: String, test: suspend TestContext.() -> Unit) { @@ -36,7 +55,13 @@ class ContextScope( xdisabled = false, test = test, config = TestCaseConfig(), - type = TestType.Test + type = TestType.Test, + descriptor = descriptor.append( + Name(testName.name), + DisplayName(testName.displayName), + TestType.Test, + Source.TestSource(sourceRef().fileName, sourceRef().lineNumber), + ), ) } @@ -48,7 +73,13 @@ class ContextScope( xdisabled = false, test = test, config = TestCaseConfig(), - type = TestType.Test + type = TestType.Test, + descriptor = descriptor.append( + Name(testName.name), + DisplayName(testName.displayName), + TestType.Test, + Source.TestSource(sourceRef().fileName, sourceRef().lineNumber), + ), ) } } @@ -59,16 +90,23 @@ class ContextScope( fun describe(name: String, test: suspend DescribeScope.() -> Unit) { val testName = createTestName("Describe: ", name, false) val description = ScriptSpec().description().append(testName, TestType.Container) + val d = description.toDescriptor(sourceRef()).append( + Name(testName.name), + DisplayName(testName.displayName), + TestType.Container, + Source.TestSource(sourceRef().fileName, sourceRef().lineNumber), + ) ScriptRuntime.registerRootTest( testName, false, TestType.Container - ) { DescribeScope(description, it).test() } + ) { DescribeScope(description, it, d).test() } } class DescribeScope( val description: Description, val testContext: TestContext, + val descriptor: Descriptor, ) { /** @@ -76,13 +114,20 @@ class DescribeScope( */ suspend fun describe(name: String, test: suspend DescribeScope.() -> Unit) { val testName = createTestName("Describe: ", name, false) + val d = descriptor.append( + Name(testName.name), + DisplayName(testName.displayName), + TestType.Container, + Source.TestSource(sourceRef().fileName, sourceRef().lineNumber), + ) registerNestedTest( name = testName, testContext = testContext, xdisabled = false, - test = { DescribeScope(description.append(testName, TestType.Test), testContext).test() }, + test = { DescribeScope(description.append(testName, TestType.Test), testContext, d).test() }, config = TestCaseConfig(), - type = TestType.Test + type = TestType.Test, + descriptor = d, ) } @@ -94,7 +139,13 @@ class DescribeScope( xdisabled = false, test = test, config = TestCaseConfig(), - type = TestType.Test + type = TestType.Test, + descriptor = descriptor.append( + Name(testName.name), + DisplayName(testName.displayName), + TestType.Test, + Source.TestSource(sourceRef().fileName, sourceRef().lineNumber), + ), ) } } @@ -105,8 +156,9 @@ private suspend fun registerNestedTest( test: suspend TestContext.() -> Unit, config: TestCaseConfig, testContext: TestContext, - type: TestType + type: TestType, + descriptor: Descriptor.TestDescriptor, ) { val activeConfig = if (xdisabled) config.copy(enabled = false) else config - testContext.registerTestCase(name, test, activeConfig, type) + testContext.registerTestCase(name = name, test = test, config = activeConfig, type = type, descriptor = descriptor) } diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/NestedTest.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/NestedTest.kt index f5798a340ce..a3cb1946b2d 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/NestedTest.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/NestedTest.kt @@ -3,6 +3,7 @@ package io.kotest.core.test import io.kotest.core.factory.FactoryId import io.kotest.core.SourceRef import io.kotest.core.config.configuration +import io.kotest.core.plan.Descriptor import io.kotest.core.spec.Spec /** @@ -15,7 +16,8 @@ data class NestedTest( val config: TestCaseConfig, val type: TestType, val sourceRef: SourceRef, - val factoryId: FactoryId? + val factoryId: FactoryId?, + val descriptor: Descriptor.TestDescriptor? ) /** @@ -30,7 +32,8 @@ fun NestedTest.toTestCase(spec: Spec, parent: Description): TestCase { type = type, config = config, factoryId = factoryId, - assertionMode = null + assertionMode = null, + descriptor = descriptor, ) return if (configuration.testNameAppendTags) { TestCase.appendTagsInDisplayName(testCase) diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/TestCase.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/TestCase.kt index 75e397abf08..504dbbdfd37 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/TestCase.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/TestCase.kt @@ -1,8 +1,10 @@ package io.kotest.core.test import io.kotest.core.SourceRef +import io.kotest.core.config.ExperimentalKotest import io.kotest.core.factory.FactoryId import io.kotest.core.internal.tags.allTags +import io.kotest.core.plan.Descriptor import io.kotest.core.sourceRef import io.kotest.core.spec.Spec @@ -48,7 +50,10 @@ data class TestCase( val factoryId: FactoryId? = null, // assertion mode can be set to control errors/warnings in a test // if null, defaults will be applied - val assertionMode: AssertionMode? = null + val assertionMode: AssertionMode? = null, + + // only set for scripts + @ExperimentalKotest val descriptor: Descriptor.TestDescriptor? = null, ) { val displayName = description.displayName() diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/TestCaseExecutionListener.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/TestCaseExecutionListener.kt index 984552483cb..b7eb339b2b2 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/TestCaseExecutionListener.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/TestCaseExecutionListener.kt @@ -1,8 +1,5 @@ package io.kotest.core.test -import io.kotest.core.test.TestCase -import io.kotest.core.test.TestResult - interface TestCaseExecutionListener { fun testStarted(testCase: TestCase) {} fun testIgnored(testCase: TestCase) {} diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/TestContext.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/TestContext.kt index c59e534eb94..4085b8f2a4d 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/TestContext.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/TestContext.kt @@ -1,5 +1,6 @@ package io.kotest.core.test +import io.kotest.core.plan.Descriptor import io.kotest.core.sourceRef import io.kotest.core.spec.KotestDsl import kotlinx.coroutines.CoroutineScope @@ -36,11 +37,12 @@ interface TestContext : CoroutineScope { name: DescriptionName.TestName, test: suspend TestContext.() -> Unit, config: TestCaseConfig, - type: TestType + type: TestType, + descriptor: Descriptor.TestDescriptor? = null, ) { when (testCase.type) { TestType.Container -> { - val nested = NestedTest(name, test, config, type, sourceRef(), testCase.factoryId) + val nested = NestedTest(name, test, config, type, sourceRef(), testCase.factoryId, descriptor) registerTestCase(nested) } TestType.Test -> throw InvalidTestConstructionException("Cannot add a nested test to '${testCase.displayName}' because it is not a test container") diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/KotestEngine.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/KotestEngine.kt index cebc032b1ca..fcbf3ce87c3 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/KotestEngine.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/KotestEngine.kt @@ -3,7 +3,7 @@ package io.kotest.engine import io.kotest.core.Tags import io.kotest.core.config.configuration import io.kotest.core.filter.TestFilter -import io.kotest.core.script.ScriptSpec +import io.kotest.core.plan.Descriptor import io.kotest.core.spec.Spec import io.kotest.core.spec.afterProject import io.kotest.core.spec.beforeProject @@ -118,14 +118,11 @@ class KotestEngine(private val config: KotestEngineConfig) { // scripts always run sequentially log("KotestEngine: Launching ${plan.scripts.size} scripts") if (plan.scripts.isNotEmpty()) { - config.listener.specStarted(ScriptSpec::class) plan.scripts.forEach { scriptKClass -> log(scriptKClass.java.methods.toList().toString()) ScriptExecutor(config.listener) .execute(scriptKClass) - .onFailure { config.listener.specFinished(ScriptSpec::class, it, emptyMap()) } } - config.listener.specFinished(ScriptSpec::class, null, emptyMap()) log("KotestEngine: Script execution completed") } diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/NotificationManager.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/NotificationManager.kt index 2233c7f38b3..60f07eb2512 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/NotificationManager.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/NotificationManager.kt @@ -3,7 +3,7 @@ package io.kotest.engine import io.kotest.core.config.configuration import io.kotest.core.config.specInstantiationListeners import io.kotest.core.config.testListeners -import io.kotest.core.plan.TestPlanNode +import io.kotest.core.plan.Descriptor import io.kotest.core.spec.Spec import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult @@ -18,9 +18,9 @@ import kotlin.reflect.KClass class NotificationManager(private val listener: TestEngineListener) { /** - * Notifies listeners that we are about to start execution of a [TestPlanNode]. + * Notifies listeners that we are about to start execution of a [Descriptor]. */ - suspend fun specStarted(spec: TestPlanNode.SpecNode) = Try { + suspend fun specStarted(spec: Descriptor.SpecDescriptor) = Try { log("NotificationManager:specStarted $spec") listener.specStarted(spec) @@ -28,9 +28,9 @@ class NotificationManager(private val listener: TestEngineListener) { } suspend fun specFinished( - spec: TestPlanNode.SpecNode, + spec: Descriptor.SpecDescriptor, error: Throwable?, - results: Map + results: Map ) = Try { log("NotificationManager:specFinished $spec") listener.specFinished(spec, error, results) diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/CompositeTestEngineListener.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/CompositeTestEngineListener.kt index dc32011d39f..bba284fd5d4 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/CompositeTestEngineListener.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/CompositeTestEngineListener.kt @@ -1,6 +1,6 @@ package io.kotest.engine.listener -import io.kotest.core.plan.TestPlanNode +import io.kotest.core.plan.Descriptor import io.kotest.core.spec.Spec import io.kotest.core.test.Description import io.kotest.core.test.TestCase @@ -29,24 +29,28 @@ class CompositeTestEngineListener(private val listeners: List + results: Map ) { listeners.forEach { it.specFinished(spec, t, results) } } - override fun specStarted(spec: TestPlanNode.SpecNode) { - listeners.forEach { it.specStarted(spec) } + override fun testFinished(descriptor: Descriptor.TestDescriptor, result: TestResult) { + listeners.forEach { it.testFinished(descriptor, result) } + } + + override fun testIgnored(descriptor: Descriptor.TestDescriptor, reason: String?) { + listeners.forEach { it.testIgnored(descriptor, reason) } } - override fun testStarted(description: Description) { - listeners.forEach { it.testStarted(description) } + override fun testStarted(descriptor: Descriptor.TestDescriptor) { + listeners.forEach { it.testStarted(descriptor) } } override fun testStarted(testCase: TestCase) { diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/IsolationTestEngineListener.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/IsolationTestEngineListener.kt index ea9355c585e..8e06098be95 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/IsolationTestEngineListener.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/IsolationTestEngineListener.kt @@ -2,15 +2,16 @@ package io.kotest.engine.listener -import io.kotest.core.plan.TestPlanNode +import io.kotest.core.plan.Descriptor +import io.kotest.core.script.ScriptSpec import io.kotest.core.spec.Spec -import io.kotest.core.test.Description import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import io.kotest.core.spec.toDescription import io.kotest.framework.discovery.log import java.util.concurrent.atomic.AtomicReference import kotlin.reflect.KClass +import kotlinx.coroutines.runBlocking /** * Wraps a [TestEngineListener] methods to ensure that only test notifications @@ -82,6 +83,18 @@ class IsolationTestEngineListener(val listener: TestEngineListener) : TestEngine } } + override fun testStarted(descriptor: Descriptor.TestDescriptor) { + synchronized(listener) { + if (runningSpec.get() == descriptor.spec()?.classname || descriptor.spec()?.script == true) { + listener.testStarted(descriptor) + } else { + queue { + testStarted(descriptor) + } + } + } + } + override fun testIgnored(testCase: TestCase, reason: String?) { synchronized(listener) { if (runningSpec.get() == testCase.spec::class.toDescription().path().value) { @@ -94,50 +107,50 @@ class IsolationTestEngineListener(val listener: TestEngineListener) : TestEngine } } - override fun testFinished(testCase: TestCase, result: TestResult) { + override fun testIgnored(descriptor: Descriptor.TestDescriptor, reason: String?) { synchronized(listener) { - if (runningSpec.get() == testCase.spec::class.toDescription().path().value) { - listener.testFinished(testCase, result) + if (runningSpec.get() == descriptor.spec()?.classname || descriptor.spec()?.script == true) { + listener.testIgnored(descriptor, reason) } else { queue { - testFinished(testCase, result) + testIgnored(descriptor, reason) } } } } - override fun specStarted(kclass: KClass) { + override fun testFinished(testCase: TestCase, result: TestResult) { synchronized(listener) { - log("IsolationTestEngineListener: specStarted $kclass") - if (runningSpec.compareAndSet(null, kclass.toDescription().path().value)) { - listener.specStarted(kclass) + if (runningSpec.get() == testCase.spec::class.toDescription().path().value) { + listener.testFinished(testCase, result) } else { queue { - specStarted(kclass) + testFinished(testCase, result) } } } } - override fun testFinished(description: Description, result: TestResult) { + override fun testFinished(descriptor: Descriptor.TestDescriptor, result: TestResult) { synchronized(listener) { - if (runningSpec.get() == description.spec().path().value) { - listener.testFinished(description, result) + if (runningSpec.get() == descriptor.spec()?.classname || descriptor.spec()?.script == true) { + listener.testFinished(descriptor, result) } else { queue { - testFinished(description, result) + testFinished(descriptor, result) } } } } - override fun testStarted(description: Description) { + override fun specStarted(kclass: KClass) { synchronized(listener) { - if (runningSpec.get() == description.spec().path().value) { - listener.testStarted(description) + log("IsolationTestEngineListener: specStarted $kclass") + if (runningSpec.compareAndSet(null, kclass.toDescription().path().value)) { + listener.specStarted(kclass) } else { queue { - testStarted(description) + specStarted(kclass) } } } @@ -162,15 +175,41 @@ class IsolationTestEngineListener(val listener: TestEngineListener) : TestEngine } } - override fun specFinished( - spec: TestPlanNode.SpecNode, + override suspend fun specFinished( + descriptor: Descriptor.SpecDescriptor, t: Throwable?, - results: Map + results: Map ) { - listener.specFinished(spec, t, results) + synchronized(listener) { + if (runningSpec.get() == descriptor.classname || descriptor.spec()?.script == true) { + runBlocking { + listener.specFinished(descriptor, t, results) + } + runningSpec.set(null) + replay() + } else { + queue { + runBlocking { + listener.specFinished(descriptor, t, results) + } + } + } + } } - override fun specStarted(spec: TestPlanNode.SpecNode) { - listener.specStarted(spec) + override suspend fun specStarted(descriptor: Descriptor.SpecDescriptor) { + synchronized(listener) { + if (runningSpec.compareAndSet(null, descriptor.classname)) { + runBlocking { + listener.specStarted(descriptor) + } + } else { + queue { + runBlocking { + listener.specStarted(descriptor) + } + } + } + } } } diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/SynchronizedTestEngineListener.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/SynchronizedTestEngineListener.kt index be83c6119af..4d72fbb1df8 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/SynchronizedTestEngineListener.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/SynchronizedTestEngineListener.kt @@ -1,11 +1,11 @@ package io.kotest.engine.listener -import io.kotest.core.plan.TestPlanNode +import io.kotest.core.plan.Descriptor import io.kotest.core.spec.Spec -import io.kotest.core.test.Description import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import kotlin.reflect.KClass +import kotlinx.coroutines.runBlocking /** * Wraps a [TestEngineListener]s methods in synchronized calls to ensure no race conditions. @@ -66,31 +66,41 @@ class SynchronizedTestEngineListener(private val listener: TestEngineListener) : } } - override fun specFinished( - spec: TestPlanNode.SpecNode, + override suspend fun specFinished( + spec: Descriptor.SpecDescriptor, t: Throwable?, - results: Map + results: Map ) { synchronized(listener) { - listener.specFinished(spec, t, results) + runBlocking { + listener.specFinished(spec, t, results) + } } } - override fun specStarted(spec: TestPlanNode.SpecNode) { + override suspend fun specStarted(spec: Descriptor.SpecDescriptor) { synchronized(listener) { - listener.specStarted(spec) + runBlocking { + listener.specStarted(spec) + } } } - override fun testFinished(description: Description, result: TestResult) { + override fun testFinished(descriptor: Descriptor.TestDescriptor, result: TestResult) { synchronized(listener) { - listener.testFinished(description, result) + listener.testFinished(descriptor, result) } } - override fun testStarted(description: Description) { + override fun testIgnored(descriptor: Descriptor.TestDescriptor, reason: String?) { synchronized(listener) { - listener.testStarted(description) + listener.testIgnored(descriptor, reason) + } + } + + override fun testStarted(descriptor: Descriptor.TestDescriptor) { + synchronized(listener) { + listener.testStarted(descriptor) } } } diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/TestEngineListener.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/TestEngineListener.kt index 5a730746d3f..8d0fb61ad39 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/TestEngineListener.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/TestEngineListener.kt @@ -1,8 +1,8 @@ package io.kotest.engine.listener -import io.kotest.core.plan.TestPlanNode +import io.kotest.core.config.ExperimentalKotest +import io.kotest.core.plan.Descriptor import io.kotest.core.spec.Spec -import io.kotest.core.test.Description import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import kotlin.reflect.KClass @@ -34,7 +34,6 @@ interface TestEngineListener { * Is invoked once per [Spec] to indicate that this spec is about to * begin execution. */ - @Deprecated("Moving to notifications by nodes") fun specStarted(kclass: KClass) { } @@ -42,7 +41,7 @@ interface TestEngineListener { * Is invoked once per [Spec] to indicate that this spec is about to * begin execution. */ - fun specStarted(spec: TestPlanNode.SpecNode) {} + suspend fun specStarted(descriptor: Descriptor.SpecDescriptor) {} /** * Is invoked once per [Spec] to indicate that all [TestCase] instances @@ -52,10 +51,16 @@ interface TestEngineListener { * @param t if not null, then an error that occured when trying to execute this spec * @param results if t is null, then the results of the tests that were submitted. */ - @Deprecated("Moving to notifications by nodes") - fun specFinished(kclass: KClass, t: Throwable?, results: Map) {} + fun specFinished(kclass: KClass, t: Throwable?, results: Map) { + } - fun specFinished(spec: TestPlanNode.SpecNode, t: Throwable?, results: Map) {} + @ExperimentalKotest + suspend fun specFinished( + descriptor: Descriptor.SpecDescriptor, + t: Throwable?, + results: Map + ) { + } /** * Invoked if a [TestCase] is about to be executed (is active). @@ -63,15 +68,23 @@ interface TestEngineListener { */ fun testStarted(testCase: TestCase) {} - fun testStarted(description: Description) {} - - fun testFinished(description: Description, result: TestResult) {} + /** + * Invoked if a [TestCase] is about to be executed (is active). + * Will not be invoked if the test is ignored. + */ + @ExperimentalKotest + fun testStarted(descriptor: Descriptor.TestDescriptor) { + } /** * Invoked if a [TestCase] will not be executed because it is ignored (not active). */ fun testIgnored(testCase: TestCase, reason: String?) {} + @ExperimentalKotest + fun testIgnored(descriptor: Descriptor.TestDescriptor, reason: String?) { + } + /** * Invoked when all the invocations of a [TestCase] have completed. * This function will only be invoked if a test case was active. @@ -79,6 +92,10 @@ interface TestEngineListener { */ fun testFinished(testCase: TestCase, result: TestResult) {} + @ExperimentalKotest + fun testFinished(descriptor: Descriptor.TestDescriptor, result: TestResult) { + } + /** * Invoked each time an instance of a [Spec] is created. * A spec may be created once per class, or one per [TestCase]. diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/script/ScriptExecutor.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/script/ScriptExecutor.kt index 4e9721cd1c5..1404b1f024d 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/script/ScriptExecutor.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/script/ScriptExecutor.kt @@ -2,6 +2,8 @@ package io.kotest.engine.script import io.kotest.core.DuplicatedTestNameException import io.kotest.core.internal.TestCaseExecutor +import io.kotest.core.plan.Descriptor +import io.kotest.core.plan.toDescriptor import io.kotest.core.script.ScriptRuntime import io.kotest.core.test.DescriptionName import io.kotest.core.test.NestedTest @@ -11,6 +13,7 @@ import io.kotest.core.test.TestContext import io.kotest.core.test.TestResult import io.kotest.core.test.toTestCase import io.kotest.engine.ExecutorExecutionContext +import io.kotest.engine.NotificationManager import io.kotest.engine.listener.TestEngineListener import io.kotest.engine.toTestResult import io.kotest.fp.Try @@ -28,7 +31,8 @@ import kotlin.script.templates.standard.ScriptTemplateWithArgs */ class ScriptExecutor(private val listener: TestEngineListener) { - private val results = ConcurrentHashMap() + private val results = ConcurrentHashMap() + private val n = NotificationManager(listener) /** * Executes the given test [ScriptTemplateWithArgs]. @@ -36,38 +40,42 @@ class ScriptExecutor(private val listener: TestEngineListener) { suspend fun execute(kclass: KClass): Try { log("ScriptExecutor: execute [$kclass]") ScriptRuntime.reset() + val descriptor = Descriptor.fromScriptClass(kclass) return createInstance(kclass) - .flatMap { runTests() } + .flatMap { n.specStarted(descriptor) } + .flatMap { runTests(descriptor) } + .onFailure { n.specFinished(descriptor, it, emptyMap()) } + .onSuccess { n.specFinished(descriptor, null, results.toMap()) } } - /** - * Invokes the script by finding and executing the generated main method. - */ - private suspend fun runScript(script: ScriptTemplateWithArgs): Try = Try { -// ScriptRuntime.reset() - -// BasicJvmScriptEvaluator().invoke( -// KJvmCompiledScript( -// null, -// ScriptCompilationConfiguration.Default, -// script.javaClass.name, -// resultField = null, -// compiledModule = KJvmCompiledModuleFromClassLoader(this.javaClass.classLoader), -// ), ScriptEvaluationConfiguration() -// ) - - // - //val main = script.javaClass.getMethod("main", Array::class.java) - //log("ScriptExecutor: Invoking script main [$main]") - //main.invoke(null, emptyArray()) - } +// /** +// * Invokes the script by finding and executing the generated main method. +// */ +// private suspend fun runScript(script: ScriptTemplateWithArgs): Try = Try { +//// ScriptRuntime.reset() +// +//// BasicJvmScriptEvaluator().invoke( +//// KJvmCompiledScript( +//// null, +//// ScriptCompilationConfiguration.Default, +//// script.javaClass.name, +//// resultField = null, +//// compiledModule = KJvmCompiledModuleFromClassLoader(this.javaClass.classLoader), +//// ), ScriptEvaluationConfiguration() +//// ) +// +// // +// //val main = script.javaClass.getMethod("main", Array::class.java) +// //log("ScriptExecutor: Invoking script main [$main]") +// //main.invoke(null, emptyArray()) +// } /** * Executes the tests registered with the script execution runtime. */ - private suspend fun runTests() = Try { + private suspend fun runTests(descriptor: Descriptor.SpecDescriptor): Try = Try { log("ScriptExecutor: Executing tests from script") - ScriptRuntime.materializeRootTests().forEach { testCase -> + ScriptRuntime.materializeRootTests(descriptor).forEach { testCase -> runTest(testCase, coroutineContext) } } @@ -87,20 +95,20 @@ class ScriptExecutor(private val listener: TestEngineListener) { ) { val testExecutor = TestCaseExecutor(object : TestCaseExecutionListener { override fun testStarted(testCase: TestCase) { - listener.testStarted(testCase) + listener.testStarted(testCase.descriptor!!) } override fun testIgnored(testCase: TestCase) { - listener.testIgnored(testCase, null) + listener.testIgnored(testCase.descriptor!!, null) } override fun testFinished(testCase: TestCase, result: TestResult) { - listener.testFinished(testCase, result) + listener.testFinished(testCase.descriptor!!, result) } }, ExecutorExecutionContext, {}, { t, duration -> toTestResult(t, duration) }) val result = testExecutor.execute(testCase, Context(testCase, coroutineContext)) - results[testCase] = result + results[testCase.description.toDescriptor(testCase.source) as Descriptor.TestDescriptor] = result } inner class Context( diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/script/instantiate.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/script/instantiate.kt index 010d72df65d..fadcede9ddd 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/script/instantiate.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/script/instantiate.kt @@ -2,7 +2,6 @@ package io.kotest.engine.script import io.kotest.fp.Try import kotlin.reflect.KClass -import kotlin.reflect.jvm.jvmName import kotlin.script.templates.standard.ScriptTemplateWithArgs /** @@ -12,7 +11,7 @@ internal fun createAndInitializeScript( clazz: KClass, loader: ClassLoader ): Try = Try { - javaReflectNewInstance(clazz.jvmName, loader) + javaReflectNewInstance(clazz.qualifiedName!!, loader) } internal fun javaReflectNewInstance(name: String, loader: ClassLoader): ScriptTemplateWithArgs { diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/active/IsActiveTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/active/IsActiveTest.kt index ee15e9c122d..2d3537b6f39 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/active/IsActiveTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/active/IsActiveTest.kt @@ -12,7 +12,7 @@ import io.kotest.core.filter.TestFilterResult import io.kotest.core.filter.toTestFilterResult import io.kotest.core.internal.isActive import io.kotest.core.internal.isActiveInternal -import io.kotest.core.plan.TestPlanNode +import io.kotest.core.plan.Descriptor import io.kotest.core.spec.Isolate import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.StringSpec @@ -176,8 +176,8 @@ class IsActiveTest : StringSpec() { "isActive should use extensions when registered" { val ext = object : IsActiveExtension { - override suspend fun isActive(node: TestPlanNode): Boolean { - return node.name.name.contains("activateme") + override suspend fun isActive(descriptor: Descriptor): Boolean { + return descriptor.name.value.contains("activateme") } } @@ -189,11 +189,11 @@ class IsActiveTest : StringSpec() { SomeTestClass() ) {}.isActive() shouldBe false - // this should be active because the extension says it is, even though it's disabled by a bang - TestCase.test( - SomeTestClass::class.toDescription().appendTest("!activateme"), - SomeTestClass() - ) {}.isActive() shouldBe true +// // this should be active because the extension says it is, even though it's disabled by a bang +// TestCase.test( +// SomeTestClass::class.toDescription().appendTest("!activateme"), +// SomeTestClass() +// ) {}.isActive() shouldBe true configuration.deregisterExtension(ext) } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/plan/ScriptDescriptorTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/plan/ScriptDescriptorTest.kt new file mode 100644 index 00000000000..8cfc00c47a0 --- /dev/null +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/plan/ScriptDescriptorTest.kt @@ -0,0 +1,19 @@ +package com.sksamuel.kotest.engine.plan + +import io.kotest.core.plan.Descriptor +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class ScriptDescriptorTest : FunSpec() { + init { + test("classname should be set when generating a script name") { + Descriptor.fromScriptClass(ScriptDescriptorTest::class).classname shouldBe "com.sksamuel.kotest.engine.plan.ScriptDescriptorTest" + } + test("name should be set when generating a script name") { + Descriptor.fromScriptClass(ScriptDescriptorTest::class).name.value shouldBe "com.sksamuel.kotest.engine.plan.ScriptDescriptorTest" + } + test("test path should be set when generating a script name") { + Descriptor.fromScriptClass(ScriptDescriptorTest::class).testPath().value shouldBe "kotest/com.sksamuel.kotest.engine.plan.ScriptDescriptorTest" + } + } +} diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/plan/SpecDescriptorTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/plan/SpecDescriptorTest.kt new file mode 100644 index 00000000000..458bb407e90 --- /dev/null +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/plan/SpecDescriptorTest.kt @@ -0,0 +1,24 @@ +package com.sksamuel.kotest.engine.plan + +import io.kotest.core.plan.Descriptor +import io.kotest.core.spec.DisplayName +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +@DisplayName("GGGGGGGGG") +class SpecDescriptorTest : FunSpec() { + init { + test("@DisplayName should be used when generating a spec name") { + Descriptor.fromSpecClass(SpecDescriptorTest::class).displayName.value shouldBe "GGGGGGGGG" + } + test("classname should be set when generating a spec name") { + Descriptor.fromSpecClass(SpecDescriptorTest::class).classname shouldBe "com.sksamuel.kotest.engine.plan.SpecDescriptorTest" + } + test("name should be set when generating a spec name") { + Descriptor.fromSpecClass(SpecDescriptorTest::class).name.value shouldBe "com.sksamuel.kotest.engine.plan.SpecDescriptorTest" + } + test("test path should be set when generating a spec name") { + Descriptor.fromSpecClass(SpecDescriptorTest::class).testPath().value shouldBe "kotest/com.sksamuel.kotest.engine.plan.SpecDescriptorTest" + } + } +} diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/plan/SpecNameTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/plan/SpecNameTest.kt deleted file mode 100644 index 24132d8c483..00000000000 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/plan/SpecNameTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.sksamuel.kotest.engine.plan - -import io.kotest.core.plan.NodeName -import io.kotest.core.spec.DisplayName -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe - -@DisplayName("GGGGGGGGG") -class SpecNameTest : FunSpec() { - init { - test("@DisplayName should be used when generating a spec name") { - NodeName.fromSpecClass(SpecNameTest::class).displayName shouldBe "GGGGGGGGG" - } - test("fqn should be set when generating a spec name") { - NodeName.fromSpecClass(SpecNameTest::class).fqn shouldBe "com.sksamuel.kotest.engine.plan.SpecNameTest" - } - test("name should be set when generating a spec name") { - NodeName.fromSpecClass(SpecNameTest::class).name shouldBe "com.sksamuel.kotest.engine.plan.SpecNameTest" - } - test("full name should be set when generating a spec name") { - NodeName.fromSpecClass(SpecNameTest::class).fullName shouldBe "kotest/com.sksamuel.kotest.engine.plan.SpecNameTest" - } - } -} diff --git a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/JUnitTestEngineListener.kt b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/JUnitTestEngineListener.kt index 8e3ce8f5fb8..3396182b873 100644 --- a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/JUnitTestEngineListener.kt +++ b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/JUnitTestEngineListener.kt @@ -6,8 +6,9 @@ import io.kotest.engine.listener.TestEngineListener import io.kotest.engine.writeSpecFailures import io.kotest.core.listeners.AfterProjectListenerException import io.kotest.core.listeners.BeforeProjectListenerException -import io.kotest.core.plan.TestPlanNode -import io.kotest.core.test.Description +import io.kotest.core.plan.Descriptor +import io.kotest.core.plan.toDescriptor +import io.kotest.core.sourceRef import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import io.kotest.core.test.TestStatus @@ -73,9 +74,8 @@ class JUnitTestEngineListener( val root: EngineDescriptor, ) : TestEngineListener { - // contains a mapping of a Description to a junit TestDescription, so we can look up the parent + // contains a mapping of a TestId to a junit TestDescription, so we can look up the parent // when we need to register a new test - private val descriptors = mutableMapOf() private val descriptorsByTestId = mutableMapOf() // contains any spec that failed so we can write out the failed specs file @@ -135,7 +135,7 @@ class JUnitTestEngineListener( try { val descriptor = kclass.descriptor(root) - descriptors[kclass.toDescription()] = descriptor + descriptorsByTestId[kclass.toDescription().toDescriptor(sourceRef()).id()] = descriptor log("Registering junit dynamic test and notifiying start: $descriptor") listener.dynamicTestRegistered(descriptor) @@ -146,7 +146,7 @@ class JUnitTestEngineListener( } } - override fun specStarted(spec: TestPlanNode.SpecNode) { + override suspend fun specStarted(spec: Descriptor.SpecDescriptor) { log("specStarted [${spec.name}]") // reset the flags for this spec @@ -154,10 +154,10 @@ class JUnitTestEngineListener( hasIgnoredTest = false try { - val descriptor = spec.name.descriptor(root) + val descriptor = spec.descriptor(root) - // we need to store the descriptor for later, because junit checks by identity - descriptorsByTestId[spec.id] = descriptor + // we need to store the descriptor for later, because junit checks by identity so we can't just recreate it later + descriptorsByTestId[spec.id()] = descriptor log("Registering junit dynamic test and notifiying start: $descriptor") listener.dynamicTestRegistered(descriptor) @@ -168,14 +168,14 @@ class JUnitTestEngineListener( } } - override fun specFinished( - spec: TestPlanNode.SpecNode, + override suspend fun specFinished( + spec: Descriptor.SpecDescriptor, t: Throwable?, - results: Map + results: Map ) { log("specFinished [${spec.name}]") - val descriptor = descriptorsByTestId[spec.id] + val descriptor = descriptorsByTestId[spec.id()] ?: throw RuntimeException("Error retrieving description for spec: ${spec.name}") // if the spec itself had an error then we must make sure we add at least one nested test so that @@ -202,7 +202,7 @@ class JUnitTestEngineListener( ) { log("specFinished [$kclass]") - val descriptor = descriptors[kclass.toDescription()] + val descriptor = descriptorsByTestId[kclass.toDescription().toDescriptor(sourceRef()).id()] ?: throw RuntimeException("Error retrieving description for spec: ${kclass.qualifiedName}") // if the spec itself had an error then we must make sure we add at least one nested test so that @@ -236,7 +236,7 @@ class JUnitTestEngineListener( private fun ensureSpecIsVisible(kclass: KClass, t: Throwable) { if (!hasVisibleTest) { val description = kclass.toDescription() - val spec = descriptors[description]!! + val spec = descriptorsByTestId[description.toDescriptor(sourceRef()).id()]!! val test = spec.append( description.append(createTestName("Spec execution failed"), TestType.Test), TestDescriptor.Type.TEST, null, Segment.Test @@ -256,13 +256,29 @@ class JUnitTestEngineListener( hasVisibleTest = true } + override fun testStarted(descriptor: Descriptor.TestDescriptor) { + val descriptor = createTestDescriptor(descriptor) + log("Registering junit dynamic test: $descriptor") + listener.dynamicTestRegistered(descriptor) + log("Notifying junit that execution has started: $descriptor") + listener.executionStarted(descriptor) + hasVisibleTest = true + } + override fun testFinished(testCase: TestCase, result: TestResult) { - val descriptor = descriptors[testCase.description] + val descriptor = descriptorsByTestId[testCase.description.toDescriptor(testCase.source).id()] ?: throw RuntimeException("Error retrieving description for: ${testCase.description}") log("Notifying junit that a test has finished [$descriptor]") listener.executionFinished(descriptor, result.testExecutionResult()) } + override fun testFinished(descriptor: Descriptor.TestDescriptor, result: TestResult) { + val descriptor = descriptorsByTestId[descriptor.id()] + ?: throw RuntimeException("Error retrieving description for: $descriptor") + log("Notifying junit that a test has finished [$descriptor]") + listener.executionFinished(descriptor, result.testExecutionResult()) + } + override fun testIgnored(testCase: TestCase, reason: String?) { val descriptor = createTestDescriptor(testCase) hasIgnoredTest = true @@ -271,6 +287,14 @@ class JUnitTestEngineListener( listener.executionSkipped(descriptor, reason) } + override fun testIgnored(descriptor: Descriptor.TestDescriptor, reason: String?) { + val descriptor = createTestDescriptor(descriptor) + hasIgnoredTest = true + log("Notifying junit that a test was ignored [$descriptor]") + listener.dynamicTestRegistered(descriptor) + listener.executionSkipped(descriptor, reason) + } + private fun createAndRegisterTest(name: String): TestDescriptor { val descriptor = root.append(name, TestDescriptor.Type.TEST, null, Segment.Spec) listener.dynamicTestRegistered(descriptor) @@ -278,17 +302,29 @@ class JUnitTestEngineListener( } private fun createTestDescriptor(testCase: TestCase): TestDescriptor { - val parent = descriptors[testCase.description.parent] + val parent = descriptorsByTestId[testCase.description.parent.toDescriptor(testCase.source).id()] if (parent == null) { val msg = "Cannot find parent description for: ${testCase.description}" log(msg) error(msg) } val descriptor = parent.descriptor(testCase) - descriptors[testCase.description] = descriptor + descriptorsByTestId[testCase.description.toDescriptor(testCase.source).id()] = descriptor return descriptor } + private fun createTestDescriptor(descriptor: Descriptor.TestDescriptor): TestDescriptor { + val parent = descriptorsByTestId[descriptor.parent.id()] + if (parent == null) { + val msg = "Cannot find parent description for: $descriptor" + log(msg) + error(msg) + } + val td = parent.descriptor(descriptor) + descriptorsByTestId[descriptor.id()] = td + return td + } + /** * Returns a JUnit [TestExecutionResult] populated from the values of the Kotest [TestResult]. */ diff --git a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/descriptors.kt b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/descriptors.kt index e5b1489385b..dffe928a4dc 100644 --- a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/descriptors.kt +++ b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/descriptors.kt @@ -1,7 +1,9 @@ package io.kotest.runner.junit.platform import io.kotest.core.internal.KotestEngineSystemProperties -import io.kotest.core.plan.NodeName +import io.kotest.core.plan.Descriptor +import io.kotest.core.plan.DisplayName +import io.kotest.core.plan.Source import io.kotest.core.test.Description import io.kotest.core.spec.Spec import io.kotest.core.spec.toDescription @@ -38,6 +40,10 @@ sealed class Segment { override val value: String = "spec" } + object Script : Segment() { + override val value: String = "script" + } + object Test : Segment() { override val value: String = "test" } @@ -56,9 +62,9 @@ fun KClass.descriptor(parent: TestDescriptor): TestDescriptor { * Creates a new spec-level [TestDescriptor] from the given spec name, appending it to the * parent [TestDescriptor]. The created descriptor will have segment type [Segment.Spec]. */ -fun NodeName.SpecName.descriptor(parent: TestDescriptor): TestDescriptor { - val source = ClassSource.from(this.fqn) - return parent.append(displayName, TestDescriptor.Type.CONTAINER, source, Segment.Spec) +fun Descriptor.SpecDescriptor.descriptor(parent: TestDescriptor): TestDescriptor { + val source = ClassSource.from(this.classname ?: "") + return parent.append(displayName.value, TestDescriptor.Type.CONTAINER, source, Segment.Script) } /** @@ -80,6 +86,25 @@ fun TestDescriptor.descriptor(testCase: TestCase): TestDescriptor { return append(testCase.description, type, source, Segment.Test) } +/** + * Creates a [TestDescriptor] for the given [Descriptor.TestDescriptor] and attaches it to the receiver as a child. + * The created descriptor will have segment type [Segment.Test]. + */ +fun TestDescriptor.descriptor(desc: Descriptor.TestDescriptor): TestDescriptor { + + val source = FileSource.from(File(desc.source.filename), FilePosition.from(desc.source.lineNumber)) + + // there is a bug in gradle 4.7+ whereby CONTAINER_AND_TEST breaks test reporting or hangs the build, as it is not handled + // see https://github.com/gradle/gradle/issues/4912 + // so we can't use CONTAINER_AND_TEST for our test scopes, but simply container + // update jan 2020: Seems we can use CONTAINER_AND_TEST now in gradle 6, and CONTAINER is invisible in output + val type = when (desc.type) { + TestType.Container -> if (System.getProperty(KotestEngineSystemProperties.gradle5) == "true") TestDescriptor.Type.CONTAINER else TestDescriptor.Type.CONTAINER_AND_TEST + TestType.Test -> TestDescriptor.Type.TEST + } + return append(desc.displayName, type, source, Segment.Test) +} + /** * Creates a new [TestDescriptor] appended to the receiver and adds it as a child of the receiver. */ @@ -95,6 +120,21 @@ fun TestDescriptor.append( segment ) +/** + * Creates a new [TestDescriptor] appended to the receiver and adds it as a child of the receiver. + */ +fun TestDescriptor.append( + displayName: DisplayName, + type: TestDescriptor.Type, + source: TestSource?, + segment: Segment +): TestDescriptor = append( + displayName.value, + type, + source, + segment +) + /** * Creates a new [TestDescriptor] appended to the receiver and adds it as a child of the receiver. */ From a3aa47122aa8b379f6e5d1bbac7f0178a2a70700 Mon Sep 17 00:00:00 2001 From: sksamuel Date: Sun, 31 Jan 2021 20:26:07 -0600 Subject: [PATCH 2/5] Added Descriptor abstraction; updated scripts to use name of containing file as parent spec. --- .../src/jvmMain/kotlin/io/kotest/engine/KotestEngine.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/KotestEngine.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/KotestEngine.kt index fcbf3ce87c3..805de7b9a73 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/KotestEngine.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/KotestEngine.kt @@ -3,7 +3,6 @@ package io.kotest.engine import io.kotest.core.Tags import io.kotest.core.config.configuration import io.kotest.core.filter.TestFilter -import io.kotest.core.plan.Descriptor import io.kotest.core.spec.Spec import io.kotest.core.spec.afterProject import io.kotest.core.spec.beforeProject From ddf1e037daae77d67cf84a127da80e1994e775c7 Mon Sep 17 00:00:00 2001 From: sksamuel Date: Sun, 31 Jan 2021 20:30:47 -0600 Subject: [PATCH 3/5] Added top level should for scripting --- .../src/commonMain/kotlin/io/kotest/core/script/styles.kt | 7 +++++++ .../kotlin/io/kotest/core/test/DescriptionName.kt | 3 --- .../kotlin/com/sksamuel/kotest/engine/scriptTest.kts | 7 ++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/script/styles.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/script/styles.kt index 57716186afe..967244f8607 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/script/styles.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/script/styles.kt @@ -20,6 +20,13 @@ fun test(name: String, test: suspend TestContext.() -> Unit) { ScriptRuntime.registerRootTest(createTestName(name), false, TestType.Test, test) } +/** + * Registers a root test, with the given name. + */ +fun should(name: String, test: suspend TestContext.() -> Unit) { + ScriptRuntime.registerRootTest(createTestName(name), false, TestType.Test, test) +} + /** * Registers a root context scope, which allows further tests to be registered with test and should keywords. */ diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/DescriptionName.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/DescriptionName.kt index 09ac8933d7d..75833dcd259 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/DescriptionName.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/DescriptionName.kt @@ -47,10 +47,8 @@ sealed class DescriptionName { } } -@Deprecated("This is intended to be replaced by logic from NodeName") fun createTestName(name: String) = createTestName(null, name, false) -@Deprecated("This is intended to be replaced by logic from NodeName") fun createTestName(prefix: String?, name: String, defaultIncludeAffix: Boolean): DescriptionName.TestName = createTestName( prefix, @@ -73,7 +71,6 @@ fun createTestName(prefix: String?, name: String, defaultIncludeAffix: Boolean): * @param testNameCase a [TestNameCase] parameter to adjust the capitalisation of the display name * @param includeAffixesInDisplayName if true then the prefix and/or suffix will be included in the display name. */ -@Deprecated("This is intended to be replaced by logic from NodeName") fun createTestName( prefix: String?, name: String, diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/scriptTest.kts b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/scriptTest.kts index 8471ccb216f..de710fc8a68 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/scriptTest.kts +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/scriptTest.kts @@ -2,15 +2,20 @@ package com.sksamuel.kotest.engine import io.kotest.core.script.context import io.kotest.core.script.describe +import io.kotest.core.script.should import io.kotest.core.script.test import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldHaveLength import io.kotest.matchers.string.shouldHaveLengthBetween -test("this is a fun spec test") { +test("this is a fun spec style") { 1 + 2 shouldBe 3 } +should("this is a should spec style") { + "foo".shouldHaveLength(3) +} + describe("this is a describe spec context") { it("and this is a describe spec test") { "a" + "b" shouldBe "ab" From 950a4f3a62aa37449b61777707db8de22b8532cf Mon Sep 17 00:00:00 2001 From: sksamuel Date: Mon, 1 Feb 2021 19:07:21 -0600 Subject: [PATCH 4/5] Fixing junit listener --- .../kotlin/io/kotest/core/plan/descriptors.kt | 1 - .../junit/platform/JUnitTestEngineListener.kt | 28 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/descriptors.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/descriptors.kt index 0d9a72d10a2..c1e6eddfb44 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/descriptors.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/plan/descriptors.kt @@ -287,6 +287,5 @@ fun Description.toDescriptor(sourceRef: SourceRef): Descriptor { data class Name(val value: String) data class DisplayName(val value: String) -data class TestPath(val value: String) fun TestPath.append(component: String) = TestPath(listOf(this.value, component).joinToString("/")) diff --git a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/JUnitTestEngineListener.kt b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/JUnitTestEngineListener.kt index 3396182b873..7465f4cb0b8 100644 --- a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/JUnitTestEngineListener.kt +++ b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/JUnitTestEngineListener.kt @@ -14,7 +14,7 @@ import io.kotest.core.test.TestResult import io.kotest.core.test.TestStatus import io.kotest.core.test.TestType import io.kotest.core.spec.toDescription -import io.kotest.core.test.TestId +import io.kotest.core.test.TestPath import io.kotest.core.test.createTestName import io.kotest.mpp.log import org.junit.platform.engine.EngineExecutionListener @@ -74,9 +74,9 @@ class JUnitTestEngineListener( val root: EngineDescriptor, ) : TestEngineListener { - // contains a mapping of a TestId to a junit TestDescription, so we can look up the parent + // contains a mapping of junit TestDescriptors, so we can look up the parent // when we need to register a new test - private val descriptorsByTestId = mutableMapOf() + private val descriptors = mutableMapOf() // contains any spec that failed so we can write out the failed specs file private val failedSpecs = mutableSetOf>() @@ -135,7 +135,7 @@ class JUnitTestEngineListener( try { val descriptor = kclass.descriptor(root) - descriptorsByTestId[kclass.toDescription().toDescriptor(sourceRef()).id()] = descriptor + descriptors[kclass.toDescription().toDescriptor(sourceRef()).testPath()] = descriptor log("Registering junit dynamic test and notifiying start: $descriptor") listener.dynamicTestRegistered(descriptor) @@ -157,7 +157,7 @@ class JUnitTestEngineListener( val descriptor = spec.descriptor(root) // we need to store the descriptor for later, because junit checks by identity so we can't just recreate it later - descriptorsByTestId[spec.id()] = descriptor + descriptors[spec.testPath()] = descriptor log("Registering junit dynamic test and notifiying start: $descriptor") listener.dynamicTestRegistered(descriptor) @@ -175,7 +175,7 @@ class JUnitTestEngineListener( ) { log("specFinished [${spec.name}]") - val descriptor = descriptorsByTestId[spec.id()] + val descriptor = descriptors[spec.testPath()] ?: throw RuntimeException("Error retrieving description for spec: ${spec.name}") // if the spec itself had an error then we must make sure we add at least one nested test so that @@ -202,7 +202,7 @@ class JUnitTestEngineListener( ) { log("specFinished [$kclass]") - val descriptor = descriptorsByTestId[kclass.toDescription().toDescriptor(sourceRef()).id()] + val descriptor = descriptors[kclass.toDescription().toDescriptor(sourceRef()).testPath()] ?: throw RuntimeException("Error retrieving description for spec: ${kclass.qualifiedName}") // if the spec itself had an error then we must make sure we add at least one nested test so that @@ -236,7 +236,7 @@ class JUnitTestEngineListener( private fun ensureSpecIsVisible(kclass: KClass, t: Throwable) { if (!hasVisibleTest) { val description = kclass.toDescription() - val spec = descriptorsByTestId[description.toDescriptor(sourceRef()).id()]!! + val spec = descriptors[description.toDescriptor(sourceRef()).testPath()]!! val test = spec.append( description.append(createTestName("Spec execution failed"), TestType.Test), TestDescriptor.Type.TEST, null, Segment.Test @@ -266,14 +266,14 @@ class JUnitTestEngineListener( } override fun testFinished(testCase: TestCase, result: TestResult) { - val descriptor = descriptorsByTestId[testCase.description.toDescriptor(testCase.source).id()] + val descriptor = descriptors[testCase.description.toDescriptor(testCase.source).testPath()] ?: throw RuntimeException("Error retrieving description for: ${testCase.description}") log("Notifying junit that a test has finished [$descriptor]") listener.executionFinished(descriptor, result.testExecutionResult()) } override fun testFinished(descriptor: Descriptor.TestDescriptor, result: TestResult) { - val descriptor = descriptorsByTestId[descriptor.id()] + val descriptor = descriptors[descriptor.testPath()] ?: throw RuntimeException("Error retrieving description for: $descriptor") log("Notifying junit that a test has finished [$descriptor]") listener.executionFinished(descriptor, result.testExecutionResult()) @@ -302,26 +302,26 @@ class JUnitTestEngineListener( } private fun createTestDescriptor(testCase: TestCase): TestDescriptor { - val parent = descriptorsByTestId[testCase.description.parent.toDescriptor(testCase.source).id()] + val parent = descriptors[testCase.description.parent.toDescriptor(testCase.source).testPath()] if (parent == null) { val msg = "Cannot find parent description for: ${testCase.description}" log(msg) error(msg) } val descriptor = parent.descriptor(testCase) - descriptorsByTestId[testCase.description.toDescriptor(testCase.source).id()] = descriptor + descriptors[testCase.description.toDescriptor(testCase.source).testPath()] = descriptor return descriptor } private fun createTestDescriptor(descriptor: Descriptor.TestDescriptor): TestDescriptor { - val parent = descriptorsByTestId[descriptor.parent.id()] + val parent = descriptors[descriptor.parent.testPath()] if (parent == null) { val msg = "Cannot find parent description for: $descriptor" log(msg) error(msg) } val td = parent.descriptor(descriptor) - descriptorsByTestId[descriptor.id()] = td + descriptors[descriptor.testPath()] = td return td } From 6457681562c1f26d8641561d5010b767bf8bcf3f Mon Sep 17 00:00:00 2001 From: sksamuel Date: Mon, 1 Feb 2021 19:53:04 -0600 Subject: [PATCH 5/5] Updated isolation test listeenr --- .../engine/listener/IsolationTestEngineListener.kt | 8 ++++---- kotest-property/build.gradle.kts | 14 +++++--------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/IsolationTestEngineListener.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/IsolationTestEngineListener.kt index 8e06098be95..da3e65dca6b 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/IsolationTestEngineListener.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/listener/IsolationTestEngineListener.kt @@ -85,7 +85,7 @@ class IsolationTestEngineListener(val listener: TestEngineListener) : TestEngine override fun testStarted(descriptor: Descriptor.TestDescriptor) { synchronized(listener) { - if (runningSpec.get() == descriptor.spec()?.classname || descriptor.spec()?.script == true) { + if (runningSpec.get() == descriptor.spec()?.classname) { listener.testStarted(descriptor) } else { queue { @@ -109,7 +109,7 @@ class IsolationTestEngineListener(val listener: TestEngineListener) : TestEngine override fun testIgnored(descriptor: Descriptor.TestDescriptor, reason: String?) { synchronized(listener) { - if (runningSpec.get() == descriptor.spec()?.classname || descriptor.spec()?.script == true) { + if (runningSpec.get() == descriptor.spec()?.classname) { listener.testIgnored(descriptor, reason) } else { queue { @@ -133,7 +133,7 @@ class IsolationTestEngineListener(val listener: TestEngineListener) : TestEngine override fun testFinished(descriptor: Descriptor.TestDescriptor, result: TestResult) { synchronized(listener) { - if (runningSpec.get() == descriptor.spec()?.classname || descriptor.spec()?.script == true) { + if (runningSpec.get() == descriptor.spec()?.classname) { listener.testFinished(descriptor, result) } else { queue { @@ -181,7 +181,7 @@ class IsolationTestEngineListener(val listener: TestEngineListener) : TestEngine results: Map ) { synchronized(listener) { - if (runningSpec.get() == descriptor.classname || descriptor.spec()?.script == true) { + if (runningSpec.get() == descriptor.classname) { runBlocking { listener.specFinished(descriptor, t, results) } diff --git a/kotest-property/build.gradle.kts b/kotest-property/build.gradle.kts index a766247ed2f..a9fef70efd9 100644 --- a/kotest-property/build.gradle.kts +++ b/kotest-property/build.gradle.kts @@ -37,14 +37,6 @@ kotlin { iosArm32() } - targets.all { - compilations.all { - kotlinOptions { - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.RequiresOptIn" - } - } - } - sourceSets { val commonMain by getting { @@ -107,11 +99,15 @@ kotlin { val tvosMain by getting { dependsOn(desktopMain) } + + all { + languageSettings.useExperimentalAnnotation("kotlin.time.ExperimentalTime") + languageSettings.useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference") + } } } tasks.withType().configureEach { - kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" kotlinOptions.jvmTarget = "1.8" kotlinOptions.apiVersion = "1.4" }