diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleGenerateReportTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleGenerateReportTask.kt index 6448eec5dc2..c98992bed07 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleGenerateReportTask.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleGenerateReportTask.kt @@ -1,52 +1,70 @@ package datadog.gradle.plugin.muzzle.tasks +import datadog.gradle.plugin.muzzle.MuzzleDirective +import datadog.gradle.plugin.muzzle.MuzzleExtension import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils +import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.highest +import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.lowest +import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.resolveInstrumentationAndJarVersions import datadog.gradle.plugin.muzzle.TestedArtifact -import org.eclipse.aether.util.version.GenericVersionScheme +import datadog.gradle.plugin.muzzle.mainSourceSet +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.RepositorySystemSession +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.getByType +import java.net.URL +import java.net.URLClassLoader import java.util.TreeMap +import java.util.function.BiFunction abstract class MuzzleGenerateReportTask : AbstractMuzzleReportTask() { + @get:Input + abstract val reportDirectives: ListProperty + + @get:Classpath + abstract val instrumentationClasspath: ConfigurableFileCollection + init { - description = "Print instrumentation version report" - } + description = "Generate this instrumentation's dependency version report" + + // Repository metadata can change without any local task input changing. + outputs.upToDateWhen { false } - private val versionReports = project.fileTree(project.rootProject.layout.buildDirectory.dir(MUZZLE_DEPS_RESULTS)) { - include("*.csv") + val extension = project.extensions.getByType() + val runtimeClasspath = project.mainSourceSet.runtimeClasspath + val directives = project.providers.provider { + extension.directives.filter { !it.isCoreJdk && !it.skipFromReport } + } + + reportDirectives.set(directives) + instrumentationClasspath.from(directives.map { if (it.isEmpty()) emptyList() else runtimeClasspath }) } - /** - * Merges all muzzle report CSVs in the build directory into a single map and writes the merged results to a CSV. - */ @TaskAction - fun mergeReports() { - val map = TreeMap() - val versionScheme = GenericVersionScheme() - versionReports.forEach { - logger.info("Processing muzzle report: $it") - it.useLines { lines -> - lines.forEachIndexed { idx, line -> - if (idx == 0) return@forEachIndexed // skip header - val split = line.split(",") - val parsed = TestedArtifact( - split[0], - split[1], - split[2], - versionScheme.parseVersion(split[3]), - versionScheme.parseVersion(split[4]) + fun dumpVersionRanges() { + val system: RepositorySystem = MuzzleMavenRepoUtils.newRepositorySystem() + val session: RepositorySystemSession = MuzzleMavenRepoUtils.newRepositorySystemSession(system) + val versions = TreeMap() + reportDirectives.get().forEach { directive -> + val range = MuzzleMavenRepoUtils.resolveVersionRange(directive, system, session) + val cp = instrumentationClasspath.map { it.toURI().toURL() }.toTypedArray() + val partials = URLClassLoader(cp, null).use { cl -> + resolveInstrumentationAndJarVersions(directive, cl, range.lowestVersion, range.highestVersion) + } + partials.forEach { (key, value) -> + versions.merge(key, value, BiFunction { x, y -> + TestedArtifact( + x.instrumentation, x.group, x.module, + lowest(x.lowVersion, y.lowVersion), + highest(x.highVersion, y.highVersion) ) - map.merge(parsed.key(), parsed) { x, y -> - TestedArtifact( - x.instrumentation, - x.group, - x.module, - MuzzleMavenRepoUtils.lowest(x.lowVersion, y.lowVersion), - MuzzleMavenRepoUtils.highest(x.highVersion, y.highVersion) - ) - } - } + }) } } - dumpVersionsToCsv(map) + dumpVersionsToCsv(versions) } } diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleMergeReportsTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleMergeReportsTask.kt index 1043f0d683c..93ec54e45af 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleMergeReportsTask.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleMergeReportsTask.kt @@ -1,66 +1,59 @@ package datadog.gradle.plugin.muzzle.tasks -import datadog.gradle.plugin.muzzle.MuzzleDirective -import datadog.gradle.plugin.muzzle.MuzzleExtension import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils -import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.highest -import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.lowest -import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.resolveInstrumentationAndJarVersions import datadog.gradle.plugin.muzzle.TestedArtifact -import datadog.gradle.plugin.muzzle.mainSourceSet -import org.eclipse.aether.RepositorySystem -import org.eclipse.aether.RepositorySystemSession -import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.provider.ListProperty -import org.gradle.api.tasks.Classpath -import org.gradle.api.tasks.Input +import org.eclipse.aether.util.version.GenericVersionScheme +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.getByType -import java.net.URL -import java.net.URLClassLoader import java.util.TreeMap -import java.util.function.BiFunction abstract class MuzzleMergeReportsTask : AbstractMuzzleReportTask() { - @get:Input - abstract val reportDirectives: ListProperty - - @get:Classpath - abstract val instrumentationClasspath: ConfigurableFileCollection - init { - description = "Print instrumentation version report" + description = "Merge generated dependency version reports into one CSV" + } - val extension = project.extensions.getByType() - val runtimeClasspath = project.mainSourceSet.runtimeClasspath - val directives = project.providers.provider { - extension.directives.filter { !it.isCoreJdk && !it.skipFromReport } + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + val versionReports = project.files( + project.rootProject.allprojects.flatMap { producer -> + producer.tasks.withType(MuzzleGenerateReportTask::class.java).map { it.versionsFile } } + ).minus(project.files(versionsFile)).asFileTree - reportDirectives.set(directives) - instrumentationClasspath.from(directives.map { if (it.isEmpty()) emptyList() else runtimeClasspath }) - } - + /** + * Merges existing CSVs from current report generators and writes the merged results to a CSV. + */ @TaskAction - fun dumpVersionRanges() { - val system: RepositorySystem = MuzzleMavenRepoUtils.newRepositorySystem() - val session: RepositorySystemSession = MuzzleMavenRepoUtils.newRepositorySystemSession(system) - val versions = TreeMap() - reportDirectives.get().forEach { directive -> - val range = MuzzleMavenRepoUtils.resolveVersionRange(directive, system, session) - val cp = instrumentationClasspath.map { it.toURI().toURL() }.toTypedArray() - val cl = URLClassLoader(cp, null) - val partials = resolveInstrumentationAndJarVersions(directive, cl, range.lowestVersion, range.highestVersion) - partials.forEach { (key, value) -> - versions.merge(key, value, BiFunction { x, y -> - TestedArtifact( - x.instrumentation, x.group, x.module, - lowest(x.lowVersion, y.lowVersion), - highest(x.highVersion, y.highVersion) + fun mergeReports() { + val map = TreeMap() + val versionScheme = GenericVersionScheme() + versionReports.forEach { + logger.info("Processing muzzle report: $it") + it.useLines { lines -> + lines.forEachIndexed { idx, line -> + if (idx == 0) return@forEachIndexed // skip header + val split = line.split(",") + val parsed = TestedArtifact( + split[0], + split[1], + split[2], + versionScheme.parseVersion(split[3]), + versionScheme.parseVersion(split[4]) ) - }) + map.merge(parsed.key(), parsed) { x, y -> + TestedArtifact( + x.instrumentation, + x.group, + x.module, + MuzzleMavenRepoUtils.lowest(x.lowVersion, y.lowVersion), + MuzzleMavenRepoUtils.highest(x.highVersion, y.highVersion) + ) + } + } } } - dumpVersionsToCsv(versions) + dumpVersionsToCsv(map) } } diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleReportConfigurationCacheTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleReportConfigurationCacheTest.kt index 04357799b2b..8f2878bdffc 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleReportConfigurationCacheTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleReportConfigurationCacheTest.kt @@ -2,11 +2,14 @@ package datadog.gradle.plugin.muzzle import org.assertj.core.api.Assertions.assertThat import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource class MuzzleReportConfigurationCacheTest : MuzzlePluginTestFixture() { @Test - fun `dependency report executes with configuration cache reuse`() { + fun `dependency report refreshes version ranges with configuration cache reuse`() { val mavenRepo = createMavenRepoFixture() mavenRepo.publishVersions("com.example", "demo", listOf("1.0.0")) @@ -25,7 +28,7 @@ class MuzzleReportConfigurationCacheTest : MuzzlePluginTestFixture() { pass { group = "com.example" module = "demo" - versions = "1.0.0" + versions = "[1.0.0,)" } } """ @@ -34,10 +37,9 @@ class MuzzleReportConfigurationCacheTest : MuzzlePluginTestFixture() { writeRootProject("""layout.buildDirectory.set(layout.projectDirectory.dir("relocated/build"))""") val args = arrayOf( - ":dd-java-agent:instrumentation:demo:mergeMuzzleReports", + ":dd-java-agent:instrumentation:demo:generateMuzzleReport", "--configuration-cache", "--configuration-cache-problems=fail", - "--rerun-tasks", "--stacktrace" ) val env = mapOf("MAVEN_REPOSITORY_PROXY" to mavenRepo.repoUrl) @@ -46,15 +48,202 @@ class MuzzleReportConfigurationCacheTest : MuzzlePluginTestFixture() { assertThat(first.task(args[0])?.outcome).describedAs(first.output).isEqualTo(SUCCESS) assertThat(first.output).contains("Configuration cache entry stored") + val report = file("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation_demo.csv") + assertThat(report.readText()).isEqualTo( + "instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion\n" + + "test-instrumentation,com.example,demo,1.0.0,1.0.0\n" + ) + + mavenRepo.publishVersions("com.example", "demo", listOf("2.0.0")) + val reused = run(*args, env = env) assertThat(reused.task(args[0])?.outcome).describedAs(reused.output).isEqualTo(SUCCESS) assertThat(reused.output).contains("Reusing configuration cache") - assertThat(file("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation_demo.csv").readText()) + assertThat(report.readText()) .isEqualTo( "instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion\n" + - "test-instrumentation,com.example,demo,1.0.0,1.0.0\n" + "test-instrumentation,com.example,demo,1.0.0,2.0.0\n" + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun `merge reflects changed reports with configuration cache reuse`(rerunTasks: Boolean) { + writeProject( + """ + plugins { + id("java") + id("dd-trace-java.muzzle") + } + """ + ) + addSubproject("dd-java-agent:instrumentation", + """ + plugins { + id("java") + id("dd-trace-java.muzzle") + } + """ + ) + listOf("first", "second", "third").forEach { producer -> + addSubproject("dd-java-agent:instrumentation:$producer", + """ + plugins { + id("java") + id("dd-trace-java.muzzle") + } + """ ) + } + writeRootProject("""layout.buildDirectory.set(layout.projectDirectory.dir("relocated/build"))""") + writeFile("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation_first.csv", + """ + instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion + first-instrumentation,com.example,demo,1.0.0,2.0.0 + """ + ) + writeFile("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation_second.csv", + """ + instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion + first-instrumentation,com.example,demo,1.5.0,3.0.0 + second-instrumentation,com.example,other,3.0.0,4.0.0 + """ + ) + + val args = arrayOf( + ":dd-java-agent:instrumentation:mergeMuzzleReports", + "--configuration-cache", + "--configuration-cache-problems=fail", + "--stacktrace" + ) + + val first = run(*args) + assertThat(first.task(args[0])?.outcome).describedAs(first.output).isEqualTo(SUCCESS) + assertThat(first.task(":dd-java-agent:instrumentation:compileMuzzle")).isNull() + assertThat(first.task(":dd-java-agent:instrumentation:demo:generateMuzzleReport")).isNull() + assertThat(first.output).contains("Configuration cache entry stored") + + val reused = run(*args) + assertThat(reused.task(args[0])?.outcome).describedAs(reused.output).isEqualTo(UP_TO_DATE) + assertThat(reused.output).contains("Reusing configuration cache") + + val report = file("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation.csv") + assertThat(report.readText()) + .isEqualTo( + "instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion\n" + + "first-instrumentation,com.example,demo,1.0.0,3.0.0\n" + + "second-instrumentation,com.example,other,3.0.0,4.0.0\n" + ) + + writeFile("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation_second.csv", + """ + instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion + first-instrumentation,com.example,demo,1.5.0,2.0.0 + """ + ) + + val refreshArgs = if (rerunTasks) args + "--rerun-tasks" else args + val changed = run(*refreshArgs) + assertThat(changed.task(args[0])?.outcome).describedAs(changed.output).isEqualTo(SUCCESS) + assertThat(changed.output).contains("Reusing configuration cache") + assertThat(report.readText()).isEqualTo( + "instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion\n" + + "first-instrumentation,com.example,demo,1.0.0,2.0.0\n" + ) + + writeFile("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation_third.csv", + """ + instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion + third-instrumentation,com.example,new,4.0.0,5.0.0 + """ + ) + val added = run(*refreshArgs) + assertThat(added.task(args[0])?.outcome).describedAs(added.output).isEqualTo(SUCCESS) + assertThat(added.output).contains("Reusing configuration cache") + assertThat(report.readText()).isEqualTo( + "instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion\n" + + "first-instrumentation,com.example,demo,1.0.0,2.0.0\n" + + "third-instrumentation,com.example,new,4.0.0,5.0.0\n" + ) + + assertThat(file("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation_first.csv").delete()).isTrue() + assertThat(file("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation_second.csv").delete()).isTrue() + val removed = run(*refreshArgs) + assertThat(removed.task(args[0])?.outcome).describedAs(removed.output).isEqualTo(SUCCESS) + assertThat(removed.output).contains("Reusing configuration cache") + assertThat(report.readText()).isEqualTo( + "instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion\n" + + "third-instrumentation,com.example,new,4.0.0,5.0.0\n" + ) + } + + @Test + fun `merge ignores reports from removed and renamed producers`() { + val producerScript = """ + plugins { + id("java") + id("dd-trace-java.muzzle") + } + """ + writeProject(producerScript) + addSubproject("dd-java-agent:instrumentation", producerScript) + val currentSettings = file("settings.gradle.kts").readText() + addSubproject("dd-java-agent:instrumentation:deleted", producerScript) + writeRootProject("""layout.buildDirectory.set(layout.projectDirectory.dir("relocated/build"))""") + + val active = writeFile("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation_demo.csv", + """ + instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion + active-instrumentation,com.example,demo,1.0.0,2.0.0 + """ + ) + val orphan = writeFile("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation_deleted.csv", + """ + instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion + deleted-instrumentation,com.example,deleted,3.0.0,4.0.0 + """ + ) + val args = arrayOf( + ":dd-java-agent:instrumentation:mergeMuzzleReports", + "--configuration-cache", + "--configuration-cache-problems=fail", + "--stacktrace" + ) + val report = file("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation.csv") + + val first = run(*args) + assertThat(first.task(args[0])?.outcome).describedAs(first.output).isEqualTo(SUCCESS) + assertThat(report.readText()).isEqualTo( + "instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion\n" + + "active-instrumentation,com.example,demo,1.0.0,2.0.0\n" + + "deleted-instrumentation,com.example,deleted,3.0.0,4.0.0\n" + ) + + writeSettings(currentSettings) + val removed = run(*args) + assertThat(removed.task(args[0])?.outcome).describedAs(removed.output).isEqualTo(SUCCESS) + assertThat(report.readText()).isEqualTo(active.readText()) + assertThat(orphan).exists() + + writeSettings(currentSettings.replace(":instrumentation:demo", ":instrumentation:renamed")) + writeFile("dd-java-agent/instrumentation/renamed/build.gradle.kts", producerScript) + val renamed = writeFile("relocated/build/muzzle-deps-results/dd-java-agent_instrumentation_renamed.csv", + """ + instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion + renamed-instrumentation,com.example,demo,1.5.0,1.5.0 + """ + ) + val refreshed = run(*args) + assertThat(refreshed.task(args[0])?.outcome).describedAs(refreshed.output).isEqualTo(SUCCESS) + assertThat(report.readText()).isEqualTo(renamed.readText()) + assertThat(active).exists() + assertThat(orphan).exists() + + val reused = run(*args) + assertThat(reused.task(args[0])?.outcome).describedAs(reused.output).isEqualTo(UP_TO_DATE) + assertThat(reused.output).contains("Reusing configuration cache") + assertThat(report.readText()).isEqualTo(renamed.readText()) } @Test