diff --git a/.agents/tasks/fix-test-source-set-duplicates.md b/.agents/tasks/fix-test-source-set-duplicates.md new file mode 100644 index 0000000000..d65b11964a --- /dev/null +++ b/.agents/tasks/fix-test-source-set-duplicates.md @@ -0,0 +1,54 @@ +--- +slug: fix-test-source-set-duplicates +branch: claude/vibrant-jang-fa76b9 +owner: claude +status: in-review +started: 2026-06-30 +--- + +## Goal + +Fix [issue #19](https://github.com/SpineEventEngine/compiler/issues/19): +generated source sets in tests may contain duplicates. The Compiler reads the +`protoc` output and writes processed code into a separate directory; both must +not reach the Java/Kotlin compiler at once, or compilation fails with +`duplicate class` errors. This "sometimes does not work for the test source +set." + +## Context + +- `protoc` writes to `build/generated/sources/proto//{java,kotlin}`; + the Compiler writes to `generated//{java,kotlin}`. +- Deduplication lived in `configureCompileTasks` (live compile-task filtering) in + ProtoData until a 2023 commit ("Improve filtering of duplicated generated + sources") replaced it with an eager, one-time source-set rewrite, now in + tool-base's `GeneratedSourcePlugin.configureSourceSetDirs`. +- The eager rewrite is fragile: plugin-application order, a symlinked project + path (the old `residesIn` compared a canonical path to a merely absolute one), + or a consumer plugin re-adding the directory can leave the `protoc` output in + the source set. + +## Plan + +- [x] Reproduce: `test-source-set` fixture with protos in `main` + `test`; an + `afterEvaluate` re-adds the `protoc` output dir to the `test` source set, + reproducing the leaked state. Confirmed `compileTestJava` fails with + `duplicate class`. +- [x] Fix (compiler-side, order-independent): re-introduce live compile-task + filtering in `Plugin.excludeProtocOutputFromCompilation()` — re-set + `JavaCompile.source` to a filtered view and add an `exclude` spec to + `KotlinCompile`. Skips the deprecated in-place mode. +- [x] Harden `Paths.residesIn` to canonicalize both operands (symlink-safe). +- [x] Regression test `keep duplicate generated classes out of the 'test' + compilation` asserts `testClasses` succeeds. +- [x] Version bump `2.0.0-SNAPSHOT.057` → `.058`. +- [x] Full `PluginSpec` green: 16 tests, 0 failures (1 pre-existing skip). + `:gradle-plugin:detekt` (the `check` gate) passes. + +## Log +- 2026-06-30 — reproduced RED (duplicate class in `compileTestJava`), implemented + the fix, verified GREEN with a marker proving the fixed plugin code executed in + the inner testkit build. +- Note: local incremental/build-cache is stale in this checkout; verifying + functional tests needs `--rerun-tasks --no-build-cache` and `arch -arm64` + (gradle-doctor Rosetta check). See auto-memory `gradle-arch-arm64`. diff --git a/build.gradle.kts b/build.gradle.kts index 920bd99ed9..fdd006043f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -128,7 +128,7 @@ KoverConfig.applyTo(project) * publishing in the root project. */ val projectsToPublish: Set = the().modules -val localPublish by tasks.registering { +val localPublish = tasks.register("localPublish") { /* Integration tests need the plugin subproject published to Maven Local too because they apply the plugin. @@ -187,7 +187,7 @@ fun materializeTestsLink(linkName: String) { * This build should run _only_ if all tests of all modules passed. * Otherwise, integration tests make little sense. */ -val integrationTest by tasks.registering(RunBuild::class) { +val integrationTest = tasks.register("integrationTest") { directory = "$rootDir/tests" /* The `tests` build consumes the Compiler published to Maven Local by `localPublish`, so the build cache in that build exercises the plugin under development. */ @@ -195,7 +195,7 @@ val integrationTest by tasks.registering(RunBuild::class) { dependsOn(localPublish) subprojects.forEach { it.tasks.findByName("test")?.let { testTask -> - this@registering.dependsOn(testTask) + this@register.dependsOn(testTask) } } doFirst { diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt b/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt index 0344819f22..b31cd3087f 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,11 +41,16 @@ import java.nio.file.WatchService * * After the first usage, the instances of this type delegate all calls to the internally * created instance of [Path] created with [createTempDirectory]. + * + * The directory is created under the [shared base directory][SpineTempDir], which is removed + * when the JVM — the Gradle daemon — shuts down. Build tasks delete their own directories + * eagerly as the primary cleanup; this shutdown removal is a safety net, so a directory does + * not outlive the daemon even when a build fails before its eager cleanup runs. */ @Suppress("TooManyFunctions") class LazyTempPath(private val prefix: String) : Path { - private val delegate: Path by lazy { createTempDirectory(prefix) } + private val delegate: Path by lazy { createTempDirectory(SpineTempDir.path, prefix) } override fun compareTo(other: Path): Int = delegate.compareTo(other) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/fs/SpineTempDir.kt b/buildSrc/src/main/kotlin/io/spine/gradle/fs/SpineTempDir.kt new file mode 100644 index 0000000000..d6e856e01f --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/fs/SpineTempDir.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * 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. + */ + +package io.spine.gradle.fs + +import java.nio.file.Files.createDirectories +import java.nio.file.Files.createTempDirectory +import java.nio.file.Path + +/** + * A per-JVM parent directory for the temporary directories created by the build. + * + * The directory is created [lazily][path] under a common, attributable namespace — + * `/io.spine.gradle.fs`, named after the package of [LazyTempPath] — so + * that leftover files are easy to attribute. Within that namespace, each JVM gets its own + * subdirectory named after the process id, so concurrent Gradle daemons never delete one + * another's temporary files. + * + * Upon creation, the per-JVM directory is scheduled for recursive removal when the JVM + * shuts down. This is a safety net should the explicit cleanup performed by the build + * tasks not run — for example, when a build fails before reaching it. The shared namespace + * directory itself is intentionally left in place: deleting it on shutdown could wipe + * directories still in use by another JVM running on the same machine. + * + * @see LazyTempPath + */ +internal object SpineTempDir { + + /** + * The per-JVM directory, created on the first access and removed on JVM shutdown. + */ + val path: Path by lazy { createPerJvmDir() } + + private fun createPerJvmDir(): Path { + val namespace = Path.of(systemTempDir(), LazyTempPath::class.java.packageName) + createDirectories(namespace) + // A per-JVM directory keeps concurrent Gradle daemons from deleting one another's + // files when their shutdown hooks fire. The PID makes a leftover directory easy + // to attribute; `createTempDirectory` adds a random suffix so that a reused PID + // still yields a unique directory. + val pid = ProcessHandle.current().pid() + val jvmDir = createTempDirectory(namespace, "$pid-") + deleteRecursivelyOnShutdown(jvmDir) + return jvmDir + } + + /** + * Obtains the value of the system property pointing to the temporary directory. + */ + private fun systemTempDir(): String = + checkNotNull(System.getProperty("java.io.tmpdir")) { + "The `java.io.tmpdir` system property is not set." + } + + /** + * Requests the recursive removal of the given [directory] when the JVM shuts down. + * + * @see Runtime.addShutdownHook + */ + private fun deleteRecursivelyOnShutdown(directory: Path) { + val runtime = Runtime.getRuntime() + runtime.addShutdownHook(Thread { + val deleted = directory.toFile().deleteRecursively() + if (!deleted) { + System.err.println("Unable to delete the temporary directory `$directory`.") + } + }) + } +} diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/fs/LazyTempPathSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/fs/LazyTempPathSpec.kt new file mode 100644 index 0000000000..260e852fe2 --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/fs/LazyTempPathSpec.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * 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. + */ + +package io.spine.gradle.fs + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`LazyTempPath` should") +class LazyTempPathSpec { + + @Test + fun `create the directory on the first use`() { + val directory = LazyTempPath("created").toFile() + + directory.exists() shouldBe true + directory.isDirectory shouldBe true + } + + @Test + fun `create the directory under the system temporary directory`() { + val path = LazyTempPath("under-tmp").toString() + + path shouldContain systemTempDir() + } + + @Test + fun `create the directory under a folder named after its package`() { + val path = LazyTempPath("under-base").toString() + + path shouldContain LazyTempPath::class.java.packageName + } + + @Test + fun `place all instances under the same base directory`() { + val first = LazyTempPath("first").toFile() + val second = LazyTempPath("second").toFile() + + first.parentFile shouldBe second.parentFile + first.parentFile.toString() shouldBe SpineTempDir.path.toString() + } +} + +private fun systemTempDir(): String = System.getProperty("java.io.tmpdir") diff --git a/buildSrc/src/main/kotlin/version-to-resources.gradle.kts b/buildSrc/src/test/kotlin/io/spine/gradle/fs/SpineTempDirSpec.kt similarity index 61% rename from buildSrc/src/main/kotlin/version-to-resources.gradle.kts rename to buildSrc/src/test/kotlin/io/spine/gradle/fs/SpineTempDirSpec.kt index 6ac408c574..8bf42500a8 100644 --- a/buildSrc/src/main/kotlin/version-to-resources.gradle.kts +++ b/buildSrc/src/test/kotlin/io/spine/gradle/fs/SpineTempDirSpec.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,36 +24,31 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -plugins { - java -} +package io.spine.gradle.fs -val versionDir = layout.buildDirectory.dir("version").get().asFile.path +import io.kotest.matchers.shouldBe +import java.nio.file.Path +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test -/** - * This file, containing the version of ProtoData, is generated at build time and included into - * the project's resources. - * - * Please search for the usages of "version.txt" when making changes. - */ -val versionFile = "$versionDir/version.txt" +@DisplayName("`SpineTempDir` should") +class SpineTempDirSpec { -sourceSets { - main { - resources.srcDir(versionDir) - } -} + @Test + fun `place its per-JVM directory under the package-named namespace`() { + val namespace = Path.of( + System.getProperty("java.io.tmpdir"), + LazyTempPath::class.java.packageName + ) -val createVersionFile by tasks.registering { + SpineTempDir.path.parent shouldBe namespace + } - inputs.property("version", project.version) - outputs.file(versionFile) + @Test + fun `create the directory on access`() { + val directory = SpineTempDir.path.toFile() - doLast { - file(versionFile).writeText(project.version.toString()) + directory.exists() shouldBe true + directory.isDirectory shouldBe true } } - -tasks.processResources { - dependsOn(createVersionFile) -} diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 3e15e65aa2..207c1056eb 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -36,7 +36,6 @@ import io.spine.gradle.publish.setup plugins { module application - `version-to-resources` `write-manifest` `build-proto-model` `maven-publish` diff --git a/config b/config index fdf78d7a2c..73d246ad7e 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit fdf78d7a2c2372cead94ea29e9155707e200b94e +Subproject commit 73d246ad7ebe4e3b8e3f0c14335152435daa1b20 diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index eda7690486..8726f5f5e8 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine.tools:compiler-api:2.0.0-SNAPSHOT.058` +# Dependencies of `io.spine.tools:compiler-api:2.0.0-SNAPSHOT.059` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -1099,14 +1099,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using +This report was generated on **Wed Jul 01 16:19:17 WEST 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:compiler-api-tests:2.0.0-SNAPSHOT.058` +# Dependencies of `io.spine.tools:compiler-api-tests:2.0.0-SNAPSHOT.059` ## Runtime ## Compile, tests, and tooling @@ -1476,14 +1476,14 @@ This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using +This report was generated on **Wed Jul 01 16:19:16 WEST 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:compiler-backend:2.0.0-SNAPSHOT.058` +# Dependencies of `io.spine.tools:compiler-backend:2.0.0-SNAPSHOT.059` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -2586,14 +2586,14 @@ This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using +This report was generated on **Wed Jul 01 16:19:17 WEST 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:compiler-cli:2.0.0-SNAPSHOT.058` +# Dependencies of `io.spine.tools:compiler-cli:2.0.0-SNAPSHOT.059` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -3855,14 +3855,14 @@ This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using +This report was generated on **Wed Jul 01 16:19:17 WEST 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:compiler-gradle-api:2.0.0-SNAPSHOT.058` +# Dependencies of `io.spine.tools:compiler-gradle-api:2.0.0-SNAPSHOT.059` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -4879,14 +4879,14 @@ This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using +This report was generated on **Wed Jul 01 16:19:17 WEST 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:compiler-gradle-plugin:2.0.0-SNAPSHOT.058` +# Dependencies of `io.spine.tools:compiler-gradle-plugin:2.0.0-SNAPSHOT.059` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -5947,14 +5947,14 @@ This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using +This report was generated on **Wed Jul 01 16:19:17 WEST 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:compiler-jvm:2.0.0-SNAPSHOT.058` +# Dependencies of `io.spine.tools:compiler-jvm:2.0.0-SNAPSHOT.059` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -7074,14 +7074,14 @@ This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using +This report was generated on **Wed Jul 01 16:19:17 WEST 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:compiler-params:2.0.0-SNAPSHOT.058` +# Dependencies of `io.spine.tools:compiler-params:2.0.0-SNAPSHOT.059` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -8172,14 +8172,14 @@ This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using +This report was generated on **Wed Jul 01 16:19:17 WEST 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:compiler-protoc-plugin:2.0.0-SNAPSHOT.058` +# Dependencies of `io.spine.tools:compiler-protoc-plugin:2.0.0-SNAPSHOT.059` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -9012,14 +9012,14 @@ This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using +This report was generated on **Wed Jul 01 16:19:17 WEST 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:compiler-test-env:2.0.0-SNAPSHOT.058` +# Dependencies of `io.spine.tools:compiler-test-env:2.0.0-SNAPSHOT.059` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -10118,14 +10118,14 @@ This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using +This report was generated on **Wed Jul 01 16:19:17 WEST 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:compiler-testlib:2.0.0-SNAPSHOT.058` +# Dependencies of `io.spine.tools:compiler-testlib:2.0.0-SNAPSHOT.059` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. @@ -11331,6 +11331,6 @@ This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jun 30 23:13:38 WEST 2026** using +This report was generated on **Wed Jul 01 16:19:17 WEST 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 93d68f4c7b..b2e383144b 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 compiler -2.0.0-SNAPSHOT.058 +2.0.0-SNAPSHOT.059 2015 diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts index c6cb5f80b1..d5583ad6a8 100644 --- a/gradle-plugin/build.gradle.kts +++ b/gradle-plugin/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,6 @@ plugins { `java-gradle-plugin` `maven-publish` id("com.gradle.plugin-publish").version("1.3.1") - `version-to-resources` `write-manifest` } @@ -69,13 +68,10 @@ artifactMeta { } } -@Suppress( - "UnstableApiUsage" /* testing suites feature */, - "unused" /* suite variable names obtained via `by` calls. */ -) +@Suppress("UnstableApiUsage") /* testing suites feature */ testing { suites { - val test by getting(JvmTestSuite::class) { + getByName("test") { useJUnitJupiter(JUnit.version) dependencies { implementation(Kotlin.GradlePlugin.lib) @@ -86,7 +82,7 @@ testing { } } - val functionalTest by registering(JvmTestSuite::class) { + register("functionalTest") { useJUnitJupiter(JUnit.version) dependencies { implementation(Kotlin.GradlePlugin.lib) @@ -124,8 +120,7 @@ dependencies { * Make functional tests depend on publishing all the submodules to Maven Local so that * the Gradle plugin can get all the dependencies when it's applied to the test projects. */ -@Suppress("unused") -val functionalTest: Task by tasks.getting { +tasks.getByName("functionalTest") { val task = this productionModules.forEach { subproject -> task.dependsOn(":${subproject.name}:publishToMavenLocal") @@ -137,15 +132,14 @@ java { withJavadocJar() } -val compilerVersion: String by extra +val compilerVersion = extra["compilerVersion"] as String val isSnapshot = compilerVersion.isSnapshot() -val publishPlugins: Task by tasks.getting { +val publishPlugins: Task = tasks.getByName("publishPlugins") { enabled = !isSnapshot } -@Suppress("unused") -val publish: Task by tasks.getting { +tasks.getByName("publish") { if (!isSnapshot) { dependsOn(publishPlugins) } @@ -179,13 +173,12 @@ gradlePlugin { tags.set(listOf("spine", "ddd", "protobuf", "compiler", "code-generation", "codegen")) } } - val functionalTest by sourceSets.getting + val functionalTest = sourceSets.getByName("functionalTest") testSourceSets( functionalTest ) } - tasks { check { dependsOn(testing.suites.named("functionalTest")) diff --git a/gradle-plugin/src/functionalTest/kotlin/io/spine/tools/compiler/gradle/plugin/PluginSpec.kt b/gradle-plugin/src/functionalTest/kotlin/io/spine/tools/compiler/gradle/plugin/PluginSpec.kt index 9caee83331..0d31200598 100644 --- a/gradle-plugin/src/functionalTest/kotlin/io/spine/tools/compiler/gradle/plugin/PluginSpec.kt +++ b/gradle-plugin/src/functionalTest/kotlin/io/spine/tools/compiler/gradle/plugin/PluginSpec.kt @@ -119,6 +119,34 @@ class PluginSpec { createProject("java-kotlin-test") } + /** + * Verifies that the `test` source set compiles even when the `protoc` output + * directory ends up among its source directories. + * + * The Compiler reads the `protoc` output and writes the processed code into + * a separate directory, which is added to the source set. The plugin keeps the + * `protoc` output directory out of the compilation so that each generated class + * is compiled once. That filtering must hold for the `test` source set too — + * see [issue #19](https://github.com/SpineEventEngine/compiler/issues/19). + * + * The `test-source-set` project re-adds the `protoc` output directory to the + * `test` source set after the plugin has configured it, reproducing the state + * in which the directory leaked back in. Without the compilation-level filter + * the `test` sources contain each generated class twice, and the compilation + * fails with duplicate class errors. + */ + @Test + fun `keep duplicate generated classes out of the 'test' compilation`() { + createProject("test-source-set") + val testClasses = TaskName.of("testClasses") + + val result = project.executeTask(testClasses) + + result[CompilerTaskName(SourceSetName.test)] shouldBe SUCCESS + result[TaskName.of("compileTestJava")] shouldBe SUCCESS + result[TaskName.of("compileTestKotlin")] shouldBe SUCCESS + } + private fun launchAndExpectResult(expected: TaskOutcome) { val result = launch() diff --git a/gradle-plugin/src/functionalTest/resources/test-source-set/build.gradle.kts b/gradle-plugin/src/functionalTest/resources/test-source-set/build.gradle.kts new file mode 100644 index 0000000000..268c92ce6d --- /dev/null +++ b/gradle-plugin/src/functionalTest/resources/test-source-set/build.gradle.kts @@ -0,0 +1,94 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * 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. + */ + +import com.google.protobuf.gradle.ProtobufExtension +import io.spine.dependency.lib.Protobuf +import io.spine.gradle.repo.standardToSpineSdk +import org.gradle.api.tasks.SourceSetContainer + +buildscript { + standardSpineSdkRepositories() +} + +group = "io.spine.tools.test" +version = "1.0.0-SNAPSHOT" + +plugins { + java + kotlin("jvm") + id("com.google.protobuf") + id("@COMPILER_PLUGIN_ID@") version "@COMPILER_VERSION@" +} + +repositories { + mavenLocal() // Must come first for `compiler-test-env`. + standardToSpineSdk() +} + +spine { + compiler { + plugins( + "io.spine.tools.compiler.test.NoOpRendererPlugin", + "io.spine.tools.compiler.test.TestPlugin" + ) + } +} +configurations.all { + resolutionStrategy { + force( + io.spine.dependency.local.Base.lib, + ) + } +} + +dependencies { + spineCompiler("io.spine.tools:compiler-test-env:+") + Protobuf.libs.forEach { implementation(it) } +} + +// Simulate the `protoc` output directory leaking back into the `test` source set. +// +// The Compiler reads the `protoc` output from `build/generated/sources/proto/test` +// and writes its own output under `generated/test`. The plugin removes the +// `protoc` directories from the source sets to avoid compiling the same class +// twice. That removal is a one-time rewrite of the source-set directories and is +// fragile: depending on the plugin-configuration order, a symlinked project path, +// or a consumer plugin re-adding the directory, the `protoc` output may end up in +// the `test` source set anyway. We reproduce that final state directly by +// re-adding the directory after the plugin has configured the source set. +// +// Without compile-task-level filtering, `compileTestJava` and `compileTestKotlin` +// then see each generated class twice and fail with "duplicate class" errors. +afterEvaluate { + val sourceSets = project.extensions.getByType(SourceSetContainer::class.java) + val test = sourceSets.getByName("test") + // The `protoc` output base directory of the Protobuf Gradle Plugin + // (`build/generated/sources/proto` by default). + val protocBaseDir = project.extensions.getByType(ProtobufExtension::class.java) + .generatedFilesBaseDir + test.java.srcDir("$protocBaseDir/test/java") + test.java.srcDir("$protocBaseDir/test/kotlin") +} diff --git a/gradle-plugin/src/functionalTest/resources/test-source-set/settings.gradle.kts b/gradle-plugin/src/functionalTest/resources/test-source-set/settings.gradle.kts new file mode 100644 index 0000000000..f5ecc1b72b --- /dev/null +++ b/gradle-plugin/src/functionalTest/resources/test-source-set/settings.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright 2022, TeamDev. All rights reserved. + * + * 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 + * + * 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. + */ + +pluginManagement { + repositories { + mavenLocal() + } +} diff --git a/gradle-plugin/src/functionalTest/resources/test-source-set/src/main/proto/main_scope.proto b/gradle-plugin/src/functionalTest/resources/test-source-set/src/main/proto/main_scope.proto new file mode 100644 index 0000000000..9ed9c60dd6 --- /dev/null +++ b/gradle-plugin/src/functionalTest/resources/test-source-set/src/main/proto/main_scope.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package spine.compiler.test; + +option java_package = "io.spine.tools.compiler.test"; +option java_outer_classname = "MainScopeProto"; +option java_multiple_files = true; + +// A `main`-scope definition so that the `main` source set has generated code too. +message MainScoped { +} diff --git a/gradle-plugin/src/functionalTest/resources/test-source-set/src/test/proto/test_scope.proto b/gradle-plugin/src/functionalTest/resources/test-source-set/src/test/proto/test_scope.proto new file mode 100644 index 0000000000..38ba072050 --- /dev/null +++ b/gradle-plugin/src/functionalTest/resources/test-source-set/src/test/proto/test_scope.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package spine.compiler.test; + +option java_package = "io.spine.tools.compiler.test"; +option java_outer_classname = "TestScopeProto"; +option java_multiple_files = true; + +// A `test`-scope definition. The Compiler generates code for it under +// `generated/test`, while `protoc` puts its output under +// `build/generated/sources/proto/test`. Both must not end up in the `test` +// source set at the same time, or the compilation fails with duplicate classes. +message TestScoped { +} diff --git a/gradle-plugin/src/main/kotlin/io/spine/tools/compiler/gradle/plugin/Paths.kt b/gradle-plugin/src/main/kotlin/io/spine/tools/compiler/gradle/plugin/Paths.kt index 431e3a7d92..e4a917481f 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/tools/compiler/gradle/plugin/Paths.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/tools/compiler/gradle/plugin/Paths.kt @@ -55,6 +55,27 @@ internal fun Iterable.excluding(excludeDir: File): Set = /** * Tells if this file resides in the given [directory]. + * + * Both paths are resolved to their canonical form before the comparison, so the + * check is not defeated by symbolic links. Comparing a canonical path against + * a merely absolute one (which keeps symlinks unresolved) could otherwise yield + * a false negative when the project resides under a symlinked path — leaving the + * `protoc` output directory unfiltered and the generated classes duplicated. + * + * The comparison is performed on [Path] name components, so a sibling directory + * such as `…/test2` is not mistaken for residing in `…/test`. */ internal fun File.residesIn(directory: File): Boolean = - canonicalFile.startsWith(directory.absolutePath) + residesIn(directory.canonicalFile.toPath()) + +/** + * Tells if this file resides under [canonicalDir] — a path that is *already* in + * canonical form. + * + * Resolving a path to its canonical form is a filesystem operation. This overload + * lets a caller canonicalize the directory once and reuse the result across many + * files — for example, while filtering a large source set — instead of + * re-canonicalizing the directory on every call, as the `File`-typed overload does. + */ +internal fun File.residesIn(canonicalDir: Path): Boolean = + canonicalFile.toPath().startsWith(canonicalDir) diff --git a/gradle-plugin/src/main/kotlin/io/spine/tools/compiler/gradle/plugin/Plugin.kt b/gradle-plugin/src/main/kotlin/io/spine/tools/compiler/gradle/plugin/Plugin.kt index 86882879ac..7cbefd5787 100644 --- a/gradle-plugin/src/main/kotlin/io/spine/tools/compiler/gradle/plugin/Plugin.kt +++ b/gradle-plugin/src/main/kotlin/io/spine/tools/compiler/gradle/plugin/Plugin.kt @@ -56,6 +56,7 @@ import io.spine.tools.compiler.params.WorkingDirectory import io.spine.tools.gradle.lib.LibraryPlugin import io.spine.tools.gradle.lib.spineExtension import io.spine.tools.gradle.project.hasJavaOrKotlin +import io.spine.tools.gradle.project.hasKotlin import io.spine.tools.gradle.project.sourceSets import io.spine.tools.gradle.task.SpineTaskGroup import io.spine.tools.meta.ArtifactMeta @@ -67,11 +68,16 @@ import io.spine.tools.protobuf.gradle.protobufExtension import java.io.File import java.nio.file.Path import org.gradle.api.Project +import org.gradle.api.file.FileTreeElement +import org.gradle.api.specs.Spec import org.gradle.api.tasks.Delete import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.api.tasks.util.PatternFilterable import org.gradle.kotlin.dsl.exclude import org.gradle.kotlin.dsl.register +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask /** * The Gradle plugin of the Spine Compiler. @@ -299,6 +305,74 @@ private fun Project.configureWithProtobufPlugin(compilerVersion: String) { } setProtocPluginArtifact(protocPlugin) configureGenerateProtoTasks() + excludeProtocOutputFromCompilation() + } +} + +/** + * Excludes the `protoc` output directory from the input of the Java and Kotlin + * compilation tasks of this project. + * + * The Compiler takes the code generated by `protoc` as its input and writes the + * processed code into a separate directory (`generated` by default). The plugin + * adds that directory to the source sets. The `protoc` output directory + * ([protocOutputDir]) must be kept out of the compilation: otherwise each + * generated class is compiled twice — once from the `protoc` output and once + * from the Compiler output — and the build fails with duplicate class errors. + * + * The plugin already removes the `protoc` directories from the source sets via + * [configureSourceSetDirs][io.spine.tools.protobuf.gradle.plugin.configureSourceSetDirs]. + * That removal rewrites the source-set directories once during configuration and + * does not always hold for the `test` source set: depending on the order in which + * the plugins are applied, a symlinked project path, or another plugin re-adding + * the directory, the `protoc` output may end up back in the source set. This + * function adds an order-independent line of defence by filtering the compilation + * input directly. The filter is evaluated when a compilation task resolves its + * sources, so it holds regardless of when the directory was added to the source + * set. + * + * The deprecated in-place mode, in which the Compiler overwrites the `protoc` + * output instead of writing to a separate directory, is left untouched: there the + * generated code legitimately resides in the `protoc` output directory, so + * filtering it out would drop it from compilation. + * + * @see [GenerateProtoTask.configureSourceSetDirs] + */ +private fun Project.excludeProtocOutputFromCompilation() { + afterEvaluate { + // Canonicalize the `protoc` output directory once and reuse it while + // filtering each source file, instead of re-canonicalizing it per file. + val protocOutput = protocOutputDir.canonicalFile.toPath() + val compilerWritesInPlace = generatedDir.toFile().residesIn(protocOutput) + if (compilerWritesInPlace) { + return@afterEvaluate + } + // `JavaCompile` ignores an `exclude(Spec)` predicate for the files it + // passes to `javac`, so its source is re-set to a filtered view of + // itself. The view wraps the task's own (live) source and preserves the + // task dependencies it carries, so the generated code is still produced + // before compilation. + tasks.withType(JavaCompile::class.java).configureEach { task -> + task.source = task.source.filter { file -> + !file.residesIn(protocOutput) + }.asFileTree + } + // `KotlinCompile` honors `exclude(Spec)` — a live predicate that needs no + // source re-wiring. The cast targets `KotlinCompile` (which is a + // `PatternFilterable`); any other `KotlinCompilationTask` is skipped. + // + // The Kotlin Gradle Plugin is a `compileOnly` dependency, so its task type + // is not on the runtime classpath. The reference to `KotlinCompilationTask` + // is therefore guarded by `hasKotlin()` (a name-based check), keeping the + // plugin loadable for consumers that apply it without Kotlin. + if (hasKotlin()) { + val underProtocOutput = Spec { element -> + element.file.residesIn(protocOutput) + } + tasks.withType(KotlinCompilationTask::class.java).configureEach { task -> + (task as? PatternFilterable)?.exclude(underProtocOutput) + } + } } } diff --git a/gradle-plugin/src/test/kotlin/io/spine/tools/compiler/gradle/plugin/ExcludeProtocOutputSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/tools/compiler/gradle/plugin/ExcludeProtocOutputSpec.kt new file mode 100644 index 0000000000..52e77aff11 --- /dev/null +++ b/gradle-plugin/src/test/kotlin/io/spine/tools/compiler/gradle/plugin/ExcludeProtocOutputSpec.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * 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. + */ + +package io.spine.tools.compiler.gradle.plugin + +import com.google.protobuf.gradle.ProtobufPlugin +import io.kotest.matchers.shouldBe +import java.io.File +import org.gradle.api.internal.project.ProjectInternal +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.kotlin.dsl.apply +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +@DisplayName("The plugin should exclude the `protoc` output from compilation") +internal class ExcludeProtocOutputSpec { + + /** + * Reproduces the state in which the `protoc` output directory is among the + * source directories of the `main` source set, and verifies that the file it + * contains does not reach the `compileJava` task — see + * [issue #19](https://github.com/SpineEventEngine/compiler/issues/19). + */ + @Test + fun `keeping it out of the Java compile task source`(@TempDir projectDir: File) { + val project = ProjectBuilder.builder().withProjectDir(projectDir).build() + project.group = "io.spine.stubs" + with(project) { + apply(plugin = "java") + apply() + apply() + repositories.mavenLocal() + } + + // A file under the default `protoc` output directory, added to the source set + // as if the source-set-level deduplication had not removed it. + val protocJavaDir = projectDir.resolve("build/generated/sources/proto/main/java") + protocJavaDir.mkdirs() + val leaked = protocJavaDir.resolve("Leaked.java") + leaked.writeText("class Leaked {}") + val sourceSets = project.extensions.getByType(SourceSetContainer::class.java) + sourceSets.getByName("main").java.srcDir(protocJavaDir) + + (project as ProjectInternal).evaluate() + + val compileJava = project.tasks.getByName("compileJava") as JavaCompile + compileJava.source.contains(leaked) shouldBe false + } +} diff --git a/gradle-plugin/src/test/kotlin/io/spine/tools/compiler/gradle/plugin/PathsSpec.kt b/gradle-plugin/src/test/kotlin/io/spine/tools/compiler/gradle/plugin/PathsSpec.kt new file mode 100644 index 0000000000..4dd113b7ed --- /dev/null +++ b/gradle-plugin/src/test/kotlin/io/spine/tools/compiler/gradle/plugin/PathsSpec.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * 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. + */ + +package io.spine.tools.compiler.gradle.plugin + +import io.kotest.matchers.shouldBe +import java.io.File +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +@DisplayName("`File.residesIn` should") +internal class PathsSpec { + + @Test + fun `treat a nested file as residing in the directory`(@TempDir dir: File) { + val root = dir.resolve("test").apply { mkdirs() } + val nested = root.resolve("java/Stub.java") + + nested.residesIn(root) shouldBe true + } + + @Test + fun `treat the directory itself as residing in it`(@TempDir dir: File) { + val root = dir.resolve("test").apply { mkdirs() } + + root.residesIn(root) shouldBe true + } + + @Test + fun `not treat a sibling directory as residing in it`(@TempDir dir: File) { + val test = dir.resolve("test").apply { mkdirs() } + val sibling = dir.resolve("test2").apply { mkdirs() } + + sibling.residesIn(test) shouldBe false + } + + @Test + fun `accept a directory that is already canonical`(@TempDir dir: File) { + val root = dir.resolve("test").apply { mkdirs() } + val nested = root.resolve("Stub.java") + + nested.residesIn(root.canonicalFile.toPath()) shouldBe true + } +} diff --git a/version.gradle.kts b/version.gradle.kts index 7d7b1687d5..a31766223d 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -30,10 +30,11 @@ * This version is also used by integration test projects. * E.g. see `tests/consumer/build.gradle.kts`. */ -val compilerVersion: String by extra("2.0.0-SNAPSHOT.058") +private val compilerVersion = "2.0.0-SNAPSHOT.059" +extra.set("compilerVersion", compilerVersion) /** * The version, same as [compilerVersion], which is used for publishing * the Compiler Maven artifacts. */ -val versionToPublish by extra(compilerVersion) +extra.set("versionToPublish", compilerVersion)