From eb823662256c6516313d739104e838fd86f28a87 Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Tue, 3 Nov 2026 20:09:35 +0100 Subject: [PATCH 1/3] TS-48500 Gradle plugin: Accept read-only JaCoCo excludes The plugin appended its default excludes to the build's excludes list in place, so a read-only list such as Kotlin's listOf(...) made applying the plugin fail with an UnsupportedOperationException. It now assigns a new list with the same content and order. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../kotlin/com/teamscale/TeamscalePlugin.kt | 2 +- .../TeamscalePluginBuildExcludesTest.kt | 59 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginBuildExcludesTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 59602f706..b0f0de534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ We use [semantic versioning](http://semver.org/): - [fix] _teamscale-gradle-plugin_, _teamscale-maven-plugin_: Testwise coverage reports no longer drop a class directory that contains an archive (JAR, WAR, EAR, ZIP) that contains a JAR. - [fix] _agent_: With `duplicates=FAIL`, the error for a duplicate, non-identical class now names the class and how to resolve the problem. - [fix] _agent_, _teamscale-gradle-plugin_, _teamscale-maven-plugin_: A corrupt class file whose class name cannot be read is skipped with a warning instead of failing the coverage report. +- [fix] _teamscale-gradle-plugin_: The plugin now applies when the build sets `jacoco.excludes` of a test task to a read-only list (e.g. `listOf(...)` in the Kotlin DSL). Previously applying the plugin failed with an `UnsupportedOperationException`. # 39.0.0 - [breaking] _agent_: The deprecated Docker image `cqse/teamscale-jacoco-agent` is no longer published. Use `cqse/teamscale-java-profiler` instead, which has replaced it since version 36.5.0. diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscalePlugin.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscalePlugin.kt index ea6260885..e0ad4f43a 100755 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscalePlugin.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscalePlugin.kt @@ -141,7 +141,7 @@ class TeamscalePlugin : Plugin { AgentPortGenerator::class.java ) {} project.tasks.withType { - jacoco.excludes?.addAll(DEFAULT_EXCLUDES) + jacoco.excludes?.let { buildExcludes -> jacoco.excludes = buildExcludes + DEFAULT_EXCLUDES } val extension = this.extensions.create( TEAMSCALE_EXTENSION_NAME, teamscaleJacocoAgentConfiguration, jacoco diff --git a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginBuildExcludesTest.kt b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginBuildExcludesTest.kt new file mode 100644 index 000000000..c1f636312 --- /dev/null +++ b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginBuildExcludesTest.kt @@ -0,0 +1,59 @@ +package com.teamscale + +import org.assertj.core.api.Assertions.assertThat +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.named +import org.gradle.kotlin.dsl.withType +import org.gradle.testfixtures.ProjectBuilder +import org.gradle.testing.jacoco.plugins.JacocoPlugin +import org.gradle.testing.jacoco.plugins.JacocoTaskExtension +import org.junit.jupiter.api.Test as JUnitTest + +/** Tests how the plugin combines its default excludes with the JaCoCo excludes that the build sets. */ +class TeamscalePluginBuildExcludesTest { + + private val buildExclude = "*\$MockitoMock\$*" + + @JUnitTest + fun `plugin appends its default excludes to read-only excludes of the build`() { + val excludes = testTaskExcludesAfterApplyingPlugin(listOf(buildExclude)) + + assertThat(excludes) + .`as`("The excludes should be the excludes of the build followed by the default excludes") + .containsExactlyElementsOf(listOf(buildExclude) + defaultExcludes()) + } + + @JUnitTest + fun `plugin appends its default excludes to mutable excludes of the build`() { + val excludes = testTaskExcludesAfterApplyingPlugin(mutableListOf(buildExclude)) + + assertThat(excludes) + .`as`("The excludes should be the excludes of the build followed by the default excludes") + .containsExactlyElementsOf(listOf(buildExclude) + defaultExcludes()) + } + + private fun testTaskExcludesAfterApplyingPlugin(buildExcludes: List): List? { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply(JavaPlugin::class.java) + project.pluginManager.apply(JacocoPlugin::class.java) + project.tasks.withType().configureEach { + extensions.configure { excludes = buildExcludes } + } + + project.pluginManager.apply(TeamscalePlugin::class.java) + return testTaskExcludesOf(project) + } + + private fun defaultExcludes(): List { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply(TeamscalePlugin::class.java) + return testTaskExcludesOf(project).orEmpty() + } + + private fun testTaskExcludesOf(project: Project) = + project.tasks.named("test").get().extensions.getByType().excludes +} From ae34975848aff661bebe8e533b15ce0ac79f1431 Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Wed, 4 Nov 2026 07:37:16 +0100 Subject: [PATCH 2/3] TS-48500 Annotate the build-excludes tests with plain @Test Teamscale exempts @Test methods from the interface comment check, but it does not resolve the JUnitTest import alias. Co-Authored-By: Claude Opus 5.5 --- .../teamscale/TeamscalePluginBuildExcludesTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginBuildExcludesTest.kt b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginBuildExcludesTest.kt index c1f636312..27f6e59e1 100644 --- a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginBuildExcludesTest.kt +++ b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginBuildExcludesTest.kt @@ -3,7 +3,7 @@ package com.teamscale import org.assertj.core.api.Assertions.assertThat import org.gradle.api.Project import org.gradle.api.plugins.JavaPlugin -import org.gradle.api.tasks.testing.Test +import org.gradle.api.tasks.testing.Test as TestTask import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.named @@ -11,14 +11,14 @@ import org.gradle.kotlin.dsl.withType import org.gradle.testfixtures.ProjectBuilder import org.gradle.testing.jacoco.plugins.JacocoPlugin import org.gradle.testing.jacoco.plugins.JacocoTaskExtension -import org.junit.jupiter.api.Test as JUnitTest +import org.junit.jupiter.api.Test /** Tests how the plugin combines its default excludes with the JaCoCo excludes that the build sets. */ class TeamscalePluginBuildExcludesTest { private val buildExclude = "*\$MockitoMock\$*" - @JUnitTest + @Test fun `plugin appends its default excludes to read-only excludes of the build`() { val excludes = testTaskExcludesAfterApplyingPlugin(listOf(buildExclude)) @@ -27,7 +27,7 @@ class TeamscalePluginBuildExcludesTest { .containsExactlyElementsOf(listOf(buildExclude) + defaultExcludes()) } - @JUnitTest + @Test fun `plugin appends its default excludes to mutable excludes of the build`() { val excludes = testTaskExcludesAfterApplyingPlugin(mutableListOf(buildExclude)) @@ -40,7 +40,7 @@ class TeamscalePluginBuildExcludesTest { val project = ProjectBuilder.builder().build() project.pluginManager.apply(JavaPlugin::class.java) project.pluginManager.apply(JacocoPlugin::class.java) - project.tasks.withType().configureEach { + project.tasks.withType().configureEach { extensions.configure { excludes = buildExcludes } } @@ -55,5 +55,5 @@ class TeamscalePluginBuildExcludesTest { } private fun testTaskExcludesOf(project: Project) = - project.tasks.named("test").get().extensions.getByType().excludes + project.tasks.named("test").get().extensions.getByType().excludes } From cb277fb6cd8b1b4410bfc39cba857f97391eeb19 Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Wed, 4 Nov 2026 07:45:39 +0100 Subject: [PATCH 3/3] TS-48500 Gradle plugin: Accept read-only JUnit engine excludes The test task action added the impacted test engine to the build's engine excludes in place, so a read-only set such as setOf(...) failed every plain test run. The build-excludes tests now also fail when the plugin stops adding its default excludes. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- .../TestImpactConfigurationAction.kt | 2 +- .../TeamscalePluginBuildExcludesTest.kt | 7 +++- .../TestImpactConfigurationActionTest.kt | 35 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TestImpactConfigurationActionTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index b0f0de534..e47c8fd49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ We use [semantic versioning](http://semver.org/): - [fix] _teamscale-gradle-plugin_, _teamscale-maven-plugin_: Testwise coverage reports no longer drop a class directory that contains an archive (JAR, WAR, EAR, ZIP) that contains a JAR. - [fix] _agent_: With `duplicates=FAIL`, the error for a duplicate, non-identical class now names the class and how to resolve the problem. - [fix] _agent_, _teamscale-gradle-plugin_, _teamscale-maven-plugin_: A corrupt class file whose class name cannot be read is skipped with a warning instead of failing the coverage report. -- [fix] _teamscale-gradle-plugin_: The plugin now applies when the build sets `jacoco.excludes` of a test task to a read-only list (e.g. `listOf(...)` in the Kotlin DSL). Previously applying the plugin failed with an `UnsupportedOperationException`. +- [fix] _teamscale-gradle-plugin_: The plugin now works when the build sets `jacoco.excludes` or the JUnit Platform `excludeEngines` of a test task to a read-only collection (for example, `listOf(...)` or `setOf(...)` in the Kotlin DSL). Previously, applying the plugin or running the test task failed with an `UnsupportedOperationException`. # 39.0.0 - [breaking] _agent_: The deprecated Docker image `cqse/teamscale-jacoco-agent` is no longer published. Use `cqse/teamscale-java-profiler` instead, which has replaced it since version 36.5.0. diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TestImpactConfigurationAction.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TestImpactConfigurationAction.kt index 3efac7ce8..842a9652a 100644 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TestImpactConfigurationAction.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TestImpactConfigurationAction.kt @@ -53,7 +53,7 @@ class TestImpactConfigurationAction( writeProperty("excludedEngines", testFrameworkOptions.excludeEngines.joinToString(",")) testFrameworkOptions.includeEngines = setOf(IMPACTED_TEST_ENGINE_ID) } else { - testFrameworkOptions.excludeEngines.add(IMPACTED_TEST_ENGINE_ID) + testFrameworkOptions.excludeEngines = testFrameworkOptions.excludeEngines + IMPACTED_TEST_ENGINE_ID } } diff --git a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginBuildExcludesTest.kt b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginBuildExcludesTest.kt index 27f6e59e1..6a9354cd3 100644 --- a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginBuildExcludesTest.kt +++ b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginBuildExcludesTest.kt @@ -51,7 +51,12 @@ class TeamscalePluginBuildExcludesTest { private fun defaultExcludes(): List { val project = ProjectBuilder.builder().build() project.pluginManager.apply(TeamscalePlugin::class.java) - return testTaskExcludesOf(project).orEmpty() + val defaultExcludes = testTaskExcludesOf(project).orEmpty() + + assertThat(defaultExcludes) + .`as`("The plugin should add default excludes to a test task without excludes of the build") + .isNotEmpty() + return defaultExcludes } private fun testTaskExcludesOf(project: Project) = diff --git a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TestImpactConfigurationActionTest.kt b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TestImpactConfigurationActionTest.kt new file mode 100644 index 000000000..87be68c8e --- /dev/null +++ b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TestImpactConfigurationActionTest.kt @@ -0,0 +1,35 @@ +package com.teamscale + +import com.teamscale.extension.TeamscalePluginExtension +import com.teamscale.extension.TeamscaleTaskExtension +import org.assertj.core.api.Assertions.assertThat +import org.gradle.api.tasks.testing.Test as TestTask +import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.named +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Test + +/** Tests how [TestImpactConfigurationAction] configures the JUnit Platform engines of a test task. */ +class TestImpactConfigurationActionTest { + + @Test + fun `plugin adds the impacted test engine to read-only engine excludes of the build`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply(TeamscalePlugin::class.java) + val testTask = project.tasks.named("test").get() + testTask.useJUnitPlatform { excludeEngines = setOf("junit-vintage") } + + runTestImpactConfigurationAction(testTask) + + assertThat((testTask.options as JUnitPlatformOptions).excludeEngines) + .`as`("The test task should exclude the engine of the build and the impacted test engine") + .containsExactlyInAnyOrder("junit-vintage", "teamscale-test-impacted") + } + + private fun runTestImpactConfigurationAction(testTask: TestTask) { + val pluginExtension = testTask.project.extensions.getByType() + val taskExtension = testTask.extensions.getByType() + TestImpactConfigurationAction(pluginExtension, taskExtension).execute(testTask) + } +}