Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Descriptor abstraction; updated scripts to use name of containi… #2029

Merged
merged 5 commits into from Feb 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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.
Expand Down
@@ -1,25 +1,22 @@
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 {

/**
* 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
}
Expand Up @@ -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
Expand All @@ -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<IsActiveExtension>().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<IsActiveExtension>()
.all { it.isActive(descriptor) }
}

/**
Expand Down
Expand Up @@ -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<String, String>)
data class TestPlan(val root: Descriptor, val config: Map<String, String>)
@@ -0,0 +1,291 @@
@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<DisplayNameAnnotation>()?.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<Descriptor> = 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)

fun TestPath.append(component: String) = TestPath(listOf(this.value, component).joinToString("/"))