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

Wildcard support in launcher #3200

Merged
merged 4 commits into from Oct 2, 2022
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 @@ -81,6 +81,17 @@ sealed interface Descriptor {
*/
fun isRootTest() = this is TestDescriptor && this.parent.isSpec()

/**
* Returns true if this type equals that type. For example
* if this is a spec and the rhs is also spec
*/
fun isEqualType(that: Descriptor): Boolean {
return when (this) {
is SpecDescriptor -> that.isSpec()
is TestDescriptor -> that.isTestCase()
}
}

/**
* Returns the depth of this node, where the [SpecDescriptor] has depth of 0,
* a root test has depth 1 and so on.
Expand Down Expand Up @@ -131,6 +142,26 @@ sealed interface Descriptor {
fun isOnPath(description: Descriptor): Boolean =
this == description || this.isAncestorOf(description)

/**
* Returns the prefix of the descriptor starting with the root (spec)
*/
fun getTreePrefix(): List<Descriptor> {
val ret = mutableListOf<Descriptor>()
var x = this
loop@ while(true) {
ret.add(0, x)
when(x) {
is SpecDescriptor -> {
break@loop
}
is TestDescriptor -> {
x = x.parent
}
}
}
return ret
}

/**
* Returns the [SpecDescriptor] parent for this [Descriptor].
* If this is already a spec descriptor, then returns itself.
Expand All @@ -143,7 +174,21 @@ sealed interface Descriptor {

data class DescriptorId(
val value: String,
)
) {

/**
* Treats the lhs and rhs both as wildcard regex one by one and check if it matches the other
*/
fun wildCardMatch(id: DescriptorId) :Boolean {
val thisRegex = with(this.value) {
("\\Q$this\\E").replace("*", "\\E.*\\Q").toRegex()
}
val thatRegex = with(id.value) {
("\\Q$this\\E").replace("*", "\\E.*\\Q").toRegex()
}
return (thisRegex.matches(id.value) || thatRegex.matches(this.value))
}
}

fun SpecDescriptor.append(name: TestName): TestDescriptor =
TestDescriptor(this, DescriptorId(name.testName))
Expand Down
Expand Up @@ -35,12 +35,41 @@ class TestPathTestCaseFilter(
} else desc.append(name.trim())
}


private fun List<Descriptor>.prefixesWithWildcardMatch(that: List<Descriptor>): Boolean {
if (this.isEmpty()) {
return true
}
if (this.size > that.size) {
return false
}

val first = this[0]
val second = that[0]
if (!first.isEqualType(second)) {
return false
}
return first.id.wildCardMatch(second.id) && this.subList(1, this.size).prefixesWithWildcardMatch(that.subList(1, that.size))
}

/**
* This filter is called in a tree like manner so there are two cases possible
* first we check if the group we are currently running is parent of the test filter.
* Returning true at this point means that test engine will keep on going recursively deeper into the
* hierarchy
* Eventually we are deep into the hierarchy where we have runnable targets that will be on the
* path of the filters which means they can be run
*/
override fun filter(descriptor: Descriptor): TestFilterResult {
val descriptorPrefix = descriptor.getTreePrefix()
val target1Prefix = target1.getTreePrefix()
val target2Prefix = target2.getTreePrefix()
val onTestFilterPath = descriptorPrefix.prefixesWithWildcardMatch(target1Prefix)
|| descriptorPrefix.prefixesWithWildcardMatch(target2Prefix)
val testOnDescriptorPath = target1Prefix.prefixesWithWildcardMatch(descriptorPrefix)
|| target2Prefix.prefixesWithWildcardMatch(descriptorPrefix)
return when {
target1.isOnPath(descriptor) ||
target2.isOnPath(descriptor) ||
descriptor.isOnPath(target1) ||
descriptor.isOnPath(target2) -> TestFilterResult.Include
onTestFilterPath || testOnDescriptorPath -> TestFilterResult.Include
else -> TestFilterResult.Exclude("Excluded by test path filter: '$testPath'")
}
}
Expand Down
@@ -0,0 +1,149 @@
package com.sksamuel.kotest.engine.launcher

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.core.spec.style.FunSpec
import io.kotest.core.test.TestCase
import io.kotest.core.test.TestResult
import io.kotest.core.test.TestType
import io.kotest.engine.launcher.LauncherArgs
import io.kotest.engine.launcher.setupLauncher
import io.kotest.engine.listener.AbstractTestEngineListener
import io.kotest.matchers.booleans.shouldBeFalse
import io.kotest.matchers.shouldBe
import kotlin.reflect.KClass

