Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.Internal
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
Expand Down Expand Up @@ -72,6 +74,51 @@ abstract class MuzzleTask @Inject constructor(
@get:Optional
val muzzleDirective: Property<MuzzleDirective> = objects.property()

/**
* 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
val javaLauncher: Property<JavaLauncher> = objects.property<JavaLauncher>().convention(
muzzleDirective.map { it.javaVersion }.flatMap { version ->
javaToolchainService.launcherFor {
languageVersion.set(JavaLanguageVersion.of(version))
}
}
).apply { finalizeValueOnRead() }

/**
* 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 validationJvmIdentity = providers.provider {
val metadata = javaLauncher.orNull?.metadata
if (metadata != null) {
mapOf(
"languageVersion" to metadata.languageVersion.asInt().toString(),
"vendor" to metadata.vendor,
"runtimeVersion" to metadata.javaRuntimeVersion,
"vmVersion" to metadata.jvmVersion,
Comment thread
bric3 marked this conversation as resolved.
"operatingSystem" to System.getProperty("os.name"),
"architecture" to System.getProperty("os.arch"),
)
} 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
}
}

@get:OutputFile
val result: RegularFileProperty = objects.fileProperty().convention(
project.layout.buildDirectory.file("reports/$name.txt")
Expand All @@ -95,29 +142,25 @@ 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.
workerExecutor.noIsolation()
}
workQueue.submit(MuzzleAction::class.java) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
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() {
@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 {
id("java")
id("dd-trace-java.muzzle")
}

repositories { maven { url = uri("${repo.repoUrl}") } }

$directive

// 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) }
}
"""
)
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", "os.name", "os.arch"),
taskName,
if (check == "coreJdk") SUCCESS else UP_TO_DATE,
mapOf("MAVEN_REPOSITORY_PROXY" to repo.repoUrl),
)
}

@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
import org.gradle.jvm.toolchain.JavaInstallationMetadata
import org.gradle.jvm.toolchain.JavaLauncher

plugins {
id("java")
id("dd-trace-java.muzzle")
}

repositories { maven { url = uri("${repo.repoUrl}") } }

muzzle { pass { $directive } }

// 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").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<MuzzleTask>().configureEach {
if (name == "$taskName" && changedField != null) {
javaLauncher.set(launcher.map { SelectedLauncher(it, changedField) })
}
}
"""
)
assertCacheTracks(
listOf("vendor", "runtimeVersion", "vmVersion", "os.name", "os.arch"),
taskName,
env = mapOf("MAVEN_REPOSITORY_PROXY" to repo.repoUrl),
)
}

@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<MuzzleTask>().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<String>,
taskName: String = "muzzle-AssertPass-core-jdk",
changedOutcome: TaskOutcome = SUCCESS,
env: Map<String, String> = emptyMap(),
) {
writeFile("settings.gradle.kts", "buildCache { local { directory = file(\"task-cache\") } }", append = true)
writeNoopScanPlugin()
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", 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", env = env)
assertThat(restored.task(task)?.outcome).describedAs(restored.output).isEqualTo(FROM_CACHE)

val changed = run("muzzle", "--build-cache", "-PchangedJvmField=$field", env = env)
assertThat(changed.task(task)?.outcome).describedAs(changed.output).isEqualTo(changedOutcome)
}
}
}
2 changes: 2 additions & 0 deletions docs/how_to_test.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
`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**.
They test features that require a more complex environment setup.
Expand Down
Loading