From 3c059758b4433449cdcddac8f325bd495f6c05de Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Mon, 16 Feb 2026 14:02:16 +0100 Subject: [PATCH 01/22] chore: New MuzzlePlugin tests --- .../muzzle/MuzzlePluginIntegrationTest.kt | 139 ++++++++++++++++++ .../plugin/muzzle/MuzzlePluginTestFixture.kt | 96 ++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt create mode 100644 buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt new file mode 100644 index 00000000000..e1456b54d22 --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt @@ -0,0 +1,139 @@ +package datadog.gradle.plugin.muzzle + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File + +class MuzzlePluginIntegrationTest { + + @Test + fun `muzzle with pass directive writes junit report`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + pass { + name = 'expected-pass' + coreJdk() + } + } + """ + ) + fixture.writeScanPlugin( + """ + if (!assertPass) { + throw new IllegalStateException("unexpected fail assertion for " + muzzleDirective); + } + """ + ) + + val buildResult = fixture.run(":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace") + + val reportFile = fixture.findSingleMuzzleJUnitReport() + val report = fixture.parseXml(reportFile) + val suite = report.documentElement + assertEquals("testsuite", suite.tagName) + assertEquals(":dd-java-agent:instrumentation:demo", suite.getAttribute("name")) + assertEquals("1", suite.getAttribute("tests")) + assertEquals("0", suite.getAttribute("failures")) + + val passCase = findTestCase(report, "muzzle-AssertPass-core-jdk") + assertEquals(0, passCase.getElementsByTagName("failure").length) + assertTrue(buildResult.output.contains(":dd-java-agent:instrumentation:demo:muzzle-end")) + } + + @Test + fun `muzzle without directives writes default junit report`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + """ + ) + fixture.writeScanPlugin( + """ + if (!assertPass) { + throw new IllegalStateException("unexpected fail assertion for " + muzzleDirective); + } + """ + ) + + fixture.run(":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace") + + val reportFile = fixture.findSingleMuzzleJUnitReport() + val report = fixture.parseXml(reportFile) + val suite = report.documentElement + assertEquals(":dd-java-agent:instrumentation:demo", suite.getAttribute("name")) + assertEquals("1", suite.getAttribute("tests")) + assertEquals("0", suite.getAttribute("failures")) + + val defaultCase = findTestCase(report, "muzzle") + assertEquals(0, defaultCase.getElementsByTagName("failure").length) + } + + @Test + fun `non muzzle invocation does not register muzzle end task`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + pass { + coreJdk() + } + } + """ + ) + + val buildResult = fixture.run(":dd-java-agent:instrumentation:demo:tasks", "--all") + + assertFalse(buildResult.output.contains("muzzle-end")) + } + + @Test + fun `muzzle plugin wires bootstrap and tooling project classpaths`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + """ + ) + + val bootstrapDependencies = fixture.run( + ":dd-java-agent:instrumentation:demo:dependencies", + "--configuration", + "muzzleBootstrap" + ) + assertTrue(bootstrapDependencies.output.contains("project :dd-java-agent:agent-bootstrap")) + + val toolingDependencies = fixture.run( + ":dd-java-agent:instrumentation:demo:dependencies", + "--configuration", + "muzzleTooling" + ) + assertTrue(toolingDependencies.output.contains("project :dd-java-agent:agent-tooling")) + } + + private fun findTestCase(document: org.w3c.dom.Document, name: String): org.w3c.dom.Element = + (0 until document.getElementsByTagName("testcase").length) + .map { document.getElementsByTagName("testcase").item(it) as org.w3c.dom.Element } + .first { it.getAttribute("name") == name } +} diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt new file mode 100644 index 00000000000..9e101526a4a --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt @@ -0,0 +1,96 @@ +package datadog.gradle.plugin.muzzle + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.w3c.dom.Document +import java.io.File +import javax.xml.parsers.DocumentBuilderFactory + +internal class MuzzlePluginTestFixture( + private val projectDir: File, +) { + fun writeProject(instrumentationBuildScript: String) { + file("settings.gradle").writeText( + """ + rootProject.name = 'muzzle-e2e' + include ':dd-java-agent:agent-bootstrap' + include ':dd-java-agent:agent-tooling' + include ':dd-java-agent:instrumentation:demo' + """.trimIndent() + ) + + file("dd-java-agent/agent-bootstrap/build.gradle").writeText( + """ + plugins { + id 'java' + } + + tasks.register('compileMain_java11Java') + """.trimIndent() + ) + + file("dd-java-agent/agent-tooling/build.gradle").writeText( + """ + plugins { + id 'java' + } + """.trimIndent() + ) + + file("dd-java-agent/instrumentation/demo/build.gradle").writeText(instrumentationBuildScript.trimIndent()) + } + + fun writePassingScanPlugin() { + writeScanPlugin("// pass") + } + + fun writeScanPlugin(assertionBody: String) { + file("dd-java-agent/instrumentation/demo/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleVersionScanPlugin.java") + .writeText( + """ + package datadog.trace.agent.tooling.muzzle; + + public final class MuzzleVersionScanPlugin { + private MuzzleVersionScanPlugin() {} + + public static void assertInstrumentationMuzzled( + ClassLoader instrumentationClassLoader, + ClassLoader testApplicationClassLoader, + boolean assertPass, + String muzzleDirective) { + $assertionBody + } + } + """.trimIndent() + ) + } + + fun run(vararg args: String): BuildResult = + GradleRunner.create() + .withTestKitDir(File(projectDir, ".gradle-test-kit")) + .withDebug(true) + .withPluginClasspath() + .withProjectDir(projectDir) + .withArguments(*args) + .build() + + fun findSingleMuzzleJUnitReport(): File { + val reports = projectDir.walkTopDown() + .filter { it.isFile && it.name.startsWith("TEST-muzzle-") && it.extension == "xml" } + .toList() + require(reports.size == 1) { + "Expected exactly one JUnit muzzle report, but found ${reports.size}" + } + return reports.single() + } + + fun parseXml(xmlFile: File): Document { + val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + return builder.parse(xmlFile) + } + + private fun file(path: String): File = + File(projectDir, path).also { file -> + file.parentFile?.mkdirs() + } +} From beaf4a0c5e752c03f46da1360f7e2dcbacbdda35 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 17 Feb 2026 10:47:52 +0100 Subject: [PATCH 02/22] chore: Factors out the task planification out of muzzle plugin for easier testing --- .../gradle/plugin/muzzle/MuzzlePlugin.kt | 41 ++---- .../planner/MavenMuzzleResolutionService.kt | 23 ++++ .../muzzle/planner/MuzzleResolutionService.kt | 19 +++ .../plugin/muzzle/planner/MuzzleTaskPlan.kt | 14 ++ .../muzzle/planner/MuzzleTaskPlanner.kt | 55 ++++++++ .../muzzle/MuzzlePluginIntegrationTest.kt | 57 ++++++++ .../plugin/muzzle/MuzzlePluginTestFixture.kt | 16 ++- .../muzzle/planner/MuzzleTaskPlannerTest.kt | 126 ++++++++++++++++++ 8 files changed, 316 insertions(+), 35 deletions(-) create mode 100644 buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MavenMuzzleResolutionService.kt create mode 100644 buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleResolutionService.kt create mode 100644 buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlan.kt create mode 100644 buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlanner.kt create mode 100644 buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt index ae15cc58fcb..66024e9f17c 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt @@ -1,13 +1,11 @@ package datadog.gradle.plugin.muzzle -import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.inverseOf -import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.muzzleDirectiveToArtifacts -import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.resolveVersionRange import datadog.gradle.plugin.muzzle.tasks.MuzzleEndTask import datadog.gradle.plugin.muzzle.tasks.MuzzleGenerateReportTask import datadog.gradle.plugin.muzzle.tasks.MuzzleGetReferencesTask import datadog.gradle.plugin.muzzle.tasks.MuzzleMergeReportsTask import datadog.gradle.plugin.muzzle.tasks.MuzzleTask +import datadog.gradle.plugin.muzzle.planner.MuzzleTaskPlanner import org.eclipse.aether.artifact.Artifact import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Plugin @@ -117,40 +115,19 @@ class MuzzlePlugin : Plugin { val system = MuzzleMavenRepoUtils.newRepositorySystem() val session = MuzzleMavenRepoUtils.newRepositorySystemSession(system) + val taskPlanner = MuzzleTaskPlanner.from(system, session) project.afterEvaluate { // use runAfter to set up task finalizers in version order var runAfter: TaskProvider = muzzleTask val muzzleReportTasks = mutableListOf>() - - project.extensions.getByType().directives.forEach { directive -> - project.logger.debug("configuring {}", directive) - - if (directive.isCoreJdk) { - runAfter = addMuzzleTask(directive, null, project, runAfter, muzzleBootstrap, muzzleTooling) - muzzleReportTasks.add(runAfter) - } else { - val range = resolveVersionRange(directive, system, session) - - muzzleDirectiveToArtifacts(directive, range).forEach { - runAfter = addMuzzleTask(directive, it, project, runAfter, muzzleBootstrap, muzzleTooling) - muzzleReportTasks.add(runAfter) - } - - if (directive.assertInverse) { - inverseOf(directive, system, session).forEach { inverseDirective -> - val inverseRange = resolveVersionRange(inverseDirective, system, session) - - muzzleDirectiveToArtifacts(inverseDirective, inverseRange).forEach { - runAfter = addMuzzleTask(inverseDirective, it, project, runAfter, muzzleBootstrap, muzzleTooling) - muzzleReportTasks.add(runAfter) - } - } - } - } - project.logger.info("configured $directive") + val directives = project.extensions.getByType().directives + taskPlanner.plan(directives).forEach { plan -> + runAfter = registerMuzzleTask(plan.directive, plan.artifact, project, runAfter, muzzleBootstrap, muzzleTooling) + muzzleReportTasks.add(runAfter) + project.logger.info("configured ${plan.directive}") } - if (muzzleReportTasks.isEmpty() && !project.extensions.getByType().directives.any { it.assertPass }) { + if (muzzleReportTasks.isEmpty() && !directives.any { it.assertPass }) { muzzleReportTasks.add(muzzleTask) } @@ -180,7 +157,7 @@ class MuzzlePlugin : Plugin { * @param muzzleTooling The configuration provider for agent tooling dependencies. * @return The muzzle task provider. */ - private fun addMuzzleTask( + private fun registerMuzzleTask( muzzleDirective: MuzzleDirective, versionArtifact: Artifact?, instrumentationProject: Project, diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MavenMuzzleResolutionService.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MavenMuzzleResolutionService.kt new file mode 100644 index 00000000000..ee9d7c38369 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MavenMuzzleResolutionService.kt @@ -0,0 +1,23 @@ +package datadog.gradle.plugin.muzzle.planner + +import datadog.gradle.plugin.muzzle.MuzzleDirective +import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.RepositorySystemSession +import org.eclipse.aether.artifact.Artifact + +/** + * Default [MuzzleResolutionService] implementation backed by Maven/Aether resolution. + */ +internal class MavenMuzzleResolutionService( + private val system: RepositorySystem, + private val session: RepositorySystemSession, +) : MuzzleResolutionService { + override fun resolveArtifacts(directive: MuzzleDirective): Set { + val range = MuzzleMavenRepoUtils.resolveVersionRange(directive, system, session) + return MuzzleMavenRepoUtils.muzzleDirectiveToArtifacts(directive, range) + } + + override fun inverseOf(directive: MuzzleDirective): Set = + MuzzleMavenRepoUtils.inverseOf(directive, system, session) +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleResolutionService.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleResolutionService.kt new file mode 100644 index 00000000000..bcdd81427e9 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleResolutionService.kt @@ -0,0 +1,19 @@ +package datadog.gradle.plugin.muzzle.planner + +import datadog.gradle.plugin.muzzle.MuzzleDirective +import org.eclipse.aether.artifact.Artifact + +/** + * Resolves muzzle directives into concrete artifacts and inverse directives. + */ +internal interface MuzzleResolutionService { + /** + * Resolves all dependency artifacts to test for the given directive. + */ + fun resolveArtifacts(directive: MuzzleDirective): Set + + /** + * Computes directives representing the inverse of the given directive. + */ + fun inverseOf(directive: MuzzleDirective): Set +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlan.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlan.kt new file mode 100644 index 00000000000..6b3a3dbd5f0 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlan.kt @@ -0,0 +1,14 @@ +package datadog.gradle.plugin.muzzle.planner + +import datadog.gradle.plugin.muzzle.MuzzleDirective +import org.eclipse.aether.artifact.Artifact + +/** + * Planned unit of muzzle work for task creation. + * + * For `coreJdk()` directives, [artifact] is `null`. + */ +internal data class MuzzleTaskPlan( + val directive: MuzzleDirective, + val artifact: Artifact?, +) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlanner.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlanner.kt new file mode 100644 index 00000000000..107fbf2f2d9 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlanner.kt @@ -0,0 +1,55 @@ +package datadog.gradle.plugin.muzzle.planner + +import datadog.gradle.plugin.muzzle.MuzzleDirective +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.RepositorySystemSession + +/** + * Expands configured directives into ordered task plans. + */ +internal class MuzzleTaskPlanner( + private val resolutionService: MuzzleResolutionService, +) { + companion object { + fun from(system: RepositorySystem, session: RepositorySystemSession): MuzzleTaskPlanner = + MuzzleTaskPlanner(MavenMuzzleResolutionService(system, session)) + } + + /** + * Expands declared muzzle directives into executable task plans. + * + * Planning rules: + * - Core-JDK directives (`coreJdk()`) create exactly one [MuzzleTaskPlan] with `artifact = null`. + * - Non-core directives are resolved with [MuzzleResolutionService.resolveArtifacts], creating one + * plan per resolved artifact. + * - If a non-core directive has `assertInverse = true`, inverse directives are obtained from + * [MuzzleResolutionService.inverseOf], then each inverse directive is resolved and expanded with + * the same one-plan-per-artifact rule. + * + * Ordering: + * - The input [directives] order is preserved. + * - Direct plans for a directive are emitted before its inverse plans. + * - Artifact plan order follows the iteration order returned by the resolution service. + * + * No de-duplication is performed here. If needed, de-duplication must be handled by callers or by + * the resolution service implementation. + */ + fun plan(directives: List): List = buildList { + directives.forEach { directive -> + if (directive.isCoreJdk) { + add(MuzzleTaskPlan(directive, null)) + } else { + resolutionService.resolveArtifacts(directive).forEach { artifact -> + add(MuzzleTaskPlan(directive, artifact)) + } + if (directive.assertInverse) { + resolutionService.inverseOf(directive).forEach { inverseDirective -> + resolutionService.resolveArtifacts(inverseDirective).forEach { artifact -> + add(MuzzleTaskPlan(inverseDirective, artifact)) + } + } + } + } + } + } +} diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt index e1456b54d22..07012d0dd50 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt @@ -3,9 +3,12 @@ package datadog.gradle.plugin.muzzle import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue +import org.gradle.testkit.runner.TaskOutcome.SUCCESS import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.io.File +import java.nio.file.Files +import kotlin.io.path.readText class MuzzlePluginIntegrationTest { @@ -132,6 +135,60 @@ class MuzzlePluginIntegrationTest { assertTrue(toolingDependencies.output.contains("project :dd-java-agent:agent-tooling")) } + @Test + fun `muzzle executes exactly planned core-jdk tasks and writes task results`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + pass { coreJdk() } + fail { coreJdk() } + } + """ + ) + fixture.writeScanPlugin( + """ + // pass + """ + ) + + val result = fixture.run(":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace") + val muzzleTaskPath = ":dd-java-agent:instrumentation:demo:muzzle" + val passDirectiveTaskPath = ":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk" + val failDirectiveTaskPath = ":dd-java-agent:instrumentation:demo:muzzle-AssertFail-core-jdk" + val endTaskPath = ":dd-java-agent:instrumentation:demo:muzzle-end" + + assertEquals(SUCCESS, result.task(muzzleTaskPath)?.outcome) + assertEquals(SUCCESS, result.task(passDirectiveTaskPath)?.outcome) + assertEquals(SUCCESS, result.task(failDirectiveTaskPath)?.outcome) + assertEquals(SUCCESS, result.task(endTaskPath)?.outcome) + + val muzzleChainInOrder = result.tasks + .map { it.path } + .filter { + it == muzzleTaskPath || + it == passDirectiveTaskPath || + it == failDirectiveTaskPath || + it == endTaskPath + } + assertEquals( + listOf(muzzleTaskPath, passDirectiveTaskPath, failDirectiveTaskPath, endTaskPath), + muzzleChainInOrder + ) + + val passDirectiveResult = fixture.resultFile("muzzle-AssertPass-core-jdk") + val failDirectiveResult = fixture.resultFile("muzzle-AssertFail-core-jdk") + assertTrue(Files.isRegularFile(passDirectiveResult)) + assertTrue(Files.isRegularFile(failDirectiveResult)) + assertEquals("PASSING", passDirectiveResult.readText()) + assertEquals("PASSING", failDirectiveResult.readText()) + } + private fun findTestCase(document: org.w3c.dom.Document, name: String): org.w3c.dom.Element = (0 until document.getElementsByTagName("testcase").length) .map { document.getElementsByTagName("testcase").item(it) as org.w3c.dom.Element } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt index 9e101526a4a..0292820e90e 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt @@ -2,8 +2,10 @@ package datadog.gradle.plugin.muzzle import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.UnexpectedBuildResultException import org.w3c.dom.Document import java.io.File +import java.nio.file.Path import javax.xml.parsers.DocumentBuilderFactory internal class MuzzlePluginTestFixture( @@ -65,14 +67,19 @@ internal class MuzzlePluginTestFixture( ) } - fun run(vararg args: String): BuildResult = - GradleRunner.create() + fun run(vararg args: String, expectFailure: Boolean = false): BuildResult { + val runner = GradleRunner.create() .withTestKitDir(File(projectDir, ".gradle-test-kit")) .withDebug(true) .withPluginClasspath() .withProjectDir(projectDir) .withArguments(*args) - .build() + return try { + if (expectFailure) runner.buildAndFail() else runner.build() + } catch (e: UnexpectedBuildResultException) { + e.buildResult + } + } fun findSingleMuzzleJUnitReport(): File { val reports = projectDir.walkTopDown() @@ -89,6 +96,9 @@ internal class MuzzlePluginTestFixture( return builder.parse(xmlFile) } + fun resultFile(taskName: String) = + projectDir.toPath().resolve("dd-java-agent/instrumentation/demo/build/reports/$taskName.txt") + private fun file(path: String): File = File(projectDir, path).also { file -> file.parentFile?.mkdirs() diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt new file mode 100644 index 00000000000..05cefc8b8b1 --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt @@ -0,0 +1,126 @@ +package datadog.gradle.plugin.muzzle.planner + +import datadog.gradle.plugin.muzzle.MuzzleDirective +import org.eclipse.aether.artifact.Artifact +import org.eclipse.aether.artifact.DefaultArtifact +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class MuzzleTaskPlannerTest { + + @Test + fun `coreJdk directive does not call resolution service`() { + val directive = MuzzleDirective().apply { + assertPass = true + coreJdk() + } + val fakeService = FakeResolutionService() + + val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive)) + val expectedPlans = listOf( + MuzzleTaskPlan(directive, null) + ) + + assertEquals(expectedPlans, plans) + assertEquals(0, fakeService.resolveCalls) + assertEquals(0, fakeService.inverseCalls) + } + + @Test + fun `artifact directive creates one plan per resolved artifact version`() { + val directive = MuzzleDirective().apply { + group = "com.example" + module = "demo" + versions = "[1.0,2.0)" + assertPass = true + } + val artifacts = linkedSetOf( + artifact("1.0.0"), + artifact("1.1.0"), + artifact("1.2.0"), + artifact("1.3.0") + ) + val fakeService = FakeResolutionService( + artifactsByDirective = mapOf(directive to artifacts) + ) + + val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive)) + + assertEquals( + listOf( + MuzzleTaskPlan(directive, artifact("1.0.0")), + MuzzleTaskPlan(directive, artifact("1.1.0")), + MuzzleTaskPlan(directive, artifact("1.2.0")), + MuzzleTaskPlan(directive, artifact("1.3.0")), + ), + plans + ) + assertEquals(1, fakeService.resolveCalls) + assertEquals(0, fakeService.inverseCalls) + } + + @Test + fun `assertInverse adds inverse plans on top of declared range plans`() { + val directive = MuzzleDirective().apply { + group = "com.example" + module = "demo" + versions = "[3.0,)" + assertPass = true + assertInverse = true + } + val inversedDirective = MuzzleDirective().apply { + group = "com.example" + module = "demo" + versions = "[2.7,3.0)" + assertPass = false + } + val directArtifactV1 = artifact("3.12.13") + val directArtifactV2 = artifact("4.4.1") + val directArtifactV3 = artifact("5.3.2") + val inverseArtifactV1 = artifact("2.7.5") + val inverseArtifactV2 = artifact("2.8.1") + val fakeService = FakeResolutionService( + artifactsByDirective = mapOf( + directive to linkedSetOf(directArtifactV1, directArtifactV2, directArtifactV3), + inversedDirective to linkedSetOf(inverseArtifactV1, inverseArtifactV2) + ), + inverseByDirective = mapOf(directive to linkedSetOf(inversedDirective)) + ) + + val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive)) + + assertEquals( + listOf( + MuzzleTaskPlan(directive, directArtifactV1), + MuzzleTaskPlan(directive, directArtifactV2), + MuzzleTaskPlan(directive, directArtifactV3), + MuzzleTaskPlan(inversedDirective, inverseArtifactV1), + MuzzleTaskPlan(inversedDirective, inverseArtifactV2), + ), plans) + assertEquals(2, fakeService.resolveCalls, "main directive + additional one for the inverse directive") + assertEquals(1, fakeService.inverseCalls) + } + + private fun artifact(version: String) = + DefaultArtifact("com.example", "demo", "", "jar", version) + + private class FakeResolutionService( + private val artifactsByDirective: Map> = emptyMap(), + private val inverseByDirective: Map> = emptyMap(), + ) : MuzzleResolutionService { + var resolveCalls: Int = 0 + private set + var inverseCalls: Int = 0 + private set + + override fun resolveArtifacts(directive: MuzzleDirective): Set { + resolveCalls++ + return artifactsByDirective[directive].orEmpty() + } + + override fun inverseOf(directive: MuzzleDirective): Set { + inverseCalls++ + return inverseByDirective[directive].orEmpty() + } + } +} From dce345c2e27e9bff49f97430ba1dfcb62af2ae68 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 17 Feb 2026 10:48:25 +0100 Subject: [PATCH 03/22] feat: Easier testing for buildSrc tests --- buildSrc/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 3a13ed61339..b791fcf0f89 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -105,7 +105,7 @@ testing { val test by getting(JvmTestSuite::class) { targets.configureEach { testTask.configure { - enabled = project.hasProperty("runBuildSrcTests") + enabled = providers.systemProperty("runBuildSrcTests").isPresent or providers.systemProperty("idea.active").isPresent } } } From 2695763daed65234b68a894027258974020a961e Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 17 Feb 2026 11:17:16 +0100 Subject: [PATCH 04/22] chore: New plan tests --- .../muzzle/planner/MuzzleTaskPlannerTest.kt | 279 ++++++++++++++++-- 1 file changed, 260 insertions(+), 19 deletions(-) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt index 05cefc8b8b1..e5f0f353bb1 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt @@ -8,6 +8,36 @@ import org.junit.jupiter.api.Test class MuzzleTaskPlannerTest { + @Test + fun `empty directives list returns empty plans`() { + val fakeService = FakeResolutionService() + + val plans = MuzzleTaskPlanner(fakeService).plan(emptyList()) + + assertEquals(emptyList(), plans) + assertEquals(0, fakeService.resolveCalls) + assertEquals(0, fakeService.inverseCalls) + } + + @Test + fun `directive with no resolved artifacts returns empty plans`() { + val directive = MuzzleDirective().apply { + group = "com.example" + module = "nonexistent" + versions = "[99.0,100.0)" + assertPass = true + } + val fakeService = FakeResolutionService( + artifactsByDirective = mapOf(directive to emptySet()) + ) + + val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive)) + + assertEquals(emptyList(), plans) + assertEquals(1, fakeService.resolveCalls) + assertEquals(0, fakeService.inverseCalls) + } + @Test fun `coreJdk directive does not call resolution service`() { val directive = MuzzleDirective().apply { @@ -17,11 +47,8 @@ class MuzzleTaskPlannerTest { val fakeService = FakeResolutionService() val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive)) - val expectedPlans = listOf( - MuzzleTaskPlan(directive, null) - ) - assertEquals(expectedPlans, plans) + assertEquals(listOf(MuzzleTaskPlan(directive, null)), plans) assertEquals(0, fakeService.resolveCalls) assertEquals(0, fakeService.inverseCalls) } @@ -35,10 +62,10 @@ class MuzzleTaskPlannerTest { assertPass = true } val artifacts = linkedSetOf( - artifact("1.0.0"), - artifact("1.1.0"), - artifact("1.2.0"), - artifact("1.3.0") + artifact(version = "1.0.0"), + artifact(version = "1.1.0"), + artifact(version = "1.2.0"), + artifact(version = "1.3.0") ) val fakeService = FakeResolutionService( artifactsByDirective = mapOf(directive to artifacts) @@ -48,10 +75,10 @@ class MuzzleTaskPlannerTest { assertEquals( listOf( - MuzzleTaskPlan(directive, artifact("1.0.0")), - MuzzleTaskPlan(directive, artifact("1.1.0")), - MuzzleTaskPlan(directive, artifact("1.2.0")), - MuzzleTaskPlan(directive, artifact("1.3.0")), + MuzzleTaskPlan(directive, artifact(version = "1.0.0")), + MuzzleTaskPlan(directive, artifact(version = "1.1.0")), + MuzzleTaskPlan(directive, artifact(version = "1.2.0")), + MuzzleTaskPlan(directive, artifact(version = "1.3.0")), ), plans ) @@ -59,6 +86,48 @@ class MuzzleTaskPlannerTest { assertEquals(0, fakeService.inverseCalls) } + @Test + fun `multiple directives processed together preserves order`() { + val directive1 = MuzzleDirective().apply { + group = "com.example" + module = "first" + versions = "[1.0,2.0)" + assertPass = true + } + val directive2 = MuzzleDirective().apply { + group = "com.example" + module = "second" + versions = "[2.0,3.0)" + assertPass = true + } + val directive3 = MuzzleDirective().apply { + group = "com.example" + module = "third" + versions = "[3.0,4.0)" + assertPass = false + } + val fakeService = FakeResolutionService( + artifactsByDirective = mapOf( + directive1 to linkedSetOf(artifact("first", "1.5.0")), + directive2 to linkedSetOf(artifact("second", "2.5.0")), + directive3 to linkedSetOf(artifact("third", "3.5.0")) + ) + ) + + val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive1, directive2, directive3)) + + assertEquals( + listOf( + MuzzleTaskPlan(directive1, artifact("first", "1.5.0")), + MuzzleTaskPlan(directive2, artifact("second", "2.5.0")), + MuzzleTaskPlan(directive3, artifact("third", "3.5.0")), + ), + plans + ) + assertEquals(3, fakeService.resolveCalls) + assertEquals(0, fakeService.inverseCalls) + } + @Test fun `assertInverse adds inverse plans on top of declared range plans`() { val directive = MuzzleDirective().apply { @@ -74,11 +143,11 @@ class MuzzleTaskPlannerTest { versions = "[2.7,3.0)" assertPass = false } - val directArtifactV1 = artifact("3.12.13") - val directArtifactV2 = artifact("4.4.1") - val directArtifactV3 = artifact("5.3.2") - val inverseArtifactV1 = artifact("2.7.5") - val inverseArtifactV2 = artifact("2.8.1") + val directArtifactV1 = artifact(version = "3.12.13") + val directArtifactV2 = artifact(version = "4.4.1") + val directArtifactV3 = artifact(version = "5.3.2") + val inverseArtifactV1 = artifact(version = "2.7.5") + val inverseArtifactV2 = artifact(version = "2.8.1") val fakeService = FakeResolutionService( artifactsByDirective = mapOf( directive to linkedSetOf(directArtifactV1, directArtifactV2, directArtifactV3), @@ -101,8 +170,180 @@ class MuzzleTaskPlannerTest { assertEquals(1, fakeService.inverseCalls) } - private fun artifact(version: String) = - DefaultArtifact("com.example", "demo", "", "jar", version) + @Test + fun `multiple artifacts with inverse creates comprehensive plan set`() { + val directive = MuzzleDirective().apply { + group = "io.netty" + module = "netty-codec-http" + versions = "[4.1.0,)" + assertPass = true + assertInverse = true + } + val inverseDirective = MuzzleDirective().apply { + group = "io.netty" + module = "netty-codec-http" + versions = "[4.0.0,4.1.0)" + assertPass = false + } + val passArtifacts = linkedSetOf( + artifact("netty-codec-http", "4.1.0"), + artifact("netty-codec-http", "4.1.50"), + artifact("netty-codec-http", "4.2.0") + ) + val failArtifacts = linkedSetOf( + artifact("netty-codec-http", "4.0.30") + ) + val fakeService = FakeResolutionService( + artifactsByDirective = mapOf( + directive to passArtifacts, + inverseDirective to failArtifacts + ), + inverseByDirective = mapOf( + directive to linkedSetOf(inverseDirective) + ) + ) + + val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive)) + + assertEquals(4, plans.size, "Should have 3 pass plans + 1 inverse fail plan") + assertEquals( + listOf( + MuzzleTaskPlan(directive, artifact("netty-codec-http", "4.1.0")), + MuzzleTaskPlan(directive, artifact("netty-codec-http", "4.1.50")), + MuzzleTaskPlan(directive, artifact("netty-codec-http", "4.2.0")), + MuzzleTaskPlan(inverseDirective, artifact("netty-codec-http", "4.0.30")), + ), + plans + ) + assertEquals(2, fakeService.resolveCalls) + assertEquals(1, fakeService.inverseCalls) + } + + @Test + fun `mix of coreJdk and artifact directives`() { + val coreJdkDirective = MuzzleDirective().apply { + assertPass = true + coreJdk() + } + val artifactDirective = MuzzleDirective().apply { + group = "com.example" + module = "demo" + versions = "[1.0,2.0)" + assertPass = true + } + val fakeService = FakeResolutionService( + artifactsByDirective = mapOf( + artifactDirective to linkedSetOf(artifact("demo", "1.5.0")) + ) + ) + + val plans = MuzzleTaskPlanner(fakeService).plan(listOf(coreJdkDirective, artifactDirective)) + + assertEquals( + listOf( + MuzzleTaskPlan(coreJdkDirective, null), + MuzzleTaskPlan(artifactDirective, artifact("demo", "1.5.0")), + ), + plans + ) + assertEquals(1, fakeService.resolveCalls) + assertEquals(0, fakeService.inverseCalls) + } + + @Test + fun `mix of pass and fail directives`() { + val passDirective = MuzzleDirective().apply { + group = "com.example" + module = "demo" + versions = "[2.0,)" + assertPass = true + } + val failDirective = MuzzleDirective().apply { + name = "before-2.0" + group = "com.example" + module = "demo" + versions = "[,2.0)" + assertPass = false + } + val fakeService = FakeResolutionService( + artifactsByDirective = mapOf( + passDirective to linkedSetOf(artifact("demo", "2.5.0"), artifact("demo", "3.0.0")), + failDirective to linkedSetOf(artifact("demo", "1.5.0")) + ) + ) + + val plans = MuzzleTaskPlanner(fakeService).plan(listOf(passDirective, failDirective)) + + assertEquals( + listOf( + MuzzleTaskPlan(passDirective, artifact("demo", "2.5.0")), + MuzzleTaskPlan(passDirective, artifact("demo", "3.0.0")), + MuzzleTaskPlan(failDirective, artifact("demo", "1.5.0")), + ), + plans + ) + assertEquals(2, fakeService.resolveCalls) + assertEquals(0, fakeService.inverseCalls) + } + + @Test + fun `multiple directives with assertInverse`() { + val directive1 = MuzzleDirective().apply { + group = "com.example" + module = "first" + versions = "[3.0,)" + assertPass = true + assertInverse = true + } + val directive2 = MuzzleDirective().apply { + group = "com.example" + module = "second" + versions = "[2.0,)" + assertPass = true + assertInverse = true + } + val inverse1 = MuzzleDirective().apply { + group = "com.example" + module = "first" + versions = "[2.0,3.0)" + assertPass = false + } + val inverse2 = MuzzleDirective().apply { + group = "com.example" + module = "second" + versions = "[1.0,2.0)" + assertPass = false + } + val fakeService = FakeResolutionService( + artifactsByDirective = mapOf( + directive1 to linkedSetOf(artifact("first", "3.5.0")), + directive2 to linkedSetOf(artifact("second", "2.5.0")), + inverse1 to linkedSetOf(artifact("first", "2.5.0")), + inverse2 to linkedSetOf(artifact("second", "1.5.0")) + ), + inverseByDirective = mapOf( + directive1 to linkedSetOf(inverse1), + directive2 to linkedSetOf(inverse2) + ) + ) + + val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive1, directive2)) + + assertEquals( + listOf( + MuzzleTaskPlan(directive1, artifact("first", "3.5.0")), + MuzzleTaskPlan(inverse1, artifact("first", "2.5.0")), + MuzzleTaskPlan(directive2, artifact("second", "2.5.0")), + MuzzleTaskPlan(inverse2, artifact("second", "1.5.0")), + ), + plans + ) + assertEquals(4, fakeService.resolveCalls) + assertEquals(2, fakeService.inverseCalls) + } + + private fun artifact(module: String = "demo", version: String) = + DefaultArtifact("com.example", module, "", "jar", version) private class FakeResolutionService( private val artifactsByDirective: Map> = emptyMap(), From dbd7215038df7a310bbc7cacda9f210a1c7ae201 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 17 Feb 2026 12:27:53 +0100 Subject: [PATCH 05/22] chore: First full integration test --- buildSrc/build.gradle.kts | 1 + .../plugin/muzzle/MuzzleMavenRepoUtils.kt | 5 +- .../datadog/gradle/plugin/GradleFixture.kt | 142 ++++++++++++++++++ .../muzzle/MuzzlePluginIntegrationTest.kt | 95 ++++++++++++ .../plugin/muzzle/MuzzlePluginTestFixture.kt | 69 ++++----- 5 files changed, 274 insertions(+), 38 deletions(-) create mode 100644 buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index b791fcf0f89..3b79a7a3224 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -78,6 +78,7 @@ dependencies { implementation("org.eclipse.aether", "aether-connector-basic", "1.1.0") implementation("org.eclipse.aether", "aether-transport-http", "1.1.0") + implementation("org.eclipse.aether", "aether-transport-file", "1.1.0") implementation("org.apache.maven", "maven-aether-provider", "3.3.9") implementation("com.github.zafarkhaja:java-semver:0.10.2") diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt index 13e8752cb27..86611299bd5 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt @@ -12,6 +12,7 @@ import org.eclipse.aether.resolution.VersionRangeRequest import org.eclipse.aether.resolution.VersionRangeResult import org.eclipse.aether.spi.connector.RepositoryConnectorFactory import org.eclipse.aether.spi.connector.transport.TransporterFactory +import org.eclipse.aether.transport.file.FileTransporterFactory import org.eclipse.aether.transport.http.HttpTransporterFactory import org.eclipse.aether.version.Version import org.gradle.api.GradleException @@ -34,13 +35,15 @@ internal object MuzzleMavenRepoUtils { } /** - * Create new RepositorySystem for muzzle's Maven/Aether resoltions. + * Create new RepositorySystem for muzzle's Maven/Aether resolutions. + * Supports both HTTP/HTTPS and file:// repositories. */ @JvmStatic fun newRepositorySystem(): RepositorySystem { val locator = MavenRepositorySystemUtils.newServiceLocator().apply { addService(RepositoryConnectorFactory::class.java, BasicRepositoryConnectorFactory::class.java) addService(TransporterFactory::class.java, HttpTransporterFactory::class.java) + addService(TransporterFactory::class.java, FileTransporterFactory::class.java) } return locator.getService(RepositorySystem::class.java) } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt new file mode 100644 index 00000000000..88a10686453 --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt @@ -0,0 +1,142 @@ +package datadog.gradle.plugin + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.UnexpectedBuildResultException +import org.w3c.dom.Document +import java.io.File +import java.nio.file.Path +import java.security.MessageDigest +import java.util.jar.JarOutputStream +import javax.xml.parsers.DocumentBuilderFactory + +/** + * Base fixture for Gradle plugin integration tests. + * Provides common functionality for setting up test projects and running Gradle builds. + */ +internal open class GradleFixture( + protected val projectDir: File, +) { + /** + * Runs Gradle with the specified arguments. + * + * @param args Gradle task names and arguments + * @param expectFailure Whether the build is expected to fail + * @param env Environment variables to set (merged with system environment) + * @return The build result + */ + fun run(vararg args: String, expectFailure: Boolean = false, env: Map = emptyMap()): BuildResult { + val runner = GradleRunner.create() + .withTestKitDir(File(projectDir, ".gradle-test-kit")) + .withPluginClasspath() + .withProjectDir(projectDir) + .withEnvironment(System.getenv() + env) + .withArguments(*args) + return try { + if (expectFailure) runner.buildAndFail() else runner.build() + } catch (e: UnexpectedBuildResultException) { + e.buildResult + } + } + + /** + * Parses an XML file into a DOM Document. + */ + fun parseXml(xmlFile: File): Document { + val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + return builder.parse(xmlFile) + } + + /** + * Creates or gets a file in the project directory, ensuring parent directories exist. + */ + protected fun file(path: String): File = + File(projectDir, path).also { file -> + file.parentFile?.mkdirs() + } + + /** + * Creates a fake local Maven repository with the specified artifacts and versions. + * Generates proper POM files, JAR files, maven-metadata.xml, and checksums. + * + * @param group Maven group ID + * @param module Maven artifact ID + * @param versions List of versions to create + * @return The repository root directory + */ + fun createFakeMavenRepo( + group: String, + module: String, + versions: List, + ): File { + require(versions.isNotEmpty()) { "versions must not be empty" } + val repoDir = File(projectDir, "fake-maven-repo").apply { mkdirs() } + val groupPath = group.replace('.', '/') + val moduleDir = File(repoDir, "$groupPath/$module").apply { mkdirs() } + + versions.forEach { version -> + val versionDir = File(moduleDir, version).apply { mkdirs() } + val pomFile = File(versionDir, "$module-$version.pom") + pomFile.writeText( + """ + + 4.0.0 + $group + $module + $version + jar + + """.trimIndent() + ) + writeChecksum(pomFile) + + val jarFile = File(versionDir, "$module-$version.jar") + createEmptyJar(jarFile.toPath()) + writeChecksum(jarFile) + } + + val metadataFile = File(moduleDir, "maven-metadata.xml") + metadataFile.writeText( + """ + + $group + $module + + ${versions.last()} + ${versions.last()} + + ${versions.joinToString("\n") { " $it" }} + + 20260216120000 + + + """.trimIndent() + ) + writeChecksum(metadataFile) + + return repoDir + } + + /** + * Generates SHA-1 and MD5 checksum files for a given file. + */ + private fun writeChecksum(file: File) { + val content = file.readBytes() + val sha1 = MessageDigest.getInstance("SHA-1").digest(content) + .joinToString("") { "%02x".format(it) } + File(file.parentFile, "${file.name}.sha1").writeText(sha1) + + val md5 = MessageDigest.getInstance("MD5").digest(content) + .joinToString("") { "%02x".format(it) } + File(file.parentFile, "${file.name}.md5").writeText(md5) + } + + /** + * Creates an empty JAR file at the specified path. + */ + private fun createEmptyJar(path: Path) { + JarOutputStream(path.toFile().outputStream()).use { /* empty jar */ } + } +} diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt index 07012d0dd50..632c778b440 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt @@ -189,6 +189,101 @@ class MuzzlePluginIntegrationTest { assertEquals("PASSING", failDirectiveResult.readText()) } + @Test + fun `artifact directive resolves multiple versions from version range`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + // Create fake local Maven repo with multiple versions + val repoDir = fixture.createFakeMavenRepo( + group = "com.example.test", + module = "demo-lib", + versions = listOf("1.0.0", "1.1.0", "1.2.0", "2.0.0") + ) + + // Configure muzzle to use the fake repo (file:// URI now supported!) + val repoUrl = repoDir.toURI().toString() + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + // Gradle repositories for artifact download + repositories { + maven { + url = uri('$repoUrl') + metadataSources { + mavenPom() + artifact() + // Disable checksum validation for fake repo + } + } + } + + muzzle { + pass { + group = 'com.example.test' + module = 'demo-lib' + versions = '[1.0.0,2.0.0)' // Should resolve 1.0.0, 1.1.0, 1.2.0 but NOT 2.0.0 + } + } + """ + ) + fixture.writeScanPlugin("// pass") + + // Use MAVEN_REPOSITORY_PROXY to point Muzzle's resolution to our fake repo + val result = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + "--stacktrace", + env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) // force the use of our fake repo over maven central + ) + + // First check: did the build succeed? + assertTrue( + result.output.contains("BUILD SUCCESSFUL"), + "Build should succeed. Output:\n${result.output.take(3000)}" + ) + + // Check for evidence of muzzle execution + val hasMuzzleTasks = result.tasks.any { it.path.contains("muzzle") } + assertTrue(hasMuzzleTasks, "Should have executed some muzzle tasks") + + // Verify tasks were created for versions in range + assertTrue( + result.output.contains("demo-lib-1.0.0") || result.output.contains("demo-lib:1.0.0"), + "Should find demo-lib version 1.0.0 in output" + ) + assertTrue( + result.output.contains("demo-lib-1.1.0") || result.output.contains("demo-lib:1.1.0"), + "Should find demo-lib version 1.1.0 in output" + ) + assertTrue( + result.output.contains("demo-lib-1.2.0") || result.output.contains("demo-lib:1.2.0"), + "Should find demo-lib version 1.2.0 in output" + ) + + // Verify JUnit report has test cases for each version + val reportFile = fixture.findSingleMuzzleJUnitReport() + val report = fixture.parseXml(reportFile) + val suite = report.documentElement + + // Should have 3 version-specific tests (at minimum) + val testCount = suite.getAttribute("tests").toInt() + assertTrue(testCount >= 3, "Should have at least 3 tests for 3 versions, got $testCount") + assertEquals("0", suite.getAttribute("failures"), "Should have no failures") + + // Verify at least one test case exists for one of the versions + val testCases = (0 until report.getElementsByTagName("testcase").length) + .map { report.getElementsByTagName("testcase").item(it) as org.w3c.dom.Element } + .map { it.getAttribute("name") } + + assertTrue( + testCases.any { it.contains("demo-lib-1.0.0") }, + "Should have test case for demo-lib-1.0.0. Found: ${testCases.take(5)}" + ) + } + private fun findTestCase(document: org.w3c.dom.Document, name: String): org.w3c.dom.Element = (0 until document.getElementsByTagName("testcase").length) .map { document.getElementsByTagName("testcase").item(it) as org.w3c.dom.Element } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt index 0292820e90e..bbbbceb9422 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt @@ -1,16 +1,20 @@ package datadog.gradle.plugin.muzzle -import org.gradle.testkit.runner.BuildResult -import org.gradle.testkit.runner.GradleRunner -import org.gradle.testkit.runner.UnexpectedBuildResultException -import org.w3c.dom.Document +import datadog.gradle.plugin.GradleFixture import java.io.File -import java.nio.file.Path -import javax.xml.parsers.DocumentBuilderFactory +/** + * Test fixture for muzzle plugin integration tests. + * Extends GradleFixture with muzzle-specific functionality. + */ internal class MuzzlePluginTestFixture( - private val projectDir: File, -) { + projectDir: File, +) : GradleFixture(projectDir) { + + /** + * Writes the basic Gradle project structure for muzzle testing. + * Creates a multi-project build with agent-bootstrap, agent-tooling, and instrumentation modules. + */ fun writeProject(instrumentationBuildScript: String) { file("settings.gradle").writeText( """ @@ -26,7 +30,7 @@ internal class MuzzlePluginTestFixture( plugins { id 'java' } - + tasks.register('compileMain_java11Java') """.trimIndent() ) @@ -42,19 +46,27 @@ internal class MuzzlePluginTestFixture( file("dd-java-agent/instrumentation/demo/build.gradle").writeText(instrumentationBuildScript.trimIndent()) } - fun writePassingScanPlugin() { - writeScanPlugin("// pass") + /** + * Writes a muzzle scan plugin that always passes. + */ + fun writeNoopScanPlugin() { + writeScanPlugin("// noop") } + /** + * Writes a muzzle scan plugin with custom assertion logic. + * + * @param assertionBody Java code to execute in the assertion method + */ fun writeScanPlugin(assertionBody: String) { file("dd-java-agent/instrumentation/demo/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleVersionScanPlugin.java") .writeText( """ package datadog.trace.agent.tooling.muzzle; - + public final class MuzzleVersionScanPlugin { private MuzzleVersionScanPlugin() {} - + public static void assertInstrumentationMuzzled( ClassLoader instrumentationClassLoader, ClassLoader testApplicationClassLoader, @@ -67,20 +79,10 @@ internal class MuzzlePluginTestFixture( ) } - fun run(vararg args: String, expectFailure: Boolean = false): BuildResult { - val runner = GradleRunner.create() - .withTestKitDir(File(projectDir, ".gradle-test-kit")) - .withDebug(true) - .withPluginClasspath() - .withProjectDir(projectDir) - .withArguments(*args) - return try { - if (expectFailure) runner.buildAndFail() else runner.build() - } catch (e: UnexpectedBuildResultException) { - e.buildResult - } - } - + /** + * Finds the single JUnit XML report generated by muzzle tests. + * Throws if zero or multiple reports are found. + */ fun findSingleMuzzleJUnitReport(): File { val reports = projectDir.walkTopDown() .filter { it.isFile && it.name.startsWith("TEST-muzzle-") && it.extension == "xml" } @@ -91,16 +93,9 @@ internal class MuzzlePluginTestFixture( return reports.single() } - fun parseXml(xmlFile: File): Document { - val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder() - return builder.parse(xmlFile) - } - + /** + * Returns the path to a muzzle result file for the given task name. + */ fun resultFile(taskName: String) = projectDir.toPath().resolve("dd-java-agent/instrumentation/demo/build/reports/$taskName.txt") - - private fun file(path: String): File = - File(projectDir, path).also { file -> - file.parentFile?.mkdirs() - } } From 4f9bd6c8f8b1526fc5ba69e2ed1dc1ea3b5ccf80 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 17 Feb 2026 17:35:50 +0100 Subject: [PATCH 06/22] chore: New muzzle integration tests --- .../datadog/gradle/plugin/GradleFixture.kt | 47 ++- .../muzzle/MuzzlePluginIntegrationTest.kt | 337 +++++++++++++++++- .../muzzle/planner/MuzzleTaskPlannerTest.kt | 2 +- 3 files changed, 369 insertions(+), 17 deletions(-) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt index 88a10686453..8b2d6f88ea1 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt @@ -62,12 +62,14 @@ internal open class GradleFixture( * @param group Maven group ID * @param module Maven artifact ID * @param versions List of versions to create + * @param jarContentBuilder Optional lambda to add entries to the JAR * @return The repository root directory */ fun createFakeMavenRepo( group: String, module: String, versions: List, + jarContentBuilder: ((JarOutputStream) -> Unit)? = null ): File { require(versions.isNotEmpty()) { "versions must not be empty" } val repoDir = File(projectDir, "fake-maven-repo").apply { mkdirs() } @@ -93,7 +95,7 @@ internal open class GradleFixture( writeChecksum(pomFile) val jarFile = File(versionDir, "$module-$version.jar") - createEmptyJar(jarFile.toPath()) + createJar(jarFile.toPath(), group, module, version, jarContentBuilder) writeChecksum(jarFile) } @@ -134,9 +136,46 @@ internal open class GradleFixture( } /** - * Creates an empty JAR file at the specified path. + * Creates a JAR file at the specified path with standard Maven metadata, optionally with custom content. + * + * @param path Path where the JAR should be created + * @param group Maven group ID + * @param module Maven artifact ID + * @param version Maven version + * @param contentBuilder Optional lambda to add additional entries to the JAR */ - private fun createEmptyJar(path: Path) { - JarOutputStream(path.toFile().outputStream()).use { /* empty jar */ } + private fun createJar( + path: Path, + group: String, + module: String, + version: String, + contentBuilder: ((JarOutputStream) -> Unit)? = null + ) { + JarOutputStream(path.toFile().outputStream()).use { jos -> + // Add standard Maven metadata files + val metadataPath = "META-INF/maven/$group/$module" + + // Add pom.properties + jos.putNextEntry(java.util.zip.ZipEntry("$metadataPath/pom.properties")) + jos.write("groupId=$group\nartifactId=$module\nversion=$version\n".toByteArray()) + jos.closeEntry() + + // Add pom.xml + jos.putNextEntry(java.util.zip.ZipEntry("$metadataPath/pom.xml")) + jos.write( + """ + + 4.0.0 + $group + $module + $version + + """.trimIndent().toByteArray() + ) + jos.closeEntry() + + // Add any custom content + contentBuilder?.invoke(jos) + } } } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt index 632c778b440..76605e9826b 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.io.File @@ -193,14 +194,12 @@ class MuzzlePluginIntegrationTest { fun `artifact directive resolves multiple versions from version range`(@TempDir projectDir: File) { val fixture = MuzzlePluginTestFixture(projectDir) - // Create fake local Maven repo with multiple versions val repoDir = fixture.createFakeMavenRepo( group = "com.example.test", module = "demo-lib", versions = listOf("1.0.0", "1.1.0", "1.2.0", "2.0.0") ) - // Configure muzzle to use the fake repo (file:// URI now supported!) val repoUrl = repoDir.toURI().toString() fixture.writeProject( """ @@ -232,24 +231,21 @@ class MuzzlePluginIntegrationTest { ) fixture.writeScanPlugin("// pass") - // Use MAVEN_REPOSITORY_PROXY to point Muzzle's resolution to our fake repo + // Leveraging MAVEN_REPOSITORY_PROXY to point to our fake repo over maven central val result = fixture.run( ":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace", - env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) // force the use of our fake repo over maven central + env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) ) - // First check: did the build succeed? assertTrue( result.output.contains("BUILD SUCCESSFUL"), "Build should succeed. Output:\n${result.output.take(3000)}" ) - // Check for evidence of muzzle execution val hasMuzzleTasks = result.tasks.any { it.path.contains("muzzle") } assertTrue(hasMuzzleTasks, "Should have executed some muzzle tasks") - // Verify tasks were created for versions in range assertTrue( result.output.contains("demo-lib-1.0.0") || result.output.contains("demo-lib:1.0.0"), "Should find demo-lib version 1.0.0 in output" @@ -263,27 +259,344 @@ class MuzzlePluginIntegrationTest { "Should find demo-lib version 1.2.0 in output" ) - // Verify JUnit report has test cases for each version val reportFile = fixture.findSingleMuzzleJUnitReport() val report = fixture.parseXml(reportFile) val suite = report.documentElement - - // Should have 3 version-specific tests (at minimum) val testCount = suite.getAttribute("tests").toInt() assertTrue(testCount >= 3, "Should have at least 3 tests for 3 versions, got $testCount") assertEquals("0", suite.getAttribute("failures"), "Should have no failures") - // Verify at least one test case exists for one of the versions val testCases = (0 until report.getElementsByTagName("testcase").length) .map { report.getElementsByTagName("testcase").item(it) as org.w3c.dom.Element } .map { it.getAttribute("name") } - assertTrue( testCases.any { it.contains("demo-lib-1.0.0") }, "Should have test case for demo-lib-1.0.0. Found: ${testCases.take(5)}" ) } + @Test + fun `named directive is passed to scan plugin`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + pass { + name = 'my-custom-check' + coreJdk() + } + } + """ + ) + + // The real MuzzleVersionScanPlugin uses the directive name to filter InstrumenterModules + fixture.writeScanPlugin( + """ + if (!"my-custom-check".equals(muzzleDirective)) { + throw new IllegalStateException( + "Expected muzzleDirective to be 'my-custom-check', but got: '" + muzzleDirective + "'" + ); + } + + System.out.println("Directive name passed correctly: " + muzzleDirective); + """ + ) + + val result = fixture.run(":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace") + + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + assertTrue( + result.output.contains("Directive name passed correctly: my-custom-check"), + "Should confirm 'my-custom-check' was passed to scan plugin" + ) + } + + @Test + fun `non-existent artifact fails with clear error message`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + pass { + group = 'com.example.nonexistent' + module = 'does-not-exist' + versions = '[1.0.0,2.0.0)' + } + } + """ + ) + fixture.writeScanPlugin("// pass") + + val result = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + "--stacktrace", + env = mapOf("MAVEN_REPOSITORY_PROXY" to "https://repo1.maven.org/maven2/") + ) + + assertTrue(result.output.contains("BUILD FAILED"), "Build should fail for non-existent artifact") + assertTrue( + result.output.contains("version range resolution failed") || + result.output.contains("Could not resolve") || + result.output.contains("not found") || + result.output.contains("Failed to resolve"), + "Should have error message about resolution failure" + ) + } + + @Test + fun `pass directive that fails validation causes build failure`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + pass { + coreJdk() + } + } + """ + ) + + // Real implementation throws RuntimeException when !passed && assertPass (line 70 of MuzzleVersionScanPlugin) + fixture.writeScanPlugin( + """ + if (assertPass) { + System.err.println("FAILED MUZZLE VALIDATION: mismatches:"); + System.err.println("-- missing class Foo"); + throw new RuntimeException("Instrumentation failed Muzzle validation"); + } + """ + ) + + val result = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + "--stacktrace" + ) + + assertTrue(result.output.contains("BUILD FAILED"), "Build should fail when pass directive fails validation") + assertTrue( + result.output.contains("Muzzle validation failed") || + result.output.contains("Instrumentation failed"), + "Should contain error message from scan plugin" + ) + } + + @Test + @Disabled("Current implementation doesn't fail build when fail directive unexpectedly passes - MuzzleTask catches exceptions") + fun `fail directive that passes validation causes build failure`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + fail { + coreJdk() + } + } + """ + ) + + // Scan plugin simulates successful validation when it should fail + // Real MuzzleVersionScanPlugin throws RuntimeException when passed && !assertPass + fixture.writeScanPlugin( + """ + if (!assertPass) { + System.err.println("MUZZLE PASSED BUT FAILURE WAS EXPECTED"); + throw new RuntimeException("Instrumentation unexpectedly passed Muzzle validation"); + } + """ + ) + + val result = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + "--stacktrace" + ) + + // Expected behavior: build should fail when fail directive unexpectedly passes + assertTrue(result.output.contains("BUILD FAILED"), "Build should fail when fail directive unexpectedly passes") + assertTrue( + result.output.contains("unexpectedly passed") || + result.output.contains("FAILURE WAS EXPECTED"), + "Should indicate that fail directive passed when it shouldn't have" + ) + } + + @Test + fun `additional dependencies are added to muzzle test classpath`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + // Create a fake Maven repo with a fake additional dependency + // The JAR will automatically include standard Maven metadata + val repoDir = fixture.createFakeMavenRepo( + group = "com.example.extra", + module = "extra-lib", + versions = listOf("1.0.0") + ) + + val repoUrl = repoDir.toURI().toString() + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + repositories { + maven { + url = uri('$repoUrl') + metadataSources { + mavenPom() + artifact() + } + } + } + + muzzle { + pass { + coreJdk() + extraDependency('com.example.extra:extra-lib:1.0.0') + } + } + """ + ) + + // Scan plugin verifies that the additional dependency JAR is in the classpath + fixture.writeScanPlugin( + """ + java.io.InputStream resource = testApplicationClassLoader.getResourceAsStream("META-INF/maven/com.example.extra/extra-lib/pom.properties"); + if (resource != null) { + try { + resource.close(); + } catch (java.io.IOException e) { + // Ignore + } + System.out.println("✓ Additional dependency (extra-lib) found in test classpath"); + } else { + throw new RuntimeException("Additional dependency (extra-lib) not found in test classpath"); + } + """ + ) + + val result = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + "--stacktrace", + env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) + ) + + assertTrue( + result.output.contains("BUILD SUCCESSFUL"), + "Build should succeed. Output:\n${result.output.take(2000)}" + ) + assertTrue( + result.output.contains("✓ Additional dependency (extra-lib) found in test classpath"), + "Additional dependency should be loadable from test classpath" + ) + } + + @Test + fun `excluded dependencies are removed from muzzle test classpath`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + // Create a fake repo with an artifact that has transitive dependencies + val repoDir = fixture.createFakeMavenRepo( + group = "com.example.test", + module = "with-transitive", + versions = listOf("1.0.0") + ) + + // Manually create a POM with a transitive dependency + val pomFile = repoDir.resolve("com/example/test/with-transitive/1.0.0/with-transitive-1.0.0.pom") + pomFile.writeText( + """ + + 4.0.0 + com.example.test + with-transitive + 1.0.0 + + + com.google.guava + guava + 31.0-jre + + + + """.trimIndent() + ) + + val repoUrl = repoDir.toURI().toString() + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + repositories { + maven { + url = uri('$repoUrl') + metadataSources { + mavenPom() + artifact() + } + } + mavenCentral() + } + + muzzle { + pass { + group = 'com.example.test' + module = 'with-transitive' + versions = '1.0.0' + excludeDependency('com.google.guava:guava') + } + } + """ + ) + + // Scan plugin verifies that guava is NOT in the classpath (it was excluded) + fixture.writeScanPlugin( + """ + try { + testApplicationClassLoader.loadClass("com.google.common.collect.ImmutableList"); + throw new RuntimeException("Unexpected excluded dependency (guava) SHOULD NOT be in test classpath but was found"); + } catch (ClassNotFoundException e) { + System.out.println("Excluded dependency (guava) correctly not in test classpath"); + } + """ + ) + + val result = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + "--stacktrace", + env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) + ) + + assertTrue(result.output.contains("BUILD SUCCESSFUL")) + assertTrue( + result.output.contains("Excluded dependency (guava) correctly not in test classpath"), + "Excluded dependency should not be loadable from test classpath" + ) + } + private fun findTestCase(document: org.w3c.dom.Document, name: String): org.w3c.dom.Element = (0 until document.getElementsByTagName("testcase").length) .map { document.getElementsByTagName("testcase").item(it) as org.w3c.dom.Element } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt index e5f0f353bb1..9159d591fe3 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt @@ -341,7 +341,7 @@ class MuzzleTaskPlannerTest { assertEquals(4, fakeService.resolveCalls) assertEquals(2, fakeService.inverseCalls) } - + private fun artifact(module: String = "demo", version: String) = DefaultArtifact("com.example", module, "", "jar", version) From 79620c3ceb5c4772dc1d02a7dc17974efca6f726 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 18 Feb 2026 10:59:31 +0100 Subject: [PATCH 07/22] chore: New muzzle integration tests about reacting to environment --- .../muzzle/MuzzlePluginIntegrationTest.kt | 166 +++++++++++++++++- 1 file changed, 164 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt index 76605e9826b..02d2e55da3e 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt @@ -2,6 +2,7 @@ package datadog.gradle.plugin.muzzle import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertTrue import org.gradle.testkit.runner.TaskOutcome.SUCCESS import org.junit.jupiter.api.Disabled @@ -488,7 +489,7 @@ class MuzzlePluginIntegrationTest { } catch (java.io.IOException e) { // Ignore } - System.out.println("✓ Additional dependency (extra-lib) found in test classpath"); + System.out.println("Additional dependency (extra-lib) found in test classpath"); } else { throw new RuntimeException("Additional dependency (extra-lib) not found in test classpath"); } @@ -506,7 +507,7 @@ class MuzzlePluginIntegrationTest { "Build should succeed. Output:\n${result.output.take(2000)}" ) assertTrue( - result.output.contains("✓ Additional dependency (extra-lib) found in test classpath"), + result.output.contains("Additional dependency (extra-lib) found in test classpath"), "Additional dependency should be loadable from test classpath" ) } @@ -597,6 +598,167 @@ class MuzzlePluginIntegrationTest { ) } + @Test + fun `java plugin applied after muzzle plugin`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + fixture.writeProject( + """ + plugins { + id 'dd-trace-java.muzzle' + } + + // applied after muzzle plugin + apply plugin: 'java' + + muzzle { + pass { + coreJdk() + } + } + """ + ) + fixture.writeScanPlugin("// pass") + + val result = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + "--stacktrace" + ) + + assertTrue(result.output.contains("BUILD SUCCESSFUL")) + val muzzleTask = result.task(":dd-java-agent:instrumentation:demo:muzzle") + assertNotNull(muzzleTask, "Muzzle task should have run") + assertEquals(SUCCESS, muzzleTask?.outcome) + } + + @Test + fun `java plugin applied before muzzle plugin`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' apply false // Declared but not applied + } + + // Apply muzzle plugin after java using imperative syntax + apply plugin: 'dd-trace-java.muzzle' + + muzzle { + pass { + coreJdk() + } + } + """ + ) + fixture.writeScanPlugin("// pass") + + val result = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + "--stacktrace" + ) + + assertTrue(result.output.contains("BUILD SUCCESSFUL")) + val muzzleTask = result.task(":dd-java-agent:instrumentation:demo:muzzle") + assertNotNull(muzzleTask, "Muzzle task should have run") + assertEquals(SUCCESS, muzzleTask?.outcome) + } + + @Test + fun `plugin behavior without java plugin should no-op`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + fixture.writeProject( + """ + plugins { + id 'dd-trace-java.muzzle' + // NO java plugin applied + } + + muzzle { + pass { + coreJdk() + } + } + """ + ) + fixture.writeScanPlugin("// pass") + + val result = fixture.run( + ":dd-java-agent:instrumentation:demo:tasks", + "--all" + ) + + // Should not create muzzle tasks when java plugin is not applied + assertFalse( + result.output.contains("muzzle"), + "Should not create muzzle tasks without java plugin" + ) + } + + @Test + fun `missing dd-java-agent projects error handling`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + // Create a minimal settings.gradle without the dd-java-agent structure + File(projectDir, "settings.gradle").also { it.parentFile?.mkdirs() }.writeText( + """ + rootProject.name = 'muzzle-test' + include ':instrumentation:demo' + """.trimIndent() + ) + + File(projectDir, "instrumentation/demo/build.gradle").also { it.parentFile?.mkdirs() }.writeText( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + pass { + coreJdk() + } + } + """.trimIndent() + ) + + // Still need to write the scan plugin + File(projectDir, "instrumentation/demo/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleVersionScanPlugin.java") + .also { it.parentFile?.mkdirs() } + .writeText( + """ + package datadog.trace.agent.tooling.muzzle; + + public final class MuzzleVersionScanPlugin { + private MuzzleVersionScanPlugin() {} + + public static void assertInstrumentationMuzzled( + ClassLoader instrumentationClassLoader, + ClassLoader testApplicationClassLoader, + boolean assertPass, + String muzzleDirective) { + // pass + } + } + """.trimIndent() + ) + + val result = fixture.run( + ":instrumentation:demo:tasks", + "--stacktrace" + ) + + // Should fail with clear error about missing projects + assertTrue( + result.output.contains("BUILD FAILED") || + result.output.contains(":dd-java-agent:agent-bootstrap project not found") || + result.output.contains(":dd-java-agent:agent-tooling project not found"), + "Should fail with clear error about missing dd-java-agent projects" + ) + } + private fun findTestCase(document: org.w3c.dom.Document, name: String): org.w3c.dom.Element = (0 until document.getElementsByTagName("testcase").length) .map { document.getElementsByTagName("testcase").item(it) as org.w3c.dom.Element } From 128e4b2556dbf3e8d1032138b72be5183c6af579 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 18 Feb 2026 11:02:03 +0100 Subject: [PATCH 08/22] chore: Add language for easier code review --- .../gradle/plugin/muzzle/MuzzlePluginTestFixture.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt index bbbbceb9422..94eb3ca4706 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt @@ -1,6 +1,7 @@ package datadog.gradle.plugin.muzzle import datadog.gradle.plugin.GradleFixture +import org.intellij.lang.annotations.Language import java.io.File /** @@ -15,8 +16,9 @@ internal class MuzzlePluginTestFixture( * Writes the basic Gradle project structure for muzzle testing. * Creates a multi-project build with agent-bootstrap, agent-tooling, and instrumentation modules. */ - fun writeProject(instrumentationBuildScript: String) { + fun writeProject(@Language("Groovy") instrumentationBuildScript: String) { file("settings.gradle").writeText( + // language=Groovy """ rootProject.name = 'muzzle-e2e' include ':dd-java-agent:agent-bootstrap' @@ -26,6 +28,7 @@ internal class MuzzlePluginTestFixture( ) file("dd-java-agent/agent-bootstrap/build.gradle").writeText( + // language=Groovy """ plugins { id 'java' @@ -36,6 +39,7 @@ internal class MuzzlePluginTestFixture( ) file("dd-java-agent/agent-tooling/build.gradle").writeText( + // language=Groovy """ plugins { id 'java' @@ -58,9 +62,10 @@ internal class MuzzlePluginTestFixture( * * @param assertionBody Java code to execute in the assertion method */ - fun writeScanPlugin(assertionBody: String) { + fun writeScanPlugin(@Language("JAVA") assertionBody: String) { file("dd-java-agent/instrumentation/demo/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleVersionScanPlugin.java") .writeText( + // language=JAVA """ package datadog.trace.agent.tooling.muzzle; From 63f4d30c8835eb1dd31eff39a505de7805e1788f Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 18 Feb 2026 11:05:13 +0100 Subject: [PATCH 09/22] test: Rename --- ...lePluginIntegrationTest.kt => MuzzlePluginFunctionalTest.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/{MuzzlePluginIntegrationTest.kt => MuzzlePluginFunctionalTest.kt} (99%) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt similarity index 99% rename from buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt rename to buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt index 02d2e55da3e..22210b70d02 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginIntegrationTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt @@ -12,7 +12,7 @@ import java.io.File import java.nio.file.Files import kotlin.io.path.readText -class MuzzlePluginIntegrationTest { +class MuzzlePluginFunctionalTest { @Test fun `muzzle with pass directive writes junit report`(@TempDir projectDir: File) { From 482d91c5cb3b5b8f862f51101aaf05916cf948eb Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 18 Feb 2026 11:24:48 +0100 Subject: [PATCH 10/22] test: Uses task outcome --- .../muzzle/MuzzlePluginFunctionalTest.kt | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt index 22210b70d02..da1a2c22d56 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt @@ -2,11 +2,11 @@ package datadog.gradle.plugin.muzzle import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertTrue import org.gradle.testkit.runner.TaskOutcome.SUCCESS import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNull import org.junit.jupiter.api.io.TempDir import java.io.File import java.nio.file.Files @@ -41,6 +41,9 @@ class MuzzlePluginFunctionalTest { ) val buildResult = fixture.run(":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace") + assertEquals(SUCCESS, buildResult.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + assertEquals(SUCCESS, buildResult.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + assertEquals(SUCCESS, buildResult.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome) val reportFile = fixture.findSingleMuzzleJUnitReport() val report = fixture.parseXml(reportFile) @@ -52,7 +55,6 @@ class MuzzlePluginFunctionalTest { val passCase = findTestCase(report, "muzzle-AssertPass-core-jdk") assertEquals(0, passCase.getElementsByTagName("failure").length) - assertTrue(buildResult.output.contains(":dd-java-agent:instrumentation:demo:muzzle-end")) } @Test @@ -74,7 +76,8 @@ class MuzzlePluginFunctionalTest { """ ) - fixture.run(":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace") + val result = fixture.run(":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace") + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) val reportFile = fixture.findSingleMuzzleJUnitReport() val report = fixture.parseXml(reportFile) @@ -244,21 +247,13 @@ class MuzzlePluginFunctionalTest { "Build should succeed. Output:\n${result.output.take(3000)}" ) - val hasMuzzleTasks = result.tasks.any { it.path.contains("muzzle") } - assertTrue(hasMuzzleTasks, "Should have executed some muzzle tasks") + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome) - assertTrue( - result.output.contains("demo-lib-1.0.0") || result.output.contains("demo-lib:1.0.0"), - "Should find demo-lib version 1.0.0 in output" - ) - assertTrue( - result.output.contains("demo-lib-1.1.0") || result.output.contains("demo-lib:1.1.0"), - "Should find demo-lib version 1.1.0 in output" - ) - assertTrue( - result.output.contains("demo-lib-1.2.0") || result.output.contains("demo-lib:1.2.0"), - "Should find demo-lib version 1.2.0 in output" - ) + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-demo-lib-1.0.0")?.outcome) + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-demo-lib-1.1.0")?.outcome) + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-demo-lib-1.2.0")?.outcome) + assertNull(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-demo-lib-2.0.0")?.outcome, "Should not check against test-demo-lib:2.0.0") val reportFile = fixture.findSingleMuzzleJUnitReport() val report = fixture.parseXml(reportFile) @@ -506,6 +501,8 @@ class MuzzlePluginFunctionalTest { result.output.contains("BUILD SUCCESSFUL"), "Build should succeed. Output:\n${result.output.take(2000)}" ) + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) assertTrue( result.output.contains("Additional dependency (extra-lib) found in test classpath"), "Additional dependency should be loadable from test classpath" @@ -592,6 +589,8 @@ class MuzzlePluginFunctionalTest { ) assertTrue(result.output.contains("BUILD SUCCESSFUL")) + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-with-transitive-1.0.0")?.outcome) assertTrue( result.output.contains("Excluded dependency (guava) correctly not in test classpath"), "Excluded dependency should not be loadable from test classpath" @@ -626,9 +625,8 @@ class MuzzlePluginFunctionalTest { ) assertTrue(result.output.contains("BUILD SUCCESSFUL")) - val muzzleTask = result.task(":dd-java-agent:instrumentation:demo:muzzle") - assertNotNull(muzzleTask, "Muzzle task should have run") - assertEquals(SUCCESS, muzzleTask?.outcome) + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome) } @Test @@ -660,9 +658,8 @@ class MuzzlePluginFunctionalTest { ) assertTrue(result.output.contains("BUILD SUCCESSFUL")) - val muzzleTask = result.task(":dd-java-agent:instrumentation:demo:muzzle") - assertNotNull(muzzleTask, "Muzzle task should have run") - assertEquals(SUCCESS, muzzleTask?.outcome) + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome) } @Test From bcc05314d819bf1d98835c404715ab34c687603b Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 18 Feb 2026 13:09:40 +0100 Subject: [PATCH 11/22] test: Simplify some test --- .../muzzle/MuzzlePluginFunctionalTest.kt | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt index da1a2c22d56..5ca0d251513 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt @@ -1,5 +1,6 @@ package datadog.gradle.plugin.muzzle +import datadog.gradle.plugin.GradleFixture import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue @@ -687,7 +688,6 @@ class MuzzlePluginFunctionalTest { "--all" ) - // Should not create muzzle tasks when java plugin is not applied assertFalse( result.output.contains("muzzle"), "Should not create muzzle tasks without java plugin" @@ -696,8 +696,6 @@ class MuzzlePluginFunctionalTest { @Test fun `missing dd-java-agent projects error handling`(@TempDir projectDir: File) { - val fixture = MuzzlePluginTestFixture(projectDir) - // Create a minimal settings.gradle without the dd-java-agent structure File(projectDir, "settings.gradle").also { it.parentFile?.mkdirs() }.writeText( """ @@ -721,33 +719,14 @@ class MuzzlePluginFunctionalTest { """.trimIndent() ) - // Still need to write the scan plugin - File(projectDir, "instrumentation/demo/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleVersionScanPlugin.java") - .also { it.parentFile?.mkdirs() } - .writeText( - """ - package datadog.trace.agent.tooling.muzzle; + // No need to create MuzzleVersionScanPlugin - the error happens during configuration + // phase before any task execution, so the scan plugin is never invoked - public final class MuzzleVersionScanPlugin { - private MuzzleVersionScanPlugin() {} - - public static void assertInstrumentationMuzzled( - ClassLoader instrumentationClassLoader, - ClassLoader testApplicationClassLoader, - boolean assertPass, - String muzzleDirective) { - // pass - } - } - """.trimIndent() - ) - - val result = fixture.run( + val result = GradleFixture(projectDir).run( ":instrumentation:demo:tasks", "--stacktrace" ) - // Should fail with clear error about missing projects assertTrue( result.output.contains("BUILD FAILED") || result.output.contains(":dd-java-agent:agent-bootstrap project not found") || From 34056dbba7d23b92b156c99441ba707dc79df19c Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 18 Feb 2026 15:11:03 +0100 Subject: [PATCH 12/22] test: Add new avoidance tests, simplify hasRelevantTask check --- .../gradle/plugin/muzzle/MuzzlePlugin.kt | 11 +- .../datadog/gradle/plugin/GradleFixture.kt | 30 +++++ .../muzzle/MuzzlePluginFunctionalTest.kt | 62 ++++++++++- .../muzzle/MuzzlePluginPerformanceTest.kt | 104 ++++++++++++++++++ .../plugin/muzzle/MuzzlePluginTestFixture.kt | 18 ++- 5 files changed, 204 insertions(+), 21 deletions(-) create mode 100644 buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt index 66024e9f17c..876c2234218 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt @@ -99,14 +99,15 @@ class MuzzlePlugin : Plugin { project.tasks.register("mergeMuzzleReports") val hasRelevantTask = project.gradle.startParameter.taskNames.any { taskName -> - // removing leading ':' if present - val muzzleTaskName = taskName.removePrefix(":") - val projectPath = project.path.removePrefix(":") - muzzleTaskName == "muzzle" || "$projectPath:muzzle" == muzzleTaskName || - muzzleTaskName == "runMuzzle" + val taskProjectPath = taskName.substringBeforeLast(":", "") + val taskNameOnly = taskName.substringAfterLast(":") + val isRelevantForProject = taskProjectPath.isEmpty() || taskProjectPath == project.path + + isRelevantForProject && taskNameOnly.endsWith("muzzle", ignoreCase = true) } if (!hasRelevantTask) { // Adding muzzle dependencies has a large config overhead. Stop unless muzzle is explicitly run. + project.logger.info("No muzzle tasks invoked for ${project.path}, skipping muzzle task planification") return } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt index 8b2d6f88ea1..7bbe69a2895 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt @@ -3,6 +3,7 @@ package datadog.gradle.plugin import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.UnexpectedBuildResultException +import org.intellij.lang.annotations.Language import org.w3c.dom.Document import java.io.File import java.nio.file.Path @@ -39,6 +40,35 @@ internal open class GradleFixture( } } + /** + * Adds a subproject to the build. + * Updates settings.gradle and creates the build script for the subproject. + * + * @param projectPath The project path (e.g., "dd-java-agent:instrumentation:other") + * @param buildScript The build script content for the subproject + */ + fun addSubproject(projectPath: String, @Language("Groovy") buildScript: String) { + // Add to settings.gradle + val settingsFile = file("settings.gradle") + if (settingsFile.exists()) { + settingsFile.appendText("\ninclude ':$projectPath'") + } else { + settingsFile.writeText("include ':$projectPath'") + } + + file("${projectPath.replace(':', '/')}/build.gradle") + .writeText(buildScript.trimIndent()) + } + + /** + * Writes the root project's build.gradle file. + * + * @param buildScript The build script content for the root project + */ + fun writeRootProject(@Language("Groovy") buildScript: String) { + file("build.gradle").writeText(buildScript.trimIndent()) + } + /** * Parses an XML file into a DOM Document. */ diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt index 5ca0d251513..87623600c99 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt @@ -9,11 +9,63 @@ import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertNull import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource import java.io.File import java.nio.file.Files import kotlin.io.path.readText class MuzzlePluginFunctionalTest { + @ParameterizedTest + @ValueSource(strings = ["muzzle", ":dd-java-agent:instrumentation:demo:muzzle", "runMuzzle"]) + fun `detects muzzle invocation with various task names`( + taskName: String, + @TempDir projectDir: File + ) { + val fixture = MuzzlePluginTestFixture(projectDir) + + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + pass { + coreJdk() + } + } + """ + ) + + // Add runMuzzle aggregator task at root level (like in dd-trace-java.ci-jobs.gradle.kts) + fixture.writeRootProject( + """ + tasks.register('runMuzzle') { + dependsOn(':dd-java-agent:instrumentation:demo:muzzle') + } + """ + ) + + fixture.writeNoopScanPlugin() + + val result = fixture.run(taskName, "--stacktrace") + + assertTrue( + result.tasks.any { it.path.contains("muzzle") }, + "Should create muzzle tasks when '$taskName' is requested" + ) + assertFalse( + result.output.contains("No muzzle tasks invoked, skipping muzzle task planification"), + "Should not skip muzzle task planification when '$taskName' is requested" + ) + assertTrue( + result.task(":dd-java-agent:instrumentation:demo:muzzle") != null || + result.tasks.any { it.path.contains("muzzle-Assert") }, + "Should execute muzzle tasks when '$taskName' is requested" + ) + } @Test fun `muzzle with pass directive writes junit report`(@TempDir projectDir: File) { @@ -234,7 +286,7 @@ class MuzzlePluginFunctionalTest { } """ ) - fixture.writeScanPlugin("// pass") + fixture.writeNoopScanPlugin() // Leveraging MAVEN_REPOSITORY_PROXY to point to our fake repo over maven central val result = fixture.run( @@ -332,7 +384,7 @@ class MuzzlePluginFunctionalTest { } """ ) - fixture.writeScanPlugin("// pass") + fixture.writeNoopScanPlugin() val result = fixture.run( ":dd-java-agent:instrumentation:demo:muzzle", @@ -618,7 +670,7 @@ class MuzzlePluginFunctionalTest { } """ ) - fixture.writeScanPlugin("// pass") + fixture.writeNoopScanPlugin() val result = fixture.run( ":dd-java-agent:instrumentation:demo:muzzle", @@ -651,7 +703,7 @@ class MuzzlePluginFunctionalTest { } """ ) - fixture.writeScanPlugin("// pass") + fixture.writeNoopScanPlugin() val result = fixture.run( ":dd-java-agent:instrumentation:demo:muzzle", @@ -681,7 +733,7 @@ class MuzzlePluginFunctionalTest { } """ ) - fixture.writeScanPlugin("// pass") + fixture.writeNoopScanPlugin() val result = fixture.run( ":dd-java-agent:instrumentation:demo:tasks", diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt new file mode 100644 index 00000000000..baf612b91bb --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt @@ -0,0 +1,104 @@ +package datadog.gradle.plugin.muzzle + +import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import java.io.File + +class MuzzlePluginPerformanceTest { + + @Test + fun `task graph does not include muzzle tasks when not requested`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + pass { + group = 'com.example.test' + module = 'some-lib' + versions = '[1.0.0,2.0.0)' + } + } + """ + ) + fixture.writeNoopScanPlugin() + + val result = fixture.run( + ":dd-java-agent:instrumentation:demo:tasks", + "--all", + "--info" + ) + + assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:tasks")?.outcome) + + assertFalse( + result.tasks.any() { it.path.contains("muzzle") }, + "Should not create or execute any muzzle tasks when not requested" + ) + assertTrue( + result.output.contains("No muzzle tasks invoked for :dd-java-agent:instrumentation:demo, skipping muzzle task planification"), + "Should log early return when muzzle not requested" + ) + } + + @Test + fun `does not configure muzzle when other project muzzle task is requested`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + muzzle { + pass { coreJdk() } + } + """ + ) + fixture.writeNoopScanPlugin() + fixture.addSubproject("dd-java-agent:instrumentation:other", + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + muzzle { + pass { coreJdk() } + } + """ + ) + + val result = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + "--stacktrace", + "--info" + ) + + assertTrue( + result.tasks.any { it.path.contains("demo") && it.path.contains("muzzle") }, + "Should execute muzzle tasks for demo project" + ) + assertTrue( + result.tasks.none() { it.path.contains("other") && it.path.contains("muzzle") }, + "Should NOT create or register execute muzzle tasks for other project" + ) + assertTrue( + result.output.lines().any { line -> + line.contains("No muzzle tasks invoked for :dd-java-agent:instrumentation:other, skipping muzzle task planification") + }, + "Other project should skip muzzle configuration when demo project's muzzle is requested" + ) + } +} diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt index 94eb3ca4706..c0d6bd5bb13 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt @@ -21,33 +21,28 @@ internal class MuzzlePluginTestFixture( // language=Groovy """ rootProject.name = 'muzzle-e2e' - include ':dd-java-agent:agent-bootstrap' - include ':dd-java-agent:agent-tooling' - include ':dd-java-agent:instrumentation:demo' """.trimIndent() ) - file("dd-java-agent/agent-bootstrap/build.gradle").writeText( - // language=Groovy + addSubproject("dd-java-agent:agent-bootstrap", """ plugins { id 'java' } tasks.register('compileMain_java11Java') - """.trimIndent() + """ ) - file("dd-java-agent/agent-tooling/build.gradle").writeText( - // language=Groovy + addSubproject("dd-java-agent:agent-tooling", """ plugins { id 'java' } - """.trimIndent() + """ ) - file("dd-java-agent/instrumentation/demo/build.gradle").writeText(instrumentationBuildScript.trimIndent()) + addSubproject("dd-java-agent:instrumentation:demo", instrumentationBuildScript) } /** @@ -59,11 +54,12 @@ internal class MuzzlePluginTestFixture( /** * Writes a muzzle scan plugin with custom assertion logic. + * The plugin is written to the agent-tooling project where it belongs. * * @param assertionBody Java code to execute in the assertion method */ fun writeScanPlugin(@Language("JAVA") assertionBody: String) { - file("dd-java-agent/instrumentation/demo/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleVersionScanPlugin.java") + file("dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleVersionScanPlugin.java") .writeText( // language=JAVA """ From 0d99dcc6b03a6d562d3fe74c3e7192d7b1685fe9 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 18 Feb 2026 16:59:10 +0100 Subject: [PATCH 13/22] test: Adds basic up-to-date testcase --- .../datadog/gradle/plugin/GradleFixture.kt | 124 ++++++++++++++---- .../muzzle/MuzzlePluginPerformanceTest.kt | 101 ++++++++++++++ 2 files changed, 202 insertions(+), 23 deletions(-) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt index 7bbe69a2895..d921503b51d 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt @@ -107,29 +107,109 @@ internal open class GradleFixture( val moduleDir = File(repoDir, "$groupPath/$module").apply { mkdirs() } versions.forEach { version -> - val versionDir = File(moduleDir, version).apply { mkdirs() } - val pomFile = File(versionDir, "$module-$version.pom") - pomFile.writeText( - """ - - 4.0.0 - $group - $module - $version - jar - - """.trimIndent() - ) - writeChecksum(pomFile) - - val jarFile = File(versionDir, "$module-$version.jar") - createJar(jarFile.toPath(), group, module, version, jarContentBuilder) - writeChecksum(jarFile) + createMavenVersion(moduleDir, group, module, version, jarContentBuilder) } val metadataFile = File(moduleDir, "maven-metadata.xml") + writeMavenMetadata(metadataFile, group, module, versions) + + return repoDir + } + + /** + * Adds a new version to an existing fake Maven repository. + * Updates the maven-metadata.xml to include the new version. + * + * @param repoDir The root directory of the fake Maven repository + * @param group Maven group ID + * @param module Maven artifact ID + * @param version Version to add + * @param jarContentBuilder Optional lambda to add entries to the JAR + */ + fun addVersionToFakeMavenRepo( + repoDir: File, + group: String, + module: String, + version: String, + jarContentBuilder: ((JarOutputStream) -> Unit)? = null + ) { + val groupPath = group.replace('.', '/') + val moduleDir = File(repoDir, "$groupPath/$module") + require(moduleDir.exists()) { "Module directory does not exist: $moduleDir" } + + // Create version artifacts (POM + JAR with checksums) + createMavenVersion(moduleDir, group, module, version, jarContentBuilder) + + // Read existing versions from metadata + val metadataFile = File(moduleDir, "maven-metadata.xml") + val existingVersions = if (metadataFile.exists()) { + val content = metadataFile.readText() + val versionRegex = "([^<]+)".toRegex() + versionRegex.findAll(content).map { it.groupValues[1] }.toList() + } else { + emptyList() + } + + // Add new version and update metadata + val allVersions = (existingVersions + version).distinct().sorted() + writeMavenMetadata(metadataFile, group, module, allVersions) + } + + /** + * Creates a single Maven version with POM and JAR artifacts (including checksums). + * + * @param moduleDir The module directory (e.g., repo/com/example/artifact) + * @param group Maven group ID + * @param module Maven artifact ID + * @param version Version to create + * @param jarContentBuilder Optional lambda to add entries to the JAR + */ + private fun createMavenVersion( + moduleDir: File, + group: String, + module: String, + version: String, + jarContentBuilder: ((JarOutputStream) -> Unit)? = null + ) { + val versionDir = File(moduleDir, version).apply { mkdirs() } + + // Create POM file + val pomFile = File(versionDir, "$module-$version.pom") + pomFile.writeText( + """ + + 4.0.0 + $group + $module + $version + jar + + """.trimIndent() + ) + writeChecksum(pomFile) + + // Create JAR file + val jarFile = File(versionDir, "$module-$version.jar") + createJar(jarFile.toPath(), group, module, version, jarContentBuilder) + writeChecksum(jarFile) + } + + /** + * Writes maven-metadata.xml for a module with the given versions. + * + * @param metadataFile The maven-metadata.xml file to write + * @param group Maven group ID + * @param module Maven artifact ID + * @param versions List of versions (should be sorted) + */ + private fun writeMavenMetadata( + metadataFile: File, + group: String, + module: String, + versions: List + ) { metadataFile.writeText( """ @@ -141,14 +221,12 @@ internal open class GradleFixture( ${versions.joinToString("\n") { " $it" }} - 20260216120000 + ${System.currentTimeMillis() / 1000} """.trimIndent() ) writeChecksum(metadataFile) - - return repoDir } /** diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt index baf612b91bb..d9ef801c49b 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt @@ -1,8 +1,10 @@ package datadog.gradle.plugin.muzzle import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir @@ -101,4 +103,103 @@ class MuzzlePluginPerformanceTest { "Other project should skip muzzle configuration when demo project's muzzle is requested" ) } + + @Test + fun `muzzle tasks are up-to-date when nothing changes`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + val repoDir = fixture.createFakeMavenRepo( + group = "com.example.test", + module = "up-to-date-lib", + versions = listOf("1.0.0", "1.1.0") + ) + + val repoUrl = repoDir.toURI().toString() + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + repositories { + maven { + url = uri('$repoUrl') + metadataSources { + mavenPom() + artifact() + } + } + } + + muzzle { + pass { + group = 'com.example.test' + module = 'up-to-date-lib' + versions = '[1.0.0,2.0.0)' + } + } + """ + ) + fixture.writeNoopScanPlugin() + + // First run - should execute the tasks + run { + val firstRun = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) + ) + + assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, + "First run should execute muzzle task") + assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.0.0")?.outcome) + assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.1.0")?.outcome) + assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome, + "First run should execute muzzle-end task") + } + + // Second run without changes - assertion tasks should be up-to-date + run { + val secondRun = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) + ) + + assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, + "First run should execute muzzle task") + assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.0.0")?.outcome, + "1.0.0 assertion task should be up-to-date") + assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.1.0")?.outcome, + "1.1.0 assertion task should be up-to-date") + assertEquals(SUCCESS, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome, + "First run should execute muzzle-end task") + } + + // Third run after adding new version - should NOT be up-to-date + // Add version 1.2.0 to the fake Maven repo + run { + fixture.addVersionToFakeMavenRepo( + repoDir = repoDir, + group = "com.example.test", + module = "up-to-date-lib", + version = "1.2.0" + ) + + val thirdRun = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) + ) + + assertEquals(UP_TO_DATE, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, + "First run should execute muzzle task") + assertEquals(UP_TO_DATE, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.0.0")?.outcome, + "1.0.0 assertion task should be up-to-date") + assertEquals(UP_TO_DATE, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.1.0")?.outcome, + "1.1.0 assertion task should be up-to-date") + assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.2.0")?.outcome, + "New 1.2.0 assertion task should be created and execute") + assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome, + "First run should execute muzzle-end task") + } + } } From d1f50d38e35f7fab403c1812174799074ac172e2 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 18 Feb 2026 17:34:39 +0100 Subject: [PATCH 14/22] test: Refactors maven fixture to support more usecases --- .../datadog/gradle/plugin/GradleFixture.kt | 209 +----------------- .../datadog/gradle/plugin/MavenRepoFixture.kt | 191 ++++++++++++++++ .../muzzle/MuzzlePluginFunctionalTest.kt | 29 ++- .../muzzle/MuzzlePluginPerformanceTest.kt | 41 ++-- .../plugin/muzzle/MuzzlePluginTestFixture.kt | 4 +- 5 files changed, 226 insertions(+), 248 deletions(-) create mode 100644 buildSrc/src/test/kotlin/datadog/gradle/plugin/MavenRepoFixture.kt diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt index d921503b51d..3bb1f85f2d5 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt @@ -6,18 +6,13 @@ import org.gradle.testkit.runner.UnexpectedBuildResultException import org.intellij.lang.annotations.Language import org.w3c.dom.Document import java.io.File -import java.nio.file.Path -import java.security.MessageDigest -import java.util.jar.JarOutputStream import javax.xml.parsers.DocumentBuilderFactory /** * Base fixture for Gradle plugin integration tests. * Provides common functionality for setting up test projects and running Gradle builds. */ -internal open class GradleFixture( - protected val projectDir: File, -) { +internal open class GradleFixture(protected val projectDir: File) { /** * Runs Gradle with the specified arguments. * @@ -84,206 +79,4 @@ internal open class GradleFixture( File(projectDir, path).also { file -> file.parentFile?.mkdirs() } - - /** - * Creates a fake local Maven repository with the specified artifacts and versions. - * Generates proper POM files, JAR files, maven-metadata.xml, and checksums. - * - * @param group Maven group ID - * @param module Maven artifact ID - * @param versions List of versions to create - * @param jarContentBuilder Optional lambda to add entries to the JAR - * @return The repository root directory - */ - fun createFakeMavenRepo( - group: String, - module: String, - versions: List, - jarContentBuilder: ((JarOutputStream) -> Unit)? = null - ): File { - require(versions.isNotEmpty()) { "versions must not be empty" } - val repoDir = File(projectDir, "fake-maven-repo").apply { mkdirs() } - val groupPath = group.replace('.', '/') - val moduleDir = File(repoDir, "$groupPath/$module").apply { mkdirs() } - - versions.forEach { version -> - createMavenVersion(moduleDir, group, module, version, jarContentBuilder) - } - - val metadataFile = File(moduleDir, "maven-metadata.xml") - writeMavenMetadata(metadataFile, group, module, versions) - - return repoDir - } - - /** - * Adds a new version to an existing fake Maven repository. - * Updates the maven-metadata.xml to include the new version. - * - * @param repoDir The root directory of the fake Maven repository - * @param group Maven group ID - * @param module Maven artifact ID - * @param version Version to add - * @param jarContentBuilder Optional lambda to add entries to the JAR - */ - fun addVersionToFakeMavenRepo( - repoDir: File, - group: String, - module: String, - version: String, - jarContentBuilder: ((JarOutputStream) -> Unit)? = null - ) { - val groupPath = group.replace('.', '/') - val moduleDir = File(repoDir, "$groupPath/$module") - require(moduleDir.exists()) { "Module directory does not exist: $moduleDir" } - - // Create version artifacts (POM + JAR with checksums) - createMavenVersion(moduleDir, group, module, version, jarContentBuilder) - - // Read existing versions from metadata - val metadataFile = File(moduleDir, "maven-metadata.xml") - val existingVersions = if (metadataFile.exists()) { - val content = metadataFile.readText() - val versionRegex = "([^<]+)".toRegex() - versionRegex.findAll(content).map { it.groupValues[1] }.toList() - } else { - emptyList() - } - - // Add new version and update metadata - val allVersions = (existingVersions + version).distinct().sorted() - writeMavenMetadata(metadataFile, group, module, allVersions) - } - - /** - * Creates a single Maven version with POM and JAR artifacts (including checksums). - * - * @param moduleDir The module directory (e.g., repo/com/example/artifact) - * @param group Maven group ID - * @param module Maven artifact ID - * @param version Version to create - * @param jarContentBuilder Optional lambda to add entries to the JAR - */ - private fun createMavenVersion( - moduleDir: File, - group: String, - module: String, - version: String, - jarContentBuilder: ((JarOutputStream) -> Unit)? = null - ) { - val versionDir = File(moduleDir, version).apply { mkdirs() } - - // Create POM file - val pomFile = File(versionDir, "$module-$version.pom") - pomFile.writeText( - """ - - 4.0.0 - $group - $module - $version - jar - - """.trimIndent() - ) - writeChecksum(pomFile) - - // Create JAR file - val jarFile = File(versionDir, "$module-$version.jar") - createJar(jarFile.toPath(), group, module, version, jarContentBuilder) - writeChecksum(jarFile) - } - - /** - * Writes maven-metadata.xml for a module with the given versions. - * - * @param metadataFile The maven-metadata.xml file to write - * @param group Maven group ID - * @param module Maven artifact ID - * @param versions List of versions (should be sorted) - */ - private fun writeMavenMetadata( - metadataFile: File, - group: String, - module: String, - versions: List - ) { - metadataFile.writeText( - """ - - $group - $module - - ${versions.last()} - ${versions.last()} - - ${versions.joinToString("\n") { " $it" }} - - ${System.currentTimeMillis() / 1000} - - - """.trimIndent() - ) - writeChecksum(metadataFile) - } - - /** - * Generates SHA-1 and MD5 checksum files for a given file. - */ - private fun writeChecksum(file: File) { - val content = file.readBytes() - val sha1 = MessageDigest.getInstance("SHA-1").digest(content) - .joinToString("") { "%02x".format(it) } - File(file.parentFile, "${file.name}.sha1").writeText(sha1) - - val md5 = MessageDigest.getInstance("MD5").digest(content) - .joinToString("") { "%02x".format(it) } - File(file.parentFile, "${file.name}.md5").writeText(md5) - } - - /** - * Creates a JAR file at the specified path with standard Maven metadata, optionally with custom content. - * - * @param path Path where the JAR should be created - * @param group Maven group ID - * @param module Maven artifact ID - * @param version Maven version - * @param contentBuilder Optional lambda to add additional entries to the JAR - */ - private fun createJar( - path: Path, - group: String, - module: String, - version: String, - contentBuilder: ((JarOutputStream) -> Unit)? = null - ) { - JarOutputStream(path.toFile().outputStream()).use { jos -> - // Add standard Maven metadata files - val metadataPath = "META-INF/maven/$group/$module" - - // Add pom.properties - jos.putNextEntry(java.util.zip.ZipEntry("$metadataPath/pom.properties")) - jos.write("groupId=$group\nartifactId=$module\nversion=$version\n".toByteArray()) - jos.closeEntry() - - // Add pom.xml - jos.putNextEntry(java.util.zip.ZipEntry("$metadataPath/pom.xml")) - jos.write( - """ - - 4.0.0 - $group - $module - $version - - """.trimIndent().toByteArray() - ) - jos.closeEntry() - - // Add any custom content - contentBuilder?.invoke(jos) - } - } } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/MavenRepoFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/MavenRepoFixture.kt new file mode 100644 index 00000000000..02216f1c7fb --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/MavenRepoFixture.kt @@ -0,0 +1,191 @@ +package datadog.gradle.plugin + +import java.io.File +import java.nio.file.Path +import java.security.MessageDigest +import java.util.jar.JarOutputStream + +/** + * Test fixture for creating and managing fake Maven repositories. + * Provides utilities to create Maven artifacts with proper structure and metadata. + * + * The fake Maven repository is automatically created in the constructor. + */ +class MavenRepoFixture(projectDir: File) { + + /** The root directory of the fake Maven repository */ + val repoDir: File = File(projectDir, "fake-maven-repo").apply { mkdirs() } + + /** + * Gets the repository URL for use in Gradle configuration. + */ + val repoUrl: String + get() = repoDir.toURI().toString() + + /** + * Publishes versions to the fake Maven repository for the specified module. + * If the module already exists, adds the new versions to the existing ones. + * Creates the module directory if it doesn't exist. + * + * @param group Maven group ID + * @param module Maven artifact ID + * @param versions List of versions to publish (will be merged with existing versions) + * @param jarContentBuilder Optional lambda to add entries to the JAR + */ + fun publishVersions( + group: String, + module: String, + versions: List, + jarContentBuilder: ((JarOutputStream) -> Unit)? = null + ) { + require(versions.isNotEmpty()) { "versions must not be empty" } + val groupPath = group.replace('.', '/') + val moduleDir = File(repoDir, "$groupPath/$module").apply { mkdirs() } + + // Create all version artifacts + versions.forEach { version -> + createMavenVersion(moduleDir, group, module, version, jarContentBuilder) + } + + // Read existing versions from metadata and merge with new versions + val metadataFile = File(moduleDir, "maven-metadata.xml") + val existingVersions = if (metadataFile.exists()) { + val content = metadataFile.readText() + val versionRegex = "([^<]+)".toRegex() + versionRegex.findAll(content).map { it.groupValues[1] }.toList() + } else { + emptyList() + } + + // Merge and sort all versions + val allVersions = (existingVersions + versions).distinct().sorted() + writeMavenMetadata(metadataFile, group, module, allVersions) + } + + /** + * Creates a single Maven version with POM and JAR artifacts (including checksums). + * + * @param moduleDir The module directory (e.g., repo/com/example/artifact) + * @param group Maven group ID + * @param module Maven artifact ID + * @param version Version to create + * @param jarContentBuilder Optional lambda to add entries to the JAR + */ + private fun createMavenVersion( + moduleDir: File, + group: String, + module: String, + version: String, + jarContentBuilder: ((JarOutputStream) -> Unit)? = null + ) { + val versionDir = File(moduleDir, version).apply { mkdirs() } + + val pomFile = File(versionDir, "$module-$version.pom") + pomFile.writeText( + """ + + 4.0.0 + $group + $module + $version + jar + + """.trimIndent() + ) + writeChecksum(pomFile) + + // Create JAR file + val jarFile = File(versionDir, "$module-$version.jar") + createJar(jarFile.toPath(), group, module, version, jarContentBuilder) + writeChecksum(jarFile) + } + + /** + * Writes maven-metadata.xml for a module with the given versions. + * + * @param metadataFile The maven-metadata.xml file to write + * @param group Maven group ID + * @param module Maven artifact ID + * @param versions List of versions (should be sorted) + */ + private fun writeMavenMetadata( + metadataFile: File, + group: String, + module: String, + versions: List + ) { + metadataFile.writeText( + """ + + $group + $module + + ${versions.last()} + ${versions.last()} + + ${versions.joinToString("\n") { " $it" }} + + ${System.currentTimeMillis() / 1000} + + + """.trimIndent() + ) + writeChecksum(metadataFile) + } + + /** + * Generates SHA-1 and MD5 checksum files for a given file. + */ + private fun writeChecksum(file: File) { + val content = file.readBytes() + val sha1 = MessageDigest.getInstance("SHA-1").digest(content) + .joinToString("") { "%02x".format(it) } + File(file.parentFile, "${file.name}.sha1").writeText(sha1) + + val md5 = MessageDigest.getInstance("MD5").digest(content) + .joinToString("") { "%02x".format(it) } + File(file.parentFile, "${file.name}.md5").writeText(md5) + } + + /** + * Creates a JAR file at the specified path with standard Maven metadata, optionally with custom content. + * + * @param path Path where the JAR should be created + * @param group Maven group ID + * @param module Maven artifact ID + * @param version Maven version + * @param contentBuilder Optional lambda to add custom entries to the JAR + */ + private fun createJar( + path: Path, + group: String, + module: String, + version: String, + contentBuilder: ((JarOutputStream) -> Unit)? = null + ) { + JarOutputStream(path.toFile().outputStream()).use { jos -> + // Add Maven metadata + val pomProperties = """ + groupId=$group + artifactId=$module + version=$version + """.trimIndent() + + val pomPropertiesPath = "META-INF/maven/$group/$module/pom.properties" + jos.putNextEntry(java.util.jar.JarEntry(pomPropertiesPath)) + jos.write(pomProperties.toByteArray()) + jos.closeEntry() + + // Add custom content if provided + contentBuilder?.invoke(jos) + + // Add manifest if not provided by contentBuilder + val manifestEntry = java.util.jar.JarEntry("META-INF/MANIFEST.MF") + jos.putNextEntry(manifestEntry) + jos.write("Manifest-Version: 1.0\n".toByteArray()) + jos.closeEntry() + } + } +} diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt index 87623600c99..5d4259aedfb 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt @@ -1,6 +1,7 @@ package datadog.gradle.plugin.muzzle import datadog.gradle.plugin.GradleFixture +import datadog.gradle.plugin.MavenRepoFixture import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue @@ -250,14 +251,13 @@ class MuzzlePluginFunctionalTest { @Test fun `artifact directive resolves multiple versions from version range`(@TempDir projectDir: File) { val fixture = MuzzlePluginTestFixture(projectDir) + val mavenRepoFixture = MavenRepoFixture(projectDir) - val repoDir = fixture.createFakeMavenRepo( + mavenRepoFixture.publishVersions( group = "com.example.test", module = "demo-lib", versions = listOf("1.0.0", "1.1.0", "1.2.0", "2.0.0") ) - - val repoUrl = repoDir.toURI().toString() fixture.writeProject( """ plugins { @@ -268,7 +268,7 @@ class MuzzlePluginFunctionalTest { // Gradle repositories for artifact download repositories { maven { - url = uri('$repoUrl') + url = uri('${mavenRepoFixture.repoUrl}') metadataSources { mavenPom() artifact() @@ -292,7 +292,7 @@ class MuzzlePluginFunctionalTest { val result = fixture.run( ":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace", - env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) + env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) assertTrue( @@ -491,16 +491,15 @@ class MuzzlePluginFunctionalTest { @Test fun `additional dependencies are added to muzzle test classpath`(@TempDir projectDir: File) { val fixture = MuzzlePluginTestFixture(projectDir) + val mavenRepoFixture = MavenRepoFixture(projectDir) // Create a fake Maven repo with a fake additional dependency // The JAR will automatically include standard Maven metadata - val repoDir = fixture.createFakeMavenRepo( + mavenRepoFixture.publishVersions( group = "com.example.extra", module = "extra-lib", versions = listOf("1.0.0") ) - - val repoUrl = repoDir.toURI().toString() fixture.writeProject( """ plugins { @@ -510,7 +509,7 @@ class MuzzlePluginFunctionalTest { repositories { maven { - url = uri('$repoUrl') + url = uri('${mavenRepoFixture.repoUrl}') metadataSources { mavenPom() artifact() @@ -547,7 +546,7 @@ class MuzzlePluginFunctionalTest { val result = fixture.run( ":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace", - env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) + env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) assertTrue( @@ -565,16 +564,17 @@ class MuzzlePluginFunctionalTest { @Test fun `excluded dependencies are removed from muzzle test classpath`(@TempDir projectDir: File) { val fixture = MuzzlePluginTestFixture(projectDir) + val mavenRepoFixture = MavenRepoFixture(projectDir) // Create a fake repo with an artifact that has transitive dependencies - val repoDir = fixture.createFakeMavenRepo( + mavenRepoFixture.publishVersions( group = "com.example.test", module = "with-transitive", versions = listOf("1.0.0") ) // Manually create a POM with a transitive dependency - val pomFile = repoDir.resolve("com/example/test/with-transitive/1.0.0/with-transitive-1.0.0.pom") + val pomFile = mavenRepoFixture.repoDir.resolve("com/example/test/with-transitive/1.0.0/with-transitive-1.0.0.pom") pomFile.writeText( """ @@ -593,7 +593,6 @@ class MuzzlePluginFunctionalTest { """.trimIndent() ) - val repoUrl = repoDir.toURI().toString() fixture.writeProject( """ plugins { @@ -603,7 +602,7 @@ class MuzzlePluginFunctionalTest { repositories { maven { - url = uri('$repoUrl') + url = uri('${mavenRepoFixture.repoUrl}') metadataSources { mavenPom() artifact() @@ -638,7 +637,7 @@ class MuzzlePluginFunctionalTest { val result = fixture.run( ":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace", - env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) + env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) assertTrue(result.output.contains("BUILD SUCCESSFUL")) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt index d9ef801c49b..de041d100f3 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt @@ -1,15 +1,13 @@ package datadog.gradle.plugin.muzzle +import datadog.gradle.plugin.MavenRepoFixture import org.gradle.testkit.runner.TaskOutcome.SUCCESS import org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource import java.io.File class MuzzlePluginPerformanceTest { @@ -107,14 +105,14 @@ class MuzzlePluginPerformanceTest { @Test fun `muzzle tasks are up-to-date when nothing changes`(@TempDir projectDir: File) { val fixture = MuzzlePluginTestFixture(projectDir) + val mavenRepoFixture = MavenRepoFixture(projectDir) - val repoDir = fixture.createFakeMavenRepo( + mavenRepoFixture.publishVersions( group = "com.example.test", - module = "up-to-date-lib", + module = "example-lib", versions = listOf("1.0.0", "1.1.0") ) - val repoUrl = repoDir.toURI().toString() fixture.writeProject( """ plugins { @@ -124,7 +122,7 @@ class MuzzlePluginPerformanceTest { repositories { maven { - url = uri('$repoUrl') + url = uri('${mavenRepoFixture.repoUrl}') metadataSources { mavenPom() artifact() @@ -135,7 +133,7 @@ class MuzzlePluginPerformanceTest { muzzle { pass { group = 'com.example.test' - module = 'up-to-date-lib' + module = 'example-lib' versions = '[1.0.0,2.0.0)' } } @@ -147,13 +145,13 @@ class MuzzlePluginPerformanceTest { run { val firstRun = fixture.run( ":dd-java-agent:instrumentation:demo:muzzle", - env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) + env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, "First run should execute muzzle task") - assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.0.0")?.outcome) - assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.1.0")?.outcome) + assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.0.0")?.outcome) + assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.1.0")?.outcome) assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome, "First run should execute muzzle-end task") } @@ -162,14 +160,14 @@ class MuzzlePluginPerformanceTest { run { val secondRun = fixture.run( ":dd-java-agent:instrumentation:demo:muzzle", - env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) + env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, "First run should execute muzzle task") - assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.0.0")?.outcome, + assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.0.0")?.outcome, "1.0.0 assertion task should be up-to-date") - assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.1.0")?.outcome, + assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.1.0")?.outcome, "1.1.0 assertion task should be up-to-date") assertEquals(SUCCESS, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome, "First run should execute muzzle-end task") @@ -178,25 +176,24 @@ class MuzzlePluginPerformanceTest { // Third run after adding new version - should NOT be up-to-date // Add version 1.2.0 to the fake Maven repo run { - fixture.addVersionToFakeMavenRepo( - repoDir = repoDir, + mavenRepoFixture.publishVersions( group = "com.example.test", - module = "up-to-date-lib", - version = "1.2.0" + module = "example-lib", + versions = listOf("1.2.0") ) val thirdRun = fixture.run( ":dd-java-agent:instrumentation:demo:muzzle", - env = mapOf("MAVEN_REPOSITORY_PROXY" to repoUrl) + env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) assertEquals(UP_TO_DATE, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, "First run should execute muzzle task") - assertEquals(UP_TO_DATE, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.0.0")?.outcome, + assertEquals(UP_TO_DATE, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.0.0")?.outcome, "1.0.0 assertion task should be up-to-date") - assertEquals(UP_TO_DATE, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.1.0")?.outcome, + assertEquals(UP_TO_DATE, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.1.0")?.outcome, "1.1.0 assertion task should be up-to-date") - assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-up-to-date-lib-1.2.0")?.outcome, + assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.2.0")?.outcome, "New 1.2.0 assertion task should be created and execute") assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome, "First run should execute muzzle-end task") diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt index c0d6bd5bb13..026ef5b0d9d 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt @@ -8,9 +8,7 @@ import java.io.File * Test fixture for muzzle plugin integration tests. * Extends GradleFixture with muzzle-specific functionality. */ -internal class MuzzlePluginTestFixture( - projectDir: File, -) : GradleFixture(projectDir) { +internal class MuzzlePluginTestFixture(projectDir: File) : GradleFixture(projectDir) { /** * Writes the basic Gradle project structure for muzzle testing. From a5f34547254f0736a941eb0ce3255bb5e998533a Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 18 Feb 2026 18:18:49 +0100 Subject: [PATCH 15/22] test: Ensures case is not up-to-date when classpath changes --- .../plugin/muzzle/MuzzleVersionUtils.kt | 2 +- .../muzzle/MuzzlePluginPerformanceTest.kt | 192 ++++++++++++++++++ .../plugin/muzzle/MuzzleVersionUtilsTest.kt | 162 +++++++++++++++ 3 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleVersionUtilsTest.kt diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleVersionUtils.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleVersionUtils.kt index f5d61653e17..9a12471775b 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleVersionUtils.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleVersionUtils.kt @@ -68,7 +68,7 @@ internal object MuzzleVersionUtils { /** * Select a random set of versions to test */ - private val RANGE_COUNT_LIMIT = 25 + internal val RANGE_COUNT_LIMIT = 25 /** * Select a random set of versions to test, limiting the range for efficiency. diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt index de041d100f3..59c7ee352dc 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt @@ -199,4 +199,196 @@ class MuzzlePluginPerformanceTest { "First run should execute muzzle-end task") } } + + @Test + fun `muzzle tasks invalidated when instrumentation code changes`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + pass { coreJdk() } + } + """ + ) + fixture.writeNoopScanPlugin() + + // First run - should execute the tasks + run { + val firstRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") + + assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, + "First run should execute muzzle task") + assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, + "First run should execute coreJdk assertion task" + ) + } + + // Second run without changes - assertion tasks should be up-to-date + run { + val secondRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") + + assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, + "Second run should be up-to-date") + assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, + "coreJdk assertion task should be up-to-date" + ) + } + + // Third run after changing instrumentation code - should be invalidated + run { + val demoSourceDir = File(projectDir, "dd-java-agent/instrumentation/demo/src/main/java/com/example") + demoSourceDir.mkdirs() + File(demoSourceDir, "Demo.java").writeText( + """ + package com.example; + + public class Demo { + public void doSomething() {} + } + """.trimIndent() + ) + + val thirdRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") + + assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, + "Third run should execute after instrumentation code change") + assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, + "coreJdk assertion task should be invalidated and re-execute" + ) + } + } + + @Test + fun `muzzle tasks invalidated when tooling classpath changes`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + pass { coreJdk() } + } + """ + ) + fixture.writeNoopScanPlugin() + + // First run - should execute the tasks + run { + val firstRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") + + assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, + "First run should execute muzzle task") + assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, + "First run should execute coreJdk assertion task" + ) + } + + // Second run without changes - assertion tasks should be up-to-date + run { + val secondRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") + + assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, + "Second run should be up-to-date") + assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, + "coreJdk assertion task should be up-to-date" + ) + } + + // Third run after changing agent-tooling code - should be invalidated + run { + val toolingSourceDir = File(projectDir, "dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling") + toolingSourceDir.mkdirs() + File(toolingSourceDir, "Extra.java").writeText( + """ + package datadog.trace.agent.tooling; + + public class Extra { + public void extraMethod() {} + } + """.trimIndent() + ) + + val thirdRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") + + assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, + "Third run should execute after tooling classpath change") + assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, + "coreJdk assertion task should be invalidated and re-execute" + ) + } + } + + @Test + fun `muzzle tasks invalidated when bootstrap classpath changes`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + muzzle { + pass { coreJdk() } + } + """ + ) + fixture.writeNoopScanPlugin() + + // First run - should execute the tasks + run { + val firstRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") + + assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, + "First run should execute muzzle task") + assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, + "First run should execute coreJdk assertion task" + ) + } + + // Second run without changes - assertion tasks should be up-to-date + run { + val secondRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") + + assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, + "Second run should be up-to-date") + assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, + "coreJdk assertion task should be up-to-date" + ) + } + + // Third run after changing agent-bootstrap code - should be invalidated + run { + val bootstrapSourceDir = File(projectDir, "dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap") + bootstrapSourceDir.mkdirs() + File(bootstrapSourceDir, "Helper.java").writeText( + """ + package datadog.trace.bootstrap; + + public class Helper { + public void help() {} + } + """.trimIndent() + ) + + val thirdRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") + + assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, + "Third run should execute after bootstrap classpath change") + assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, + "coreJdk assertion task should be invalidated and re-execute" + ) + } + } } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleVersionUtilsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleVersionUtilsTest.kt new file mode 100644 index 00000000000..aa98984cfc1 --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleVersionUtilsTest.kt @@ -0,0 +1,162 @@ +package datadog.gradle.plugin.muzzle + +import datadog.gradle.plugin.muzzle.MuzzleVersionUtils.RANGE_COUNT_LIMIT +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.resolution.VersionRangeRequest +import org.eclipse.aether.resolution.VersionRangeResult +import org.eclipse.aether.util.version.GenericVersionScheme +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.junit.jupiter.params.provider.ValueSource + +class MuzzleVersionUtilsTest { + + private val versionScheme = GenericVersionScheme() + + @ParameterizedTest(name = "[{index}] filters pre-release: {0}") + @ValueSource( + strings = + [ + "2.0.0-SNAPSHOT", // -snapshot + "2.0.0-RC1", // rc + "2.0.0.CR1", // .cr + "2.0.0-alpha", // alpha + "2.0.0-beta.1", // beta + "2.0.0-b2", // -b + "2.0.0.M1", // .m + "2.0.0-m1", // -m + "2.0.0-dev", // -dev + "2.0.0-ea", // -ea + "2.0.0-atlassian-3", // -atlassian- + "2.0-public_draft", // public_draft + "2.0.0-cr1", // -cr + "2.0-preview", // -preview + "2.0.0.redhat-1", // redhat + "2.7.3m2", // END_NMN_PATTERN ^.*\.[0-9]+[mM][0-9]+$ + "2.0.0-1a2b3c4d", // GIT_SHA_PATTERN ^.*-[0-9a-f]{7,}$ + ]) + fun `filterAndLimitVersions filters out pre-release versions when includeSnapshots is false`( + preRelease: String + ) { + val result = createVersionRangeResult("1.0.0", preRelease, "3.0.0") + + val filtered = + MuzzleVersionUtils.filterAndLimitVersions(result, emptySet(), includeSnapshots = false) + + assertFalse(filtered.any { it.toString() == preRelease }) { + "Expected '$preRelease' to be filtered out" + } + assertTrue(filtered.any { it.toString() == "1.0.0" }) + assertTrue(filtered.any { it.toString() == "3.0.0" }) + } + + @ParameterizedTest(name = "[{index}] includeSnapshots=true keeps ''{0}'', skipVersions={1}") + @MethodSource("includeSnapshotsCases") + fun `with includeSnapshots=true, keeps pre-release versions and still respects skipVersions`( + preRelease: String, + skipVersions: Set + ) { + // preRelease major.minor = 1.0, surrounded by 2.0 and 3.0-RC1 (distinct major.minor) + val result = createVersionRangeResult(preRelease, "2.0.0", "3.0.0-RC1") + + val filtered = + MuzzleVersionUtils.filterAndLimitVersions(result, skipVersions, includeSnapshots = true) + + assertTrue(filtered.any { it.toString() == preRelease }) { + "Expected '$preRelease' to be kept when includeSnapshots=true" + } + skipVersions.forEach { skipped -> + assertFalse(filtered.any { it.toString() == skipped }) { + "Expected '$skipped' to be absent due to skipVersions" + } + } + } + + @ParameterizedTest(name = "[{index}] skips exact version: {0}") + @ValueSource(strings = ["1.1.0", "1.3.0", "2.0.0"]) + fun `can skip exact versions`(versionToSkip: String) { + val result = createVersionRangeResult("1.0.0", "1.1.0", "1.2.0", "1.3.0", "2.0.0", "3.0.0") + + val filtered = + MuzzleVersionUtils.filterAndLimitVersions( + result, setOf(versionToSkip), includeSnapshots = false) + + assertFalse(filtered.any { it.toString() == versionToSkip }) + } + + @Test + fun `skip versions is case sensitive`() { + val result = createVersionRangeResult("1.0.0", "2.0.0-custom", "3.0.0") + + val filtered = + MuzzleVersionUtils.filterAndLimitVersions( + result, setOf("2.0.0-Custom"), includeSnapshots = false) + + assertTrue(filtered.any { it.toString() == "2.0.0-custom" }) { + "Expected '2.0.0-custom' to be kept because skipVersions entry 'Custom' does not match lowercased 'custom'" + } + } + + @Test + fun `trim version range larger than the limit`() { + // 30 versions with distinct major.minor: 1.0.0, 1.1.0, ..., 1.29.0 + val versions = (0..29).map { "1.$it.0" }.toTypedArray() + val result = createVersionRangeResult(*versions) + + val filtered = + MuzzleVersionUtils.filterAndLimitVersions(result, emptySet(), includeSnapshots = false) + + assertTrue(filtered.size < RANGE_COUNT_LIMIT) { "Expected fewer than 25 versions after trimming, got ${filtered.size}" } + assertTrue(filtered.isNotEmpty()) + assertTrue(filtered.any { it == result.lowestVersion }) { + "lowestVersion (${result.lowestVersion}) must be preserved" + } + assertTrue(filtered.any { it == result.highestVersion }) { + "highestVersion (${result.highestVersion}) must be preserved" + } + val originalSet = versions.toSet() + assertTrue(filtered.all { it.toString() in originalSet }) { + "All filtered versions must come from the original set" + } + } + + @ParameterizedTest(name = "[{index}] {0} version(s) pass through unchanged") + @ValueSource(ints = [1, 2, 3, 10, 24]) + fun `should limit large ranges`(count: Int) { + val versionStrings = (0 until count).map { "$it.0.0" }.toTypedArray() + val result = createVersionRangeResult(*versionStrings) + + val filtered = + MuzzleVersionUtils.filterAndLimitVersions(result, emptySet(), includeSnapshots = false) + + assertEquals(count, filtered.size) + versionStrings.forEach { v -> assertTrue(filtered.any { it.toString() == v }) } + } + + companion object { + @JvmStatic + fun includeSnapshotsCases() = listOf( + Arguments.of("1.0.0-SNAPSHOT", emptySet()), + Arguments.of("1.0.0-RC1", emptySet()), + Arguments.of("1.0.0-alpha", emptySet()), + Arguments.of("1.0.0-beta.1", emptySet()), + Arguments.of("1.0.0-b2", emptySet()), + // skipVersions is still respected even when includeSnapshots=true + Arguments.of("1.0.0-SNAPSHOT", setOf("2.0.0")), + ) + } + + private fun createVersionRangeResult(vararg versionStrings: String): VersionRangeResult { + val artifact = DefaultArtifact("com.example:test:[1.0,)") + val request = VersionRangeRequest(artifact, emptyList(), null) + val versions = versionStrings.map { versionScheme.parseVersion(it) }.sorted() + // lowestVersion/highestVersion are computed as versions[0] and versions[last] + return VersionRangeResult(request).apply { this.versions = versions } + } +} + From 67cc62d333b825e3ff50985e8ec19dc77266e81a Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 19 Feb 2026 15:46:44 +0100 Subject: [PATCH 16/22] test: Adds some muzzle plugin utils --- .../plugin/muzzle/MuzzlePluginUtilsTest.kt | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginUtilsTest.kt diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginUtilsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginUtilsTest.kt new file mode 100644 index 00000000000..258ede7d158 --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginUtilsTest.kt @@ -0,0 +1,70 @@ +package datadog.gradle.plugin.muzzle + +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.kotlin.dsl.getByType +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class MuzzlePluginUtilsTest { + @Test + fun `pathSlug for root project is empty`() { + val root = ProjectBuilder.builder().withName("root").build() + assertEquals("", root.pathSlug) + } + + @ParameterizedTest(name = "[{index}] path ''{0}'' → slug ''{1}''") + @CsvSource( + value = + [ + "foo, foo", + "foo_bar_baz, foo_bar_baz", // underscores are preserved (only colons are replaced) + ]) + fun `pathSlug for single-level child project`(childName: String, expectedSlug: String) { + val root = ProjectBuilder.builder().withName("root").build() + val child = ProjectBuilder.builder().withParent(root).withName(childName.trim()).build() + assertEquals(expectedSlug.trim(), child.pathSlug) + } + + @Test + fun `pathSlug for deeply nested project replaces colons with underscores`() { + val root = ProjectBuilder.builder().withName("root").build() + val foo = ProjectBuilder.builder().withParent(root).withName("foo").build() + val bar = ProjectBuilder.builder().withParent(foo).withName("bar").build() + val baz = ProjectBuilder.builder().withParent(bar).withName("baz").build() + + assertEquals("foo_bar_baz", baz.pathSlug) + } + + @Test + fun `allMainSourceSet includes main and excludes test`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("java") + + val sourceSets = project.allMainSourceSet + + assertTrue(sourceSets.any { it.name == "main" }) + assertFalse(sourceSets.any { it.name == "test" }) + } + + @Test + fun `allMainSourceSet includes all source sets whose name starts with main`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("java") + project.extensions.getByType().apply { + create("mainLegacy") + create("mainJava8") + } + + val sourceSets = project.allMainSourceSet + + assertEquals(3, sourceSets.size) + assertTrue(sourceSets.any { it.name == "main" }) + assertTrue(sourceSets.any { it.name == "mainLegacy" }) + assertTrue(sourceSets.any { it.name == "mainJava8" }) + } +} From 2d35a7a45fe66da0c17208e23f3d6d3a986bc58d Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 19 Feb 2026 15:48:19 +0100 Subject: [PATCH 17/22] test: adds muzzle directive tests --- .../plugin/muzzle/MuzzleDirectiveTest.kt | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleDirectiveTest.kt diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleDirectiveTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleDirectiveTest.kt new file mode 100644 index 00000000000..b011b3a653a --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleDirectiveTest.kt @@ -0,0 +1,162 @@ +package datadog.gradle.plugin.muzzle + +import org.eclipse.aether.repository.RemoteRepository +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class MuzzleDirectiveTest { + + @ParameterizedTest(name = "[{index}] nameSlug(''{0}'') == ''{1}''") + @CsvSource( + value = + [ + "simple, simple", + "My Directive, My-Directive", + "foo/bar@baz#123, foo-bar-baz-123", + ]) + fun `nameSlug replaces non-alphanumeric characters with dashes`(input: String, expected: String) { + val directive = MuzzleDirective().apply { name = input } + assertEquals(expected.trim(), directive.nameSlug) + } + + @Test + fun `nameSlug returns empty string for empty name`() { + val directive = MuzzleDirective().apply { name = "" } + assertEquals("", directive.nameSlug) + } + + @Test + fun `nameSlug trims leading and trailing whitespace before replacing`() { + val directive = MuzzleDirective().apply { name = " spaces " } + assertEquals("spaces", directive.nameSlug) + } + + @Test + fun `nameSlug returns empty string when name is null`() { + val directive = MuzzleDirective() // name defaults to null + assertEquals("", directive.nameSlug) + } + + @Test + fun `getRepositories returns defaults unchanged when no additional repos`() { + val directive = MuzzleDirective() + val defaults = listOf(RemoteRepository.Builder("central", "default", "https://repo1.maven.org/maven2/").build()) + + val repos = directive.getRepositories(defaults) + + // Same reference — no copy is made when additionalRepositories is empty + assertTrue(repos === defaults) + } + + @Test + fun `getRepositories appends additional repositories after defaults`() { + val directive = + MuzzleDirective().apply { + extraRepository("myrepo", "https://example.com/repo") + extraRepository("otherrepo", "https://other.example.com/repo", "default") + } + val defaults = + listOf( + RemoteRepository.Builder("central", "default", "https://repo1.maven.org/maven2/").build()) + + val repos = directive.getRepositories(defaults) + + assertEquals(3, repos.size) + assertEquals("central", repos[0].id) + assertEquals("myrepo", repos[1].id) + assertEquals("otherrepo", repos[2].id) + } + + @Test + fun `coreJdk without version sets isCoreJdk true and javaVersion null`() { + val directive = MuzzleDirective() + directive.coreJdk() + + assertTrue(directive.isCoreJdk) + assertNull(directive.javaVersion) + } + + @Test + fun `coreJdk with version sets isCoreJdk true and javaVersion`() { + val directive = MuzzleDirective() + directive.coreJdk("17") + + assertTrue(directive.isCoreJdk) + assertEquals("17", directive.javaVersion) + } + + @ParameterizedTest(name = "[{index}] coreJdk={0}, assertPass={1} → {2}") + @CsvSource( + value = + [ + "true, true, Pass-core-jdk", + "true, false, Fail-core-jdk", + ]) + fun `toString for coreJdk directive`(isCoreJdk: Boolean, assertPass: Boolean, expected: String) { + val directive = + MuzzleDirective().apply { + if (isCoreJdk) coreJdk() + this.assertPass = assertPass + } + assertEquals(expected, directive.toString()) + } + + @ParameterizedTest(name = "[{index}] assertPass={0} → prefix ''{1}''") + @CsvSource( + value = + [ + "true, pass", + "false, fail", + ]) + fun `toString for non-coreJdk directive includes group module versions`( + assertPass: Boolean, + prefix: String + ) { + val directive = + MuzzleDirective().apply { + group = "com.example" + module = "mylib" + versions = "[1.0,2.0)" + this.assertPass = assertPass + } + + assertEquals("$prefix com.example:mylib:[1.0,2.0)", directive.toString()) + } + + @Test + fun `extraDependency accumulates multiple entries in order`() { + val directive = MuzzleDirective() + directive.extraDependency("com.example:dep1:1.0") + directive.extraDependency("com.example:dep2:2.0") + directive.extraDependency("com.example:dep3:3.0") + + assertEquals( + listOf("com.example:dep1:1.0", "com.example:dep2:2.0", "com.example:dep3:3.0"), + directive.additionalDependencies) + } + + @Test + fun `excludeDependency accumulates multiple entries in order`() { + val directive = MuzzleDirective() + directive.excludeDependency("com.example:excluded1") + directive.excludeDependency("com.example:excluded2") + + assertEquals( + listOf("com.example:excluded1", "com.example:excluded2"), directive.excludedDependencies) + } + + @Test + fun `extraRepository accumulates multiple entries in order`() { + val directive = MuzzleDirective() + directive.extraRepository("repo1", "https://repo1.example.com") + directive.extraRepository("repo2", "https://repo2.example.com", "p2") + + assertEquals(2, directive.additionalRepositories.size) + assertEquals(Triple("repo1", "default", "https://repo1.example.com"), directive.additionalRepositories[0]) + assertEquals(Triple("repo2", "p2", "https://repo2.example.com"), directive.additionalRepositories[1]) + } +} From e55b3beaa19a103e08709cc5bea0459b13104d78 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 19 Feb 2026 16:22:20 +0100 Subject: [PATCH 18/22] test: adds muzzle repo tests --- .../plugin/muzzle/MuzzleMavenRepoUtils.kt | 10 +- .../plugin/muzzle/MuzzleMavenRepoUtilsTest.kt | 230 ++++++++++++++++++ 2 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt index 86611299bd5..998e0357b18 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt @@ -69,7 +69,8 @@ internal object MuzzleMavenRepoUtils { fun inverseOf( muzzleDirective: MuzzleDirective, system: RepositorySystem, - session: RepositorySystemSession + session: RepositorySystemSession, + defaultRepos: List = MUZZLE_REPOS ): Set { val allVersionsArtifact = DefaultArtifact( muzzleDirective.group, @@ -77,7 +78,7 @@ internal object MuzzleMavenRepoUtils { "jar", "[,)" ) - val repos = muzzleDirective.getRepositories(MUZZLE_REPOS) + val repos = muzzleDirective.getRepositories(defaultRepos) val allRangeRequest = VersionRangeRequest().apply { repositories = repos artifact = allVersionsArtifact @@ -122,7 +123,8 @@ internal object MuzzleMavenRepoUtils { fun resolveVersionRange( muzzleDirective: MuzzleDirective, system: RepositorySystem, - session: RepositorySystemSession + session: RepositorySystemSession, + defaultRepos: List = MUZZLE_REPOS ): VersionRangeResult { val directiveArtifact: Artifact = DefaultArtifact( muzzleDirective.group, @@ -132,7 +134,7 @@ internal object MuzzleMavenRepoUtils { muzzleDirective.versions ) val rangeRequest = VersionRangeRequest().apply { - repositories = muzzleDirective.getRepositories(MUZZLE_REPOS) + repositories = muzzleDirective.getRepositories(defaultRepos) artifact = directiveArtifact } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt new file mode 100644 index 00000000000..6a4f656db4a --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt @@ -0,0 +1,230 @@ +package datadog.gradle.plugin.muzzle + +import datadog.gradle.plugin.MavenRepoFixture +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.repository.RemoteRepository +import org.eclipse.aether.resolution.VersionRangeRequest +import org.eclipse.aether.resolution.VersionRangeResult +import org.eclipse.aether.util.version.GenericVersionScheme +import org.gradle.api.GradleException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import java.io.File + +class MuzzleMavenRepoUtilsTest { + + @TempDir + lateinit var tempDir: File + + private val system = MuzzleMavenRepoUtils.newRepositorySystem() + + private val versionScheme = GenericVersionScheme() + + @Test + fun `resolveVersionRange resolves all versions matching an open range`() { + val repo = publishAndGetRepo("com.example", "mylib", listOf("1.0.0", "2.0.0", "3.0.0")) + val directive = MuzzleDirective().apply { + group = "com.example" + module = "mylib" + versions = "[1.0,)" + } + + val result = MuzzleMavenRepoUtils.resolveVersionRange(directive, system, newSession(), listOf(repo)) + + val resolvedVersions = result.versions.map { it.toString() } + assertEquals(listOf("1.0.0", "2.0.0", "3.0.0"), resolvedVersions) + } + + @Test + fun `resolveVersionRange respects bounded version range`() { + val repo = publishAndGetRepo("com.example", "mylib", listOf("1.0.0", "2.0.0", "3.0.0", "4.0.0", "5.0.0")) + val directive = MuzzleDirective().apply { + group = "com.example" + module = "mylib" + versions = "[2.0,4.0)" + } + + val result = MuzzleMavenRepoUtils.resolveVersionRange(directive, system, newSession(), listOf(repo)) + + val resolvedVersions = result.versions.map { it.toString() } + assertEquals(listOf("2.0.0", "3.0.0"), resolvedVersions) + } + + @Test + fun `resolveVersionRange throws IllegalStateException when resolution consistently fails`() { + val emptyRepo = RemoteRepository.Builder("empty", "default", File(tempDir, "empty").apply { mkdirs() }.toURI().toString()).build() + val directive = MuzzleDirective().apply { + group = "com.example" + module = "nonexistent" + versions = "[1.0,)" + } + + assertThrows { + MuzzleMavenRepoUtils.resolveVersionRange(directive, system, newSession(), listOf(emptyRepo)) + } + } + + @Test + fun `resolveVersionRange includes directive extra repositories`() { + val repoA = publishAndGetRepo("com.example", "mylib", listOf("1.0.0", "2.0.0"), subDir = "repoA") + val fixtureB = MavenRepoFixture(File(tempDir, "repoB")) + fixtureB.publishVersions("com.example", "mylib", listOf("3.0.0")) + val directive = MuzzleDirective().apply { + group = "com.example" + module = "mylib" + versions = "[1.0,)" + extraRepository("repoB", fixtureB.repoUrl) + } + + val result = MuzzleMavenRepoUtils.resolveVersionRange(directive, system, newSession(), listOf(repoA)) + + val resolvedVersions = result.versions.map { it.toString() } + assertTrue(resolvedVersions.containsAll(listOf("1.0.0", "2.0.0", "3.0.0"))) { + "Expected all 3 versions from both repos, got: $resolvedVersions" + } + } + + @Test + fun `inverseOf returns directives outside range, inverts assertPass, and preserves properties`() { + val repo = publishAndGetRepo("com.example", "mylib", listOf("1.0.0", "2.0.0", "3.0.0", "4.0.0", "5.0.0")) + val directive = MuzzleDirective().apply { + name = "mytest" + group = "com.example" + module = "mylib" + versions = "[2.0,4.0)" + assertPass = true + excludeDependency("com.other:dep") + includeSnapshots = false + } + + val result = MuzzleMavenRepoUtils.inverseOf(directive, system, newSession(), listOf(repo)) + + val resultVersions = result.map { it.versions }.toSet() + // Versions inside [2.0, 4.0) are 2.0.0 and 3.0.0 — they should NOT appear + assertFalse(resultVersions.contains("2.0.0")) { "2.0.0 is inside range and must not appear in inverse" } + assertFalse(resultVersions.contains("3.0.0")) { "3.0.0 is inside range and must not appear in inverse" } + // Versions outside range: 1.0.0, 4.0.0, 5.0.0 + assertTrue(resultVersions.containsAll(listOf("1.0.0", "4.0.0", "5.0.0"))) { + "Expected versions outside [2.0,4.0), got: $resultVersions" + } + + // assertPass must be inverted + assertTrue(result.all { !it.assertPass }) { "All inverse directives must have assertPass=false" } + + // Directive properties must be preserved + assertTrue(result.all { it.name == "mytest" }) { "name must be preserved" } + assertTrue(result.all { it.group == "com.example" }) { "group must be preserved" } + assertTrue(result.all { it.module == "mylib" }) { "module must be preserved" } + assertTrue(result.all { it.excludedDependencies == listOf("com.other:dep") }) { "excludedDependencies must be preserved" } + assertTrue(result.all { !it.includeSnapshots }) { "includeSnapshots must be preserved" } + } + + @ParameterizedTest(name = "[{index}] highest({0}, {1}) == {2}") + @CsvSource( + value = + [ + "1.0.0, 2.0.0, 2.0.0", + "2.0.0, 1.0.0, 2.0.0", + "3.5.1, 3.5.1, 3.5.1", // equal — either is acceptable + ]) + fun `highest returns the greater version`(a: String, b: String, expected: String) { + val result = MuzzleMavenRepoUtils.highest(version(a), version(b)) + assertEquals(version(expected), result) + } + + @ParameterizedTest(name = "[{index}] lowest({0}, {1}) == {2}") + @CsvSource( + value = + [ + "1.0.0, 2.0.0, 1.0.0", + "2.0.0, 1.0.0, 1.0.0", + "3.5.1, 3.5.1, 3.5.1", // equal — either is acceptable + ]) + fun `lowest returns the lesser version`(a: String, b: String, expected: String) { + val result = MuzzleMavenRepoUtils.lowest(version(a), version(b)) + assertEquals(version(expected), result) + } + + @Test + fun `muzzleDirectiveToArtifacts throws GradleException when all versions are filtered out`() { + val directive = + MuzzleDirective().apply { + group = "com.example" + module = "test" + includeSnapshots = false // SNAPSHOT and RC will be filtered + } + // All versions are pre-release; none survive filterAndLimitVersions + val rangeResult = createVersionRangeResult("1.0.0-SNAPSHOT", "2.0.0-RC1") + + assertThrows { + MuzzleMavenRepoUtils.muzzleDirectiveToArtifacts(directive, rangeResult) + } + } + + @Test + fun `muzzleDirectiveToArtifacts produces artifacts with correct coordinates`() { + val directive = + MuzzleDirective().apply { + group = "com.example" + module = "mylib" + // classifier is null → DefaultArtifact receives "" + includeSnapshots = false + } + // Distinct major.minor versions so lowAndHighForMajorMinor keeps all three + val rangeResult = createVersionRangeResult("1.0.0", "2.0.0", "3.0.0") + + val artifacts = MuzzleMavenRepoUtils.muzzleDirectiveToArtifacts(directive, rangeResult) + + assertEquals(3, artifacts.size) + assertTrue(artifacts.all { it.groupId == "com.example" }) { "All artifacts must have groupId 'com.example'" } + assertTrue(artifacts.all { it.artifactId == "mylib" }) { "All artifacts must have artifactId 'mylib'" } + assertTrue(artifacts.all { it.extension == "jar" }) { "All artifacts must have extension 'jar'" } + assertTrue(artifacts.all { it.classifier == "" }) { "All artifacts must have empty classifier" } + assertEquals(setOf("1.0.0", "2.0.0", "3.0.0"), artifacts.map { it.version }.toSet()) + } + + @Test + fun `muzzleDirectiveToArtifacts propagates classifier to artifacts`() { + val directive = + MuzzleDirective().apply { + group = "com.example" + module = "mylib" + classifier = "tests" + includeSnapshots = false + } + val rangeResult = createVersionRangeResult("1.0.0", "2.0.0") + + val artifacts = MuzzleMavenRepoUtils.muzzleDirectiveToArtifacts(directive, rangeResult) + + assertTrue(artifacts.all { it.classifier == "tests" }) + } + + private fun newSession() = MuzzleMavenRepoUtils.newRepositorySystemSession(system) + + private fun publishAndGetRepo( + group: String, + module: String, + versions: List, + subDir: String = "default" + ): RemoteRepository { + val fixture = MavenRepoFixture(File(tempDir, subDir)) + fixture.publishVersions(group, module, versions) + return RemoteRepository.Builder(subDir, "default", fixture.repoUrl).build() + } + + private fun version(v: String) = versionScheme.parseVersion(v) + + private fun createVersionRangeResult(vararg versionStrings: String): VersionRangeResult { + val artifact = DefaultArtifact("com.example:test:[1.0,)") + val request = VersionRangeRequest(artifact, emptyList(), null) + val versions = versionStrings.map { versionScheme.parseVersion(it) }.sorted() + // lowestVersion/highestVersion are computed as versions[0] and versions[last] + return VersionRangeResult(request).apply { this.versions = versions } + } +} From d019e9ec54684e25492628716c85b0faa292ce10 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 19 Feb 2026 17:06:21 +0100 Subject: [PATCH 19/22] test: adds assertInverse functional test --- .../muzzle/MuzzlePluginFunctionalTest.kt | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt index 5d4259aedfb..968bf76503d 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt @@ -786,6 +786,95 @@ class MuzzlePluginFunctionalTest { ) } + @Test + fun `assertInverse creates pass and fail tasks for in-range and out-of-range versions`(@TempDir projectDir: File) { + val fixture = MuzzlePluginTestFixture(projectDir) + val mavenRepoFixture = MavenRepoFixture(projectDir) + + mavenRepoFixture.publishVersions( + group = "com.example.test", + module = "inverse-lib", + versions = listOf("1.0.0", "2.0.0", "3.0.0", "4.0.0") + ) + fixture.writeProject( + """ + plugins { + id 'java' + id 'dd-trace-java.muzzle' + } + + // Gradle repositories for artifact download + repositories { + maven { + url = uri('${mavenRepoFixture.repoUrl}') + metadataSources { + mavenPom() + artifact() + } + } + } + + muzzle { + pass { + group = 'com.example.test' + module = 'inverse-lib' + versions = '[2.0.0,3.0.0]' + assertInverse = true + } + } + """ + ) + fixture.writeScanPlugin( + """ + System.out.println("MUZZLE_CHECK assertPass=" + assertPass); + """ + ) + + val result = fixture.run( + ":dd-java-agent:instrumentation:demo:muzzle", + "--stacktrace", + env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) + ) + + assertTrue( + result.output.contains("BUILD SUCCESSFUL"), + "Build should succeed. Output:\n${result.output.take(3000)}" + ) + + val modulePrefix = ":dd-java-agent:instrumentation:demo" + assertEquals(SUCCESS, result.task("$modulePrefix:muzzle")?.outcome) + assertEquals(SUCCESS, result.task("$modulePrefix:muzzle-end")?.outcome) + + // In-range versions — assertPass=true + assertEquals(SUCCESS, result.task("$modulePrefix:muzzle-AssertPass-com.example.test-inverse-lib-2.0.0")?.outcome) + assertEquals(SUCCESS, result.task("$modulePrefix:muzzle-AssertPass-com.example.test-inverse-lib-3.0.0")?.outcome) + + // Out-of-range versions (inverse) — assertPass=false + assertEquals(SUCCESS, result.task("$modulePrefix:muzzle-AssertFail-com.example.test-inverse-lib-1.0.0")?.outcome) + assertEquals(SUCCESS, result.task("$modulePrefix:muzzle-AssertFail-com.example.test-inverse-lib-4.0.0")?.outcome) + + assertTrue( + result.output.contains("MUZZLE_CHECK assertPass=true"), + "Should log assertPass=true for in-range versions" + ) + assertTrue( + result.output.contains("MUZZLE_CHECK assertPass=false"), + "Should log assertPass=false for out-of-range (inverse) versions" + ) + + // Verify JUnit report contains all 4 test cases with no failures + val reportFile = fixture.findSingleMuzzleJUnitReport() + val report = fixture.parseXml(reportFile) + val suite = report.documentElement + assertEquals("4", suite.getAttribute("tests"), "Should have 4 test cases (2 pass + 2 inverse fail)") + assertEquals("0", suite.getAttribute("failures"), "Should have no failures") + + findTestCase(report, "muzzle-AssertPass-com.example.test-inverse-lib-2.0.0") + findTestCase(report, "muzzle-AssertPass-com.example.test-inverse-lib-3.0.0") + findTestCase(report, "muzzle-AssertFail-com.example.test-inverse-lib-1.0.0") + findTestCase(report, "muzzle-AssertFail-com.example.test-inverse-lib-4.0.0") + } + private fun findTestCase(document: org.w3c.dom.Document, name: String): org.w3c.dom.Element = (0 until document.getElementsByTagName("testcase").length) .map { document.getElementsByTagName("testcase").item(it) as org.w3c.dom.Element } From f3036a6e8add4317c33a6f2ca6083f5545cd10d8 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 19 Feb 2026 22:14:30 +0100 Subject: [PATCH 20/22] test: Use AssertJ --- buildSrc/build.gradle.kts | 3 + .../plugin/muzzle/MuzzleDirectiveTest.kt | 52 ++-- .../plugin/muzzle/MuzzleMavenRepoUtilsTest.kt | 69 +++-- .../muzzle/MuzzlePluginFunctionalTest.kt | 271 +++++++++--------- .../muzzle/MuzzlePluginPerformanceTest.kt | 203 +++++++------ .../plugin/muzzle/MuzzlePluginUtilsTest.kt | 18 +- .../plugin/muzzle/MuzzleVersionUtilsTest.kt | 58 ++-- .../gradle/plugin/muzzle/RangeQueryTest.kt | 4 +- .../gradle/plugin/muzzle/VersionSetTest.kt | 16 +- .../muzzle/planner/MuzzleTaskPlannerTest.kt | 201 ++++++------- .../plugin/muzzle/tasks/MuzzleEndTaskTest.kt | 36 ++- gradle/libs.versions.toml | 2 + 12 files changed, 452 insertions(+), 481 deletions(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 3b79a7a3224..ad06adecc27 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -104,6 +104,9 @@ testing { @Suppress("UnstableApiUsage") suites { val test by getting(JvmTestSuite::class) { + dependencies { + implementation(libs.assertj.core) + } targets.configureEach { testTask.configure { enabled = providers.systemProperty("runBuildSrcTests").isPresent or providers.systemProperty("idea.active").isPresent diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleDirectiveTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleDirectiveTest.kt index b011b3a653a..b5c2ccaef42 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleDirectiveTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleDirectiveTest.kt @@ -1,12 +1,10 @@ package datadog.gradle.plugin.muzzle import org.eclipse.aether.repository.RemoteRepository -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource +import org.assertj.core.api.Assertions.assertThat class MuzzleDirectiveTest { @@ -20,25 +18,25 @@ class MuzzleDirectiveTest { ]) fun `nameSlug replaces non-alphanumeric characters with dashes`(input: String, expected: String) { val directive = MuzzleDirective().apply { name = input } - assertEquals(expected.trim(), directive.nameSlug) + assertThat(directive.nameSlug).isEqualTo(expected.trim()) } @Test fun `nameSlug returns empty string for empty name`() { val directive = MuzzleDirective().apply { name = "" } - assertEquals("", directive.nameSlug) + assertThat(directive.nameSlug).isEmpty() } @Test fun `nameSlug trims leading and trailing whitespace before replacing`() { val directive = MuzzleDirective().apply { name = " spaces " } - assertEquals("spaces", directive.nameSlug) + assertThat(directive.nameSlug).isEqualTo("spaces") } @Test fun `nameSlug returns empty string when name is null`() { val directive = MuzzleDirective() // name defaults to null - assertEquals("", directive.nameSlug) + assertThat(directive.nameSlug).isEmpty() } @Test @@ -49,7 +47,7 @@ class MuzzleDirectiveTest { val repos = directive.getRepositories(defaults) // Same reference — no copy is made when additionalRepositories is empty - assertTrue(repos === defaults) + assertThat(repos).isSameAs(defaults) } @Test @@ -65,10 +63,7 @@ class MuzzleDirectiveTest { val repos = directive.getRepositories(defaults) - assertEquals(3, repos.size) - assertEquals("central", repos[0].id) - assertEquals("myrepo", repos[1].id) - assertEquals("otherrepo", repos[2].id) + assertThat(repos.map { it.id }).containsExactly("central", "myrepo", "otherrepo") } @Test @@ -76,8 +71,8 @@ class MuzzleDirectiveTest { val directive = MuzzleDirective() directive.coreJdk() - assertTrue(directive.isCoreJdk) - assertNull(directive.javaVersion) + assertThat(directive.isCoreJdk).isTrue() + assertThat(directive.javaVersion).isNull() } @Test @@ -85,8 +80,8 @@ class MuzzleDirectiveTest { val directive = MuzzleDirective() directive.coreJdk("17") - assertTrue(directive.isCoreJdk) - assertEquals("17", directive.javaVersion) + assertThat(directive.isCoreJdk).isTrue() + assertThat(directive.javaVersion).isEqualTo("17") } @ParameterizedTest(name = "[{index}] coreJdk={0}, assertPass={1} → {2}") @@ -102,7 +97,7 @@ class MuzzleDirectiveTest { if (isCoreJdk) coreJdk() this.assertPass = assertPass } - assertEquals(expected, directive.toString()) + assertThat(directive.toString()).isEqualTo(expected) } @ParameterizedTest(name = "[{index}] assertPass={0} → prefix ''{1}''") @@ -124,7 +119,7 @@ class MuzzleDirectiveTest { this.assertPass = assertPass } - assertEquals("$prefix com.example:mylib:[1.0,2.0)", directive.toString()) + assertThat(directive.toString()).isEqualTo("$prefix com.example:mylib:[1.0,2.0)") } @Test @@ -134,9 +129,11 @@ class MuzzleDirectiveTest { directive.extraDependency("com.example:dep2:2.0") directive.extraDependency("com.example:dep3:3.0") - assertEquals( - listOf("com.example:dep1:1.0", "com.example:dep2:2.0", "com.example:dep3:3.0"), - directive.additionalDependencies) + assertThat(directive.additionalDependencies).containsExactly( + "com.example:dep1:1.0", + "com.example:dep2:2.0", + "com.example:dep3:3.0" + ) } @Test @@ -145,8 +142,10 @@ class MuzzleDirectiveTest { directive.excludeDependency("com.example:excluded1") directive.excludeDependency("com.example:excluded2") - assertEquals( - listOf("com.example:excluded1", "com.example:excluded2"), directive.excludedDependencies) + assertThat(directive.excludedDependencies).containsExactly( + "com.example:excluded1", + "com.example:excluded2" + ) } @Test @@ -155,8 +154,9 @@ class MuzzleDirectiveTest { directive.extraRepository("repo1", "https://repo1.example.com") directive.extraRepository("repo2", "https://repo2.example.com", "p2") - assertEquals(2, directive.additionalRepositories.size) - assertEquals(Triple("repo1", "default", "https://repo1.example.com"), directive.additionalRepositories[0]) - assertEquals(Triple("repo2", "p2", "https://repo2.example.com"), directive.additionalRepositories[1]) + assertThat(directive.additionalRepositories).containsExactly( + Triple("repo1", "default", "https://repo1.example.com"), + Triple("repo2", "p2", "https://repo2.example.com"), + ) } } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt index 6a4f656db4a..8c500a42f43 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt @@ -7,15 +7,13 @@ import org.eclipse.aether.resolution.VersionRangeRequest import org.eclipse.aether.resolution.VersionRangeResult import org.eclipse.aether.util.version.GenericVersionScheme import org.gradle.api.GradleException -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource import java.io.File +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy class MuzzleMavenRepoUtilsTest { @@ -38,7 +36,7 @@ class MuzzleMavenRepoUtilsTest { val result = MuzzleMavenRepoUtils.resolveVersionRange(directive, system, newSession(), listOf(repo)) val resolvedVersions = result.versions.map { it.toString() } - assertEquals(listOf("1.0.0", "2.0.0", "3.0.0"), resolvedVersions) + assertThat(resolvedVersions).containsExactly("1.0.0", "2.0.0", "3.0.0") } @Test @@ -53,7 +51,7 @@ class MuzzleMavenRepoUtilsTest { val result = MuzzleMavenRepoUtils.resolveVersionRange(directive, system, newSession(), listOf(repo)) val resolvedVersions = result.versions.map { it.toString() } - assertEquals(listOf("2.0.0", "3.0.0"), resolvedVersions) + assertThat(resolvedVersions).containsExactly("2.0.0", "3.0.0") } @Test @@ -65,9 +63,9 @@ class MuzzleMavenRepoUtilsTest { versions = "[1.0,)" } - assertThrows { + assertThatThrownBy { MuzzleMavenRepoUtils.resolveVersionRange(directive, system, newSession(), listOf(emptyRepo)) - } + }.isInstanceOf(IllegalStateException::class.java) } @Test @@ -85,9 +83,9 @@ class MuzzleMavenRepoUtilsTest { val result = MuzzleMavenRepoUtils.resolveVersionRange(directive, system, newSession(), listOf(repoA)) val resolvedVersions = result.versions.map { it.toString() } - assertTrue(resolvedVersions.containsAll(listOf("1.0.0", "2.0.0", "3.0.0"))) { - "Expected all 3 versions from both repos, got: $resolvedVersions" - } + assertThat(resolvedVersions) + .withFailMessage("Expected all 3 versions from both repos, got: $resolvedVersions") + .containsAll(listOf("1.0.0", "2.0.0", "3.0.0")) } @Test @@ -107,22 +105,19 @@ class MuzzleMavenRepoUtilsTest { val resultVersions = result.map { it.versions }.toSet() // Versions inside [2.0, 4.0) are 2.0.0 and 3.0.0 — they should NOT appear - assertFalse(resultVersions.contains("2.0.0")) { "2.0.0 is inside range and must not appear in inverse" } - assertFalse(resultVersions.contains("3.0.0")) { "3.0.0 is inside range and must not appear in inverse" } + assertThat(resultVersions).doesNotContain("2.0.0", "3.0.0") // Versions outside range: 1.0.0, 4.0.0, 5.0.0 - assertTrue(resultVersions.containsAll(listOf("1.0.0", "4.0.0", "5.0.0"))) { - "Expected versions outside [2.0,4.0), got: $resultVersions" + assertThat(resultVersions).contains("1.0.0", "4.0.0", "5.0.0") + + // assertPass must be inverted, and directive properties must be preserved + assertThat(result).allSatisfy { directive -> + assertThat(directive.assertPass).isFalse() + assertThat(directive.name).isEqualTo("mytest") + assertThat(directive.group).isEqualTo("com.example") + assertThat(directive.module).isEqualTo("mylib") + assertThat(directive.excludedDependencies).containsExactly("com.other:dep") + assertThat(directive.includeSnapshots).isFalse() } - - // assertPass must be inverted - assertTrue(result.all { !it.assertPass }) { "All inverse directives must have assertPass=false" } - - // Directive properties must be preserved - assertTrue(result.all { it.name == "mytest" }) { "name must be preserved" } - assertTrue(result.all { it.group == "com.example" }) { "group must be preserved" } - assertTrue(result.all { it.module == "mylib" }) { "module must be preserved" } - assertTrue(result.all { it.excludedDependencies == listOf("com.other:dep") }) { "excludedDependencies must be preserved" } - assertTrue(result.all { !it.includeSnapshots }) { "includeSnapshots must be preserved" } } @ParameterizedTest(name = "[{index}] highest({0}, {1}) == {2}") @@ -135,7 +130,7 @@ class MuzzleMavenRepoUtilsTest { ]) fun `highest returns the greater version`(a: String, b: String, expected: String) { val result = MuzzleMavenRepoUtils.highest(version(a), version(b)) - assertEquals(version(expected), result) + assertThat(result).isEqualTo(version(expected)) } @ParameterizedTest(name = "[{index}] lowest({0}, {1}) == {2}") @@ -148,7 +143,7 @@ class MuzzleMavenRepoUtilsTest { ]) fun `lowest returns the lesser version`(a: String, b: String, expected: String) { val result = MuzzleMavenRepoUtils.lowest(version(a), version(b)) - assertEquals(version(expected), result) + assertThat(result).isEqualTo(version(expected)) } @Test @@ -162,9 +157,9 @@ class MuzzleMavenRepoUtilsTest { // All versions are pre-release; none survive filterAndLimitVersions val rangeResult = createVersionRangeResult("1.0.0-SNAPSHOT", "2.0.0-RC1") - assertThrows { + assertThatThrownBy { MuzzleMavenRepoUtils.muzzleDirectiveToArtifacts(directive, rangeResult) - } + }.isInstanceOf(GradleException::class.java) } @Test @@ -181,12 +176,14 @@ class MuzzleMavenRepoUtilsTest { val artifacts = MuzzleMavenRepoUtils.muzzleDirectiveToArtifacts(directive, rangeResult) - assertEquals(3, artifacts.size) - assertTrue(artifacts.all { it.groupId == "com.example" }) { "All artifacts must have groupId 'com.example'" } - assertTrue(artifacts.all { it.artifactId == "mylib" }) { "All artifacts must have artifactId 'mylib'" } - assertTrue(artifacts.all { it.extension == "jar" }) { "All artifacts must have extension 'jar'" } - assertTrue(artifacts.all { it.classifier == "" }) { "All artifacts must have empty classifier" } - assertEquals(setOf("1.0.0", "2.0.0", "3.0.0"), artifacts.map { it.version }.toSet()) + assertThat(artifacts).hasSize(3) + assertThat(artifacts).allSatisfy { artifact -> + assertThat(artifact.groupId).isEqualTo("com.example") + assertThat(artifact.artifactId).isEqualTo("mylib") + assertThat(artifact.extension).isEqualTo("jar") + assertThat(artifact.classifier).isEmpty() + } + assertThat(artifacts.map { it.version }).containsOnly("1.0.0", "2.0.0", "3.0.0") } @Test @@ -202,7 +199,7 @@ class MuzzleMavenRepoUtilsTest { val artifacts = MuzzleMavenRepoUtils.muzzleDirectiveToArtifacts(directive, rangeResult) - assertTrue(artifacts.all { it.classifier == "tests" }) + assertThat(artifacts).allSatisfy { assertThat(it.classifier).isEqualTo("tests") } } private fun newSession() = MuzzleMavenRepoUtils.newRepositorySystemSession(system) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt index 968bf76503d..febd9de8879 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt @@ -2,18 +2,14 @@ package datadog.gradle.plugin.muzzle import datadog.gradle.plugin.GradleFixture import datadog.gradle.plugin.MavenRepoFixture -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue +import org.assertj.core.api.Assertions.assertThat import org.gradle.testkit.runner.TaskOutcome.SUCCESS import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertNull import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import java.io.File -import java.nio.file.Files import kotlin.io.path.readText class MuzzlePluginFunctionalTest { @@ -53,19 +49,14 @@ class MuzzlePluginFunctionalTest { val result = fixture.run(taskName, "--stacktrace") - assertTrue( - result.tasks.any { it.path.contains("muzzle") }, - "Should create muzzle tasks when '$taskName' is requested" - ) - assertFalse( - result.output.contains("No muzzle tasks invoked, skipping muzzle task planification"), - "Should not skip muzzle task planification when '$taskName' is requested" - ) - assertTrue( - result.task(":dd-java-agent:instrumentation:demo:muzzle") != null || - result.tasks.any { it.path.contains("muzzle-Assert") }, - "Should execute muzzle tasks when '$taskName' is requested" - ) + assertThat(result.tasks) + .withFailMessage("Should create muzzle tasks when '$taskName' is requested") + .anyMatch { it.path.contains("muzzle") } + assertThat(result.output) + .withFailMessage("Should not skip muzzle task planification when '$taskName' is requested") + .doesNotContain("No muzzle tasks invoked, skipping muzzle task planification") + assertThat(result.tasks).withFailMessage("Should execute muzzle tasks when '$taskName' is requested") + .anyMatch { it.path == ":dd-java-agent:instrumentation:demo:muzzle" || it.path.contains("muzzle-Assert") } } @Test @@ -95,20 +86,21 @@ class MuzzlePluginFunctionalTest { ) val buildResult = fixture.run(":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace") - assertEquals(SUCCESS, buildResult.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) - assertEquals(SUCCESS, buildResult.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) - assertEquals(SUCCESS, buildResult.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome) + assertThat(buildResult.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome).isEqualTo(SUCCESS) + assertThat(buildResult.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .isEqualTo(SUCCESS) + assertThat(buildResult.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome).isEqualTo(SUCCESS) val reportFile = fixture.findSingleMuzzleJUnitReport() val report = fixture.parseXml(reportFile) val suite = report.documentElement - assertEquals("testsuite", suite.tagName) - assertEquals(":dd-java-agent:instrumentation:demo", suite.getAttribute("name")) - assertEquals("1", suite.getAttribute("tests")) - assertEquals("0", suite.getAttribute("failures")) + assertThat(suite.tagName).isEqualTo("testsuite") + assertThat(suite.getAttribute("name")).isEqualTo(":dd-java-agent:instrumentation:demo") + assertThat(suite.getAttribute("tests")).isEqualTo("1") + assertThat(suite.getAttribute("failures")).isEqualTo("0") val passCase = findTestCase(report, "muzzle-AssertPass-core-jdk") - assertEquals(0, passCase.getElementsByTagName("failure").length) + assertThat(passCase.getElementsByTagName("failure").length).isEqualTo(0) } @Test @@ -131,17 +123,17 @@ class MuzzlePluginFunctionalTest { ) val result = fixture.run(":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace") - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome).isEqualTo(SUCCESS) val reportFile = fixture.findSingleMuzzleJUnitReport() val report = fixture.parseXml(reportFile) val suite = report.documentElement - assertEquals(":dd-java-agent:instrumentation:demo", suite.getAttribute("name")) - assertEquals("1", suite.getAttribute("tests")) - assertEquals("0", suite.getAttribute("failures")) + assertThat(suite.getAttribute("name")).isEqualTo(":dd-java-agent:instrumentation:demo") + assertThat(suite.getAttribute("tests")).isEqualTo("1") + assertThat(suite.getAttribute("failures")).isEqualTo("0") val defaultCase = findTestCase(report, "muzzle") - assertEquals(0, defaultCase.getElementsByTagName("failure").length) + assertThat(defaultCase.getElementsByTagName("failure").length).isEqualTo(0) } @Test @@ -164,7 +156,7 @@ class MuzzlePluginFunctionalTest { val buildResult = fixture.run(":dd-java-agent:instrumentation:demo:tasks", "--all") - assertFalse(buildResult.output.contains("muzzle-end")) + assertThat(buildResult.output).doesNotContain("muzzle-end") } @Test @@ -184,14 +176,14 @@ class MuzzlePluginFunctionalTest { "--configuration", "muzzleBootstrap" ) - assertTrue(bootstrapDependencies.output.contains("project :dd-java-agent:agent-bootstrap")) + assertThat(bootstrapDependencies.output).contains("project :dd-java-agent:agent-bootstrap") val toolingDependencies = fixture.run( ":dd-java-agent:instrumentation:demo:dependencies", "--configuration", "muzzleTooling" ) - assertTrue(toolingDependencies.output.contains("project :dd-java-agent:agent-tooling")) + assertThat(toolingDependencies.output).contains("project :dd-java-agent:agent-tooling") } @Test @@ -222,10 +214,10 @@ class MuzzlePluginFunctionalTest { val failDirectiveTaskPath = ":dd-java-agent:instrumentation:demo:muzzle-AssertFail-core-jdk" val endTaskPath = ":dd-java-agent:instrumentation:demo:muzzle-end" - assertEquals(SUCCESS, result.task(muzzleTaskPath)?.outcome) - assertEquals(SUCCESS, result.task(passDirectiveTaskPath)?.outcome) - assertEquals(SUCCESS, result.task(failDirectiveTaskPath)?.outcome) - assertEquals(SUCCESS, result.task(endTaskPath)?.outcome) + assertThat(result.task(muzzleTaskPath)?.outcome).isEqualTo(SUCCESS) + assertThat(result.task(passDirectiveTaskPath)?.outcome).isEqualTo(SUCCESS) + assertThat(result.task(failDirectiveTaskPath)?.outcome).isEqualTo(SUCCESS) + assertThat(result.task(endTaskPath)?.outcome).isEqualTo(SUCCESS) val muzzleChainInOrder = result.tasks .map { it.path } @@ -235,17 +227,15 @@ class MuzzlePluginFunctionalTest { it == failDirectiveTaskPath || it == endTaskPath } - assertEquals( - listOf(muzzleTaskPath, passDirectiveTaskPath, failDirectiveTaskPath, endTaskPath), - muzzleChainInOrder - ) + assertThat(muzzleChainInOrder) + .containsExactly(muzzleTaskPath, passDirectiveTaskPath, failDirectiveTaskPath, endTaskPath) val passDirectiveResult = fixture.resultFile("muzzle-AssertPass-core-jdk") val failDirectiveResult = fixture.resultFile("muzzle-AssertFail-core-jdk") - assertTrue(Files.isRegularFile(passDirectiveResult)) - assertTrue(Files.isRegularFile(failDirectiveResult)) - assertEquals("PASSING", passDirectiveResult.readText()) - assertEquals("PASSING", failDirectiveResult.readText()) + assertThat(passDirectiveResult).isRegularFile() + assertThat(failDirectiveResult).isRegularFile() + assertThat(passDirectiveResult.readText()).isEqualTo("PASSING") + assertThat(failDirectiveResult.readText()).isEqualTo("PASSING") } @Test @@ -295,33 +285,37 @@ class MuzzlePluginFunctionalTest { env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) - assertTrue( - result.output.contains("BUILD SUCCESSFUL"), - "Build should succeed. Output:\n${result.output.take(3000)}" - ) + assertThat(result.output) + .withFailMessage("Build should succeed. Output:\n${result.output.take(3000)}") + .contains("BUILD SUCCESSFUL") - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome) + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome).isEqualTo(SUCCESS) + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome).isEqualTo(SUCCESS) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-demo-lib-1.0.0")?.outcome) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-demo-lib-1.1.0")?.outcome) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-demo-lib-1.2.0")?.outcome) - assertNull(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-demo-lib-2.0.0")?.outcome, "Should not check against test-demo-lib:2.0.0") + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-demo-lib-1.0.0")?.outcome) + .isEqualTo(SUCCESS) + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-demo-lib-1.1.0")?.outcome) + .isEqualTo(SUCCESS) + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-demo-lib-1.2.0")?.outcome) + .isEqualTo(SUCCESS) + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-demo-lib-2.0.0")?.outcome) + .withFailMessage("Should not check against test-demo-lib:2.0.0") + .isNull() val reportFile = fixture.findSingleMuzzleJUnitReport() val report = fixture.parseXml(reportFile) val suite = report.documentElement val testCount = suite.getAttribute("tests").toInt() - assertTrue(testCount >= 3, "Should have at least 3 tests for 3 versions, got $testCount") - assertEquals("0", suite.getAttribute("failures"), "Should have no failures") + assertThat(testCount) + .withFailMessage("Should have at least 3 tests for 3 versions, got $testCount") + .isGreaterThanOrEqualTo(3) + assertThat(suite.getAttribute("failures")).withFailMessage("Should have no failures").isEqualTo("0") val testCases = (0 until report.getElementsByTagName("testcase").length) .map { report.getElementsByTagName("testcase").item(it) as org.w3c.dom.Element } .map { it.getAttribute("name") } - assertTrue( - testCases.any { it.contains("demo-lib-1.0.0") }, - "Should have test case for demo-lib-1.0.0. Found: ${testCases.take(5)}" - ) + assertThat(testCases).withFailMessage("Should have test case for demo-lib-1.0.0. Found: ${testCases.take(5)}") + .anySatisfy { assertThat(it).contains("demo-lib-1.0.0") } } @Test @@ -358,11 +352,10 @@ class MuzzlePluginFunctionalTest { val result = fixture.run(":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace") - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) - assertTrue( - result.output.contains("Directive name passed correctly: my-custom-check"), - "Should confirm 'my-custom-check' was passed to scan plugin" - ) + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .isEqualTo(SUCCESS) + assertThat(result.output).withFailMessage("Should confirm 'my-custom-check' was passed to scan plugin") + .contains("Directive name passed correctly: my-custom-check") } @Test @@ -392,14 +385,14 @@ class MuzzlePluginFunctionalTest { env = mapOf("MAVEN_REPOSITORY_PROXY" to "https://repo1.maven.org/maven2/") ) - assertTrue(result.output.contains("BUILD FAILED"), "Build should fail for non-existent artifact") - assertTrue( - result.output.contains("version range resolution failed") || - result.output.contains("Could not resolve") || - result.output.contains("not found") || - result.output.contains("Failed to resolve"), - "Should have error message about resolution failure" - ) + assertThat(result.output).withFailMessage("Build should fail for non-existent artifact").contains("BUILD FAILED") + assertThat(result.output).withFailMessage("Should have error message about resolution failure") + .containsAnyOf( + "version range resolution failed", + "Could not resolve", + "not found", + "Failed to resolve" + ) } @Test @@ -436,12 +429,10 @@ class MuzzlePluginFunctionalTest { "--stacktrace" ) - assertTrue(result.output.contains("BUILD FAILED"), "Build should fail when pass directive fails validation") - assertTrue( - result.output.contains("Muzzle validation failed") || - result.output.contains("Instrumentation failed"), - "Should contain error message from scan plugin" - ) + assertThat(result.output).withFailMessage("Build should fail when pass directive fails validation") + .contains("BUILD FAILED") + assertThat(result.output).withFailMessage("Should contain error message from scan plugin") + .containsAnyOf("Muzzle validation failed", "Instrumentation failed") } @Test @@ -480,12 +471,11 @@ class MuzzlePluginFunctionalTest { ) // Expected behavior: build should fail when fail directive unexpectedly passes - assertTrue(result.output.contains("BUILD FAILED"), "Build should fail when fail directive unexpectedly passes") - assertTrue( - result.output.contains("unexpectedly passed") || - result.output.contains("FAILURE WAS EXPECTED"), - "Should indicate that fail directive passed when it shouldn't have" - ) + assertThat(result.output) + .withFailMessage("Build should fail when fail directive unexpectedly passes") + .contains("BUILD FAILED") + assertThat(result.output).withFailMessage("Should indicate that fail directive passed when it shouldn't have") + .containsAnyOf("unexpectedly passed", "FAILURE WAS EXPECTED") } @Test @@ -549,16 +539,14 @@ class MuzzlePluginFunctionalTest { env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) - assertTrue( - result.output.contains("BUILD SUCCESSFUL"), - "Build should succeed. Output:\n${result.output.take(2000)}" - ) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) - assertTrue( - result.output.contains("Additional dependency (extra-lib) found in test classpath"), - "Additional dependency should be loadable from test classpath" - ) + assertThat(result.output) + .withFailMessage("Build should succeed. Output:\n${result.output.take(2000)}") + .contains("BUILD SUCCESSFUL") + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome).isEqualTo(SUCCESS) + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .isEqualTo(SUCCESS) + assertThat(result.output).withFailMessage("Additional dependency should be loadable from test classpath") + .contains("Additional dependency (extra-lib) found in test classpath") } @Test @@ -640,13 +628,12 @@ class MuzzlePluginFunctionalTest { env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) - assertTrue(result.output.contains("BUILD SUCCESSFUL")) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-with-transitive-1.0.0")?.outcome) - assertTrue( - result.output.contains("Excluded dependency (guava) correctly not in test classpath"), - "Excluded dependency should not be loadable from test classpath" - ) + assertThat(result.output).contains("BUILD SUCCESSFUL") + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome).isEqualTo(SUCCESS) + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-with-transitive-1.0.0")?.outcome) + .isEqualTo(SUCCESS) + assertThat(result.output).withFailMessage("Excluded dependency should not be loadable from test classpath") + .contains("Excluded dependency (guava) correctly not in test classpath") } @Test @@ -676,9 +663,9 @@ class MuzzlePluginFunctionalTest { "--stacktrace" ) - assertTrue(result.output.contains("BUILD SUCCESSFUL")) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome) + assertThat(result.output).contains("BUILD SUCCESSFUL") + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome).isEqualTo(SUCCESS) + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome).isEqualTo(SUCCESS) } @Test @@ -709,9 +696,9 @@ class MuzzlePluginFunctionalTest { "--stacktrace" ) - assertTrue(result.output.contains("BUILD SUCCESSFUL")) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome) + assertThat(result.output).contains("BUILD SUCCESSFUL") + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome).isEqualTo(SUCCESS) + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome).isEqualTo(SUCCESS) } @Test @@ -739,10 +726,9 @@ class MuzzlePluginFunctionalTest { "--all" ) - assertFalse( - result.output.contains("muzzle"), - "Should not create muzzle tasks without java plugin" - ) + assertThat(result.output) + .withFailMessage("Should not create muzzle tasks without java plugin") + .doesNotContain("muzzle") } @Test @@ -778,12 +764,12 @@ class MuzzlePluginFunctionalTest { "--stacktrace" ) - assertTrue( - result.output.contains("BUILD FAILED") || - result.output.contains(":dd-java-agent:agent-bootstrap project not found") || - result.output.contains(":dd-java-agent:agent-tooling project not found"), - "Should fail with clear error about missing dd-java-agent projects" - ) + assertThat(result.output).withFailMessage("Should fail with clear error about missing dd-java-agent projects") + .containsAnyOf( + "BUILD FAILED", + ":dd-java-agent:agent-bootstrap project not found", + ":dd-java-agent:agent-tooling project not found" + ) } @Test @@ -836,38 +822,41 @@ class MuzzlePluginFunctionalTest { env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) - assertTrue( - result.output.contains("BUILD SUCCESSFUL"), - "Build should succeed. Output:\n${result.output.take(3000)}" - ) + assertThat(result.output) + .withFailMessage("Build should succeed. Output:\n${result.output.take(3000)}") + .contains("BUILD SUCCESSFUL") val modulePrefix = ":dd-java-agent:instrumentation:demo" - assertEquals(SUCCESS, result.task("$modulePrefix:muzzle")?.outcome) - assertEquals(SUCCESS, result.task("$modulePrefix:muzzle-end")?.outcome) + assertThat(result.task("$modulePrefix:muzzle")?.outcome).isEqualTo(SUCCESS) + assertThat(result.task("$modulePrefix:muzzle-end")?.outcome).isEqualTo(SUCCESS) // In-range versions — assertPass=true - assertEquals(SUCCESS, result.task("$modulePrefix:muzzle-AssertPass-com.example.test-inverse-lib-2.0.0")?.outcome) - assertEquals(SUCCESS, result.task("$modulePrefix:muzzle-AssertPass-com.example.test-inverse-lib-3.0.0")?.outcome) + assertThat(result.task("$modulePrefix:muzzle-AssertPass-com.example.test-inverse-lib-2.0.0")?.outcome) + .isEqualTo(SUCCESS) + assertThat(result.task("$modulePrefix:muzzle-AssertPass-com.example.test-inverse-lib-3.0.0")?.outcome) + .isEqualTo(SUCCESS) // Out-of-range versions (inverse) — assertPass=false - assertEquals(SUCCESS, result.task("$modulePrefix:muzzle-AssertFail-com.example.test-inverse-lib-1.0.0")?.outcome) - assertEquals(SUCCESS, result.task("$modulePrefix:muzzle-AssertFail-com.example.test-inverse-lib-4.0.0")?.outcome) - - assertTrue( - result.output.contains("MUZZLE_CHECK assertPass=true"), - "Should log assertPass=true for in-range versions" - ) - assertTrue( - result.output.contains("MUZZLE_CHECK assertPass=false"), - "Should log assertPass=false for out-of-range (inverse) versions" - ) + assertThat(result.task("$modulePrefix:muzzle-AssertFail-com.example.test-inverse-lib-1.0.0")?.outcome) + .isEqualTo(SUCCESS) + assertThat(result.task("$modulePrefix:muzzle-AssertFail-com.example.test-inverse-lib-4.0.0")?.outcome) + .isEqualTo(SUCCESS) + + assertThat(result.output) + .withFailMessage("Should log assertPass=true for in-range versions") + .contains("MUZZLE_CHECK assertPass=true") + assertThat(result.output) + .withFailMessage("Should log assertPass=false for out-of-range (inverse) versions") + .contains("MUZZLE_CHECK assertPass=false") // Verify JUnit report contains all 4 test cases with no failures val reportFile = fixture.findSingleMuzzleJUnitReport() val report = fixture.parseXml(reportFile) val suite = report.documentElement - assertEquals("4", suite.getAttribute("tests"), "Should have 4 test cases (2 pass + 2 inverse fail)") - assertEquals("0", suite.getAttribute("failures"), "Should have no failures") + assertThat(suite.getAttribute("tests")) + .withFailMessage("Should have 4 test cases (2 pass + 2 inverse fail)") + .isEqualTo("4") + assertThat(suite.getAttribute("failures")).withFailMessage("Should have no failures").isEqualTo("0") findTestCase(report, "muzzle-AssertPass-com.example.test-inverse-lib-2.0.0") findTestCase(report, "muzzle-AssertPass-com.example.test-inverse-lib-3.0.0") diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt index 59c7ee352dc..79aaf409b7c 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginPerformanceTest.kt @@ -3,12 +3,10 @@ package datadog.gradle.plugin.muzzle import datadog.gradle.plugin.MavenRepoFixture import org.gradle.testkit.runner.TaskOutcome.SUCCESS import org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.io.File +import org.assertj.core.api.Assertions.assertThat class MuzzlePluginPerformanceTest { @@ -40,16 +38,14 @@ class MuzzlePluginPerformanceTest { "--info" ) - assertEquals(SUCCESS, result.task(":dd-java-agent:instrumentation:demo:tasks")?.outcome) + assertThat(result.task(":dd-java-agent:instrumentation:demo:tasks")?.outcome).isEqualTo(SUCCESS) - assertFalse( - result.tasks.any() { it.path.contains("muzzle") }, - "Should not create or execute any muzzle tasks when not requested" - ) - assertTrue( - result.output.contains("No muzzle tasks invoked for :dd-java-agent:instrumentation:demo, skipping muzzle task planification"), - "Should log early return when muzzle not requested" - ) + assertThat(result.tasks) + .withFailMessage("Should not create or execute any muzzle tasks when not requested") + .noneMatch { it.path.contains("muzzle") } + assertThat(result.output) + .withFailMessage("Should log early return when muzzle not requested") + .contains("No muzzle tasks invoked for :dd-java-agent:instrumentation:demo, skipping muzzle task planification") } @Test @@ -86,20 +82,15 @@ class MuzzlePluginPerformanceTest { "--info" ) - assertTrue( - result.tasks.any { it.path.contains("demo") && it.path.contains("muzzle") }, - "Should execute muzzle tasks for demo project" - ) - assertTrue( - result.tasks.none() { it.path.contains("other") && it.path.contains("muzzle") }, - "Should NOT create or register execute muzzle tasks for other project" - ) - assertTrue( - result.output.lines().any { line -> - line.contains("No muzzle tasks invoked for :dd-java-agent:instrumentation:other, skipping muzzle task planification") - }, - "Other project should skip muzzle configuration when demo project's muzzle is requested" - ) + assertThat(result.tasks) + .withFailMessage("Should execute muzzle tasks for demo project") + .anyMatch { it.path.contains("demo") && it.path.contains("muzzle") } + assertThat(result.tasks) + .withFailMessage("Should NOT create or register execute muzzle tasks for other project") + .noneMatch { it.path.contains("other") && it.path.contains("muzzle") } + assertThat(result.output.lines()) + .withFailMessage("Other project should skip muzzle configuration when demo project's muzzle is requested") + .anyMatch { it.contains("No muzzle tasks invoked for :dd-java-agent:instrumentation:other, skipping muzzle task planification") } } @Test @@ -148,12 +139,16 @@ class MuzzlePluginPerformanceTest { env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) - assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, - "First run should execute muzzle task") - assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.0.0")?.outcome) - assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.1.0")?.outcome) - assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome, - "First run should execute muzzle-end task") + assertThat(firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + .withFailMessage("First run should execute muzzle task") + .isEqualTo(SUCCESS) + assertThat(firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.0.0")?.outcome) + .isEqualTo(SUCCESS) + assertThat(firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.1.0")?.outcome) + .isEqualTo(SUCCESS) + assertThat(firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome) + .withFailMessage("First run should execute muzzle-end task") + .isEqualTo(SUCCESS) } // Second run without changes - assertion tasks should be up-to-date @@ -163,14 +158,18 @@ class MuzzlePluginPerformanceTest { env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) - assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, - "First run should execute muzzle task") - assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.0.0")?.outcome, - "1.0.0 assertion task should be up-to-date") - assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.1.0")?.outcome, - "1.1.0 assertion task should be up-to-date") - assertEquals(SUCCESS, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome, - "First run should execute muzzle-end task") + assertThat(secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + .withFailMessage("First run should execute muzzle task") + .isEqualTo(UP_TO_DATE) + assertThat(secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.0.0")?.outcome) + .withFailMessage("1.0.0 assertion task should be up-to-date") + .isEqualTo(UP_TO_DATE) + assertThat(secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.1.0")?.outcome) + .withFailMessage("1.1.0 assertion task should be up-to-date") + .isEqualTo(UP_TO_DATE) + assertThat(secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome) + .withFailMessage("First run should execute muzzle-end task") + .isEqualTo(SUCCESS) } // Third run after adding new version - should NOT be up-to-date @@ -187,16 +186,21 @@ class MuzzlePluginPerformanceTest { env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepoFixture.repoUrl) ) - assertEquals(UP_TO_DATE, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, - "First run should execute muzzle task") - assertEquals(UP_TO_DATE, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.0.0")?.outcome, - "1.0.0 assertion task should be up-to-date") - assertEquals(UP_TO_DATE, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.1.0")?.outcome, - "1.1.0 assertion task should be up-to-date") - assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.2.0")?.outcome, - "New 1.2.0 assertion task should be created and execute") - assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome, - "First run should execute muzzle-end task") + assertThat(thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + .withFailMessage("First run should execute muzzle task") + .isEqualTo(UP_TO_DATE) + assertThat(thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.0.0")?.outcome) + .withFailMessage("1.0.0 assertion task should be up-to-date") + .isEqualTo(UP_TO_DATE) + assertThat(thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.1.0")?.outcome) + .withFailMessage("1.1.0 assertion task should be up-to-date") + .isEqualTo(UP_TO_DATE) + assertThat(thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-com.example.test-example-lib-1.2.0")?.outcome) + .withFailMessage("New 1.2.0 assertion task should be created and execute") + .isEqualTo(SUCCESS) + assertThat(thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-end")?.outcome) + .withFailMessage("First run should execute muzzle-end task") + .isEqualTo(SUCCESS) } } @@ -222,22 +226,24 @@ class MuzzlePluginPerformanceTest { run { val firstRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") - assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, - "First run should execute muzzle task") - assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, - "First run should execute coreJdk assertion task" - ) + assertThat(firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + .withFailMessage("First run should execute muzzle task") + .isEqualTo(SUCCESS) + assertThat(firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .withFailMessage("First run should execute coreJdk assertion task") + .isEqualTo(SUCCESS) } // Second run without changes - assertion tasks should be up-to-date run { val secondRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") - assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, - "Second run should be up-to-date") - assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, - "coreJdk assertion task should be up-to-date" - ) + assertThat(secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + .withFailMessage("Second run should be up-to-date") + .isEqualTo(UP_TO_DATE) + assertThat(secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .withFailMessage("coreJdk assertion task should be up-to-date") + .isEqualTo(UP_TO_DATE) } // Third run after changing instrumentation code - should be invalidated @@ -256,11 +262,12 @@ class MuzzlePluginPerformanceTest { val thirdRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") - assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, - "Third run should execute after instrumentation code change") - assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, - "coreJdk assertion task should be invalidated and re-execute" - ) + assertThat(thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + .withFailMessage("Third run should execute after instrumentation code change") + .isEqualTo(SUCCESS) + assertThat(thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .withFailMessage("coreJdk assertion task should be invalidated and re-execute") + .isEqualTo(SUCCESS) } } @@ -286,22 +293,24 @@ class MuzzlePluginPerformanceTest { run { val firstRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") - assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, - "First run should execute muzzle task") - assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, - "First run should execute coreJdk assertion task" - ) + assertThat(firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + .withFailMessage("First run should execute muzzle task") + .isEqualTo(SUCCESS) + assertThat(firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .withFailMessage("First run should execute coreJdk assertion task") + .isEqualTo(SUCCESS) } // Second run without changes - assertion tasks should be up-to-date run { val secondRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") - assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, - "Second run should be up-to-date") - assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, - "coreJdk assertion task should be up-to-date" - ) + assertThat(secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + .withFailMessage("Second run should be up-to-date") + .isEqualTo(UP_TO_DATE) + assertThat(secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .withFailMessage("coreJdk assertion task should be up-to-date") + .isEqualTo(UP_TO_DATE) } // Third run after changing agent-tooling code - should be invalidated @@ -320,11 +329,12 @@ class MuzzlePluginPerformanceTest { val thirdRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") - assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, - "Third run should execute after tooling classpath change") - assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, - "coreJdk assertion task should be invalidated and re-execute" - ) + assertThat(thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + .withFailMessage("Third run should execute after tooling classpath change") + .isEqualTo(SUCCESS) + assertThat(thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .withFailMessage("coreJdk assertion task should be invalidated and re-execute") + .isEqualTo(SUCCESS) } } @@ -350,22 +360,24 @@ class MuzzlePluginPerformanceTest { run { val firstRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") - assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, - "First run should execute muzzle task") - assertEquals(SUCCESS, firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, - "First run should execute coreJdk assertion task" - ) + assertThat(firstRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + .withFailMessage("First run should execute muzzle task") + .isEqualTo(SUCCESS) + assertThat(firstRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .withFailMessage("First run should execute coreJdk assertion task") + .isEqualTo(SUCCESS) } // Second run without changes - assertion tasks should be up-to-date run { val secondRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") - assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, - "Second run should be up-to-date") - assertEquals(UP_TO_DATE, secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, - "coreJdk assertion task should be up-to-date" - ) + assertThat(secondRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + .withFailMessage("Second run should be up-to-date") + .isEqualTo(UP_TO_DATE) + assertThat(secondRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .withFailMessage("coreJdk assertion task should be up-to-date") + .isEqualTo(UP_TO_DATE) } // Third run after changing agent-bootstrap code - should be invalidated @@ -384,11 +396,12 @@ class MuzzlePluginPerformanceTest { val thirdRun = fixture.run(":dd-java-agent:instrumentation:demo:muzzle") - assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome, - "Third run should execute after bootstrap classpath change") - assertEquals(SUCCESS, thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome, - "coreJdk assertion task should be invalidated and re-execute" - ) + assertThat(thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle")?.outcome) + .withFailMessage("Third run should execute after bootstrap classpath change") + .isEqualTo(SUCCESS) + assertThat(thirdRun.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome) + .withFailMessage("coreJdk assertion task should be invalidated and re-execute") + .isEqualTo(SUCCESS) } } } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginUtilsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginUtilsTest.kt index 258ede7d158..3435a923d27 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginUtilsTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginUtilsTest.kt @@ -3,18 +3,16 @@ package datadog.gradle.plugin.muzzle import org.gradle.api.tasks.SourceSetContainer import org.gradle.kotlin.dsl.getByType import org.gradle.testfixtures.ProjectBuilder -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource +import org.assertj.core.api.Assertions.assertThat class MuzzlePluginUtilsTest { @Test fun `pathSlug for root project is empty`() { val root = ProjectBuilder.builder().withName("root").build() - assertEquals("", root.pathSlug) + assertThat(root.pathSlug).isEmpty() } @ParameterizedTest(name = "[{index}] path ''{0}'' → slug ''{1}''") @@ -27,7 +25,7 @@ class MuzzlePluginUtilsTest { fun `pathSlug for single-level child project`(childName: String, expectedSlug: String) { val root = ProjectBuilder.builder().withName("root").build() val child = ProjectBuilder.builder().withParent(root).withName(childName.trim()).build() - assertEquals(expectedSlug.trim(), child.pathSlug) + assertThat(child.pathSlug).isEqualTo(expectedSlug.trim()) } @Test @@ -37,7 +35,7 @@ class MuzzlePluginUtilsTest { val bar = ProjectBuilder.builder().withParent(foo).withName("bar").build() val baz = ProjectBuilder.builder().withParent(bar).withName("baz").build() - assertEquals("foo_bar_baz", baz.pathSlug) + assertThat(baz.pathSlug).isEqualTo("foo_bar_baz") } @Test @@ -47,8 +45,7 @@ class MuzzlePluginUtilsTest { val sourceSets = project.allMainSourceSet - assertTrue(sourceSets.any { it.name == "main" }) - assertFalse(sourceSets.any { it.name == "test" }) + assertThat(sourceSets.map { it.name }).contains("main").doesNotContain("test") } @Test @@ -62,9 +59,6 @@ class MuzzlePluginUtilsTest { val sourceSets = project.allMainSourceSet - assertEquals(3, sourceSets.size) - assertTrue(sourceSets.any { it.name == "main" }) - assertTrue(sourceSets.any { it.name == "mainLegacy" }) - assertTrue(sourceSets.any { it.name == "mainJava8" }) + assertThat(sourceSets.map { it.name }).containsExactlyInAnyOrder("main", "mainLegacy", "mainJava8") } } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleVersionUtilsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleVersionUtilsTest.kt index aa98984cfc1..9bc298a40f5 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleVersionUtilsTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleVersionUtilsTest.kt @@ -5,14 +5,12 @@ import org.eclipse.aether.artifact.DefaultArtifact import org.eclipse.aether.resolution.VersionRangeRequest import org.eclipse.aether.resolution.VersionRangeResult import org.eclipse.aether.util.version.GenericVersionScheme -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.ValueSource +import org.assertj.core.api.Assertions.assertThat class MuzzleVersionUtilsTest { @@ -48,11 +46,9 @@ class MuzzleVersionUtilsTest { val filtered = MuzzleVersionUtils.filterAndLimitVersions(result, emptySet(), includeSnapshots = false) - assertFalse(filtered.any { it.toString() == preRelease }) { - "Expected '$preRelease' to be filtered out" - } - assertTrue(filtered.any { it.toString() == "1.0.0" }) - assertTrue(filtered.any { it.toString() == "3.0.0" }) + val filteredStrings = filtered.map { it.toString() } + assertThat(filteredStrings).withFailMessage("Expected '$preRelease' to be filtered out").doesNotContain(preRelease) + assertThat(filteredStrings).contains("1.0.0", "3.0.0") } @ParameterizedTest(name = "[{index}] includeSnapshots=true keeps ''{0}'', skipVersions={1}") @@ -67,13 +63,14 @@ class MuzzleVersionUtilsTest { val filtered = MuzzleVersionUtils.filterAndLimitVersions(result, skipVersions, includeSnapshots = true) - assertTrue(filtered.any { it.toString() == preRelease }) { - "Expected '$preRelease' to be kept when includeSnapshots=true" - } + val filteredStrings = filtered.map { it.toString() } + assertThat(filteredStrings) + .withFailMessage("Expected '$preRelease' to be kept when includeSnapshots=true") + .contains(preRelease) skipVersions.forEach { skipped -> - assertFalse(filtered.any { it.toString() == skipped }) { - "Expected '$skipped' to be absent due to skipVersions" - } + assertThat(filteredStrings) + .withFailMessage("Expected '$skipped' to be absent due to skipVersions") + .doesNotContain(skipped) } } @@ -86,7 +83,7 @@ class MuzzleVersionUtilsTest { MuzzleVersionUtils.filterAndLimitVersions( result, setOf(versionToSkip), includeSnapshots = false) - assertFalse(filtered.any { it.toString() == versionToSkip }) + assertThat(filtered.map { it.toString() }).doesNotContain(versionToSkip) } @Test @@ -97,9 +94,9 @@ class MuzzleVersionUtilsTest { MuzzleVersionUtils.filterAndLimitVersions( result, setOf("2.0.0-Custom"), includeSnapshots = false) - assertTrue(filtered.any { it.toString() == "2.0.0-custom" }) { - "Expected '2.0.0-custom' to be kept because skipVersions entry 'Custom' does not match lowercased 'custom'" - } + assertThat(filtered.map { it.toString() }) + .withFailMessage("Expected '2.0.0-custom' to be kept because skipVersions entry 'Custom' does not match lowercased 'custom'") + .contains("2.0.0-custom") } @Test @@ -111,18 +108,16 @@ class MuzzleVersionUtilsTest { val filtered = MuzzleVersionUtils.filterAndLimitVersions(result, emptySet(), includeSnapshots = false) - assertTrue(filtered.size < RANGE_COUNT_LIMIT) { "Expected fewer than 25 versions after trimming, got ${filtered.size}" } - assertTrue(filtered.isNotEmpty()) - assertTrue(filtered.any { it == result.lowestVersion }) { - "lowestVersion (${result.lowestVersion}) must be preserved" - } - assertTrue(filtered.any { it == result.highestVersion }) { - "highestVersion (${result.highestVersion}) must be preserved" - } - val originalSet = versions.toSet() - assertTrue(filtered.all { it.toString() in originalSet }) { - "All filtered versions must come from the original set" - } + assertThat(filtered).withFailMessage("Expected fewer than 25 versions after trimming, got ${filtered.size}") + .hasSizeLessThan(RANGE_COUNT_LIMIT) + assertThat(filtered).isNotEmpty() + val filteredStrings = filtered.map { it.toString() } + assertThat(filteredStrings).withFailMessage("lowestVersion (${result.lowestVersion}) must be preserved") + .contains(result.lowestVersion.toString()) + assertThat(filteredStrings).withFailMessage("highestVersion (${result.highestVersion}) must be preserved") + .contains(result.highestVersion.toString()) + assertThat(filteredStrings).withFailMessage("All filtered versions must come from the original set") + .isSubsetOf(*versions) } @ParameterizedTest(name = "[{index}] {0} version(s) pass through unchanged") @@ -134,8 +129,7 @@ class MuzzleVersionUtilsTest { val filtered = MuzzleVersionUtils.filterAndLimitVersions(result, emptySet(), includeSnapshots = false) - assertEquals(count, filtered.size) - versionStrings.forEach { v -> assertTrue(filtered.any { it.toString() == v }) } + assertThat(filtered.map { it.toString() }).containsExactlyInAnyOrder(*versionStrings) } companion object { diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/RangeQueryTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/RangeQueryTest.kt index 6c1623f62ef..5ced9ed1032 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/RangeQueryTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/RangeQueryTest.kt @@ -3,7 +3,7 @@ package datadog.gradle.plugin.muzzle import org.eclipse.aether.artifact.Artifact import org.eclipse.aether.artifact.DefaultArtifact import org.eclipse.aether.resolution.VersionRangeRequest -import org.gradle.internal.impldep.org.junit.Assert.assertTrue +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test class RangeQueryTest { @@ -22,6 +22,6 @@ class RangeQueryTest { // This call makes an actual network request, which may fail if network access is limited. val rangeResult = system.resolveVersionRange(session, rangeRequest) - assertTrue(rangeResult.versions.size >= 8) + assertThat(rangeResult.versions.size).isGreaterThanOrEqualTo(8) } } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/VersionSetTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/VersionSetTest.kt index ac4eef08888..0d8c164e087 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/VersionSetTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/VersionSetTest.kt @@ -1,8 +1,8 @@ package datadog.gradle.plugin.muzzle import org.eclipse.aether.version.Version -import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import org.assertj.core.api.Assertions.assertThat class VersionSetTest { @@ -26,13 +26,11 @@ class VersionSetTest { for (c in cases) { val parsed = VersionSet.ParsedVersion(c.version) - assertEquals(c.versionNumber, parsed.versionNumber, "versionNumber for ${c.version}") - assertEquals(c.ending, parsed.ending, "ending for ${c.version}") - assertEquals( - c.versionNumber shr 12, - parsed.majorMinor.toLong(), - "majorMinor for ${c.version}" - ) + assertThat(parsed.versionNumber).withFailMessage("versionNumber for ${c.version}").isEqualTo(c.versionNumber) + assertThat(parsed.ending).withFailMessage("ending for ${c.version}").isEqualTo(c.ending) + assertThat(parsed.majorMinor.toLong()) + .withFailMessage("majorMinor for ${c.version}") + .isEqualTo(c.versionNumber shr 12) } } @@ -71,7 +69,7 @@ class VersionSetTest { versionsCases.zip(expectedCases).forEach { (versions, expected) -> val versionSet = VersionSet(versions) - assertEquals(expected, versionSet.lowAndHighForMajorMinor) + assertThat(versionSet.lowAndHighForMajorMinor).isEqualTo(expected) } } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt index 9159d591fe3..3dce77a8ad2 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerTest.kt @@ -1,9 +1,9 @@ package datadog.gradle.plugin.muzzle.planner import datadog.gradle.plugin.muzzle.MuzzleDirective +import org.assertj.core.api.Assertions.assertThat import org.eclipse.aether.artifact.Artifact import org.eclipse.aether.artifact.DefaultArtifact -import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class MuzzleTaskPlannerTest { @@ -14,9 +14,9 @@ class MuzzleTaskPlannerTest { val plans = MuzzleTaskPlanner(fakeService).plan(emptyList()) - assertEquals(emptyList(), plans) - assertEquals(0, fakeService.resolveCalls) - assertEquals(0, fakeService.inverseCalls) + assertThat(plans).isEmpty() + assertThat(fakeService.resolveCalls).isEqualTo(0) + assertThat(fakeService.inverseCalls).isEqualTo(0) } @Test @@ -33,9 +33,9 @@ class MuzzleTaskPlannerTest { val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive)) - assertEquals(emptyList(), plans) - assertEquals(1, fakeService.resolveCalls) - assertEquals(0, fakeService.inverseCalls) + assertThat(plans).isEmpty() + assertThat(fakeService.resolveCalls).isEqualTo(1) + assertThat(fakeService.inverseCalls).isEqualTo(0) } @Test @@ -48,9 +48,9 @@ class MuzzleTaskPlannerTest { val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive)) - assertEquals(listOf(MuzzleTaskPlan(directive, null)), plans) - assertEquals(0, fakeService.resolveCalls) - assertEquals(0, fakeService.inverseCalls) + assertThat(plans).containsExactly(MuzzleTaskPlan(directive, null)) + assertThat(fakeService.resolveCalls).isEqualTo(0) + assertThat(fakeService.inverseCalls).isEqualTo(0) } @Test @@ -73,61 +73,55 @@ class MuzzleTaskPlannerTest { val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive)) - assertEquals( - listOf( - MuzzleTaskPlan(directive, artifact(version = "1.0.0")), - MuzzleTaskPlan(directive, artifact(version = "1.1.0")), - MuzzleTaskPlan(directive, artifact(version = "1.2.0")), - MuzzleTaskPlan(directive, artifact(version = "1.3.0")), - ), - plans + assertThat(plans).containsExactly( + MuzzleTaskPlan(directive, artifact(version = "1.0.0")), + MuzzleTaskPlan(directive, artifact(version = "1.1.0")), + MuzzleTaskPlan(directive, artifact(version = "1.2.0")), + MuzzleTaskPlan(directive, artifact(version = "1.3.0")), ) - assertEquals(1, fakeService.resolveCalls) - assertEquals(0, fakeService.inverseCalls) + assertThat(fakeService.resolveCalls).isEqualTo(1) + assertThat(fakeService.inverseCalls).isEqualTo(0) } - @Test - fun `multiple directives processed together preserves order`() { - val directive1 = MuzzleDirective().apply { - group = "com.example" - module = "first" - versions = "[1.0,2.0)" - assertPass = true - } - val directive2 = MuzzleDirective().apply { - group = "com.example" - module = "second" - versions = "[2.0,3.0)" - assertPass = true - } - val directive3 = MuzzleDirective().apply { - group = "com.example" - module = "third" - versions = "[3.0,4.0)" - assertPass = false - } - val fakeService = FakeResolutionService( - artifactsByDirective = mapOf( - directive1 to linkedSetOf(artifact("first", "1.5.0")), - directive2 to linkedSetOf(artifact("second", "2.5.0")), - directive3 to linkedSetOf(artifact("third", "3.5.0")) - ) - ) - - val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive1, directive2, directive3)) - - assertEquals( - listOf( - MuzzleTaskPlan(directive1, artifact("first", "1.5.0")), - MuzzleTaskPlan(directive2, artifact("second", "2.5.0")), - MuzzleTaskPlan(directive3, artifact("third", "3.5.0")), - ), - plans - ) - assertEquals(3, fakeService.resolveCalls) - assertEquals(0, fakeService.inverseCalls) + @Test + fun `multiple directives processed together preserves order`() { + val directive1 = MuzzleDirective().apply { + group = "com.example" + module = "first" + versions = "[1.0,2.0)" + assertPass = true + } + val directive2 = MuzzleDirective().apply { + group = "com.example" + module = "second" + versions = "[2.0,3.0)" + assertPass = true + } + val directive3 = MuzzleDirective().apply { + group = "com.example" + module = "third" + versions = "[3.0,4.0)" + assertPass = false } - + val fakeService = FakeResolutionService( + artifactsByDirective = mapOf( + directive1 to linkedSetOf(artifact("first", "1.5.0")), + directive2 to linkedSetOf(artifact("second", "2.5.0")), + directive3 to linkedSetOf(artifact("third", "3.5.0")) + ) + ) + + val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive1, directive2, directive3)) + + assertThat(plans).containsExactly( + MuzzleTaskPlan(directive1, artifact("first", "1.5.0")), + MuzzleTaskPlan(directive2, artifact("second", "2.5.0")), + MuzzleTaskPlan(directive3, artifact("third", "3.5.0")), + ) + assertThat(fakeService.resolveCalls).isEqualTo(3) + assertThat(fakeService.inverseCalls).isEqualTo(0) + } + @Test fun `assertInverse adds inverse plans on top of declared range plans`() { val directive = MuzzleDirective().apply { @@ -158,16 +152,17 @@ class MuzzleTaskPlannerTest { val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive)) - assertEquals( - listOf( - MuzzleTaskPlan(directive, directArtifactV1), - MuzzleTaskPlan(directive, directArtifactV2), - MuzzleTaskPlan(directive, directArtifactV3), - MuzzleTaskPlan(inversedDirective, inverseArtifactV1), - MuzzleTaskPlan(inversedDirective, inverseArtifactV2), - ), plans) - assertEquals(2, fakeService.resolveCalls, "main directive + additional one for the inverse directive") - assertEquals(1, fakeService.inverseCalls) + assertThat(plans).containsExactly( + MuzzleTaskPlan(directive, directArtifactV1), + MuzzleTaskPlan(directive, directArtifactV2), + MuzzleTaskPlan(directive, directArtifactV3), + MuzzleTaskPlan(inversedDirective, inverseArtifactV1), + MuzzleTaskPlan(inversedDirective, inverseArtifactV2), + ) + assertThat(fakeService.resolveCalls) + .withFailMessage("main directive + additional one for the inverse directive") + .isEqualTo(2) + assertThat(fakeService.inverseCalls).isEqualTo(1) } @Test @@ -205,18 +200,15 @@ class MuzzleTaskPlannerTest { val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive)) - assertEquals(4, plans.size, "Should have 3 pass plans + 1 inverse fail plan") - assertEquals( - listOf( - MuzzleTaskPlan(directive, artifact("netty-codec-http", "4.1.0")), - MuzzleTaskPlan(directive, artifact("netty-codec-http", "4.1.50")), - MuzzleTaskPlan(directive, artifact("netty-codec-http", "4.2.0")), - MuzzleTaskPlan(inverseDirective, artifact("netty-codec-http", "4.0.30")), - ), - plans + assertThat(plans).withFailMessage("Should have 3 pass plans + 1 inverse fail plan").hasSize(4) + assertThat(plans).containsExactly( + MuzzleTaskPlan(directive, artifact("netty-codec-http", "4.1.0")), + MuzzleTaskPlan(directive, artifact("netty-codec-http", "4.1.50")), + MuzzleTaskPlan(directive, artifact("netty-codec-http", "4.2.0")), + MuzzleTaskPlan(inverseDirective, artifact("netty-codec-http", "4.0.30")), ) - assertEquals(2, fakeService.resolveCalls) - assertEquals(1, fakeService.inverseCalls) + assertThat(fakeService.resolveCalls).isEqualTo(2) + assertThat(fakeService.inverseCalls).isEqualTo(1) } @Test @@ -239,15 +231,12 @@ class MuzzleTaskPlannerTest { val plans = MuzzleTaskPlanner(fakeService).plan(listOf(coreJdkDirective, artifactDirective)) - assertEquals( - listOf( - MuzzleTaskPlan(coreJdkDirective, null), - MuzzleTaskPlan(artifactDirective, artifact("demo", "1.5.0")), - ), - plans + assertThat(plans).containsExactly( + MuzzleTaskPlan(coreJdkDirective, null), + MuzzleTaskPlan(artifactDirective, artifact("demo", "1.5.0")), ) - assertEquals(1, fakeService.resolveCalls) - assertEquals(0, fakeService.inverseCalls) + assertThat(fakeService.resolveCalls).isEqualTo(1) + assertThat(fakeService.inverseCalls).isEqualTo(0) } @Test @@ -274,16 +263,13 @@ class MuzzleTaskPlannerTest { val plans = MuzzleTaskPlanner(fakeService).plan(listOf(passDirective, failDirective)) - assertEquals( - listOf( - MuzzleTaskPlan(passDirective, artifact("demo", "2.5.0")), - MuzzleTaskPlan(passDirective, artifact("demo", "3.0.0")), - MuzzleTaskPlan(failDirective, artifact("demo", "1.5.0")), - ), - plans + assertThat(plans).containsExactly( + MuzzleTaskPlan(passDirective, artifact("demo", "2.5.0")), + MuzzleTaskPlan(passDirective, artifact("demo", "3.0.0")), + MuzzleTaskPlan(failDirective, artifact("demo", "1.5.0")), ) - assertEquals(2, fakeService.resolveCalls) - assertEquals(0, fakeService.inverseCalls) + assertThat(fakeService.resolveCalls).isEqualTo(2) + assertThat(fakeService.inverseCalls).isEqualTo(0) } @Test @@ -329,17 +315,14 @@ class MuzzleTaskPlannerTest { val plans = MuzzleTaskPlanner(fakeService).plan(listOf(directive1, directive2)) - assertEquals( - listOf( - MuzzleTaskPlan(directive1, artifact("first", "3.5.0")), - MuzzleTaskPlan(inverse1, artifact("first", "2.5.0")), - MuzzleTaskPlan(directive2, artifact("second", "2.5.0")), - MuzzleTaskPlan(inverse2, artifact("second", "1.5.0")), - ), - plans + assertThat(plans).containsExactly( + MuzzleTaskPlan(directive1, artifact("first", "3.5.0")), + MuzzleTaskPlan(inverse1, artifact("first", "2.5.0")), + MuzzleTaskPlan(directive2, artifact("second", "2.5.0")), + MuzzleTaskPlan(inverse2, artifact("second", "1.5.0")), ) - assertEquals(4, fakeService.resolveCalls) - assertEquals(2, fakeService.inverseCalls) + assertThat(fakeService.resolveCalls).isEqualTo(4) + assertThat(fakeService.inverseCalls).isEqualTo(2) } private fun artifact(module: String = "demo", version: String) = diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleEndTaskTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleEndTaskTest.kt index e567a1611f4..2c7dc920a30 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleEndTaskTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleEndTaskTest.kt @@ -2,9 +2,6 @@ package datadog.gradle.plugin.muzzle.tasks import org.gradle.kotlin.dsl.register import org.gradle.testfixtures.ProjectBuilder -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir @@ -15,6 +12,7 @@ import javax.xml.parsers.DocumentBuilderFactory import kotlin.io.path.createDirectories import kotlin.io.path.readText import kotlin.io.path.writeText +import org.assertj.core.api.Assertions.assertThat class MuzzleEndTaskTest { @@ -70,38 +68,38 @@ class MuzzleEndTaskTest { @Test fun `junit report contains expected testsuite counters`() { val suite = junitDoc.documentElement - assertEquals("testsuite", suite.tagName) - assertEquals(":lettuce-5.0", suite.getAttribute("name")) - assertEquals("2", suite.getAttribute("tests")) - assertEquals("1", suite.getAttribute("failures")) - assertEquals("0", suite.getAttribute("errors")) - assertEquals("0", suite.getAttribute("skipped")) + assertThat(suite.tagName).isEqualTo("testsuite") + assertThat(suite.getAttribute("name")).isEqualTo(":lettuce-5.0") + assertThat(suite.getAttribute("tests")).isEqualTo("2") + assertThat(suite.getAttribute("failures")).isEqualTo("1") + assertThat(suite.getAttribute("errors")).isEqualTo("0") + assertThat(suite.getAttribute("skipped")).isEqualTo("0") } @Test fun `passed testcase has no failure node`() { val passedTestCase = findTestCaseByName(junitDoc, "muzzle-pass") - assertNotNull(passedTestCase) - assertNull(passedTestCase.getElementsByTagName("failure").item(0)) + assertThat(passedTestCase).isNotNull() + assertThat(passedTestCase.getElementsByTagName("failure").item(0)).isNull() } @Test fun `failed testcase contains failure node and message`() { val failedTestCase = findTestCaseByName(junitDoc, "muzzle-fail") - assertNotNull(failedTestCase) + assertThat(failedTestCase).isNotNull() val failureNode = failedTestCase.getElementsByTagName("failure").item(0) as Element - assertEquals("Muzzle validation failed", failureNode.getAttribute("message")) - assertEquals("java.lang.IllegalStateException: something is broken", failureNode.textContent) + assertThat(failureNode.getAttribute("message")).isEqualTo("Muzzle validation failed") + assertThat(failureNode.textContent).isEqualTo("java.lang.IllegalStateException: something is broken") } @Test fun `legacy report keeps historical shape`() { val legacySuite = legacyDoc.documentElement - assertEquals("testsuite", legacySuite.tagName) - assertEquals("1", legacySuite.getAttribute("tests")) - assertEquals("0", legacySuite.getAttribute("id")) - assertEquals("muzzle-end", legacySuite.getAttribute("name")) - assertEquals(1, legacySuite.getElementsByTagName("testcase").length) + assertThat(legacySuite.tagName).isEqualTo("testsuite") + assertThat(legacySuite.getAttribute("tests")).isEqualTo("1") + assertThat(legacySuite.getAttribute("id")).isEqualTo("0") + assertThat(legacySuite.getAttribute("name")).isEqualTo("muzzle-end") + assertThat(legacySuite.getElementsByTagName("testcase").length).isEqualTo(1) } private fun parseXml(xml: String): Document { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9146f93c1af..abd7445ca9a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -63,6 +63,7 @@ jackson = "2.20.0" moshi = "1.11.0" # Testing +assertj = "3.27.7" junit4 = "4.13.2" junit5 = "5.14.1" junit-platform = "1.14.1" @@ -145,6 +146,7 @@ jackson-databind = {module = "com.fasterxml.jackson.core:jackson-databind", vers moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } # Testing +assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } junit4 = { module = "junit:junit", version.ref = "junit4" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit5" } From f68955dfd991a1d2dc669db3a1da73d998333008 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 19 Feb 2026 22:26:02 +0100 Subject: [PATCH 21/22] test: check_build_src do not the build job to run before --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a6de275bfd9..c5063a6b190 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -471,6 +471,7 @@ test_published_artifacts: check_build_src: extends: .check_job + needs: [] variables: GRADLE_TARGET: ":buildSrc:build" From 4e9a5952a7bf12c698363d221423ae0039b1d654 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 20 Feb 2026 15:49:23 +0100 Subject: [PATCH 22/22] fix: Properly run fail directives --- .../datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt | 10 +++++++--- .../gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt | 2 -- 2 files changed, 7 insertions(+), 5 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 ec3929ba619..2d5d830ea3b 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,13 +74,17 @@ abstract class MuzzleTask @Inject constructor( @TaskAction fun muzzle() { when { + // Version-specific task: created by MuzzlePlugin for each resolved artifact. + muzzleDirective.isPresent -> { + assertMuzzle(muzzleDirective.get()) + } + // Fallback for the root "muzzle" lifecycle task when no pass{} directives are + // declared. In that case there are no version-specific pass tasks, so we assert + // the instrumentation against its own compile-time classpath as a basic sanity check. !project.extensions.getByType().directives.any { it.assertPass } -> { project.logger.info("No muzzle pass directives configured. Asserting pass against instrumentation compile-time dependencies") assertMuzzle() } - muzzleDirective.isPresent -> { - assertMuzzle(muzzleDirective.get()) - } } } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt index febd9de8879..00eb2514db0 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt @@ -4,7 +4,6 @@ import datadog.gradle.plugin.GradleFixture import datadog.gradle.plugin.MavenRepoFixture import org.assertj.core.api.Assertions.assertThat import org.gradle.testkit.runner.TaskOutcome.SUCCESS -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest @@ -436,7 +435,6 @@ class MuzzlePluginFunctionalTest { } @Test - @Disabled("Current implementation doesn't fail build when fail directive unexpectedly passes - MuzzleTask catches exceptions") fun `fail directive that passes validation causes build failure`(@TempDir projectDir: File) { val fixture = MuzzlePluginTestFixture(projectDir) fixture.writeProject(