class RunLauncherTest: FunSpec() {
init {
test("runLauncher should run tests for known class") {
val listener = SetupLauncherTestListener()
val result = setupLauncher(
LauncherArgs(
null,
"com.sksamuel.kotest.engine.launcher",
"com.sksamuel.kotest.engine.launcher.DescribeSpec1",
null, null, null), listener)
result.isFailure.shouldBeFalse()
result.getOrNull()?.launch()
listener.testFinished.sorted() shouldBe arrayOf(
"bar", "first", "foo", "nothing else", "second", "second bar", "something", "something else"
)
listener.specFinished.sorted() shouldBe arrayOf("DescribeSpec1")
}

test("runLauncher should support testpath") {
val listener = SetupLauncherTestListener()
val result = setupLauncher(
LauncherArgs(
"something",
"com.sksamuel.kotest.engine.launcher",
"com.sksamuel.kotest.engine.launcher.DescribeSpec1",
null, null, null), listener)
result.isFailure.shouldBeFalse()
result.getOrNull()?.launch()
listener.testStarted.size shouldBe 3
listener.testStarted.filter{it.type == TestType.Container}.map { it.descriptor.id.value } shouldBe arrayOf("something")
listener.testStarted.filter{it.type == TestType.Test}.map { it.descriptor.id.value } shouldBe arrayOf("first", "second")
}

test("runLauncher should support single level testpath regex") {
val listener = SetupLauncherTestListener()
val result = setupLauncher(
LauncherArgs(
"something*",
"com.sksamuel.kotest.engine.launcher",
"com.sksamuel.kotest.engine.launcher.DescribeSpec1",
null, null, null), listener)
result.isFailure.shouldBeFalse()
result.getOrNull()?.launch()
listener.testStarted.filter{it.type == TestType.Container}.map { it.descriptor.id.value } shouldBe arrayOf("something", "something else")
listener.testStarted.filter{it.type == TestType.Test}.map { it.descriptor.id.value } shouldBe arrayOf("first", "second", "foo")
}

test("runLauncher should support multi level testpath regex") {
val listener = SetupLauncherTestListener()
val result = setupLauncher(
LauncherArgs(
"something* -- first",
"com.sksamuel.kotest.engine.launcher",
"com.sksamuel.kotest.engine.launcher.DescribeSpec1",
null, null, null), listener)
result.isFailure.shouldBeFalse()
result.getOrNull()?.launch()
listener.testStarted.filter{it.type == TestType.Container}.map { it.descriptor.id.value } shouldBe arrayOf("something", "something else")
listener.testStarted.filter{it.type == TestType.Test}.map { it.descriptor.id.value } shouldBe arrayOf("first")
}

test("runLauncher should support second level testpath regex") {
val listener = SetupLauncherTestListener()
val result = setupLauncher(
LauncherArgs(
"* -- second*",
"com.sksamuel.kotest.engine.launcher",
"com.sksamuel.kotest.engine.launcher.DescribeSpec1",
null, null, null), listener)
result.isFailure.shouldBeFalse()
result.getOrNull()?.launch()
listener.testStarted.filter{it.type == TestType.Container}.map { it.descriptor.id.value } shouldBe arrayOf("something", "something else", "nothing else")
listener.testStarted.filter{it.type == TestType.Test}.map { it.descriptor.id.value } shouldBe arrayOf("second", "second bar")
}
}
}


class SetupLauncherTestListener(
): AbstractTestEngineListener() {
var specIgnored = mutableMapOf<String, KClass<*>>();
var specFinished = mutableListOf<String>()
var testStarted = mutableListOf<TestCase>()
var testFinished = mutableListOf<String>()
override suspend fun specIgnored(kclass: KClass<*>, reason: String?) {
specIgnored[kclass.qualifiedName?:""] = kclass;
super.specIgnored(kclass, reason);
}
override suspend fun specFinished(kclass: KClass<*>, result: TestResult) {
kclass.simpleName?.let{
specFinished.add(it)
}
super.specFinished(kclass, result)
}

override suspend fun testStarted(testCase: TestCase) {
testStarted.add(testCase)
}
override suspend fun testFinished(testCase: TestCase, result: TestResult) {
testFinished.add(testCase.name.testName)
super.testFinished(testCase, result)
}
}



private class DescribeSpec1: DescribeSpec() {
init {
describe("something") {
it("first") {

}

it("second") {

}
}

describe("something else") {
it ("foo") {

}
}

describe("nothing else") {
it ("bar") {

}

it ("second bar") {

}
}
}
}