From ad43c8077fee2b6bc6387b0f5a2f6b1ffe9e53c2 Mon Sep 17 00:00:00 2001 From: Mitchell Yuwono Date: Sun, 23 Oct 2022 09:53:39 +1100 Subject: [PATCH 1/4] Fix gradle filter integration within kotest runner --- .../io/kotest/core/descriptors/descriptor.kt | 8 +- .../GradleClassMethodRegexTestFilter.kt | 98 +++++++-------- .../GradlePostDiscoveryFilterExtractor.kt | 15 ++- .../GradleClassMethodRegexTestFilterTest.kt | 112 +++++++++--------- .../gradle/GradleTestPatternParserTest.kt | 90 -------------- 5 files changed, 117 insertions(+), 206 deletions(-) delete mode 100644 kotest-runner/kotest-runner-junit5/src/jvmTest/kotlin/io/kotest/runner/junit/platform/gradle/GradleTestPatternParserTest.kt diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/descriptors/descriptor.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/descriptors/descriptor.kt index b11e25feb9e..af1265612d6 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/descriptors/descriptor.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/descriptors/descriptor.kt @@ -148,9 +148,9 @@ sealed interface Descriptor { fun getTreePrefix(): List { val ret = mutableListOf() var x = this - loop@ while(true) { + loop@ while (true) { ret.add(0, x) - when(x) { + when (x) { is SpecDescriptor -> { break@loop } @@ -179,7 +179,7 @@ data class DescriptorId( /** * Treats the lhs and rhs both as wildcard regex one by one and check if it matches the other */ - fun wildCardMatch(id: DescriptorId) :Boolean { + fun wildCardMatch(id: DescriptorId): Boolean { val thisRegex = with(this.value) { ("\\Q$this\\E").replace("*", "\\E.*\\Q").toRegex() } @@ -204,7 +204,7 @@ fun Descriptor.append(name: String): TestDescriptor = * This may be the same descriptor that this method is invoked on, if that descriptor * is a root test. */ -fun TestDescriptor.root(): TestDescriptor { +tailrec fun TestDescriptor.root(): TestDescriptor { return when (parent) { is SpecDescriptor -> this // if my parent is a spec, then I am a root is TestDescriptor -> parent.root() diff --git a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilter.kt b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilter.kt index e19016169db..c9557ceadd8 100644 --- a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilter.kt +++ b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilter.kt @@ -1,6 +1,7 @@ package io.kotest.runner.junit.platform.gradle import io.kotest.core.descriptors.Descriptor +import io.kotest.core.descriptors.TestPath import io.kotest.core.filter.TestFilter import io.kotest.core.filter.TestFilterResult import io.kotest.mpp.Logger @@ -13,70 +14,53 @@ class GradleClassMethodRegexTestFilter(private val patterns: List) : Tes logger.log { Pair(descriptor.toString(), "Testing against $patterns") } return when { patterns.isEmpty() -> TestFilterResult.Include - patterns.any { match(it, descriptor) } -> TestFilterResult.Include + patterns.all { match(it, descriptor) } -> TestFilterResult.Include else -> TestFilterResult.Exclude(null) } } + /** + * Matches the pattern supplied from gradle build script or command line interface. + * Gradle supplies a well-formed regex to the engine that we can leverage to construct a well-formed regex object. + * + * - A* becomes \QA\E.* + * - A*Test becomes \QA\E.*\QTest\E + * - io.*.A*Test becomes \Qio.\E.*\Q.A\E.*\QTest\E + * - io.*.A*Test.AccountDetails* becomes \Qio.\E.*\Q.A\E.*\QTest.AccountDetails\E.* + * - io.*.A*Test.some test context* becomes \Qio.\E.*\Q.A\E.*\QTest.some test context\E.* + */ private fun match(pattern: String, descriptor: Descriptor): Boolean { - val (prefixWildcard, pck, classname, path) = GradleTestPattern.parse(pattern) - return when (descriptor) { - is Descriptor.TestDescriptor -> when (path) { - null -> true - else -> descriptor.path(false).value.startsWith(path) - } - - is Descriptor.SpecDescriptor -> when { - pck != null && classname != null && prefixWildcard -> descriptor.kclass.qualifiedName?.contains("$pck.$classname") ?: false - pck != null && classname != null -> descriptor.kclass.qualifiedName == "$pck.$classname" - pck != null && prefixWildcard -> descriptor.kclass.qualifiedName?.contains(pck) ?: true - pck != null -> descriptor.kclass.qualifiedName?.startsWith(pck) ?: true - classname != null && prefixWildcard -> descriptor.kclass.simpleName?.contains(classname) ?: false - classname != null -> descriptor.kclass.simpleName == classname - else -> true - } - } + val path = descriptor.dotSeparatedFullPath().value + val regexPattern = "^(.*)$pattern".toRegex() // matches + val laxRegexPattern = "^(.*)$pattern(.*)\$".toRegex() // matches pattern that begins with and followed by + val packagePath = descriptor.spec().kclass.java.packageName // io.kotest + + val isSimpleClassMatch = descriptor.spec().kclass.java.simpleName.matches(pattern.toRegex()) // SomeTest or *Test + val isSpecMatched = descriptor.spec().id.value.matches(regexPattern) // *.SomeTest + val isFullPathMatched = path.matches(regexPattern) // io.*.SomeTest + val isFullPathDotMatched = "$path.".matches(regexPattern) // io.*. or io.*.SomeTest.* + + val doesNotContainUppercase = pattern.replace("\\Q", "").replace("\\E", "").all { !it.isUpperCase() } + + val isPackageMatched = doesNotContainUppercase && packagePath.matches(laxRegexPattern) // io.kotest + val isPackageWithDotMatched = doesNotContainUppercase && "$packagePath.".matches(laxRegexPattern) // io.kotest.* + + return isSimpleClassMatch || + isFullPathMatched || + isFullPathDotMatched || + isSpecMatched || + isPackageMatched || + isPackageWithDotMatched } -} - -data class GradleTestPattern( - val prefixWildcard: Boolean, - val pckage: String?, - val classname: String?, - val path: String?, -) { - companion object { - - // if the regex starts with a lower case character, then we assume it is in the format package.Class.testpath - // otherwise, we assume it is in the format Class.testpath - // the .testpath is always optional, and at least Class or package must be specified - // additionally, patterns can start with a * in which case the pattern matches suffixes - fun parse(pattern: String): GradleTestPattern { - require(pattern.isNotBlank()) - - val prefixWildcard = pattern.startsWith("*") - val pattern2 = pattern - .removePrefix("*") - .removePrefix(".") - .replace("\\Q", "") // Quote start regex, added by Gradle - .replace("\\E", "") // Quote end regex, added by Gradle - val tokens = pattern2.split('.') - - // Package names shouldn't contain any upper-case letters - val classIndex = tokens.indexOfFirst { token -> token.any { it.isUpperCase() } } - - // if class is not specified, then we assume the entire string is a package - if (classIndex == -1) return GradleTestPattern(prefixWildcard, pattern2, null, null) - - // if the class is the first part, then no package is specified - val pck = if (classIndex == 0) null else tokens.take(classIndex).joinToString(".") - - val pathParts = tokens.drop(classIndex + 1) - val path = if (pathParts.isEmpty()) null else pathParts.joinToString(".") - - return GradleTestPattern(prefixWildcard, pck, tokens[classIndex], path) + /** + * Returns a gradle-compatible dot-separated full path of the given descriptor. + */ + private fun Descriptor.dotSeparatedFullPath(): TestPath = when (this) { + is Descriptor.SpecDescriptor -> TestPath(this.id.value) + is Descriptor.TestDescriptor -> when (this.parent) { + is Descriptor.SpecDescriptor -> TestPath("${this.parent.id.value}.${this.id.value}") + is Descriptor.TestDescriptor -> TestPath("${this.parent.dotSeparatedFullPath().value} -- ${this.id.value}") } - } } diff --git a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradlePostDiscoveryFilterExtractor.kt b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradlePostDiscoveryFilterExtractor.kt index aee7b46acb4..ec37e7a095e 100644 --- a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradlePostDiscoveryFilterExtractor.kt +++ b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradlePostDiscoveryFilterExtractor.kt @@ -32,10 +32,17 @@ object GradlePostDiscoveryFilterExtractor { val matcher = testMatcher(filter) logger.log { Pair(null, "TestMatcher [$matcher]") } + val buildScriptIncludePatterns = buildScriptIncludePatterns(matcher) + logger.log { Pair(null, "buildScriptIncludePatterns [$buildScriptIncludePatterns]") } + val commandLineIncludePatterns = commandLineIncludePatterns(matcher) logger.log { Pair(null, "commandLineIncludePatterns [$commandLineIncludePatterns]") } - val regexes = commandLineIncludePatterns.map { pattern(it) } + val regexes = buildList { + addAll(buildScriptIncludePatterns) + addAll(commandLineIncludePatterns) + }.map { pattern(it) } + logger.log { Pair(null, "ClassMethodNameFilter regexes [$regexes]") } regexes }.getOrElse { emptyList() } @@ -52,6 +59,12 @@ object GradlePostDiscoveryFilterExtractor { return field.get(obj) as List } + private fun buildScriptIncludePatterns(obj: Any): List { + val field = obj::class.java.getDeclaredField("buildScriptIncludePatterns") + field.isAccessible = true + return field.get(obj) as List + } + private fun pattern(obj: Any): String { val field = obj::class.java.getDeclaredField("pattern") field.isAccessible = true diff --git a/kotest-runner/kotest-runner-junit5/src/jvmTest/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilterTest.kt b/kotest-runner/kotest-runner-junit5/src/jvmTest/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilterTest.kt index cdff371b758..51bbb829ecb 100644 --- a/kotest-runner/kotest-runner-junit5/src/jvmTest/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilterTest.kt +++ b/kotest-runner/kotest-runner-junit5/src/jvmTest/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilterTest.kt @@ -9,33 +9,31 @@ import io.kotest.matchers.shouldBe class GradleClassMethodRegexTestFilterTest : FunSpec({ - test("include classes") { - + context("include classes") { val spec = GradleClassMethodRegexTestFilterTest::class.toDescriptor() + withData( + nameFn = { filters -> "should be INCLUDED when evaluating $filters" }, + listOf("\\QGradleClassMethodRegexTestFilterTest\\E"), + listOf(".*\\QthodRegexTestFilterTest\\E"), + listOf(".*\\QTest\\E"), + listOf("\\Qio.kotest.runner.junit.platform.gradle.GradleClassMethodRegexTestFilterTest\\E"), + listOf(".*\\Q.platform.gradle.GradleClassMethodRegexTestFilterTest\\E"), + listOf(".*\\Qorm.gradle.GradleClassMethodRegexTestFilterTest\\E") + ) { filters -> + GradleClassMethodRegexTestFilter(filters).filter(spec) shouldBe TestFilterResult.Include + } + } - GradleClassMethodRegexTestFilter(listOf("GradleClassMethodRegexTestFilterTest")).filter(spec) shouldBe - TestFilterResult.Include - - GradleClassMethodRegexTestFilter(listOf("*thodRegexTestFilterTest")).filter(spec) shouldBe - TestFilterResult.Include - - GradleClassMethodRegexTestFilter(listOf("GradleClassMethodRegexTestFilterTest2")).filter(spec) shouldBe - TestFilterResult.Exclude(null) - - GradleClassMethodRegexTestFilter(listOf("GradleClassMethodRegexTestFilterTes")).filter(spec) shouldBe - TestFilterResult.Exclude(null) - - GradleClassMethodRegexTestFilter(listOf("io.kotest.runner.junit.platform.gradle.GradleClassMethodRegexTestFilterTest")) - .filter(spec) shouldBe TestFilterResult.Include - - GradleClassMethodRegexTestFilter(listOf("*orm.gradle.GradleClassMethodRegexTestFilterTest")) - .filter(spec) shouldBe TestFilterResult.Include - - GradleClassMethodRegexTestFilter(listOf("*.platform.gradle.GradleClassMethodRegexTestFilterTest")) - .filter(spec) shouldBe TestFilterResult.Include - - GradleClassMethodRegexTestFilter(listOf("io.kotest.runner.junit.platform.GradleClassMethodRegexTestFilterTest")) - .filter(spec) shouldBe TestFilterResult.Exclude(null) + context("exclude classes") { + val spec = GradleClassMethodRegexTestFilterTest::class.toDescriptor() + withData( + nameFn = { filters -> "should be EXCLUDED when evaluating $filters" }, + listOf("\\QGradleClassMethodRegexTestFilterTest2\\E"), + listOf("\\QGradleClassMethodRegexTestFilterTes\\E"), + listOf("\\Qio.kotest.runner.junit.platform.GradleClassMethodRegexTestFilterTest\\E") + ) { filters -> + GradleClassMethodRegexTestFilter(filters).filter(spec) shouldBe TestFilterResult.Exclude(null) + } } context("include packages") { @@ -44,27 +42,35 @@ class GradleClassMethodRegexTestFilterTest : FunSpec({ val container = spec.append("a context") val test = container.append("nested test") - test("Exact match - includes the spec and tests within it") { - val filter = GradleClassMethodRegexTestFilter(listOf("io.kotest.runner.junit.platform.gradle")) - - filter.filter(spec) shouldBe TestFilterResult.Include - filter.filter(test) shouldBe TestFilterResult.Include + withData( + nameFn = { filters -> "should be INCLUDED when evaluating $filters" }, + listOf("\\Qio.kotest.runner.junit.platform.gradle\\E"), + listOf("\\Qio.kotest.runner.junit.platform.gradle.\\E.*"), + listOf(".*\\Qnner.junit.platform.gradle\\E"), + listOf(".*\\Qnner.junit.platform.gradle.\\E.*"), + listOf(".*\\Q.junit.platform.gradle\\E"), + listOf("\\Qio.kotest.runner.junit.platform.gra\\E.*"), + listOf("\\Qio.kotest.runner.junit\\E"), + ) { filters -> + GradleClassMethodRegexTestFilter(filters).filter(spec) shouldBe TestFilterResult.Include } - GradleClassMethodRegexTestFilter(listOf("*nner.junit.platform.gradle")) - .filter(spec) shouldBe TestFilterResult.Include - - GradleClassMethodRegexTestFilter(listOf("*.junit.platform.gradle")) - .filter(spec) shouldBe TestFilterResult.Include - - GradleClassMethodRegexTestFilter(listOf("io.kotest.runner.junit.platform.gra")) - .filter(spec) shouldBe TestFilterResult.Include - - GradleClassMethodRegexTestFilter(listOf("io.kotest.runner.junit")) - .filter(spec) shouldBe TestFilterResult.Include + withData( + nameFn = { filters -> "should be EXCLUDED when evaluating $filters" }, + listOf("\\Qio.kotest.runner.junit2\\E"), + listOf("\\Qio.kotest.runner.junit\\E", ".*\\QSpec\\E"), + ) { filters -> + GradleClassMethodRegexTestFilter(filters).filter(spec) shouldBe TestFilterResult.Exclude(null) + } - GradleClassMethodRegexTestFilter(listOf("io.kotest.runner.junit2")) - .filter(spec) shouldBe TestFilterResult.Exclude(null) + withData( + nameFn = { filters -> "should be INCLUDED when container and test were evaluated using $filters" }, + listOf("\\QGradleClassMethodRegexTestFilterTest.a context\\E.*"), + listOf(".*\\QTest\\E", "\\QGradleClassMethodRegex\\E.*\\Q.a context\\E.*"), + ) { filters -> + GradleClassMethodRegexTestFilter(filters).filter(container) shouldBe TestFilterResult.Include + GradleClassMethodRegexTestFilter(filters).filter(test) shouldBe TestFilterResult.Include + } } context("includes with test paths") { @@ -72,16 +78,14 @@ class GradleClassMethodRegexTestFilterTest : FunSpec({ val spec = GradleClassMethodRegexTestFilterTest::class.toDescriptor() val container = spec.append("a context") val test = container.append("nested test") - val fqcn = GradleClassMethodRegexTestFilterTest::class.qualifiedName - + val fqcn = "\\Q${GradleClassMethodRegexTestFilterTest::class.qualifiedName}\\E" withData( nameFn = { "should be INCLUDED when filter is: $it" }, - "$fqcn", - "$fqcn.a context", - "*.gradle.GradleClassMethodRegexTestFilterTest.a context", - "*adle.GradleClassMethodRegexTestFilterTest.a context", - "$fqcn.a context -- nested test", + fqcn, + "$fqcn\\Q.a context\\E.*", + ".*\\Q.gradle.GradleClassMethodRegexTestFilterTest.a context\\E.*", + ".*\\Qadle.GradleClassMethodRegexTestFilterTest.a context\\E.*" ) { filter -> GradleClassMethodRegexTestFilter(listOf(filter)) .filter(test) shouldBe TestFilterResult.Include @@ -89,10 +93,10 @@ class GradleClassMethodRegexTestFilterTest : FunSpec({ withData( nameFn = { "should be EXCLUDED when filter is: $it" }, - "$fqcn.a context2", - "$fqcn.nested test", - "$fqcn.a context -- nested test2", - "*sMethodRegexTestFilterTest.a context -- nested test2", + "$fqcn\\Q.a context2\\E", + "$fqcn\\Q.nested test\\E", + "$fqcn\\Q.a context.nested test2\\E", + ".*\\QsMethodRegexTestFilterTest.a context -- nested test2\\Q", ) { filter -> GradleClassMethodRegexTestFilter(listOf(filter)) .filter(test) shouldBe TestFilterResult.Exclude(null) diff --git a/kotest-runner/kotest-runner-junit5/src/jvmTest/kotlin/io/kotest/runner/junit/platform/gradle/GradleTestPatternParserTest.kt b/kotest-runner/kotest-runner-junit5/src/jvmTest/kotlin/io/kotest/runner/junit/platform/gradle/GradleTestPatternParserTest.kt deleted file mode 100644 index 18bf5eae10e..00000000000 --- a/kotest-runner/kotest-runner-junit5/src/jvmTest/kotlin/io/kotest/runner/junit/platform/gradle/GradleTestPatternParserTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package io.kotest.runner.junit.platform.gradle - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe - -class GradleTestPatternParserTest : FunSpec() { - init { - - test("parse package") { - GradleTestPattern.parse("org.mypackage") shouldBe GradleTestPattern(false, "org.mypackage", null, null) - GradleTestPattern.parse("org") shouldBe GradleTestPattern(false, "org", null, null) - } - - test("parse package with wildcard prefix") { - GradleTestPattern.parse("*.mypackage") shouldBe GradleTestPattern(true, "mypackage", null, null) - } - - test("parse Classname") { - GradleTestPattern.parse("MyClass") shouldBe GradleTestPattern(false, null, "MyClass", null) - GradleTestPattern.parse("A") shouldBe GradleTestPattern(false, null, "A", null) - } - - test("parse Classname with wildcard prefix") { - GradleTestPattern.parse("*Spec") shouldBe GradleTestPattern(true, null, "Spec", null) - } - - test("parse package.Classname") { - GradleTestPattern.parse("org.mypackage.MyClass") shouldBe GradleTestPattern( - false, - "org.mypackage", - "MyClass", - null, - ) - GradleTestPattern.parse("org.A") shouldBe GradleTestPattern(false, "org", "A", null) - } - - test("parse package.Classname with wildcard") { - GradleTestPattern.parse("*org.mypackage.MyClass") shouldBe GradleTestPattern( - true, - "org.mypackage", - "MyClass", - null, - ) - GradleTestPattern.parse("org.A") shouldBe GradleTestPattern(false, "org", "A", null) - } - - test("Package name containing caps") { - // This is obviously unwanted, but currently we rely on the capital letters to determine - // the class being tested. Having capital letters in a package name goes against - // Java coding conventions, so for now you're out of luck if you do it. - // https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html - - GradleTestPattern.parse("org.myPackage.MyClass.test") shouldBe - GradleTestPattern(false, "org", "myPackage", "MyClass.test") - } - - test("parse package.Classname.path") { - GradleTestPattern.parse("org.A.test with space") shouldBe GradleTestPattern( - false, - "org", - "A", - "test with space", - ) - GradleTestPattern.parse("org.A.test -- delimited and space") shouldBe - GradleTestPattern(false, "org", "A", "test -- delimited and space") - } - - test("parse Classname.path") { - GradleTestPattern.parse("MyClass.test") shouldBe - GradleTestPattern(false, null, "MyClass", "test") - - GradleTestPattern.parse("*MyClass.test") shouldBe - GradleTestPattern(true, null, "MyClass", "test") - - GradleTestPattern.parse("A.test with space") shouldBe - GradleTestPattern(false, null, "A", "test with space") - - GradleTestPattern.parse("A.test -- delimited and space") shouldBe - GradleTestPattern(false, null, "A", "test -- delimited and space") - - GradleTestPattern.parse("com.sksamuel.kotest.runner.junit5.AfterProjectListenerExceptionHandlingTest.an AfterProjectListenerException should add marker test") shouldBe - GradleTestPattern( - false, - "com.sksamuel.kotest.runner.junit5", - "AfterProjectListenerExceptionHandlingTest", - "an AfterProjectListenerException should add marker test", - ) - } - } -} From 208b79a7163cff93e9ab94c45861587208c2186e Mon Sep 17 00:00:00 2001 From: Mitchell Yuwono Date: Sun, 23 Oct 2022 10:14:54 +1100 Subject: [PATCH 2/4] add documentation to the functions for future contributors --- .../GradleClassMethodRegexTestFilter.kt | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilter.kt b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilter.kt index c9557ceadd8..6bc37afa61d 100644 --- a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilter.kt +++ b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilter.kt @@ -21,8 +21,28 @@ class GradleClassMethodRegexTestFilter(private val patterns: List) : Tes /** * Matches the pattern supplied from gradle build script or command line interface. - * Gradle supplies a well-formed regex to the engine that we can leverage to construct a well-formed regex object. * + * supports: + * - gradle test --tests "SomeTest" + * - gradle test --tests "*Test" + * - gradle test --tests "io.package.*" + * - gradle test --tests "io.package" + * - gradle test --tests "io.package.SomeTest" + * - gradle test --tests "io.package.SomeTest.first level context*" + * - gradle test --tests "io.package.SomeTest.*" + * - gradle test --tests "io.*.SomeTest" + * - gradle test --tests "SomeTest.first level context*" + * - gradle test --tests "*.first level context*" + * + * Exact nested context / test matching is NOT CURRENTLY SUPPORTED. + * Kotest support lazy test registration within nested context. Gradle test filter does not + * natively work nicely with kotest. In order to make it work we need to think of a way to + * recursively apply partial context-search as we dive deeper into the contexts. + * + * Notes to Maintainers: + * + * Gradle supplies a pattern string which corresponds to a well-formed regex object. + * This can be directly usable for kotest. * - A* becomes \QA\E.* * - A*Test becomes \QA\E.*\QTest\E * - io.*.A*Test becomes \Qio.\E.*\Q.A\E.*\QTest\E @@ -31,19 +51,20 @@ class GradleClassMethodRegexTestFilter(private val patterns: List) : Tes */ private fun match(pattern: String, descriptor: Descriptor): Boolean { val path = descriptor.dotSeparatedFullPath().value - val regexPattern = "^(.*)$pattern".toRegex() // matches - val laxRegexPattern = "^(.*)$pattern(.*)\$".toRegex() // matches pattern that begins with and followed by + val regexPattern = "^(.*)$pattern".toRegex() // matches pattern exactly + val laxRegexPattern = "^(.*)$pattern(.*)\$".toRegex() // matches pattern that can be followed by others val packagePath = descriptor.spec().kclass.java.packageName // io.kotest - val isSimpleClassMatch = descriptor.spec().kclass.java.simpleName.matches(pattern.toRegex()) // SomeTest or *Test - val isSpecMatched = descriptor.spec().id.value.matches(regexPattern) // *.SomeTest - val isFullPathMatched = path.matches(regexPattern) // io.*.SomeTest - val isFullPathDotMatched = "$path.".matches(regexPattern) // io.*. or io.*.SomeTest.* + val isSimpleClassMatch by lazy { descriptor.spec().kclass.java.simpleName.matches(pattern.toRegex()) } // SomeTest or *Test + val isSpecMatched by lazy { descriptor.spec().id.value.matches(regexPattern) } // *.SomeTest + val isFullPathMatched by lazy { path.matches(regexPattern) } // io.*.SomeTest + val isFullPathDotMatched by lazy { "$path.".matches(regexPattern) } // io.*. or io.*.SomeTest.* - val doesNotContainUppercase = pattern.replace("\\Q", "").replace("\\E", "").all { !it.isUpperCase() } + // if there's no uppercase in the supplied pattern, activate trigger relaxed matching + val doesNotContainUppercase by lazy { pattern.replace("\\Q", "").replace("\\E", "").all { !it.isUpperCase() } } - val isPackageMatched = doesNotContainUppercase && packagePath.matches(laxRegexPattern) // io.kotest - val isPackageWithDotMatched = doesNotContainUppercase && "$packagePath.".matches(laxRegexPattern) // io.kotest.* + val isPackageMatched by lazy { doesNotContainUppercase && packagePath.matches(laxRegexPattern) } // io.kotest + val isPackageWithDotMatched by lazy { doesNotContainUppercase && "$packagePath.".matches(laxRegexPattern) } // io.kotest.* return isSimpleClassMatch || isFullPathMatched || @@ -55,6 +76,14 @@ class GradleClassMethodRegexTestFilter(private val patterns: List) : Tes /** * Returns a gradle-compatible dot-separated full path of the given descriptor. + * i.e. io.package.MyTest.given something -- should do something + * + * Note: I'm forced to do this... :( + * + * We cannot use the / separator for contexts as gradle rejects that. + * Filters also seemingly only works on first "." after the class. This was severely limiting. + * The other problem is that also means we can't have "." in the test / context path because gradle doesn't + * like it and will not even give us any candidate classes. */ private fun Descriptor.dotSeparatedFullPath(): TestPath = when (this) { is Descriptor.SpecDescriptor -> TestPath(this.id.value) From b9cc276a21e93199d454872d85c59b207065c645 Mon Sep 17 00:00:00 2001 From: Mitchell Yuwono Date: Sun, 23 Oct 2022 10:38:47 +1100 Subject: [PATCH 3/4] use specId instead of kclass --- .../platform/gradle/GradleClassMethodRegexTestFilter.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilter.kt b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilter.kt index 6bc37afa61d..7438eaa4d4a 100644 --- a/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilter.kt +++ b/kotest-runner/kotest-runner-junit5/src/jvmMain/kotlin/io/kotest/runner/junit/platform/gradle/GradleClassMethodRegexTestFilter.kt @@ -53,9 +53,12 @@ class GradleClassMethodRegexTestFilter(private val patterns: List) : Tes val path = descriptor.dotSeparatedFullPath().value val regexPattern = "^(.*)$pattern".toRegex() // matches pattern exactly val laxRegexPattern = "^(.*)$pattern(.*)\$".toRegex() // matches pattern that can be followed by others - val packagePath = descriptor.spec().kclass.java.packageName // io.kotest + val packagePath = descriptor.spec().id.value.split(".").dropLast(1).joinToString(".") // io.kotest - val isSimpleClassMatch by lazy { descriptor.spec().kclass.java.simpleName.matches(pattern.toRegex()) } // SomeTest or *Test + val isSimpleClassMatch by lazy { + // SomeTest or *Test + descriptor.spec().id.value.split(".").lastOrNull()?.matches(pattern.toRegex()) ?: false + } val isSpecMatched by lazy { descriptor.spec().id.value.matches(regexPattern) } // *.SomeTest val isFullPathMatched by lazy { path.matches(regexPattern) } // io.*.SomeTest val isFullPathDotMatched by lazy { "$path.".matches(regexPattern) } // io.*. or io.*.SomeTest.* From 9ed501f206af872efa79c07e0f0453e541e8629d Mon Sep 17 00:00:00 2001 From: Mitchell Yuwono Date: Sun, 23 Oct 2022 10:50:40 +1100 Subject: [PATCH 4/4] fixes number of tests expectation in DiscoveryTest.kt --- .../kotlin/com/sksamuel/kotest/runner/junit5/DiscoveryTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotest-runner/kotest-runner-junit5/src/jvmTest/kotlin/com/sksamuel/kotest/runner/junit5/DiscoveryTest.kt b/kotest-runner/kotest-runner-junit5/src/jvmTest/kotlin/com/sksamuel/kotest/runner/junit5/DiscoveryTest.kt index 8858141b12c..b2542395222 100644 --- a/kotest-runner/kotest-runner-junit5/src/jvmTest/kotlin/com/sksamuel/kotest/runner/junit5/DiscoveryTest.kt +++ b/kotest-runner/kotest-runner-junit5/src/jvmTest/kotlin/com/sksamuel/kotest/runner/junit5/DiscoveryTest.kt @@ -61,7 +61,7 @@ class DiscoveryTest : FunSpec({ .build() val engine = KotestJunitPlatformTestEngine() val descriptor = engine.discover(req, UniqueId.forEngine("testengine")) - descriptor.classes.size shouldBe 27 + descriptor.classes.size shouldBe 26 } test("kotest should return classes if request has no included or excluded test engines") {