From fbdee3368dcf0b75580372bba7f605f3dffc2135 Mon Sep 17 00:00:00 2001 From: aristorato_expedia Date: Tue, 18 Nov 2025 17:04:27 +0000 Subject: [PATCH 1/2] WIP support for data test gutter icons --- .../kotlin/io/kotest/plugin/intellij/Test.kt | 3 +- .../io/kotest/plugin/intellij/psi/utils.kt | 5 +++ .../run/gradle/GradleTaskNamesBuilder.kt | 6 ++- ...estOrSpecGradleRunConfigurationProducer.kt | 4 ++ .../idea/TestPathRunConfigurationProducer.kt | 5 ++- .../plugin/intellij/styles/SpecStyle.kt | 7 ++++ .../plugin/intellij/styles/StringSpecStyle.kt | 40 ++++++++++++++++++- .../intellij/styles/StringSpecStyleTest.kt | 2 +- src/test/resources/stringspec.kt | 3 ++ 9 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/io/kotest/plugin/intellij/Test.kt b/src/main/kotlin/io/kotest/plugin/intellij/Test.kt index 3b60772f..3262a940 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/Test.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/Test.kt @@ -56,7 +56,8 @@ data class Test( val specClassName: KtClassOrObject, // the containing class name, which all tests must have val testType: TestType, val xdisabled: Boolean, // if true then this test was defined using one of the x methods - val psi: PsiElement // the canonical element that identifies this test + val psi: PsiElement, // the canonical element that identifies this test + val isDataTest: Boolean = false ) { // true if this test is not xdisabled and not disabled by a bang and not nested inside another disabled test diff --git a/src/main/kotlin/io/kotest/plugin/intellij/psi/utils.kt b/src/main/kotlin/io/kotest/plugin/intellij/psi/utils.kt index 895b8544..50248436 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/psi/utils.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/psi/utils.kt @@ -127,3 +127,8 @@ fun KtValueArgumentList.isSingleStringTemplateArg(): Boolean = && children[0] is KtValueArgument && children[0].children.size == 1 && children[0].children[0] is KtStringTemplateExpression + +fun LeafPsiElement.isDataTestMethodCall(dataTestMethodNames:Set): KtCallExpression? { + val lambdaCall = ifCallExpressionLambdaOpenBrace() + return lambdaCall.takeIf {lambdaCall?.functionName() in dataTestMethodNames} +} diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/gradle/GradleTaskNamesBuilder.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/gradle/GradleTaskNamesBuilder.kt index bb519cd7..84c02279 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/run/gradle/GradleTaskNamesBuilder.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/gradle/GradleTaskNamesBuilder.kt @@ -39,7 +39,11 @@ data class GradleTaskNamesBuilder( private fun includeArg(): String? { return when (test) { null -> "$PROPERTY_INCLUDE='${spec.fqName?.asString()}'" - else -> "$PROPERTY_INCLUDE='${test.descriptorPath()}'" + else -> + when(test.isDataTest){ + false -> "$PROPERTY_INCLUDE='${test.descriptorPath()}'" + true -> "$PROPERTY_INCLUDE='${spec.fqName?.asString()}'" + } } } } diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/gradle/TestOrSpecGradleRunConfigurationProducer.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/gradle/TestOrSpecGradleRunConfigurationProducer.kt index acf47ac0..dc6ec710 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/run/gradle/TestOrSpecGradleRunConfigurationProducer.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/gradle/TestOrSpecGradleRunConfigurationProducer.kt @@ -155,6 +155,10 @@ class TestOrSpecGradleRunConfigurationProducer : GradleRunConfigurationProducer( if (test != null) { // if we specified a test descriptor before, it needs to match for this configuration to be the same val descriptorArg = GradleUtils.getIncludeArg(configuration.settings.taskNames) ?: return false + if (test.isDataTest) { + val spec = element.enclosingSpec() + return spec?.fqName?.asString() == descriptorArg + } if (test.descriptorPath() == descriptorArg) return true } } diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/idea/TestPathRunConfigurationProducer.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/idea/TestPathRunConfigurationProducer.kt index 04de0820..75c58f02 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/run/idea/TestPathRunConfigurationProducer.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/idea/TestPathRunConfigurationProducer.kt @@ -51,7 +51,10 @@ class TestPathRunConfigurationProducer : LazyRunConfigurationProducer = emptySet() } diff --git a/src/main/kotlin/io/kotest/plugin/intellij/styles/StringSpecStyle.kt b/src/main/kotlin/io/kotest/plugin/intellij/styles/StringSpecStyle.kt index 624beadc..b920f81a 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/styles/StringSpecStyle.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/styles/StringSpecStyle.kt @@ -8,8 +8,11 @@ import io.kotest.plugin.intellij.TestType import io.kotest.plugin.intellij.psi.enclosingKtClassOrObject import io.kotest.plugin.intellij.psi.extractStringForStringExtensionFunctonWithRhsFinalLambda import io.kotest.plugin.intellij.psi.extractStringFromStringInvokeWithLambda +import io.kotest.plugin.intellij.psi.hasFunctionName import io.kotest.plugin.intellij.psi.ifCallExpressionLhsStringOpenQuote import io.kotest.plugin.intellij.psi.ifDotExpressionSeparator +import io.kotest.plugin.intellij.psi.isDataTestMethodCall +import io.kotest.plugin.intellij.styles.SpecStyle.Companion.dataTestDefaultTestName import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtDotQualifiedExpression @@ -28,6 +31,12 @@ object StringSpecStyle : SpecStyle { return "\"$name\" { }" } + override fun getDataTestMethodNames(): Set = + setOf( + "withData", + ) + + /** * A test of the form: * @@ -41,6 +50,27 @@ object StringSpecStyle : SpecStyle { return Test(testName, null, specClass, TestType.Test, xdisabled = false, psi = this) } + /** + * A test container of the form: + *``` + * withData(1, 2, 3) { } + * withData(listOf(1, 2, 3)) { } + * withData(nameFn = { "test $it" }, 1, 2, 3) { } + * ... any other withData permutation + *``` + * Note: even tho we build a Test, the runner will only read the `isDataTest` boolean to determine it needs to run the whole spec + */ + private fun KtCallExpression.tryWithData(): Test? { + val specClass = enclosingKtClassOrObject() ?: return null + + if (!hasFunctionName(getDataTestMethodNames().toList())) return null + + + // withData is a container because it generates multiple tests at runtime + return Test(dataTestDefaultTestName, null, specClass, TestType.Container, xdisabled = false, psi = this, isDataTest = true) + } + + /** * Matches tests of the form: * @@ -58,10 +88,11 @@ object StringSpecStyle : SpecStyle { * * "test name" { } * "test name".config(...) {} + * withData(...) { } */ override fun test(element: PsiElement): Test? { return when (element) { - is KtCallExpression -> element.tryTest() + is KtCallExpression -> element.tryTest() ?: element.tryWithData() is KtDotQualifiedExpression -> element.tryTestWithConfig() else -> null } @@ -76,6 +107,7 @@ object StringSpecStyle : SpecStyle { * * "test name" { } * "test name".config(...) {} + * withData(...) { } */ override fun test(element: LeafPsiElement): Test? { val ktcall = element.ifCallExpressionLhsStringOpenQuote() @@ -84,6 +116,12 @@ object StringSpecStyle : SpecStyle { val ktdot = element.ifDotExpressionSeparator() if (ktdot != null) return test(ktdot) + // try to find Data Test Method by finding lambda openings + val dataMethodCall = element.isDataTestMethodCall(getDataTestMethodNames()) + if (dataMethodCall != null) { + return test(dataMethodCall) + } + return null } } diff --git a/src/test/kotlin/io/kotest/plugin/intellij/styles/StringSpecStyleTest.kt b/src/test/kotlin/io/kotest/plugin/intellij/styles/StringSpecStyleTest.kt index 462521d2..f0fda1f3 100644 --- a/src/test/kotlin/io/kotest/plugin/intellij/styles/StringSpecStyleTest.kt +++ b/src/test/kotlin/io/kotest/plugin/intellij/styles/StringSpecStyleTest.kt @@ -24,7 +24,7 @@ class StringSpecStyleTest : LightJavaCodeInsightFixtureTestCase() { ) val gutters = myFixture.findAllGutters() - gutters.size shouldBe 3 + gutters.size shouldBe 4 val expected = listOf( Gutter("Run StringSpecExample", 91, AllIcons.RunConfigurations.TestState.Run_run), diff --git a/src/test/resources/stringspec.kt b/src/test/resources/stringspec.kt index e81403e9..c09f1a66 100644 --- a/src/test/resources/stringspec.kt +++ b/src/test/resources/stringspec.kt @@ -10,5 +10,8 @@ class StringSpecExample : StringSpec() { "test with config".config(enabled = false) { } + withData(1, 2, 3, 4, 5) { value -> + // test here + } } } From def0428e1d81befe3113a9020c95f17f0df05155 Mon Sep 17 00:00:00 2001 From: aristorato_expedia Date: Tue, 18 Nov 2025 17:28:37 +0000 Subject: [PATCH 2/2] fix test --- .../kotest/plugin/intellij/styles/StringSpecStyleTest.kt | 7 ++++--- src/test/resources/stringspec.kt | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/kotlin/io/kotest/plugin/intellij/styles/StringSpecStyleTest.kt b/src/test/kotlin/io/kotest/plugin/intellij/styles/StringSpecStyleTest.kt index f0fda1f3..b6ebdc30 100644 --- a/src/test/kotlin/io/kotest/plugin/intellij/styles/StringSpecStyleTest.kt +++ b/src/test/kotlin/io/kotest/plugin/intellij/styles/StringSpecStyleTest.kt @@ -27,9 +27,10 @@ class StringSpecStyleTest : LightJavaCodeInsightFixtureTestCase() { gutters.size shouldBe 4 val expected = listOf( - Gutter("Run StringSpecExample", 91, AllIcons.RunConfigurations.TestState.Run_run), - Gutter("Run test", 145), - Gutter("Run test with config", 201), + Gutter("Run StringSpecExample", 126, AllIcons.RunConfigurations.TestState.Run_run), + Gutter("Run test", 180), + Gutter("Run test with config", 236), + Gutter("Run All Spec Tests, including data tests", 299), ) expected.size shouldBe gutters.size diff --git a/src/test/resources/stringspec.kt b/src/test/resources/stringspec.kt index c09f1a66..e52f5c96 100644 --- a/src/test/resources/stringspec.kt +++ b/src/test/resources/stringspec.kt @@ -1,6 +1,7 @@ package com.sksamuel.kotest.specs.stringspec import io.kotest.core.spec.style.StringSpec +import io.kotest.datatest.withData class StringSpecExample : StringSpec() { init {