From 6897b403de0e06ff595c14535289e6bd8826adf9 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 24 Sep 2026 16:47:18 +0200 Subject: [PATCH 1/4] fix: fingerprint the Muzzle validation JVM --- .../gradle/plugin/muzzle/tasks/MuzzleTask.kt | 46 +++++++-- .../plugin/muzzle/MuzzleJvmCacheInputsTest.kt | 94 +++++++++++++++++++ docs/how_to_test.md | 2 + 3 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleJvmCacheInputsTest.kt diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt index b338e096456..2730ee98bd8 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt @@ -18,10 +18,12 @@ import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Classpath import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.jvm.toolchain.JavaLauncher import org.gradle.jvm.toolchain.JavaToolchainService import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.property @@ -72,6 +74,37 @@ abstract class MuzzleTask @Inject constructor( @get:Optional val muzzleDirective: Property = objects.property() + @get:Nested + @get:Optional + val javaLauncher: Property = objects.property().convention( + muzzleDirective.map { it.javaVersion }.flatMap { version -> + javaToolchainService.launcherFor { + languageVersion.set(JavaLanguageVersion.of(version)) + } + } + ).apply { finalizeValueOnRead() } + + // Gradle's nested launcher input does not include the vendor or full JVM versions. + @get:Input + val jvmIdentity = javaLauncher.map { launcher -> + with(launcher.metadata) { + mapOf( + "languageVersion" to languageVersion.asInt().toString(), + "vendor" to vendor, + "runtimeVersion" to javaRuntimeVersion, + "vmVersion" to jvmVersion, + ) + } + }.orElse(providers.provider { + // Workers without process isolation execute in the Gradle daemon. + mapOf( + "languageVersion" to System.getProperty("java.specification.version"), + "vendor" to System.getProperty("java.vendor"), + "runtimeVersion" to System.getProperty("java.runtime.version"), + "vmVersion" to System.getProperty("java.vm.version"), + ) + }) + @get:OutputFile val result: RegularFileProperty = objects.fileProperty().convention( project.layout.buildDirectory.file("reports/$name.txt") @@ -95,29 +128,26 @@ abstract class MuzzleTask @Inject constructor( } private fun assertMuzzle(muzzleDirective: MuzzleDirective? = null) { - val workQueue = if (muzzleDirective?.javaVersion != null) { - val javaLauncher = javaToolchainService.launcherFor { - languageVersion.set(JavaLanguageVersion.of(muzzleDirective.javaVersion!!)) - }.get() + val launcher = javaLauncher.orNull + val workQueue = if (launcher != null) { // Note process isolation leaks gradle dependencies to the child process // and may need additional code on muzzle plugin to filter those out // See https://github.com/gradle/gradle/issues/33987 workerExecutor.processIsolation { forkOptions { // datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin needs reflective access to ClassLoader.findLoadedClass - if(javaLauncher.metadata.languageVersion > JavaLanguageVersion.of(9)) { + if(launcher.metadata.languageVersion > JavaLanguageVersion.of(9)) { jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") } if (HostPlatform.isLinuxArm64()) { // Disable CDS to avoid SIGSEGVs on Linux arm64. jvmArgs("-Xshare:off") } - executable(javaLauncher.executablePath) + executable(launcher.executablePath) } } } else { - // noIsolation worker is OK for muzzle tasks as their checks will inspect classes outline - // and should not be impacted by the actual running JDK. + // The daemon JVM is included in jvmIdentity for these checks. workerExecutor.noIsolation() } workQueue.submit(MuzzleAction::class.java) { diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleJvmCacheInputsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleJvmCacheInputsTest.kt new file mode 100644 index 00000000000..e00942208df --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleJvmCacheInputsTest.kt @@ -0,0 +1,94 @@ +package datadog.gradle.plugin.muzzle + +import org.assertj.core.api.Assertions.assertThat +import org.gradle.testkit.runner.TaskOutcome.FROM_CACHE +import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE +import org.junit.jupiter.api.Test + +class MuzzleJvmCacheInputsTest : MuzzlePluginTestFixture() { + @Test + fun `muzzle cache follows the daemon JVM when no toolchain is requested`() { + writeProject( + """ + plugins { + id("java") + id("dd-trace-java.muzzle") + } + + muzzle { pass { coreJdk() } } + + // Simulate a changed daemon runtime without requiring several installed JDKs. + val changedField = providers.gradleProperty("changedJvmField").orNull + if (changedField != null) { + val original = System.getProperty(changedField) + System.setProperty(changedField, "different-runtime") + gradle.buildFinished { System.setProperty(changedField, original) } + } + """ + ) + assertCacheTracks(listOf("java.vendor", "java.runtime.version", "java.vm.version")) + } + + @Test + fun `muzzle cache follows the launcher used by the isolated worker`() { + writeProject( + """ + import datadog.gradle.plugin.muzzle.tasks.MuzzleTask + import org.gradle.jvm.toolchain.JavaInstallationMetadata + import org.gradle.jvm.toolchain.JavaLauncher + + plugins { + id("java") + id("dd-trace-java.muzzle") + } + + muzzle { pass { coreJdk(JavaVersion.current().majorVersion) } } + + // Keep the executable and major version fixed to isolate each additional input. + class SelectedLauncher( + private val delegate: JavaLauncher, + private val changedField: String, + ) : JavaLauncher by delegate { + override fun getMetadata(): JavaInstallationMetadata = + object : JavaInstallationMetadata by delegate.metadata { + override fun getVendor() = + if (changedField == "vendor") "different-vendor" else delegate.metadata.vendor + override fun getJavaRuntimeVersion() = + if (changedField == "runtimeVersion") "different-runtime" else delegate.metadata.javaRuntimeVersion + override fun getJvmVersion() = + if (changedField == "vmVersion") "different-vm" else delegate.metadata.jvmVersion + } + } + + val changedField = providers.gradleProperty("changedJvmField").orElse("") + val launcher = javaToolchains.launcherFor {} + tasks.withType().configureEach { + if (name == "muzzle-AssertPass-core-jdk") { + javaLauncher.set(launcher.map { SelectedLauncher(it, changedField.get()) }) + } + } + """ + ) + assertCacheTracks(listOf("vendor", "runtimeVersion", "vmVersion")) + } + + private fun assertCacheTracks(fields: List) { + writeFile("settings.gradle.kts", "buildCache { local { directory = file(\"task-cache\") } }", append = true) + writeNoopScanPlugin() + val task = ":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk" + val first = run("muzzle", "--build-cache") + assertThat(first.task(task)?.outcome).describedAs(first.output).isEqualTo(SUCCESS) + + val unchanged = run("muzzle", "--build-cache") + assertThat(unchanged.task(task)?.outcome).describedAs(unchanged.output).isEqualTo(UP_TO_DATE) + + for (field in fields) { + val restored = run("clean", "muzzle", "--build-cache") + assertThat(restored.task(task)?.outcome).describedAs(restored.output).isEqualTo(FROM_CACHE) + + val changed = run("muzzle", "--build-cache", "-PchangedJvmField=$field") + assertThat(changed.task(task)?.outcome).describedAs(changed.output).isEqualTo(SUCCESS) + } + } +} diff --git a/docs/how_to_test.md b/docs/how_to_test.md index a0bcefdb368..8f271ab9deb 100644 --- a/docs/how_to_test.md +++ b/docs/how_to_test.md @@ -18,6 +18,8 @@ The project leverages different types of tests: 3. The third type of tests is **Muzzle checks**. Their goal is to check the [Muzzle directives](./how_instrumentations_work.md#muzzle), making sure instrumentations are safe to load against specific library versions. + Each check fingerprints its validation JVM's vendor and full runtime/VM versions: the selected toolchain when `javaVersion` is specified, otherwise the Gradle daemon JVM. + Changing these values invalidates cached results even within the same Java major version. 4. The fourth type of tests is **integration tests**. They test features that require a more complex environment setup. From 32c756442bf6c453c10ee11b2a61efe4da0b8363 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 24 Sep 2026 17:57:10 +0200 Subject: [PATCH 2/4] fix: limit Muzzle JVM inputs to core JDK checks --- .../gradle/plugin/muzzle/tasks/MuzzleTask.kt | 42 +++++---- .../plugin/muzzle/MuzzleJvmCacheInputsTest.kt | 93 ++++++++++++++++--- docs/how_to_test.md | 2 +- 3 files changed, 101 insertions(+), 36 deletions(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt index 2730ee98bd8..48fd66c8861 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt @@ -18,7 +18,7 @@ import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Classpath import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction @@ -74,8 +74,7 @@ abstract class MuzzleTask @Inject constructor( @get:Optional val muzzleDirective: Property = objects.property() - @get:Nested - @get:Optional + @get:Internal val javaLauncher: Property = objects.property().convention( muzzleDirective.map { it.javaVersion }.flatMap { version -> javaToolchainService.launcherFor { @@ -84,26 +83,30 @@ abstract class MuzzleTask @Inject constructor( } ).apply { finalizeValueOnRead() } - // Gradle's nested launcher input does not include the vendor or full JVM versions. @get:Input - val jvmIdentity = javaLauncher.map { launcher -> - with(launcher.metadata) { + @get:Optional + val coreJdkIdentity = providers.provider { + if (muzzleDirective.orNull?.isCoreJdk != true) { + return@provider null + } + val metadata = javaLauncher.orNull?.metadata + if (metadata != null) { mapOf( - "languageVersion" to languageVersion.asInt().toString(), - "vendor" to vendor, - "runtimeVersion" to javaRuntimeVersion, - "vmVersion" to jvmVersion, + "languageVersion" to metadata.languageVersion.asInt().toString(), + "vendor" to metadata.vendor, + "runtimeVersion" to metadata.javaRuntimeVersion, + "vmVersion" to metadata.jvmVersion, + ) + } else { + // coreJdk() without a version executes in the Gradle daemon. + mapOf( + "languageVersion" to System.getProperty("java.specification.version"), + "vendor" to System.getProperty("java.vendor"), + "runtimeVersion" to System.getProperty("java.runtime.version"), + "vmVersion" to System.getProperty("java.vm.version"), ) } - }.orElse(providers.provider { - // Workers without process isolation execute in the Gradle daemon. - mapOf( - "languageVersion" to System.getProperty("java.specification.version"), - "vendor" to System.getProperty("java.vendor"), - "runtimeVersion" to System.getProperty("java.runtime.version"), - "vmVersion" to System.getProperty("java.vm.version"), - ) - }) + } @get:OutputFile val result: RegularFileProperty = objects.fileProperty().convention( @@ -147,7 +150,6 @@ abstract class MuzzleTask @Inject constructor( } } } else { - // The daemon JVM is included in jvmIdentity for these checks. workerExecutor.noIsolation() } workQueue.submit(MuzzleAction::class.java) { diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleJvmCacheInputsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleJvmCacheInputsTest.kt index e00942208df..19a1b497ccc 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleJvmCacheInputsTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleJvmCacheInputsTest.kt @@ -1,14 +1,32 @@ package datadog.gradle.plugin.muzzle import org.assertj.core.api.Assertions.assertThat +import org.gradle.testkit.runner.TaskOutcome import org.gradle.testkit.runner.TaskOutcome.FROM_CACHE +import org.gradle.testkit.runner.TaskOutcome.SKIPPED import org.gradle.testkit.runner.TaskOutcome.SUCCESS import org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource class MuzzleJvmCacheInputsTest : MuzzlePluginTestFixture() { - @Test - fun `muzzle cache follows the daemon JVM when no toolchain is requested`() { + @ParameterizedTest + @ValueSource(strings = ["coreJdk", "library", "fallback"]) + fun `only core JDK checks fingerprint the daemon JVM`(check: String) { + val repo = createMavenRepoFixture() + repo.publishVersions("com.example.test", "demo-lib", listOf("1.0.0")) + val directive = when (check) { + "coreJdk" -> "muzzle { pass { coreJdk() } }" + "library" -> """ + muzzle { pass { + group = "com.example.test" + module = "demo-lib" + versions = "[1.0.0,2.0.0)" + } } + """ + else -> "" + } writeProject( """ plugins { @@ -16,7 +34,9 @@ class MuzzleJvmCacheInputsTest : MuzzlePluginTestFixture() { id("dd-trace-java.muzzle") } - muzzle { pass { coreJdk() } } + repositories { maven { url = uri("${repo.repoUrl}") } } + + $directive // Simulate a changed daemon runtime without requiring several installed JDKs. val changedField = providers.gradleProperty("changedJvmField").orNull @@ -27,11 +47,21 @@ class MuzzleJvmCacheInputsTest : MuzzlePluginTestFixture() { } """ ) - assertCacheTracks(listOf("java.vendor", "java.runtime.version", "java.vm.version")) + val taskName = when (check) { + "coreJdk" -> "muzzle-AssertPass-core-jdk" + "library" -> "muzzle-AssertPass-com.example.test-demo-lib-1.0.0" + else -> "muzzle" + } + assertCacheTracks( + listOf("java.vendor", "java.runtime.version", "java.vm.version"), + taskName, + if (check == "coreJdk") SUCCESS else UP_TO_DATE, + mapOf("MAVEN_REPOSITORY_PROXY" to repo.repoUrl), + ) } @Test - fun `muzzle cache follows the launcher used by the isolated worker`() { + fun `core JDK cache follows the launcher used by the isolated worker`() { writeProject( """ import datadog.gradle.plugin.muzzle.tasks.MuzzleTask @@ -61,11 +91,11 @@ class MuzzleJvmCacheInputsTest : MuzzlePluginTestFixture() { } } - val changedField = providers.gradleProperty("changedJvmField").orElse("") + val changedField = providers.gradleProperty("changedJvmField").orNull val launcher = javaToolchains.launcherFor {} tasks.withType().configureEach { - if (name == "muzzle-AssertPass-core-jdk") { - javaLauncher.set(launcher.map { SelectedLauncher(it, changedField.get()) }) + if (name == "muzzle-AssertPass-core-jdk" && changedField != null) { + javaLauncher.set(launcher.map { SelectedLauncher(it, changedField) }) } } """ @@ -73,22 +103,55 @@ class MuzzleJvmCacheInputsTest : MuzzlePluginTestFixture() { assertCacheTracks(listOf("vendor", "runtimeVersion", "vmVersion")) } - private fun assertCacheTracks(fields: List) { + @ParameterizedTest + @ValueSource(strings = ["--dry-run", "-PskipMuzzle"]) + fun `skipped core JDK checks do not resolve the toolchain`(argument: String) { + writeProject( + """ + import datadog.gradle.plugin.muzzle.tasks.MuzzleTask + + plugins { + id("java") + id("dd-trace-java.muzzle") + } + + muzzle { pass { coreJdk("999") } } + + tasks.withType().configureEach { + onlyIf { !providers.gradleProperty("skipMuzzle").isPresent } + } + """ + ) + writeFile("gradle.properties", "org.gradle.java.installations.auto-download=false") + val result = run("muzzle", argument) + assertThat(result.output).contains("BUILD SUCCESSFUL") + if (argument == "-PskipMuzzle") { + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .isEqualTo(SKIPPED) + } + } + + private fun assertCacheTracks( + fields: List, + taskName: String = "muzzle-AssertPass-core-jdk", + changedOutcome: TaskOutcome = SUCCESS, + env: Map = emptyMap(), + ) { writeFile("settings.gradle.kts", "buildCache { local { directory = file(\"task-cache\") } }", append = true) writeNoopScanPlugin() - val task = ":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk" - val first = run("muzzle", "--build-cache") + val task = ":dd-java-agent:instrumentation:demo:$taskName" + val first = run("muzzle", "--build-cache", env = env) assertThat(first.task(task)?.outcome).describedAs(first.output).isEqualTo(SUCCESS) - val unchanged = run("muzzle", "--build-cache") + val unchanged = run("muzzle", "--build-cache", env = env) assertThat(unchanged.task(task)?.outcome).describedAs(unchanged.output).isEqualTo(UP_TO_DATE) for (field in fields) { - val restored = run("clean", "muzzle", "--build-cache") + val restored = run("clean", "muzzle", "--build-cache", env = env) assertThat(restored.task(task)?.outcome).describedAs(restored.output).isEqualTo(FROM_CACHE) - val changed = run("muzzle", "--build-cache", "-PchangedJvmField=$field") - assertThat(changed.task(task)?.outcome).describedAs(changed.output).isEqualTo(SUCCESS) + val changed = run("muzzle", "--build-cache", "-PchangedJvmField=$field", env = env) + assertThat(changed.task(task)?.outcome).describedAs(changed.output).isEqualTo(changedOutcome) } } } diff --git a/docs/how_to_test.md b/docs/how_to_test.md index 8f271ab9deb..6fee772fd0c 100644 --- a/docs/how_to_test.md +++ b/docs/how_to_test.md @@ -18,7 +18,7 @@ The project leverages different types of tests: 3. The third type of tests is **Muzzle checks**. Their goal is to check the [Muzzle directives](./how_instrumentations_work.md#muzzle), making sure instrumentations are safe to load against specific library versions. - Each check fingerprints its validation JVM's vendor and full runtime/VM versions: the selected toolchain when `javaVersion` is specified, otherwise the Gradle daemon JVM. + `coreJdk(version)` checks fingerprint the selected JDK's major version, vendor and full runtime/VM versions; `coreJdk()` tracks the Gradle daemon JVM instead. Changing these values invalidates cached results even within the same Java major version. 4. The fourth type of tests is **integration tests**. From aa5d8047e8fe2f99b0f4bc90a8253824fb14e795 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 24 Sep 2026 18:02:47 +0200 Subject: [PATCH 3/4] docs: explain Muzzle JVM cache inputs --- .../datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt index 48fd66c8861..e59bb9168af 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt @@ -74,6 +74,10 @@ abstract class MuzzleTask @Inject constructor( @get:Optional val muzzleDirective: Property = objects.property() + /** + * Shares one resolved toolchain between core-JDK fingerprinting and worker execution. + * Kept internal so task graph discovery does not resolve JDKs for skipped checks or dry runs. + */ @get:Internal val javaLauncher: Property = objects.property().convention( muzzleDirective.map { it.javaVersion }.flatMap { version -> @@ -83,6 +87,10 @@ abstract class MuzzleTask @Inject constructor( } ).apply { finalizeValueOnRead() } + /** + * Tracks the validation JDK because its platform classes are outside the classpath inputs. + * Vendor and full runtime/VM versions distinguish JDKs within the same Java major version. + */ @get:Input @get:Optional val coreJdkIdentity = providers.provider { From 0c935616364a8ad903247e1243282ca8eaf2fd67 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 24 Sep 2026 19:58:36 +0200 Subject: [PATCH 4/4] fix: complete Muzzle JVM cache inputs --- .../gradle/plugin/muzzle/tasks/MuzzleTask.kt | 19 +++++---- .../plugin/muzzle/MuzzleJvmCacheInputsTest.kt | 41 ++++++++++++++++--- docs/how_to_test.md | 2 +- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt index e59bb9168af..45b3f84272b 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt @@ -75,7 +75,7 @@ abstract class MuzzleTask @Inject constructor( val muzzleDirective: Property = objects.property() /** - * Shares one resolved toolchain between core-JDK fingerprinting and worker execution. + * Shares one resolved toolchain between JVM fingerprinting and worker execution. * Kept internal so task graph discovery does not resolve JDKs for skipped checks or dry runs. */ @get:Internal @@ -88,15 +88,12 @@ abstract class MuzzleTask @Inject constructor( ).apply { finalizeValueOnRead() } /** - * Tracks the validation JDK because its platform classes are outside the classpath inputs. - * Vendor and full runtime/VM versions distinguish JDKs within the same Java major version. + * Tracks the validation JVM because its platform classes are outside the classpath inputs. + * Vendor, full runtime/VM versions, OS, and architecture distinguish JVM installations. */ @get:Input @get:Optional - val coreJdkIdentity = providers.provider { - if (muzzleDirective.orNull?.isCoreJdk != true) { - return@provider null - } + val validationJvmIdentity = providers.provider { val metadata = javaLauncher.orNull?.metadata if (metadata != null) { mapOf( @@ -104,15 +101,21 @@ abstract class MuzzleTask @Inject constructor( "vendor" to metadata.vendor, "runtimeVersion" to metadata.javaRuntimeVersion, "vmVersion" to metadata.jvmVersion, + "operatingSystem" to System.getProperty("os.name"), + "architecture" to System.getProperty("os.arch"), ) - } else { + } else if (muzzleDirective.orNull?.isCoreJdk == true) { // coreJdk() without a version executes in the Gradle daemon. mapOf( "languageVersion" to System.getProperty("java.specification.version"), "vendor" to System.getProperty("java.vendor"), "runtimeVersion" to System.getProperty("java.runtime.version"), "vmVersion" to System.getProperty("java.vm.version"), + "operatingSystem" to System.getProperty("os.name"), + "architecture" to System.getProperty("os.arch"), ) + } else { + null } } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleJvmCacheInputsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleJvmCacheInputsTest.kt index 19a1b497ccc..2f5d3d6b140 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleJvmCacheInputsTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleJvmCacheInputsTest.kt @@ -53,15 +53,33 @@ class MuzzleJvmCacheInputsTest : MuzzlePluginTestFixture() { else -> "muzzle" } assertCacheTracks( - listOf("java.vendor", "java.runtime.version", "java.vm.version"), + listOf("java.vendor", "java.runtime.version", "java.vm.version", "os.name", "os.arch"), taskName, if (check == "coreJdk") SUCCESS else UP_TO_DATE, mapOf("MAVEN_REPOSITORY_PROXY" to repo.repoUrl), ) } - @Test - fun `core JDK cache follows the launcher used by the isolated worker`() { + @ParameterizedTest + @ValueSource(strings = ["coreJdk", "library"]) + fun `versioned check cache follows the launcher used by the isolated worker`(check: String) { + val repo = createMavenRepoFixture() + repo.publishVersions("com.example.test", "demo-lib", listOf("1.0.0")) + val taskName = if (check == "coreJdk") { + "muzzle-AssertPass-core-jdk" + } else { + "muzzle-AssertPass-com.example.test-demo-lib-1.0.0" + } + val directive = if (check == "coreJdk") { + "coreJdk(JavaVersion.current().majorVersion)" + } else { + """ + group = "com.example.test" + module = "demo-lib" + versions = "[1.0.0,2.0.0)" + javaVersion = JavaVersion.current().majorVersion + """ + } writeProject( """ import datadog.gradle.plugin.muzzle.tasks.MuzzleTask @@ -73,7 +91,9 @@ class MuzzleJvmCacheInputsTest : MuzzlePluginTestFixture() { id("dd-trace-java.muzzle") } - muzzle { pass { coreJdk(JavaVersion.current().majorVersion) } } + repositories { maven { url = uri("${repo.repoUrl}") } } + + muzzle { pass { $directive } } // Keep the executable and major version fixed to isolate each additional input. class SelectedLauncher( @@ -92,15 +112,24 @@ class MuzzleJvmCacheInputsTest : MuzzlePluginTestFixture() { } val changedField = providers.gradleProperty("changedJvmField").orNull + if (changedField?.startsWith("os.") == true) { + val original = System.getProperty(changedField) + System.setProperty(changedField, "different-platform") + gradle.buildFinished { System.setProperty(changedField, original) } + } val launcher = javaToolchains.launcherFor {} tasks.withType().configureEach { - if (name == "muzzle-AssertPass-core-jdk" && changedField != null) { + if (name == "$taskName" && changedField != null) { javaLauncher.set(launcher.map { SelectedLauncher(it, changedField) }) } } """ ) - assertCacheTracks(listOf("vendor", "runtimeVersion", "vmVersion")) + assertCacheTracks( + listOf("vendor", "runtimeVersion", "vmVersion", "os.name", "os.arch"), + taskName, + env = mapOf("MAVEN_REPOSITORY_PROXY" to repo.repoUrl), + ) } @ParameterizedTest diff --git a/docs/how_to_test.md b/docs/how_to_test.md index 6fee772fd0c..4478fb02833 100644 --- a/docs/how_to_test.md +++ b/docs/how_to_test.md @@ -18,7 +18,7 @@ The project leverages different types of tests: 3. The third type of tests is **Muzzle checks**. Their goal is to check the [Muzzle directives](./how_instrumentations_work.md#muzzle), making sure instrumentations are safe to load against specific library versions. - `coreJdk(version)` checks fingerprint the selected JDK's major version, vendor and full runtime/VM versions; `coreJdk()` tracks the Gradle daemon JVM instead. + `coreJdk(version)` and library checks with `javaVersion` fingerprint the selected JDK's major version, vendor, full runtime/VM versions, OS, and architecture; `coreJdk()` tracks the Gradle daemon JVM instead. Changing these values invalidates cached results even within the same Java major version. 4. The fourth type of tests is **integration tests**.