From 4700f031207465368e124a860605d4e4034d7095 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 02:20:06 +0000 Subject: [PATCH 01/15] Bump version -> `2.0.0-SNAPSHOT.452` Co-Authored-By: Claude Opus 5.5 --- version.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle.kts b/version.gradle.kts index 0faf741b..25bd3452 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -12,4 +12,4 @@ * and limitations under the License. */ -extra.set("versionToPublish", "2.0.0-SNAPSHOT.451") +extra.set("versionToPublish", "2.0.0-SNAPSHOT.452") From a6465ddbb30fe92a1817bd5593f6766e73ec5a46 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 02:35:34 +0000 Subject: [PATCH 02/15] Pass the TestKit coverage agent on the command line, allow a shared TestKit home The harness used to attach the JaCoCo agent by writing `org.gradle.jvmargs` into a `gradle.properties` of a per-module TestKit directory. That tied the directory to one module, and on CI it was empty on every run, so the first worker build of each module downloaded its dependencies and generated the Gradle API JARs again. The agent now goes into the runner arguments as `-Dorg.gradle.jvmargs`, and the consuming build may name a TestKit directory shared by all modules via the optional `io.spine.tools.gradle.testkit.coverage.home` system property. Without it, each module keeps a directory of its own. Co-Authored-By: Claude Opus 5.5 --- .../io/spine/tools/gradle/testing/Gradle.kt | 28 +--- .../tools/gradle/testing/GradleProject.kt | 57 ++++--- .../tools/gradle/testing/TestKitCoverage.kt | 150 ++++++++++++------ .../gradle/testing/TestKitCoverageSpec.kt | 127 +++++++++++++++ 4 files changed, 269 insertions(+), 93 deletions(-) create mode 100644 plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/TestKitCoverageSpec.kt diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/Gradle.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/Gradle.kt index a3600357..e0526d33 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/Gradle.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/Gradle.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.gradle.testing @@ -94,7 +82,7 @@ public fun runGradleBuild( val runner = GradleRunner.create() .withProjectDir(projectDir) .withPluginClasspath() - .withArguments(arguments) + .withArguments(arguments.withTestKitCoverage()) runner.enableTestKitCoverage() stdOutput?.let { runner.forwardStdOutput(it) diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt index ff007dc8..f69a0df1 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.gradle.testing @@ -48,9 +36,9 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * Plugin code exercised through Gradle TestKit runs in a separate worker JVM, which * JaCoCo/Kover do not instrument by default, so that out-of-process execution is * otherwise not credited to coverage. This library attaches the JaCoCo agent to the - * worker JVM automatically — for projects run via this class as well as via - * [runGradleBuild] — but only when the test JVM is started with two system - * properties, which the consuming build is responsible for setting: + * worker JVM automatically — for builds started by [executeTask] and [executeAndFail] + * of this class as well as by [runGradleBuild] — but only when the test JVM is started + * with two system properties, which the consuming build is responsible for setting: * * - `io.spine.tools.gradle.testkit.coverage.agent` — the absolute path to the * standalone JaCoCo agent JAR, that is, the artifact @@ -59,8 +47,20 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * worker should write its execution data. The harness creates a single * per-module `testkit.exec` file inside it. * - * When the properties are absent the harness is a no-op, so ordinary runs and - * consumers that do not opt in are unaffected. + * When either of these properties is absent, the harness is a no-op, so ordinary + * runs and consumers that do not opt in are unaffected. A build started from + * [runner] directly gets no agent. + * + * A third property is optional: + * + * - `io.spine.tools.gradle.testkit.coverage.home` — the TestKit directory (the + * Gradle user home of the worker builds) to share among the test JVMs of all + * modules. Sharing it means that the dependencies of the worker builds are + * downloaded, and the Gradle API JARs generated, once rather than once per + * module, and a CI cache can keep the directory between runs. Without the + * property, each module gets a directory of its own. A project set up with + * [GradleProjectSetup.withSharedTestKitDirectory] keeps using that directory + * instead. * * The snippet below is a self-contained Gradle (Kotlin DSL) setup that a module * running TestKit-based tests can copy into its `build.gradle.kts`. Pin the agent @@ -80,6 +80,9 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * * val agentJar = testKitJacocoAgent.elements.map { it.single().asFile.absolutePath } * val execDir = layout.buildDirectory.dir("jacoco-testkit") + * // Optional: a TestKit directory shared by all modules. Under `caches/`, it is + * // kept by CI caches of the Gradle user home. + * val testKitHome = gradle.gradleUserHomeDir.resolve("caches/spine-testkit") * * // Wipe the exec directory at most once per build, from the first Test task * // that actually executes. The workers append to a single per-module exec @@ -102,6 +105,7 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * dir.mkdirs() * systemProperty("io.spine.tools.gradle.testkit.coverage.agent", agentJar.get()) * systemProperty("io.spine.tools.gradle.testkit.coverage.execDir", dir.absolutePath) + * systemProperty("io.spine.tools.gradle.testkit.coverage.home", testKitHome.absolutePath) * } * } * ``` @@ -139,6 +143,9 @@ public class GradleProject internal constructor(setup: GradleProjectSetup) { /** * The runner for executing tasks. + * + * A build started from the runner directly, rather than by [executeTask] or + * [executeAndFail], runs without the TestKit coverage agent. */ public val runner: GradleRunner by lazy { val runner = GradleRunner.create() @@ -264,7 +271,7 @@ public class GradleProject internal constructor(setup: GradleProjectSetup) { private fun prepareRun(taskName: TaskName): GradleRunner { val args = arguments.forTask(taskName) - return runner.withArguments(args) + return runner.withArguments(args.withTestKitCoverage()) } /** diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt index 98fb482e..7c754752 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.gradle.testing @@ -30,52 +18,98 @@ import java.io.File import org.gradle.testkit.runner.GradleRunner /** - * Attaches the JaCoCo coverage agent to the worker JVM spawned by this runner, + * Points this runner at the TestKit directory (the Gradle user home of the worker + * build) used for collecting the coverage of the worker JVM it spawns. + * + * Use it together with [withTestKitCoverage]: this function selects the directory, + * and that one adds the argument that attaches the JaCoCo agent to the worker JVM, * so that plugin code executed out-of-process via TestKit is credited to coverage. * * Does nothing unless the test JVM was started with the [AGENT_PROPERTY] and * [EXEC_DIR_PROPERTY] system properties. This keeps the harness a no-op for * ordinary runs and for consumers that do not opt in. * - * Supplying those two properties on the test JVM is the responsibility of the + * Supplying those properties on the test JVM is the responsibility of the * consuming build. Consumer-facing setup instructions, including a copy-paste * Gradle recipe, live in the [GradleProject] KDoc (this function is `internal`, * so its own KDoc is not part of the published API documentation). * - * When enabled, the method writes a `gradle.properties` carrying a `-javaagent:…` - * argument into the TestKit directory (the Gradle user home of the worker build), - * and points the runner at that directory. The JaCoCo agent appends to a single - * per-module [execution-data file][EXEC_FILE]; on worker daemon shutdown — which - * happens after the tests complete and before the coverage report task runs — the - * data is flushed to disk. - * * @param preferredTestKitDir The TestKit directory the caller already intends to - * use (e.g. a [shared one][GradleProjectSetup.withSharedTestKitDirectory]), or - * `null` to let this method pick the per-module [TESTKIT_HOME_DIR]. + * use (e.g. the one of [GradleProjectSetup.withSharedTestKitDirectory]), or + * `null` to let [coverageTestKitDir] pick one. */ internal fun GradleRunner.enableTestKitCoverage(preferredTestKitDir: File? = null) { - val agentJar = System.getProperty(AGENT_PROPERTY) ?: return - val execDirPath = System.getProperty(EXEC_DIR_PROPERTY) ?: return + val testKitDir = coverageTestKitDir(preferredTestKitDir) ?: return + withTestKitDir(testKitDir.apply { mkdirs() }) +} - val execDir = File(execDirPath).apply { mkdirs() } - val execFile = File(execDir, EXEC_FILE) - val testKitDir = (preferredTestKitDir ?: File(execDir.parentFile, TESTKIT_HOME_DIR)) - .apply { mkdirs() } +/** + * Returns the TestKit directory for the coverage-enabled worker builds, or `null` + * when TestKit coverage is not enabled for this test JVM. + * + * In the order of precedence, the directory is: + * + * 1. The [preferred] one, which the caller already intends to use. + * 2. The one [shared by the worker builds of all modules][TESTKIT_HOME_PROPERTY], + * when the consuming build names it. + * 3. A per-module [TESTKIT_HOME_DIR] next to the execution-data directory. + */ +internal fun coverageTestKitDir(preferred: File? = null): File? { + val coverage = CoverageSetup.fromSystemProperties() ?: return null + return preferred + ?: System.getProperty(TESTKIT_HOME_PROPERTY)?.let(::File) + ?: File(coverage.execDir.parentFile, TESTKIT_HOME_DIR) +} - val jvmArg = "-javaagent:${agentJar.forProperties()}=" + +/** + * Returns these arguments with the `-Dorg.gradle.jvmargs` argument that attaches + * the JaCoCo agent to the worker JVM appended, or the same arguments when TestKit + * coverage is not enabled. + * + * The agent appends to a single per-module [execution-data file][EXEC_FILE]; on + * worker daemon shutdown — which happens after the tests complete and before the + * coverage report task runs — the data is flushed to disk. + * + * The agent is passed on the command line rather than in a `gradle.properties` file + * in the TestKit directory, so that the directory carries nothing specific to a + * module and can be shared. The argument replaces the `org.gradle.jvmargs` the + * worker build would use otherwise, such as the one of the `gradle.properties` of + * the test project, or one passed earlier in these arguments. + */ +internal fun List.withTestKitCoverage(): List { + val coverage = CoverageSetup.fromSystemProperties() ?: return this + val execFile = File(coverage.execDir, EXEC_FILE) + // Forward slashes work on every platform and need no escaping. + val jvmArg = "-javaagent:${coverage.agentJar.invariantSeparatorsPath}=" + "destfile=${execFile.invariantSeparatorsPath}," + "append=true,dumponexit=true,output=file,jmx=false" - File(testKitDir, "gradle.properties") - .writeText("org.gradle.jvmargs=$jvmArg\n") - - withTestKitDir(testKitDir) + return this + "-Dorg.gradle.jvmargs=$jvmArg" } /** - * Normalizes a file-system path for safe inclusion into a `*.properties` file - * by using forward slashes regardless of the host platform. + * The TestKit coverage setup the consuming build passed to this test JVM. + * + * @property agentJar The JaCoCo agent JAR. + * @property execDir The directory for the execution data of the worker JVMs. */ -private fun String.forProperties(): String = File(this).invariantSeparatorsPath +private class CoverageSetup(val agentJar: File, val execDir: File) { + + companion object { + + /** + * Reads the setup from the system properties of this test JVM, + * or returns `null` when TestKit coverage is not enabled for it. + */ + fun fromSystemProperties(): CoverageSetup? { + val agentJar = System.getProperty(AGENT_PROPERTY) + val execDir = System.getProperty(EXEC_DIR_PROPERTY) + if (agentJar == null || execDir == null) { + return null + } + return CoverageSetup(File(agentJar), File(execDir)) + } + } +} /** * The name of the system property carrying the absolute path to the JaCoCo @@ -111,12 +145,32 @@ private const val EXEC_DIR_PROPERTY: String = */ private const val EXEC_FILE: String = "testkit.exec" +/** + * The name of the system property carrying the absolute path to the TestKit + * directory shared by the coverage-enabled worker builds of all modules. + * + * Optional: when it is absent, each module gets its own [TESTKIT_HOME_DIR]. With a + * shared directory, the dependencies of the worker builds are downloaded, and the + * Gradle API JARs generated, once rather than once per module, and a CI cache can + * keep the directory between runs. Sharing is safe because the directory holds + * nothing specific to a module: [withTestKitCoverage] passes the agent on the + * command line, and the workers of each module, started with their own JVM + * arguments, get daemons of their own. + * + * The property takes effect only together with [AGENT_PROPERTY] and [EXEC_DIR_PROPERTY]. + * + * Set by the `io.spine.gradle.testing.enableTestKitCoverage` extension in + * `config`'s `buildSrc`; keep the two values in sync. + */ +private const val TESTKIT_HOME_PROPERTY: String = + "io.spine.tools.gradle.testkit.coverage.home" + /** * The name of the directory, placed next to the [execution-data directory][EXEC_DIR_PROPERTY], - * that serves as the dedicated Gradle user home (the TestKit directory) for the - * coverage-enabled worker builds. + * that serves as the TestKit directory of the coverage-enabled worker builds when no + * [shared one][TESTKIT_HOME_PROPERTY] is set. * - * Reusing one TestKit directory per module lets a single TestKit daemon serve all - * the test cases, keeping the agent attached with a stable `destfile`. + * Reusing one directory for all the test cases of a module lets a single TestKit + * daemon serve them. */ private const val TESTKIT_HOME_DIR: String = "testkit-home" diff --git a/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/TestKitCoverageSpec.kt b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/TestKitCoverageSpec.kt new file mode 100644 index 00000000..4dfd6713 --- /dev/null +++ b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/TestKitCoverageSpec.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.tools.gradle.testing + +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import java.io.File +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +/** + * The names of the system properties are spelled out rather than taken from + * `TestKitCoverage.kt`, so that the spec pins the contract with the consuming builds. + */ +@DisplayName("TestKit coverage harness should") +internal class TestKitCoverageSpec { + + private val savedProperties = mutableMapOf() + + @BeforeEach + fun clearProperties() { + COVERAGE_PROPERTIES.forEach { name -> + savedProperties[name] = System.getProperty(name) + System.clearProperty(name) + } + } + + @AfterEach + fun restoreProperties() { + savedProperties.forEach { (name, value) -> + if (value == null) { + System.clearProperty(name) + } else { + System.setProperty(name, value) + } + } + } + + @Test + fun `leave the arguments intact when coverage is not enabled`() { + val args = listOf("build") + + args.withTestKitCoverage() shouldBe args + coverageTestKitDir().shouldBeNull() + } + + @Test + fun `stay disabled when only the agent property is set`(@TempDir dir: File) { + System.setProperty(AGENT, dir.resolve("agent.jar").path) + val args = listOf("build") + + args.withTestKitCoverage() shouldBe args + coverageTestKitDir().shouldBeNull() + } + + @Nested + inner class `when coverage is enabled` { + + @TempDir + lateinit var dir: File + + private val agentJar by lazy { dir.resolve("jacoco/agent.jar") } + private val execDir by lazy { dir.resolve("build/jacoco-testkit") } + + @BeforeEach + fun enableCoverage() { + System.setProperty(AGENT, agentJar.path) + System.setProperty(EXEC_DIR, execDir.path) + } + + @Test + fun `append the argument that attaches the agent to the worker JVM`() { + val args = listOf("build", "--stacktrace").withTestKitCoverage() + + val execFile = execDir.resolve("testkit.exec") + args shouldBe listOf( + "build", + "--stacktrace", + "-Dorg.gradle.jvmargs=-javaagent:${agentJar.invariantSeparatorsPath}=" + + "destfile=${execFile.invariantSeparatorsPath}," + + "append=true,dumponexit=true,output=file,jmx=false" + ) + } + + @Test + fun `use a TestKit directory per module by default`() { + coverageTestKitDir() shouldBe dir.resolve("build/testkit-home") + } + + @Test + fun `use the shared TestKit directory when the build names one`() { + val shared = dir.resolve("gradle-home/caches/spine-testkit") + System.setProperty(HOME, shared.path) + + coverageTestKitDir() shouldBe shared + } + + @Test + fun `prefer the TestKit directory chosen by the caller`() { + System.setProperty(HOME, dir.resolve("shared").path) + val preferred = dir.resolve("preferred") + + coverageTestKitDir(preferred) shouldBe preferred + } + } +} + +private const val AGENT = "io.spine.tools.gradle.testkit.coverage.agent" +private const val EXEC_DIR = "io.spine.tools.gradle.testkit.coverage.execDir" +private const val HOME = "io.spine.tools.gradle.testkit.coverage.home" +private val COVERAGE_PROPERTIES = listOf(AGENT, EXEC_DIR, HOME) From 8549704c0ee08cd32cc9c0aa3efe0c93001b5778 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 02:35:35 +0000 Subject: [PATCH 03/15] Update dependency reports Co-Authored-By: Claude Opus 5.5 --- docs/dependencies/dependencies.md | 76 +++++++++++++++---------------- docs/dependencies/pom.xml | 2 +- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index ceba616e..17dc9293 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine.tools:archive:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:archive:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -832,14 +832,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:code:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:code:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1655,14 +1655,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:fixtures:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:fixtures:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.google.android. **Name** : annotations. **Version** : 4.1.1.4. @@ -2593,14 +2593,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:fs:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:fs:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -3416,14 +3416,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gradle-plugin-api:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:gradle-plugin-api:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-annotations. **Version** : 2.22. @@ -4345,14 +4345,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gradle-plugin-api-test-fixtures:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:gradle-plugin-api-test-fixtures:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-annotations. **Version** : 2.22. @@ -4865,14 +4865,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:33 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gradle-root-plugin:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:gradle-root-plugin:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -5736,14 +5736,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:intellij-platform:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:intellij-platform:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -6840,14 +6840,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:intellij-platform-java:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:intellij-platform-java:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -8719,14 +8719,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:37 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:java-code:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:java-code:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -9596,14 +9596,14 @@ This report was generated on **Tue Nov 03 02:04:37 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:jvm-tool-plugins:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:jvm-tool-plugins:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -10459,14 +10459,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:jvm-tools:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:jvm-tools:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0. @@ -11233,14 +11233,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:kotlin-code:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:kotlin-code:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -12056,14 +12056,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:plugin-base:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:plugin-base:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -12927,14 +12927,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:plugin-testlib:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:plugin-testlib:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.google.auto.value. **Name** : auto-value-annotations. **Version** : 1.11.1. @@ -13901,14 +13901,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:proto-code:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:proto-code:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -14794,14 +14794,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:36 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:protobuf-setup-plugins:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:protobuf-setup-plugins:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -15677,14 +15677,14 @@ This report was generated on **Tue Nov 03 02:04:36 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:37 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:psi:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:psi:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -16812,14 +16812,14 @@ This report was generated on **Tue Nov 03 02:04:37 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:37 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:psi-java:2.0.0-SNAPSHOT.451` +# Dependencies of `io.spine.tools:psi-java:2.0.0-SNAPSHOT.452` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -18738,6 +18738,6 @@ This report was generated on **Tue Nov 03 02:04:37 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:04:37 WET 2026** using +This report was generated on **Tue Nov 03 02:21:34 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml index bbb31acf..3b6db849 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine.tools tool-base -2.0.0-SNAPSHOT.451 +2.0.0-SNAPSHOT.452 2015 From e230615462082b9b075c24c6e2399e542f260743 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 16:02:41 +0000 Subject: [PATCH 04/15] Update `config` (./config/pull) Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 2 +- .../spine/gradle/testing/TestKitCoverage.kt | 44 +++++++++++++++++-- config | 2 +- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a7fce5a7..182bfa4c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,7 +30,7 @@ on: jobs: publish: name: Publish to Maven repositories - uses: SpineEventEngine/config/.github/workflows/publishing.yml@5ddff23286f7f6353d4714b0399ab89109c56c79 + uses: SpineEventEngine/config/.github/workflows/publishing.yml@4f80b399d33103039a10b56145cb8ffad8bd7a3d permissions: contents: read packages: write diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt b/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt index 52f66323..59ff9d6f 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt @@ -33,10 +33,12 @@ import org.gradle.kotlin.dsl.withType * * 1. Resolves the standalone JaCoCo agent JAR ([Jacoco.agent]) through a * dedicated [AGENT_CONFIGURATION] configuration. - * 2. Passes the agent JAR path and a per-module exec directory - * (`build/`[TESTKIT_COVERAGE_DIR]) to the test JVM as system properties. - * The `plugin-testlib` harness reads these and writes a `gradle.properties` - * into the worker's Gradle user home that adds `-javaagent:…` to the worker JVM. + * 2. Passes the agent JAR path, a per-module exec directory + * (`build/`[TESTKIT_COVERAGE_DIR]), and a [TestKit directory][SHARED_TESTKIT_HOME] + * shared by all modules to the test JVM as system properties. The + * `plugin-testlib` harness reads these and attaches `-javaagent:…` to the + * worker JVM. A harness version that supports the shared directory points its + * runners at it; an older one keeps a TestKit directory per module. * 3. Wipes the exec directory at most once per build invocation, from the * `doFirst` of the first `Test` task that actually executes, so stale worker * coverage from a previous run does not accumulate. Two failure modes are @@ -76,6 +78,9 @@ fun Project.enableTestKitCoverage() { val agentPath = agent.elements.map { it.single().asFile.absolutePath } val execDir = layout.buildDirectory.dir(TESTKIT_COVERAGE_DIR) + // Not a task input: where the worker builds keep their caches does not + // affect the test results. + val testKitHome = gradle.gradleUserHomeDir.resolve(SHARED_TESTKIT_HOME) // Wiped at most once per build invocation, by the first `Test` task that // actually executes — see the KDoc above for why this is a guarded `doFirst` @@ -96,6 +101,7 @@ fun Project.enableTestKitCoverage() { dir.mkdirs() systemProperty(AGENT_PROPERTY, agentPath.get()) systemProperty(EXEC_DIR_PROPERTY, dir.absolutePath) + systemProperty(TESTKIT_HOME_PROPERTY, testKitHome.absolutePath) } } } @@ -136,6 +142,36 @@ private const val AGENT_PROPERTY: String = private const val EXEC_DIR_PROPERTY: String = "io.spine.tools.gradle.testkit.coverage.execDir" +/** + * The name of the system property carrying the absolute path to the TestKit + * directory shared by the worker builds of all modules. + * + * A `plugin-testlib` version that predates the property ignores it and keeps a + * TestKit directory per module. + * + * The constant is duplicated in `io.spine.tools.gradle.testing.TestKitCoverage` + * of the `plugin-testlib` module. Keep the two values in sync. + */ +private const val TESTKIT_HOME_PROPERTY: String = + "io.spine.tools.gradle.testkit.coverage.home" + +/** + * The path, relative to the Gradle user home of the build, of the TestKit + * directory shared by the worker builds of all modules. + * + * A TestKit directory is the Gradle user home of the worker builds. An empty one + * makes the first worker build of each test JVM download its dependencies and + * generate the Gradle API JARs, which is slow on a CI runner. The directory is + * placed under `caches/`, which `gradle/actions/setup-gradle` saves and restores + * by default, unlike the rest of the Gradle user home. So it is kept between CI + * runs, and a pull request run starts with a warm one. Locally, it is shared by + * the builds of every project that uses this setup. + * + * Unlike a directory under `build/`, it is not reset by `clean`. Delete it to make + * the worker builds start from an empty one. + */ +private const val SHARED_TESTKIT_HOME: String = "caches/spine-testkit" + /** * The name of the dedicated, resolvable configuration that holds the standalone * JaCoCo agent JAR ([Jacoco.agent]) attached to TestKit worker JVMs. diff --git a/config b/config index 5ddff232..4f80b399 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 5ddff23286f7f6353d4714b0399ab89109c56c79 +Subproject commit 4f80b399d33103039a10b56145cb8ffad8bd7a3d From d71f3300a74a47d5c7e6f19d7455baba3e63d700 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 16:58:42 +0000 Subject: [PATCH 05/15] Make the TestKit coverage property names public Move the three system-property names into `public object TestKitCoverage`, so that the spec and the `GradleProject` KDoc refer to them instead of repeating the strings. Co-Authored-By: Claude Opus 5.5 --- .../tools/gradle/testing/GradleProject.kt | 30 ++--- .../tools/gradle/testing/TestKitCoverage.kt | 115 ++++++++++-------- .../gradle/testing/TestKitCoverageSpec.kt | 22 ++-- 3 files changed, 85 insertions(+), 82 deletions(-) diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt index f69a0df1..239ce198 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt @@ -40,12 +40,11 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * of this class as well as by [runGradleBuild] — but only when the test JVM is started * with two system properties, which the consuming build is responsible for setting: * - * - `io.spine.tools.gradle.testkit.coverage.agent` — the absolute path to the - * standalone JaCoCo agent JAR, that is, the artifact - * `org.jacoco:org.jacoco.agent::runtime`. - * - `io.spine.tools.gradle.testkit.coverage.execDir` — the directory into which the - * worker should write its execution data. The harness creates a single - * per-module `testkit.exec` file inside it. + * - [TestKitCoverage.AGENT_PROPERTY] — the absolute path to the standalone JaCoCo + * agent JAR, that is, the artifact `org.jacoco:org.jacoco.agent::runtime`. + * - [TestKitCoverage.EXEC_DIR_PROPERTY] — the directory into which the worker should + * write its execution data. The harness creates a single per-module `testkit.exec` + * file inside it. * * When either of these properties is absent, the harness is a no-op, so ordinary * runs and consumers that do not opt in are unaffected. A build started from @@ -53,18 +52,19 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * * A third property is optional: * - * - `io.spine.tools.gradle.testkit.coverage.home` — the TestKit directory (the - * Gradle user home of the worker builds) to share among the test JVMs of all - * modules. Sharing it means that the dependencies of the worker builds are - * downloaded, and the Gradle API JARs generated, once rather than once per - * module, and a CI cache can keep the directory between runs. Without the - * property, each module gets a directory of its own. A project set up with - * [GradleProjectSetup.withSharedTestKitDirectory] keeps using that directory - * instead. + * - [TestKitCoverage.TESTKIT_HOME_PROPERTY] — the TestKit directory (the Gradle user + * home of the worker builds) to share among the test JVMs of all modules. Sharing + * it means that the dependencies of the worker builds are downloaded, and the + * Gradle API JARs generated, once rather than once per module, and a CI cache can + * keep the directory between runs. Without the property, each module gets a + * directory of its own. A project set up with + * [GradleProjectSetup.withSharedTestKitDirectory] keeps using the directory it chose. * * The snippet below is a self-contained Gradle (Kotlin DSL) setup that a module * running TestKit-based tests can copy into its `build.gradle.kts`. Pin the agent - * to the same JaCoCo version your coverage tooling uses. + * to the same JaCoCo version your coverage tooling uses. The snippet spells out the + * property names held by [TestKitCoverage], because a build script does not see + * the classes of this library. * * ```kotlin * import java.util.concurrent.atomic.AtomicBoolean diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt index 7c754752..a4a4cd4e 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt @@ -17,6 +17,55 @@ package io.spine.tools.gradle.testing import java.io.File import org.gradle.testkit.runner.GradleRunner +/** + * The names of the system properties with which the consuming build enables + * the coverage of Gradle TestKit worker JVMs. + * + * The consuming build sets them on the test JVM, as described in [GradleProject]. + * + * The property names are part of the contract with consuming builds and must not + * change: a build sets the properties by name, and is versioned independently of + * this library. + */ +public object TestKitCoverage { + + // The `io.spine.gradle.testing.enableTestKitCoverage` extension in `config`'s + // `buildSrc` sets these properties. It repeats the names, because it does not + // depend on this library. + + /** + * The name of the system property carrying the absolute path to the JaCoCo + * agent JAR to attach to a Gradle TestKit worker JVM. The JAR is the artifact + * `org.jacoco:org.jacoco.agent::runtime`. + */ + public const val AGENT_PROPERTY: String = + "io.spine.tools.gradle.testkit.coverage.agent" + + /** + * The name of the system property carrying the absolute path to the directory + * where the TestKit worker should write its JaCoCo execution data. + */ + public const val EXEC_DIR_PROPERTY: String = + "io.spine.tools.gradle.testkit.coverage.execDir" + + /** + * The name of the system property carrying the absolute path to the TestKit + * directory shared by the coverage-enabled worker builds of all modules. + * + * Optional: when it is absent, each module gets a TestKit directory of its own. + * With a shared directory, the dependencies of the worker builds are downloaded, + * and the Gradle API JARs generated, once rather than once per module, and a CI + * cache can keep the directory between runs. Sharing is safe because the + * directory holds nothing specific to a module: the agent is passed on the + * command line, and the workers of each module, started with their own JVM + * arguments, get daemons of their own. + * + * The property takes effect only together with [AGENT_PROPERTY] and [EXEC_DIR_PROPERTY]. + */ + public const val TESTKIT_HOME_PROPERTY: String = + "io.spine.tools.gradle.testkit.coverage.home" +} + /** * Points this runner at the TestKit directory (the Gradle user home of the worker * build) used for collecting the coverage of the worker JVM it spawns. @@ -25,9 +74,10 @@ import org.gradle.testkit.runner.GradleRunner * and that one adds the argument that attaches the JaCoCo agent to the worker JVM, * so that plugin code executed out-of-process via TestKit is credited to coverage. * - * Does nothing unless the test JVM was started with the [AGENT_PROPERTY] and - * [EXEC_DIR_PROPERTY] system properties. This keeps the harness a no-op for - * ordinary runs and for consumers that do not opt in. + * Does nothing unless the test JVM was started with the + * [TestKitCoverage.AGENT_PROPERTY] and [TestKitCoverage.EXEC_DIR_PROPERTY] system + * properties. This keeps the harness a no-op for ordinary runs and for consumers + * that do not opt in. * * Supplying those properties on the test JVM is the responsibility of the * consuming build. Consumer-facing setup instructions, including a copy-paste @@ -50,14 +100,14 @@ internal fun GradleRunner.enableTestKitCoverage(preferredTestKitDir: File? = nul * In the order of precedence, the directory is: * * 1. The [preferred] one, which the caller already intends to use. - * 2. The one [shared by the worker builds of all modules][TESTKIT_HOME_PROPERTY], + * 2. The one [shared by the worker builds of all modules][TestKitCoverage.TESTKIT_HOME_PROPERTY], * when the consuming build names it. * 3. A per-module [TESTKIT_HOME_DIR] next to the execution-data directory. */ internal fun coverageTestKitDir(preferred: File? = null): File? { val coverage = CoverageSetup.fromSystemProperties() ?: return null return preferred - ?: System.getProperty(TESTKIT_HOME_PROPERTY)?.let(::File) + ?: System.getProperty(TestKitCoverage.TESTKIT_HOME_PROPERTY)?.let(::File) ?: File(coverage.execDir.parentFile, TESTKIT_HOME_DIR) } @@ -101,8 +151,8 @@ private class CoverageSetup(val agentJar: File, val execDir: File) { * or returns `null` when TestKit coverage is not enabled for it. */ fun fromSystemProperties(): CoverageSetup? { - val agentJar = System.getProperty(AGENT_PROPERTY) - val execDir = System.getProperty(EXEC_DIR_PROPERTY) + val agentJar = System.getProperty(TestKitCoverage.AGENT_PROPERTY) + val execDir = System.getProperty(TestKitCoverage.EXEC_DIR_PROPERTY) if (agentJar == null || execDir == null) { return null } @@ -111,30 +161,6 @@ private class CoverageSetup(val agentJar: File, val execDir: File) { } } -/** - * The name of the system property carrying the absolute path to the JaCoCo - * agent JAR to attach to a Gradle TestKit worker JVM. - * - * The consuming build is expected to set this property on the test JVM. Within - * the SpineEventEngine organisation it is set by the - * `io.spine.gradle.testing.enableTestKitCoverage` extension in `config`'s - * `buildSrc`, where the constant is duplicated (the `buildSrc` and the production - * source sets cannot share code). Keep the two values in sync. - */ -private const val AGENT_PROPERTY: String = - "io.spine.tools.gradle.testkit.coverage.agent" - -/** - * The name of the system property carrying the absolute path to the directory - * where the TestKit worker should write its JaCoCo execution data. - * - * Set by the consuming build on the test JVM. Within the SpineEventEngine - * organisation this is done by the `io.spine.gradle.testing.enableTestKitCoverage` - * extension in `config`'s `buildSrc`; keep the two values in sync. - */ -private const val EXEC_DIR_PROPERTY: String = - "io.spine.tools.gradle.testkit.coverage.execDir" - /** * The name of the JaCoCo execution-data file written by TestKit workers. * @@ -146,29 +172,10 @@ private const val EXEC_DIR_PROPERTY: String = private const val EXEC_FILE: String = "testkit.exec" /** - * The name of the system property carrying the absolute path to the TestKit - * directory shared by the coverage-enabled worker builds of all modules. - * - * Optional: when it is absent, each module gets its own [TESTKIT_HOME_DIR]. With a - * shared directory, the dependencies of the worker builds are downloaded, and the - * Gradle API JARs generated, once rather than once per module, and a CI cache can - * keep the directory between runs. Sharing is safe because the directory holds - * nothing specific to a module: [withTestKitCoverage] passes the agent on the - * command line, and the workers of each module, started with their own JVM - * arguments, get daemons of their own. - * - * The property takes effect only together with [AGENT_PROPERTY] and [EXEC_DIR_PROPERTY]. - * - * Set by the `io.spine.gradle.testing.enableTestKitCoverage` extension in - * `config`'s `buildSrc`; keep the two values in sync. - */ -private const val TESTKIT_HOME_PROPERTY: String = - "io.spine.tools.gradle.testkit.coverage.home" - -/** - * The name of the directory, placed next to the [execution-data directory][EXEC_DIR_PROPERTY], - * that serves as the TestKit directory of the coverage-enabled worker builds when no - * [shared one][TESTKIT_HOME_PROPERTY] is set. + * The name of the directory, placed next to the + * [execution-data directory][TestKitCoverage.EXEC_DIR_PROPERTY], that serves as the + * TestKit directory of the coverage-enabled worker builds when no + * [shared one][TestKitCoverage.TESTKIT_HOME_PROPERTY] is set. * * Reusing one directory for all the test cases of a module lets a single TestKit * daemon serve them. diff --git a/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/TestKitCoverageSpec.kt b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/TestKitCoverageSpec.kt index 4dfd6713..bdad86f0 100644 --- a/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/TestKitCoverageSpec.kt +++ b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/TestKitCoverageSpec.kt @@ -16,6 +16,9 @@ package io.spine.tools.gradle.testing import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.shouldBe +import io.spine.tools.gradle.testing.TestKitCoverage.AGENT_PROPERTY +import io.spine.tools.gradle.testing.TestKitCoverage.EXEC_DIR_PROPERTY +import io.spine.tools.gradle.testing.TestKitCoverage.TESTKIT_HOME_PROPERTY import java.io.File import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -24,10 +27,6 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir -/** - * The names of the system properties are spelled out rather than taken from - * `TestKitCoverage.kt`, so that the spec pins the contract with the consuming builds. - */ @DisplayName("TestKit coverage harness should") internal class TestKitCoverageSpec { @@ -62,7 +61,7 @@ internal class TestKitCoverageSpec { @Test fun `stay disabled when only the agent property is set`(@TempDir dir: File) { - System.setProperty(AGENT, dir.resolve("agent.jar").path) + System.setProperty(AGENT_PROPERTY, dir.resolve("agent.jar").path) val args = listOf("build") args.withTestKitCoverage() shouldBe args @@ -80,8 +79,8 @@ internal class TestKitCoverageSpec { @BeforeEach fun enableCoverage() { - System.setProperty(AGENT, agentJar.path) - System.setProperty(EXEC_DIR, execDir.path) + System.setProperty(AGENT_PROPERTY, agentJar.path) + System.setProperty(EXEC_DIR_PROPERTY, execDir.path) } @Test @@ -106,14 +105,14 @@ internal class TestKitCoverageSpec { @Test fun `use the shared TestKit directory when the build names one`() { val shared = dir.resolve("gradle-home/caches/spine-testkit") - System.setProperty(HOME, shared.path) + System.setProperty(TESTKIT_HOME_PROPERTY, shared.path) coverageTestKitDir() shouldBe shared } @Test fun `prefer the TestKit directory chosen by the caller`() { - System.setProperty(HOME, dir.resolve("shared").path) + System.setProperty(TESTKIT_HOME_PROPERTY, dir.resolve("shared").path) val preferred = dir.resolve("preferred") coverageTestKitDir(preferred) shouldBe preferred @@ -121,7 +120,4 @@ internal class TestKitCoverageSpec { } } -private const val AGENT = "io.spine.tools.gradle.testkit.coverage.agent" -private const val EXEC_DIR = "io.spine.tools.gradle.testkit.coverage.execDir" -private const val HOME = "io.spine.tools.gradle.testkit.coverage.home" -private val COVERAGE_PROPERTIES = listOf(AGENT, EXEC_DIR, HOME) +private val COVERAGE_PROPERTIES = listOf(AGENT_PROPERTY, EXEC_DIR_PROPERTY, TESTKIT_HOME_PROPERTY) From 90857ee6c7e325b31f8c1af8a010b185a0bbca63 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 17:04:31 +0000 Subject: [PATCH 06/15] Build the TestKit coverage property names from a shared prefix Co-Authored-By: Claude Opus 5.5 --- .../spine/tools/gradle/testing/TestKitCoverage.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt index a4a4cd4e..08a0067d 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt @@ -33,20 +33,23 @@ public object TestKitCoverage { // `buildSrc` sets these properties. It repeats the names, because it does not // depend on this library. + /** + * The prefix shared by the names of the properties. + */ + private const val PREFIX: String = "io.spine.tools.gradle.testkit.coverage" + /** * The name of the system property carrying the absolute path to the JaCoCo * agent JAR to attach to a Gradle TestKit worker JVM. The JAR is the artifact * `org.jacoco:org.jacoco.agent::runtime`. */ - public const val AGENT_PROPERTY: String = - "io.spine.tools.gradle.testkit.coverage.agent" + public const val AGENT_PROPERTY: String = "$PREFIX.agent" /** * The name of the system property carrying the absolute path to the directory * where the TestKit worker should write its JaCoCo execution data. */ - public const val EXEC_DIR_PROPERTY: String = - "io.spine.tools.gradle.testkit.coverage.execDir" + public const val EXEC_DIR_PROPERTY: String = "$PREFIX.execDir" /** * The name of the system property carrying the absolute path to the TestKit @@ -62,8 +65,7 @@ public object TestKitCoverage { * * The property takes effect only together with [AGENT_PROPERTY] and [EXEC_DIR_PROPERTY]. */ - public const val TESTKIT_HOME_PROPERTY: String = - "io.spine.tools.gradle.testkit.coverage.home" + public const val TESTKIT_HOME_PROPERTY: String = "$PREFIX.home" } /** From 233a4c5f40152f459117d1a76d1de97ab57a77fc Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 17:39:03 +0000 Subject: [PATCH 07/15] Say what a shared TestKit home saves The worker builds generate no Gradle API JARs and download few files. Their Gradle user home mostly holds the instrumented JARs of the plugin classpath. Co-Authored-By: Claude Opus 5.5 --- .../io/spine/tools/gradle/testing/GradleProject.kt | 8 ++++---- .../io/spine/tools/gradle/testing/TestKitCoverage.kt | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt index 239ce198..a547f8d0 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt @@ -54,10 +54,10 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * * - [TestKitCoverage.TESTKIT_HOME_PROPERTY] — the TestKit directory (the Gradle user * home of the worker builds) to share among the test JVMs of all modules. Sharing - * it means that the dependencies of the worker builds are downloaded, and the - * Gradle API JARs generated, once rather than once per module, and a CI cache can - * keep the directory between runs. Without the property, each module gets a - * directory of its own. A project set up with + * it means that the work the worker builds keep there, chiefly instrumenting the + * JARs on the classpath of the plugin under test, is done once rather than once + * per module, and a CI cache can keep its results between runs. Without the + * property, each module gets a directory of its own. A project set up with * [GradleProjectSetup.withSharedTestKitDirectory] keeps using the directory it chose. * * The snippet below is a self-contained Gradle (Kotlin DSL) setup that a module diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt index 08a0067d..239340f2 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt @@ -56,12 +56,12 @@ public object TestKitCoverage { * directory shared by the coverage-enabled worker builds of all modules. * * Optional: when it is absent, each module gets a TestKit directory of its own. - * With a shared directory, the dependencies of the worker builds are downloaded, - * and the Gradle API JARs generated, once rather than once per module, and a CI - * cache can keep the directory between runs. Sharing is safe because the - * directory holds nothing specific to a module: the agent is passed on the - * command line, and the workers of each module, started with their own JVM - * arguments, get daemons of their own. + * With a shared directory, the work the worker builds keep in their Gradle user + * home, chiefly instrumenting the JARs on the classpath of the plugin under test, + * is done once rather than once per module, and a CI cache can keep its results + * between runs. Sharing is safe because the directory holds nothing specific to + * a module: the agent is passed on the command line, and the workers of each + * module, started with their own JVM arguments, get daemons of their own. * * The property takes effect only together with [AGENT_PROPERTY] and [EXEC_DIR_PROPERTY]. */ From a4d6fa1671c61939ff05c7dd02641a3d36d8d63b Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 17:45:32 +0000 Subject: [PATCH 08/15] Say it's Gradle's instrumentation the TestKit home keeps Co-Authored-By: Claude Opus 5.5 --- .../io/spine/tools/gradle/testing/GradleProject.kt | 8 ++++---- .../io/spine/tools/gradle/testing/TestKitCoverage.kt | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt index a547f8d0..d86e442d 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt @@ -54,10 +54,10 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * * - [TestKitCoverage.TESTKIT_HOME_PROPERTY] — the TestKit directory (the Gradle user * home of the worker builds) to share among the test JVMs of all modules. Sharing - * it means that the work the worker builds keep there, chiefly instrumenting the - * JARs on the classpath of the plugin under test, is done once rather than once - * per module, and a CI cache can keep its results between runs. Without the - * property, each module gets a directory of its own. A project set up with + * it means that the worker builds create its contents, mostly Gradle's + * instrumented copies of the JARs on the classpath of the plugin under test, once + * rather than once per module, and a CI cache can keep them between runs. Without + * the property, each module gets a directory of its own. A project set up with * [GradleProjectSetup.withSharedTestKitDirectory] keeps using the directory it chose. * * The snippet below is a self-contained Gradle (Kotlin DSL) setup that a module diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt index 239340f2..41ac3000 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt @@ -56,12 +56,12 @@ public object TestKitCoverage { * directory shared by the coverage-enabled worker builds of all modules. * * Optional: when it is absent, each module gets a TestKit directory of its own. - * With a shared directory, the work the worker builds keep in their Gradle user - * home, chiefly instrumenting the JARs on the classpath of the plugin under test, - * is done once rather than once per module, and a CI cache can keep its results - * between runs. Sharing is safe because the directory holds nothing specific to - * a module: the agent is passed on the command line, and the workers of each - * module, started with their own JVM arguments, get daemons of their own. + * With a shared directory, the worker builds create the contents of their Gradle + * user home, mostly Gradle's instrumented copies of the JARs on the classpath of + * the plugin under test, once rather than once per module, and a CI cache can + * keep them between runs. Sharing is safe because the directory holds nothing + * specific to a module: the agent is passed on the command line, and the workers + * of each module, started with their own JVM arguments, get daemons of their own. * * The property takes effect only together with [AGENT_PROPERTY] and [EXEC_DIR_PROPERTY]. */ From 4c7ec608788f570446c9e6b3ddbcd97dd2faef1d Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 18:58:39 +0000 Subject: [PATCH 09/15] Leave the choice of the TestKit directory to TestKit Remove `TESTKIT_HOME_PROPERTY` and the per-module `testkit-home` fallback. With the agent on the command line, the coverage harness needs no TestKit directory of its own. TestKit takes the one named by Gradle's `org.gradle.testkit.dir` system property, or a folder under the `Test` task's temporary directory, and setting that property is up to the consuming build, apart from enabling coverage. Co-Authored-By: Claude Opus 5.5 --- .../io/spine/tools/gradle/testing/Gradle.kt | 1 - .../tools/gradle/testing/GradleProject.kt | 33 +++----- .../tools/gradle/testing/TestKitCoverage.kt | 83 ++----------------- .../gradle/testing/TestKitCoverageSpec.kt | 27 +----- 4 files changed, 21 insertions(+), 123 deletions(-) diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/Gradle.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/Gradle.kt index e0526d33..9f09c747 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/Gradle.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/Gradle.kt @@ -83,7 +83,6 @@ public fun runGradleBuild( .withProjectDir(projectDir) .withPluginClasspath() .withArguments(arguments.withTestKitCoverage()) - runner.enableTestKitCoverage() stdOutput?.let { runner.forwardStdOutput(it) } diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt index d86e442d..77b46fc0 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt @@ -50,16 +50,6 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * runs and consumers that do not opt in are unaffected. A build started from * [runner] directly gets no agent. * - * A third property is optional: - * - * - [TestKitCoverage.TESTKIT_HOME_PROPERTY] — the TestKit directory (the Gradle user - * home of the worker builds) to share among the test JVMs of all modules. Sharing - * it means that the worker builds create its contents, mostly Gradle's - * instrumented copies of the JARs on the classpath of the plugin under test, once - * rather than once per module, and a CI cache can keep them between runs. Without - * the property, each module gets a directory of its own. A project set up with - * [GradleProjectSetup.withSharedTestKitDirectory] keeps using the directory it chose. - * * The snippet below is a self-contained Gradle (Kotlin DSL) setup that a module * running TestKit-based tests can copy into its `build.gradle.kts`. Pin the agent * to the same JaCoCo version your coverage tooling uses. The snippet spells out the @@ -80,9 +70,6 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * * val agentJar = testKitJacocoAgent.elements.map { it.single().asFile.absolutePath } * val execDir = layout.buildDirectory.dir("jacoco-testkit") - * // Optional: a TestKit directory shared by all modules. Under `caches/`, it is - * // kept by CI caches of the Gradle user home. - * val testKitHome = gradle.gradleUserHomeDir.resolve("caches/spine-testkit") * * // Wipe the exec directory at most once per build, from the first Test task * // that actually executes. The workers append to a single per-module exec @@ -105,7 +92,6 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * dir.mkdirs() * systemProperty("io.spine.tools.gradle.testkit.coverage.agent", agentJar.get()) * systemProperty("io.spine.tools.gradle.testkit.coverage.execDir", dir.absolutePath) - * systemProperty("io.spine.tools.gradle.testkit.coverage.home", testKitHome.absolutePath) * } * } * ``` @@ -120,6 +106,16 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * `config`'s `buildSrc`, and the produced files are merged into Kover reports by * `KoverConfig`. Consumers outside that setup can use the snippet above instead. * + * ## TestKit directory + * + * The TestKit directory is the Gradle user home of the worker builds. Builds started + * by this class and by [runGradleBuild] use the one TestKit picks, unless the project + * is set up with [GradleProjectSetup.withSharedTestKitDirectory]. TestKit takes the + * directory named by Gradle's `org.gradle.testkit.dir` system property of the test + * JVM, or makes one under the temporary directory of the `Test` task otherwise. To + * let the test JVMs of all modules share one directory, set that property on their + * `Test` tasks. This choice is independent of the coverage setup above. + * * @see GradleProjectSetup */ public class GradleProject internal constructor(setup: GradleProjectSetup) { @@ -151,15 +147,10 @@ public class GradleProject internal constructor(setup: GradleProjectSetup) { val runner = GradleRunner.create() .withProjectDir(setup.projectDir) .withDebug(setup.debug) - val sharedTestKitDir = if (setup.useSharedTestKit) { - RootProject.testKitTempDir().toFile().apply { mkdirs() } - } else { - null - } - if (sharedTestKitDir != null) { + if (setup.useSharedTestKit) { + val sharedTestKitDir = RootProject.testKitTempDir().toFile().apply { mkdirs() } runner.withTestKitDir(sharedTestKitDir) } - runner.enableTestKitCoverage(sharedTestKitDir) runner } diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt index 41ac3000..95f1c355 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/TestKitCoverage.kt @@ -15,7 +15,6 @@ package io.spine.tools.gradle.testing import java.io.File -import org.gradle.testkit.runner.GradleRunner /** * The names of the system properties with which the consuming build enables @@ -50,67 +49,6 @@ public object TestKitCoverage { * where the TestKit worker should write its JaCoCo execution data. */ public const val EXEC_DIR_PROPERTY: String = "$PREFIX.execDir" - - /** - * The name of the system property carrying the absolute path to the TestKit - * directory shared by the coverage-enabled worker builds of all modules. - * - * Optional: when it is absent, each module gets a TestKit directory of its own. - * With a shared directory, the worker builds create the contents of their Gradle - * user home, mostly Gradle's instrumented copies of the JARs on the classpath of - * the plugin under test, once rather than once per module, and a CI cache can - * keep them between runs. Sharing is safe because the directory holds nothing - * specific to a module: the agent is passed on the command line, and the workers - * of each module, started with their own JVM arguments, get daemons of their own. - * - * The property takes effect only together with [AGENT_PROPERTY] and [EXEC_DIR_PROPERTY]. - */ - public const val TESTKIT_HOME_PROPERTY: String = "$PREFIX.home" -} - -/** - * Points this runner at the TestKit directory (the Gradle user home of the worker - * build) used for collecting the coverage of the worker JVM it spawns. - * - * Use it together with [withTestKitCoverage]: this function selects the directory, - * and that one adds the argument that attaches the JaCoCo agent to the worker JVM, - * so that plugin code executed out-of-process via TestKit is credited to coverage. - * - * Does nothing unless the test JVM was started with the - * [TestKitCoverage.AGENT_PROPERTY] and [TestKitCoverage.EXEC_DIR_PROPERTY] system - * properties. This keeps the harness a no-op for ordinary runs and for consumers - * that do not opt in. - * - * Supplying those properties on the test JVM is the responsibility of the - * consuming build. Consumer-facing setup instructions, including a copy-paste - * Gradle recipe, live in the [GradleProject] KDoc (this function is `internal`, - * so its own KDoc is not part of the published API documentation). - * - * @param preferredTestKitDir The TestKit directory the caller already intends to - * use (e.g. the one of [GradleProjectSetup.withSharedTestKitDirectory]), or - * `null` to let [coverageTestKitDir] pick one. - */ -internal fun GradleRunner.enableTestKitCoverage(preferredTestKitDir: File? = null) { - val testKitDir = coverageTestKitDir(preferredTestKitDir) ?: return - withTestKitDir(testKitDir.apply { mkdirs() }) -} - -/** - * Returns the TestKit directory for the coverage-enabled worker builds, or `null` - * when TestKit coverage is not enabled for this test JVM. - * - * In the order of precedence, the directory is: - * - * 1. The [preferred] one, which the caller already intends to use. - * 2. The one [shared by the worker builds of all modules][TestKitCoverage.TESTKIT_HOME_PROPERTY], - * when the consuming build names it. - * 3. A per-module [TESTKIT_HOME_DIR] next to the execution-data directory. - */ -internal fun coverageTestKitDir(preferred: File? = null): File? { - val coverage = CoverageSetup.fromSystemProperties() ?: return null - return preferred - ?: System.getProperty(TestKitCoverage.TESTKIT_HOME_PROPERTY)?.let(::File) - ?: File(coverage.execDir.parentFile, TESTKIT_HOME_DIR) } /** @@ -118,13 +56,19 @@ internal fun coverageTestKitDir(preferred: File? = null): File? { * the JaCoCo agent to the worker JVM appended, or the same arguments when TestKit * coverage is not enabled. * + * TestKit coverage is enabled when the test JVM was started with the + * [TestKitCoverage.AGENT_PROPERTY] and [TestKitCoverage.EXEC_DIR_PROPERTY] system + * properties. Without them, the harness is a no-op for ordinary runs and for + * consumers that do not opt in. Supplying the properties is the responsibility of + * the consuming build; the setup instructions live in the [GradleProject] KDoc. + * * The agent appends to a single per-module [execution-data file][EXEC_FILE]; on * worker daemon shutdown — which happens after the tests complete and before the * coverage report task runs — the data is flushed to disk. * * The agent is passed on the command line rather than in a `gradle.properties` file - * in the TestKit directory, so that the directory carries nothing specific to a - * module and can be shared. The argument replaces the `org.gradle.jvmargs` the + * in the TestKit directory, so that coverage does not depend on which TestKit + * directory the runner uses. The argument replaces the `org.gradle.jvmargs` the * worker build would use otherwise, such as the one of the `gradle.properties` of * the test project, or one passed earlier in these arguments. */ @@ -172,14 +116,3 @@ private class CoverageSetup(val agentJar: File, val execDir: File) { * into the Kover reports. */ private const val EXEC_FILE: String = "testkit.exec" - -/** - * The name of the directory, placed next to the - * [execution-data directory][TestKitCoverage.EXEC_DIR_PROPERTY], that serves as the - * TestKit directory of the coverage-enabled worker builds when no - * [shared one][TestKitCoverage.TESTKIT_HOME_PROPERTY] is set. - * - * Reusing one directory for all the test cases of a module lets a single TestKit - * daemon serve them. - */ -private const val TESTKIT_HOME_DIR: String = "testkit-home" diff --git a/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/TestKitCoverageSpec.kt b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/TestKitCoverageSpec.kt index bdad86f0..fa9402d8 100644 --- a/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/TestKitCoverageSpec.kt +++ b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/TestKitCoverageSpec.kt @@ -14,11 +14,9 @@ package io.spine.tools.gradle.testing -import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.shouldBe import io.spine.tools.gradle.testing.TestKitCoverage.AGENT_PROPERTY import io.spine.tools.gradle.testing.TestKitCoverage.EXEC_DIR_PROPERTY -import io.spine.tools.gradle.testing.TestKitCoverage.TESTKIT_HOME_PROPERTY import java.io.File import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -56,7 +54,6 @@ internal class TestKitCoverageSpec { val args = listOf("build") args.withTestKitCoverage() shouldBe args - coverageTestKitDir().shouldBeNull() } @Test @@ -65,7 +62,6 @@ internal class TestKitCoverageSpec { val args = listOf("build") args.withTestKitCoverage() shouldBe args - coverageTestKitDir().shouldBeNull() } @Nested @@ -96,28 +92,7 @@ internal class TestKitCoverageSpec { "append=true,dumponexit=true,output=file,jmx=false" ) } - - @Test - fun `use a TestKit directory per module by default`() { - coverageTestKitDir() shouldBe dir.resolve("build/testkit-home") - } - - @Test - fun `use the shared TestKit directory when the build names one`() { - val shared = dir.resolve("gradle-home/caches/spine-testkit") - System.setProperty(TESTKIT_HOME_PROPERTY, shared.path) - - coverageTestKitDir() shouldBe shared - } - - @Test - fun `prefer the TestKit directory chosen by the caller`() { - System.setProperty(TESTKIT_HOME_PROPERTY, dir.resolve("shared").path) - val preferred = dir.resolve("preferred") - - coverageTestKitDir(preferred) shouldBe preferred - } } } -private val COVERAGE_PROPERTIES = listOf(AGENT_PROPERTY, EXEC_DIR_PROPERTY, TESTKIT_HOME_PROPERTY) +private val COVERAGE_PROPERTIES = listOf(AGENT_PROPERTY, EXEC_DIR_PROPERTY) From 3b6d7cb8440907ea2429227d748466adc295c72f Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 21:30:30 +0000 Subject: [PATCH 10/15] Update `config` (./config/pull) Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 2 +- .../io/spine/dependency/local/Compiler.kt | 4 +- .../io/spine/dependency/local/CoreJvm.kt | 2 +- .../spine/dependency/local/CoreJvmCompiler.kt | 4 +- .../io/spine/dependency/local/ProtoTap.kt | 2 +- .../io/spine/dependency/local/ToolBase.kt | 4 +- .../io/spine/dependency/local/Validation.kt | 2 +- .../spine/gradle/testing/TestKitCoverage.kt | 49 ++--------- .../io/spine/gradle/testing/TestKitDir.kt | 84 +++++++++++++++++++ .../src/main/kotlin/kmp-module.gradle.kts | 7 ++ .../src/main/kotlin/module-testing.gradle.kts | 5 ++ config | 2 +- 12 files changed, 116 insertions(+), 51 deletions(-) create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitDir.kt diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 182bfa4c..962ebc7c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,7 +30,7 @@ on: jobs: publish: name: Publish to Maven repositories - uses: SpineEventEngine/config/.github/workflows/publishing.yml@4f80b399d33103039a10b56145cb8ffad8bd7a3d + uses: SpineEventEngine/config/.github/workflows/publishing.yml@3cc29d4e51eb6de7da4234d2677973cead2cea84 permissions: contents: read packages: write diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt index b75c0f1e..385a1217 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt @@ -60,7 +60,7 @@ object Compiler : Dependency() { * The version of the Compiler dependencies. */ override val version: String - private const val fallbackVersion = "2.0.0-SNAPSHOT.081" + private const val fallbackVersion = "2.0.0-SNAPSHOT.083" /** * The distinct version of the Compiler used by other build tools. @@ -69,7 +69,7 @@ object Compiler : Dependency() { * transitive dependencies, this is the version used to build the project itself. */ val dogfoodingVersion: String - private const val fallbackDfVersion = "2.0.0-SNAPSHOT.081" + private const val fallbackDfVersion = "2.0.0-SNAPSHOT.083" /** * The artifact for the Compiler Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt index 94f3d9d6..597049f4 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt @@ -30,7 +30,7 @@ typealias CoreJava = CoreJvm ) object CoreJvm { const val group = Spine.group - const val version = "2.0.0-SNAPSHOT.577" + const val version = "2.0.0-SNAPSHOT.611" const val coreArtifact = "spine-core" const val clientArtifact = "spine-client" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt index 8fc131ea..ef58eb0a 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt @@ -34,12 +34,12 @@ object CoreJvmCompiler { /** * The version used in the build classpath. */ - const val dogfoodingVersion = "2.0.0-SNAPSHOT.099" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.104" /** * The version to be used for integration tests. */ - const val version = "2.0.0-SNAPSHOT.099" + const val version = "2.0.0-SNAPSHOT.104" /** * The ID of the Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoTap.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoTap.kt index b9e6f18e..a7b277c2 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoTap.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoTap.kt @@ -26,7 +26,7 @@ package io.spine.dependency.local ) object ProtoTap { const val group = Spine.toolsGroup - const val version = "0.17.4" + const val version = "0.17.5" const val gradlePluginId = "io.spine.prototap" const val api = "$group:prototap-api:$version" const val gradlePlugin = "$group:prototap-gradle-plugin:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt index 7bd22c47..52b0f2a5 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt @@ -32,8 +32,8 @@ package io.spine.dependency.local ) object ToolBase { const val group = Spine.toolsGroup - const val version = "2.0.0-SNAPSHOT.428" - const val dogfoodingVersion = "2.0.0-SNAPSHOT.428" + const val version = "2.0.0-SNAPSHOT.451" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.451" const val archive = "$group:archive:$version" const val code = "$group:code:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt index 1be9aff4..1b4d5cf4 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt @@ -27,7 +27,7 @@ object Validation { /** * The version of the Validation library artifacts. */ - const val version = "2.0.0-SNAPSHOT.469" + const val version = "2.0.0-SNAPSHOT.470" const val group = Spine.toolsGroup private const val prefix = "validation" diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt b/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt index 59ff9d6f..96c0753e 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt @@ -33,12 +33,10 @@ import org.gradle.kotlin.dsl.withType * * 1. Resolves the standalone JaCoCo agent JAR ([Jacoco.agent]) through a * dedicated [AGENT_CONFIGURATION] configuration. - * 2. Passes the agent JAR path, a per-module exec directory - * (`build/`[TESTKIT_COVERAGE_DIR]), and a [TestKit directory][SHARED_TESTKIT_HOME] - * shared by all modules to the test JVM as system properties. The - * `plugin-testlib` harness reads these and attaches `-javaagent:…` to the - * worker JVM. A harness version that supports the shared directory points its - * runners at it; an older one keeps a TestKit directory per module. + * 2. Passes the agent JAR path and a per-module exec directory + * (`build/`[TESTKIT_COVERAGE_DIR]) to the test JVM as system properties. + * The `plugin-testlib` harness reads these and attaches `-javaagent:…` to the + * worker JVM. * 3. Wipes the exec directory at most once per build invocation, from the * `doFirst` of the first `Test` task that actually executes, so stale worker * coverage from a previous run does not accumulate. Two failure modes are @@ -60,6 +58,11 @@ import org.gradle.kotlin.dsl.withType * coverage. An up-to-date (non-cache) run is unaffected: the previous run's * files remain on disk and the guarded `doFirst` never deletes them. * + * The method does not choose the TestKit directory of the worker builds. + * The `module-testing` and `kmp-module` script plugins point the `Test` tasks + * of a module at a shared one (see [useSharedTestKitDir]), which a harness + * that passes its own directory to `withTestKitDir(...)` does not use. + * * The produced `.exec` files are merged into the Kover reports by * [io.spine.gradle.report.coverage.KoverConfig]. The agent emits binary * execution data rather than an XML report because that is its only file output, @@ -78,9 +81,6 @@ fun Project.enableTestKitCoverage() { val agentPath = agent.elements.map { it.single().asFile.absolutePath } val execDir = layout.buildDirectory.dir(TESTKIT_COVERAGE_DIR) - // Not a task input: where the worker builds keep their caches does not - // affect the test results. - val testKitHome = gradle.gradleUserHomeDir.resolve(SHARED_TESTKIT_HOME) // Wiped at most once per build invocation, by the first `Test` task that // actually executes — see the KDoc above for why this is a guarded `doFirst` @@ -101,7 +101,6 @@ fun Project.enableTestKitCoverage() { dir.mkdirs() systemProperty(AGENT_PROPERTY, agentPath.get()) systemProperty(EXEC_DIR_PROPERTY, dir.absolutePath) - systemProperty(TESTKIT_HOME_PROPERTY, testKitHome.absolutePath) } } } @@ -142,36 +141,6 @@ private const val AGENT_PROPERTY: String = private const val EXEC_DIR_PROPERTY: String = "io.spine.tools.gradle.testkit.coverage.execDir" -/** - * The name of the system property carrying the absolute path to the TestKit - * directory shared by the worker builds of all modules. - * - * A `plugin-testlib` version that predates the property ignores it and keeps a - * TestKit directory per module. - * - * The constant is duplicated in `io.spine.tools.gradle.testing.TestKitCoverage` - * of the `plugin-testlib` module. Keep the two values in sync. - */ -private const val TESTKIT_HOME_PROPERTY: String = - "io.spine.tools.gradle.testkit.coverage.home" - -/** - * The path, relative to the Gradle user home of the build, of the TestKit - * directory shared by the worker builds of all modules. - * - * A TestKit directory is the Gradle user home of the worker builds. An empty one - * makes the first worker build of each test JVM download its dependencies and - * generate the Gradle API JARs, which is slow on a CI runner. The directory is - * placed under `caches/`, which `gradle/actions/setup-gradle` saves and restores - * by default, unlike the rest of the Gradle user home. So it is kept between CI - * runs, and a pull request run starts with a warm one. Locally, it is shared by - * the builds of every project that uses this setup. - * - * Unlike a directory under `build/`, it is not reset by `clean`. Delete it to make - * the worker builds start from an empty one. - */ -private const val SHARED_TESTKIT_HOME: String = "caches/spine-testkit" - /** * The name of the dedicated, resolvable configuration that holds the standalone * JaCoCo agent JAR ([Jacoco.agent]) attached to TestKit worker JVMs. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitDir.kt b/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitDir.kt new file mode 100644 index 00000000..d9c60ef9 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitDir.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle.testing + +import org.gradle.api.Project +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.withType + +/** + * Configures the `Test` tasks of this project so that the Gradle TestKit builds + * they run use the TestKit directory [shared by all modules][SHARED_TESTKIT_DIR]. + * + * A TestKit directory is the Gradle user home of the builds that + * [GradleRunner][org.gradle.testkit.runner.GradleRunner] runs. It holds mostly + * Gradle's instrumented copies of the JARs on the classpath of the plugin under + * test, and the compiled build scripts of the test projects. When the directory + * is empty, the TestKit builds recreate them, which is slow on a CI runner. + * + * Without the [`org.gradle.testkit.dir`][TESTKIT_DIR_PROPERTY] system property, + * TestKit keeps the directory at `build/tmp//work/.gradle-test-kit`, in + * the temporary directory of the `Test` task. So each `Test` task of each module + * has its own, which is empty in a fresh checkout and after `clean`. This + * function passes the shared directory to the test JVM as that property, which + * `GradleRunner.create()` reads. A runner given a directory with + * `withTestKitDir(...)` still uses that one. + * + * The property is set from a `doFirst` action rather than at configuration time, + * so it is not a task input. Where TestKit builds keep their caches does not + * affect the test results, while the absolute path differs between machines. + * As an input, it would only break the reuse of the `Test` task results from + * the build cache across machines. A `Test` task that already has the property + * keeps its value, so a repeated call has no further effect. + * + * The function applies to all the `Test` tasks of the project, whether they + * run TestKit builds or not, and whether the project collects the coverage of + * those builds or not (see [enableTestKitCoverage]). + */ +fun Project.useSharedTestKitDir() { + val testKitDir = gradle.gradleUserHomeDir.resolve(SHARED_TESTKIT_DIR) + tasks.withType().configureEach { + doFirst { + if (TESTKIT_DIR_PROPERTY !in systemProperties) { + systemProperty(TESTKIT_DIR_PROPERTY, testKitDir.absolutePath) + } + } + } +} + +/** + * The name of the system property carrying the absolute path to the TestKit + * directory, which `GradleRunner.create()` reads. + * + * A runner given a directory with `withTestKitDir(...)` uses that one instead. + * + * @see org.gradle.testkit.runner.GradleRunner.withTestKitDir + */ +private const val TESTKIT_DIR_PROPERTY: String = "org.gradle.testkit.dir" + +/** + * The path, relative to the Gradle user home of the build, of the TestKit + * directory shared by the TestKit builds of all modules. + * + * The directory is placed under `caches/`, which `gradle/actions/setup-gradle` + * restores on every run but saves only on the default branch. So a pull request + * run starts with a warm directory once a run on the default branch has saved it. + * Locally, the TestKit builds of all the repositories that use this `buildSrc` + * share it. + * + * Unlike a directory under `build/`, it is not reset by `clean`. Delete it to make + * the TestKit builds start from an empty one. + */ +private const val SHARED_TESTKIT_DIR: String = "caches/spine-testkit" diff --git a/buildSrc/src/main/kotlin/kmp-module.gradle.kts b/buildSrc/src/main/kotlin/kmp-module.gradle.kts index 0e84f04a..0574f09d 100644 --- a/buildSrc/src/main/kotlin/kmp-module.gradle.kts +++ b/buildSrc/src/main/kotlin/kmp-module.gradle.kts @@ -27,6 +27,7 @@ import io.spine.gradle.kotlin.setFreeCompilerArgs import io.spine.gradle.publish.IncrementGuard import io.spine.gradle.report.license.LicenseReporter import io.spine.gradle.testing.configureLogging +import io.spine.gradle.testing.useSharedTestKitDir /** * Configures this [Project] as a Kotlin Multiplatform module. @@ -167,6 +168,12 @@ tasks { } } +/** + * Gives all the `Test` tasks the TestKit directory shared by all modules, + * as `module-testing` does (see [useSharedTestKitDir]). + */ +project.useSharedTestKitDir() + /** * Overrides the default location of Kotlin sources. * diff --git a/buildSrc/src/main/kotlin/module-testing.gradle.kts b/buildSrc/src/main/kotlin/module-testing.gradle.kts index add01783..ec256e1c 100644 --- a/buildSrc/src/main/kotlin/module-testing.gradle.kts +++ b/buildSrc/src/main/kotlin/module-testing.gradle.kts @@ -20,10 +20,14 @@ import io.spine.dependency.test.Kotest import io.spine.dependency.test.Truth import io.spine.gradle.testing.configureLogging import io.spine.gradle.testing.registerTestTasks +import io.spine.gradle.testing.useSharedTestKitDir /** * This convention plugin applies test dependencies and configures test-related tasks. * + * All the `Test` tasks of the module get the TestKit directory shared by all + * modules (see [useSharedTestKitDir]). + * * The version of the [JUnit] platform must be applied via the [BomsPlugin][io.spine.dependency.boms.BomsPlugin]: * * ```kotlin @@ -40,6 +44,7 @@ plugins { project.run { setupTests() forceTestDependencies() + useSharedTestKitDir() } dependencies { diff --git a/config b/config index 4f80b399..3cc29d4e 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 4f80b399d33103039a10b56145cb8ffad8bd7a3d +Subproject commit 3cc29d4e51eb6de7da4234d2677973cead2cea84 From e34228ba9428d4a772862310005f9d979b6546e6 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 21:48:52 +0000 Subject: [PATCH 11/15] Bump version -> `2.0.0-SNAPSHOT.460` Co-Authored-By: Claude Opus 5.5 --- version.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle.kts b/version.gradle.kts index 25bd3452..bb97fa19 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -12,4 +12,4 @@ * and limitations under the License. */ -extra.set("versionToPublish", "2.0.0-SNAPSHOT.452") +extra.set("versionToPublish", "2.0.0-SNAPSHOT.460") From b3ebab7ddd9072d3e6f4926bae9deb0dc3950d68 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 21:50:09 +0000 Subject: [PATCH 12/15] Update dependency reports Co-Authored-By: Claude Opus 5.5 --- docs/dependencies/dependencies.md | 76 +++++++++++++++---------------- docs/dependencies/pom.xml | 2 +- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index 17dc9293..cd7ba025 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine.tools:archive:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:archive:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -832,14 +832,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:code:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:code:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1655,14 +1655,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:fixtures:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:fixtures:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.google.android. **Name** : annotations. **Version** : 4.1.1.4. @@ -2593,14 +2593,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:fs:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:fs:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -3416,14 +3416,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gradle-plugin-api:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:gradle-plugin-api:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-annotations. **Version** : 2.22. @@ -4345,14 +4345,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gradle-plugin-api-test-fixtures:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:gradle-plugin-api-test-fixtures:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-annotations. **Version** : 2.22. @@ -4865,14 +4865,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:33 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gradle-root-plugin:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:gradle-root-plugin:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -5736,14 +5736,14 @@ This report was generated on **Tue Nov 03 02:21:33 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:intellij-platform:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:intellij-platform:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -6840,14 +6840,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:47 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:intellij-platform-java:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:intellij-platform-java:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -8719,14 +8719,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:47 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:java-code:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:java-code:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -9596,14 +9596,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:jvm-tool-plugins:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:jvm-tool-plugins:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -10459,14 +10459,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:jvm-tools:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:jvm-tools:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0. @@ -11233,14 +11233,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:kotlin-code:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:kotlin-code:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -12056,14 +12056,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:plugin-base:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:plugin-base:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -12927,14 +12927,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:plugin-testlib:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:plugin-testlib:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.google.auto.value. **Name** : auto-value-annotations. **Version** : 1.11.1. @@ -13901,14 +13901,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:proto-code:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:proto-code:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -14794,14 +14794,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:46 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:protobuf-setup-plugins:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:protobuf-setup-plugins:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -15677,14 +15677,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:47 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:psi:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:psi:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -16812,14 +16812,14 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:47 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:psi-java:2.0.0-SNAPSHOT.452` +# Dependencies of `io.spine.tools:psi-java:2.0.0-SNAPSHOT.460` ## Runtime 1. **Group** : be.cyberelf.nanoxml. **Name** : nanoxml. **Version** : 2.2.3. @@ -18738,6 +18738,6 @@ This report was generated on **Tue Nov 03 02:21:34 WET 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Nov 03 02:21:34 WET 2026** using +This report was generated on **Tue Nov 03 21:49:47 WET 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml index 3b6db849..3dc21609 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine.tools tool-base -2.0.0-SNAPSHOT.452 +2.0.0-SNAPSHOT.460 2015 From b62b7ec9893d1dd3de40e33f7102529101f923cc Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 22:03:11 +0000 Subject: [PATCH 13/15] Collect TestKit coverage in `plugin-testlib` as well The file also picks up the current license header. Co-Authored-By: Claude Opus 5.5 --- plugin-testlib/build.gradle.kts | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/plugin-testlib/build.gradle.kts b/plugin-testlib/build.gradle.kts index 607796b5..db1e1d32 100644 --- a/plugin-testlib/build.gradle.kts +++ b/plugin-testlib/build.gradle.kts @@ -1,36 +1,29 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ import io.spine.dependency.local.TestLib import io.spine.dependency.test.JUnit +import io.spine.gradle.testing.enableTestKitCoverage plugins { module } +// Credit coverage of the code executed in TestKit worker JVMs by the +// `GradleProjectSpec` tests. See `io.spine.gradle.testing.enableTestKitCoverage`. +enableTestKitCoverage() + dependencies { api(gradleApi()) api(gradleTestKit()) From f7059d04a9e704b7321d0db2a99587dc4057e696 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 22:03:17 +0000 Subject: [PATCH 14/15] Default the shared TestKit directory to the Gradle user home `withSharedTestKitDirectory()` now takes an optional `dir`. Given none, a project uses `caches/spine-testkit` under the Gradle user home, which `GradleUserHome.kt` looks up as Gradle does: the `gradle.user.home` system property, then `GRADLE_USER_HOME`, then `.gradle` in the user's home. Remove `RootProject.testKitTempDir()`, which put the directory under the repository root. Co-Authored-By: Claude Opus 5.5 --- .../tools/gradle/testing/GradleProject.kt | 19 ++- .../gradle/testing/GradleProjectSetup.kt | 68 ++++++----- .../tools/gradle/testing/GradleUserHome.kt | 104 +++++++++++++++++ .../spine/tools/gradle/testing/RootProject.kt | 41 ++----- .../gradle/testing/GradleProjectSetupSpec.kt | 49 +++++--- .../gradle/testing/GradleUserHomeSpec.kt | 110 ++++++++++++++++++ .../tools/gradle/testing/RootProjectSpec.kt | 33 ++---- 7 files changed, 311 insertions(+), 113 deletions(-) create mode 100644 plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleUserHome.kt create mode 100644 plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/GradleUserHomeSpec.kt diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt index 77b46fc0..730a25e1 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProject.kt @@ -108,13 +108,13 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure * * ## TestKit directory * - * The TestKit directory is the Gradle user home of the worker builds. Builds started - * by this class and by [runGradleBuild] use the one TestKit picks, unless the project - * is set up with [GradleProjectSetup.withSharedTestKitDirectory]. TestKit takes the - * directory named by Gradle's `org.gradle.testkit.dir` system property of the test - * JVM, or makes one under the temporary directory of the `Test` task otherwise. To - * let the test JVMs of all modules share one directory, set that property on their - * `Test` tasks. This choice is independent of the coverage setup above. + * The TestKit directory is the Gradle user home of the worker builds. A project set + * up with [GradleProjectSetup.withSharedTestKitDirectory] uses the directory given + * there, or `caches/spine-testkit` under the Gradle user home if given none. Other + * builds started by this class and by [runGradleBuild] use the directory TestKit + * picks: the one named by Gradle's `org.gradle.testkit.dir` system property of the + * test JVM, or one under the temporary directory of the `Test` task without it. + * This choice is independent of the coverage setup above. * * @see GradleProjectSetup */ @@ -147,9 +147,8 @@ public class GradleProject internal constructor(setup: GradleProjectSetup) { val runner = GradleRunner.create() .withProjectDir(setup.projectDir) .withDebug(setup.debug) - if (setup.useSharedTestKit) { - val sharedTestKitDir = RootProject.testKitTempDir().toFile().apply { mkdirs() } - runner.withTestKitDir(sharedTestKitDir) + setup.testKitDir()?.let { dir -> + runner.withTestKitDir(dir.apply { mkdirs() }) } runner } diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProjectSetup.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProjectSetup.kt index 98f42b34..e7240a4e 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProjectSetup.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProjectSetup.kt @@ -1,28 +1,17 @@ /* - * Copyright 2022, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ + package io.spine.tools.gradle.testing import io.spine.annotation.VisibleForTesting @@ -93,10 +82,17 @@ public class GradleProjectSetup internal constructor( internal var debug = false private set - /** Whether a [shared Gradle TestKit folder][RootProject.testKitTempDir] should be used. */ + /** Whether a [shared Gradle TestKit directory][withSharedTestKitDirectory] should be used. */ internal var useSharedTestKit = false private set + /** + * The shared TestKit directory [given][withSharedTestKitDirectory] explicitly, + * or `null` if none was given. + */ + internal var sharedTestKitDir: File? = null + private set + /** * Determines whether the plugin under test classpath is defined and * should be added to the Gradle execution classpath. @@ -364,17 +360,37 @@ public class GradleProjectSetup internal constructor( } /** - * Tells the TestKit runner to use a [single shared folder][RootProject.testKitTempDir] - * for all tests. + * Tells the TestKit runner to use a TestKit directory shared by the tests. + * + * The TestKit directory is the Gradle user home of the test builds. When it is + * shared, a build can reuse what earlier builds left there, such as Gradle's + * instrumented copies of the JARs on the classpath of the plugin under test. + * The directory is not deleted by `clean`. * - * The idea is that this folder is not deleted on `gradlew clean` phase, - * and therefore may be somewhat re-used to speed up the test execution. + * @param dir The directory to use. If `null`, the runner uses `caches/spine-testkit` + * under the Gradle user home. The Gradle user home is looked up in the order + * Gradle uses: the `gradle.user.home` system property of the test JVM, then the + * `GRADLE_USER_HOME` environment variable, then `.gradle` in the home directory + * of the user. */ - public fun withSharedTestKitDirectory() : GradleProjectSetup { + @JvmOverloads + public fun withSharedTestKitDirectory(dir: File? = null): GradleProjectSetup { this.useSharedTestKit = true + this.sharedTestKitDir = dir return this } + /** + * Obtains the TestKit directory to give the runner, or `null` to leave the choice + * to TestKit. + */ + internal fun testKitDir(): File? = + if (useSharedTestKit) { + sharedTestKitDir ?: defaultSharedTestKitDir() + } else { + null + } + /** * Creates a new project on the file system. */ diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleUserHome.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleUserHome.kt new file mode 100644 index 00000000..2bf92417 --- /dev/null +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleUserHome.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.tools.gradle.testing + +import java.io.File + +/** + * Returns the TestKit directory that a project set up with + * [GradleProjectSetup.withSharedTestKitDirectory] uses when given none: + * `caches/spine-testkit` under the [Gradle user home][gradleUserHome]. + * + * @param property Returns the value of the system property with the given name. + * @param environment Returns the value of the environment variable with the given name. + */ +internal fun defaultSharedTestKitDir( + property: (String) -> String? = System::getProperty, + environment: (String) -> String? = System::getenv +): File = gradleUserHome(property, environment) + .resolve(CACHES_DIR) + .resolve(SHARED_TESTKIT_DIR) + +/** + * Returns the Gradle user home, looked up in the order Gradle uses: + * + * 1. The `gradle.user.home` system property. + * 2. The `GRADLE_USER_HOME` environment variable. + * 3. The `.gradle` directory in the home directory of the user (`user.home`). + * + * A blank value counts as not set. The path is composed with [File], so it gets + * the separators of the platform, and a Windows path such as `D:\a\.gradle` is + * kept as given. + * + * The lookup runs in the test JVM. That JVM does not see a Gradle user home passed + * to the outer build only with the `-g` option, so its result is then the default. + * + * @param property Returns the value of the system property with the given name. + * @param environment Returns the value of the environment variable with the given name. + */ +internal fun gradleUserHome( + property: (String) -> String? = System::getProperty, + environment: (String) -> String? = System::getenv +): File { + val given = property(GRADLE_USER_HOME_PROPERTY).nonBlank() + ?: environment(GRADLE_USER_HOME_VARIABLE).nonBlank() + if (given != null) { + return File(given) + } + val userHome = checkNotNull(property(USER_HOME_PROPERTY)) { + "The `$USER_HOME_PROPERTY` system property is not set." + } + return File(userHome, GRADLE_DIR) +} + +private fun String?.nonBlank(): String? = this?.takeIf { it.isNotBlank() } + +/** + * The name of the system property with which Gradle can be given its user home, + * as by `-Dgradle.user.home=`. + */ +private const val GRADLE_USER_HOME_PROPERTY: String = "gradle.user.home" + +/** + * The name of the environment variable with which Gradle can be given its user home. + * + * It is consulted when the [GRADLE_USER_HOME_PROPERTY] system property is not set. + */ +private const val GRADLE_USER_HOME_VARIABLE: String = "GRADLE_USER_HOME" + +/** + * The name of the standard Java system property carrying the home directory of + * the user, which holds the [default Gradle user home][GRADLE_DIR]. + */ +private const val USER_HOME_PROPERTY: String = "user.home" + +/** + * The name of the directory, in the home directory of the user, that serves as + * the Gradle user home when neither [GRADLE_USER_HOME_PROPERTY] nor + * [GRADLE_USER_HOME_VARIABLE] names another one. + */ +private const val GRADLE_DIR: String = ".gradle" + +/** + * The name of the directory under the Gradle user home that holds the caches + * of Gradle and the [shared TestKit directory][SHARED_TESTKIT_DIR]. + */ +private const val CACHES_DIR: String = "caches" + +/** + * The name of the TestKit directory shared by the test builds, placed in + * the [caches directory][CACHES_DIR] of the Gradle user home. + */ +private const val SHARED_TESTKIT_DIR: String = "spine-testkit" diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/RootProject.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/RootProject.kt index de5138f3..26dd3267 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/RootProject.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/RootProject.kt @@ -1,28 +1,17 @@ /* - * Copyright 2022, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ + package io.spine.tools.gradle.testing import java.io.File @@ -80,16 +69,4 @@ internal object RootProject { fun dir(): File { return path().toFile() } - - /** - * Returns a path to a conventionally established temp directory named `.gradle-test-kit`, - * for Gradle TestKit runners. - * - * It may be used to configure TestKit runners, so that just a single folder - * is created for all integration tests. - */ - @JvmStatic - fun testKitTempDir() : Path { - return path().resolve(".gradle-test-kit") - } } diff --git a/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/GradleProjectSetupSpec.kt b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/GradleProjectSetupSpec.kt index 458aa35c..36f2d7a6 100644 --- a/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/GradleProjectSetupSpec.kt +++ b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/GradleProjectSetupSpec.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.gradle.testing @@ -114,6 +102,29 @@ class GradleProjectSetupSpec { setup.useSharedTestKit shouldBe true } + @Nested + inner class `choose the TestKit directory` { + + @Test + fun `leaving it to TestKit by default`() { + setup.testKitDir() shouldBe null + } + + @Test + fun `taking the one given`(@TempDir dir: File) { + setup.withSharedTestKitDirectory(dir) + + setup.testKitDir() shouldBe dir + } + + @Test + fun `taking the shared one under the Gradle user home when given none`() { + setup.withSharedTestKitDirectory() + + setup.testKitDir() shouldBe defaultSharedTestKitDir() + } + } + @Test fun `copy 'buildSrc' without caching`() { setup.copyBuildSrc(cachingEnabled = false) diff --git a/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/GradleUserHomeSpec.kt b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/GradleUserHomeSpec.kt new file mode 100644 index 00000000..131ab5a0 --- /dev/null +++ b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/GradleUserHomeSpec.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.tools.gradle.testing + +import io.kotest.matchers.shouldBe +import java.io.File +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledOnOs +import org.junit.jupiter.api.condition.OS + +@DisplayName("Gradle user home lookup should") +internal class GradleUserHomeSpec { + + @Test + fun `prefer the system property of the Gradle user home`() { + val home = gradleUserHome( + systemProperties("gradle.user.home" to "/opt/gradle", "user.home" to "/home/me"), + environment("GRADLE_USER_HOME" to "/var/gradle") + ) + + home shouldBe File("/opt/gradle") + } + + @Test + fun `use the 'GRADLE_USER_HOME' environment variable when the property is not set`() { + val home = gradleUserHome( + systemProperties("user.home" to "/home/me"), + environment("GRADLE_USER_HOME" to "/var/gradle") + ) + + home shouldBe File("/var/gradle") + } + + @Test + fun `default to the dot-gradle directory in the home directory of the user`() { + val home = gradleUserHome(systemProperties("user.home" to "/home/me"), environment()) + + home shouldBe File("/home/me", ".gradle") + } + + @Test + fun `treat blank values as not set`() { + val home = gradleUserHome( + systemProperties("gradle.user.home" to " ", "user.home" to "/home/me"), + environment("GRADLE_USER_HOME" to "") + ) + + home shouldBe File("/home/me", ".gradle") + } + + @Test + fun `put the shared TestKit directory under 'caches' of the Gradle user home`() { + val dir = defaultSharedTestKitDir( + systemProperties("user.home" to "/home/me"), + environment("GRADLE_USER_HOME" to "/var/gradle") + ) + + dir shouldBe File("/var/gradle/caches/spine-testkit") + } + + @Nested + @EnabledOnOs(OS.WINDOWS) + inner class `keep a Windows path` { + + @Test + fun `given with backslashes`() { + val fromVariable = defaultSharedTestKitDir( + systemProperties("user.home" to "C:\\Users\\runneradmin"), + environment("GRADLE_USER_HOME" to "D:\\a\\.gradle") + ) + val fromUserHome = defaultSharedTestKitDir( + systemProperties("user.home" to "C:\\Users\\runneradmin"), + environment() + ) + + fromVariable.path shouldBe "D:\\a\\.gradle\\caches\\spine-testkit" + fromUserHome.path shouldBe "C:\\Users\\runneradmin\\.gradle\\caches\\spine-testkit" + } + + @Test + fun `given with forward slashes`() { + val dir = defaultSharedTestKitDir( + systemProperties("user.home" to "C:\\Users\\runneradmin"), + environment("GRADLE_USER_HOME" to "D:/a/.gradle") + ) + + dir.path shouldBe "D:\\a\\.gradle\\caches\\spine-testkit" + } + } +} + +private fun systemProperties(vararg values: Pair): (String) -> String? = + mapOf(*values)::get + +private fun environment(vararg values: Pair): (String) -> String? = + mapOf(*values)::get diff --git a/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/RootProjectSpec.kt b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/RootProjectSpec.kt index d439fe42..065e9101 100644 --- a/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/RootProjectSpec.kt +++ b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/RootProjectSpec.kt @@ -1,27 +1,15 @@ /* - * Copyright 2026, TeamDev. All rights reserved. + * Copyright 2026 CodeMatters, Lda. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. */ package io.spine.tools.gradle.testing @@ -37,11 +25,4 @@ class RootProjectSpec { fun `locate the project root`() { RootProject.dir().exists() shouldBe true } - - @Test - fun `provide the shared TestKit temp directory under the project root`() { - val testKitDir = RootProject.testKitTempDir() - testKitDir.startsWith(RootProject.path()) shouldBe true - testKitDir.fileName.toString() shouldBe ".gradle-test-kit" - } } From a018b5aa281eb91fab08ca8582e472b1e05089c9 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 3 Nov 2026 22:20:23 +0000 Subject: [PATCH 15/15] Cover the Gradle user home lookup Drop the default arguments of `gradleUserHome()`, which nothing used, make the explicitly given TestKit directory private, and test the error for an unknown `user.home`. Codecov counted these as uncovered lines. Co-Authored-By: Claude Opus 5.5 --- .../spine/tools/gradle/testing/GradleProjectSetup.kt | 3 +-- .../io/spine/tools/gradle/testing/GradleUserHome.kt | 4 ++-- .../spine/tools/gradle/testing/GradleUserHomeSpec.kt | 11 +++++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProjectSetup.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProjectSetup.kt index e7240a4e..391bc1dd 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProjectSetup.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleProjectSetup.kt @@ -90,8 +90,7 @@ public class GradleProjectSetup internal constructor( * The shared TestKit directory [given][withSharedTestKitDirectory] explicitly, * or `null` if none was given. */ - internal var sharedTestKitDir: File? = null - private set + private var sharedTestKitDir: File? = null /** * Determines whether the plugin under test classpath is defined and diff --git a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleUserHome.kt b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleUserHome.kt index 2bf92417..5ce961b4 100644 --- a/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleUserHome.kt +++ b/plugin-testlib/src/main/kotlin/io/spine/tools/gradle/testing/GradleUserHome.kt @@ -49,8 +49,8 @@ internal fun defaultSharedTestKitDir( * @param environment Returns the value of the environment variable with the given name. */ internal fun gradleUserHome( - property: (String) -> String? = System::getProperty, - environment: (String) -> String? = System::getenv + property: (String) -> String?, + environment: (String) -> String? ): File { val given = property(GRADLE_USER_HOME_PROPERTY).nonBlank() ?: environment(GRADLE_USER_HOME_VARIABLE).nonBlank() diff --git a/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/GradleUserHomeSpec.kt b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/GradleUserHomeSpec.kt index 131ab5a0..48f53ab6 100644 --- a/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/GradleUserHomeSpec.kt +++ b/plugin-testlib/src/test/kotlin/io/spine/tools/gradle/testing/GradleUserHomeSpec.kt @@ -15,10 +15,12 @@ package io.spine.tools.gradle.testing import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain import java.io.File import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.condition.EnabledOnOs import org.junit.jupiter.api.condition.OS @@ -62,6 +64,15 @@ internal class GradleUserHomeSpec { home shouldBe File("/home/me", ".gradle") } + @Test + fun `fail when the home directory of the user is not known`() { + val error = assertThrows { + gradleUserHome(systemProperties(), environment()) + } + + error.message shouldContain "user.home" + } + @Test fun `put the shared TestKit directory under 'caches' of the Gradle user home`() { val dir = defaultSharedTestKitDir(