From 0686a6872df0bee5e5dc3706f60a671dc2b5bc3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:06:47 +0000 Subject: [PATCH 1/3] Initial plan From 67c6e3212ac0eaafcdce140a6496cd0852d17399 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:16:31 +0000 Subject: [PATCH 2/3] Expose lenient resolution diagnostics on failed Muzzle pass checks Co-authored-by: trask <218610+trask@users.noreply.github.com> --- gradle-plugins/build.gradle.kts | 1 + ...ry.instrumentation.muzzle-check.gradle.kts | 31 ++- .../MuzzleResolutionDiagnosticsTest.java | 187 ++++++++++++++++++ 3 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 gradle-plugins/src/test/java/io/opentelemetry/javaagent/muzzle/MuzzleResolutionDiagnosticsTest.java diff --git a/gradle-plugins/build.gradle.kts b/gradle-plugins/build.gradle.kts index 6736f916ecd8..51535701ccd4 100644 --- a/gradle-plugins/build.gradle.kts +++ b/gradle-plugins/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { } testImplementation("org.assertj:assertj-core:3.27.7") + testImplementation(gradleTestKit()) testImplementation(enforcedPlatform("org.junit:junit-bom:5.14.4")) testImplementation("org.junit.jupiter:junit-jupiter-api") diff --git a/gradle-plugins/src/main/kotlin/io.opentelemetry.instrumentation.muzzle-check.gradle.kts b/gradle-plugins/src/main/kotlin/io.opentelemetry.instrumentation.muzzle-check.gradle.kts index a0273821f3b3..028d8788fb6c 100644 --- a/gradle-plugins/src/main/kotlin/io.opentelemetry.instrumentation.muzzle-check.gradle.kts +++ b/gradle-plugins/src/main/kotlin/io.opentelemetry.instrumentation.muzzle-check.gradle.kts @@ -380,13 +380,20 @@ fun addMuzzleTask(muzzleDirective: MuzzleDirective, versionArtifact: Artifact?, // add more mismatches. For assertPass a missing transitive can cause a // false muzzle failure but never a false pass; such versions should be // skipped in the module's build.gradle.kts when found. - val configFiles = config.incoming.artifactView { lenient(true) }.files + val artifactView = config.incoming.artifactView { lenient(true) } + val configFiles = artifactView.files val muzzleShadowJarFile = shadowModule.flatMap { it.archiveFile } val muzzleToolingShadowJarFile = shadowMuzzleTooling.flatMap { it.archiveFile } val muzzleBootstrapShadowJarFile = shadowMuzzleBootstrap.flatMap { it.archiveFile } val excludedNames = muzzleDirective.excludedInstrumentationNames.get() val excludedModules = muzzleDirective.excludedInstrumentationModules.get() val shouldAssertPass = muzzleDirective.assertPass.get() + // Snapshot diagnostics as text: resolution objects cannot be captured by configuration cache. + val resolutionFailures = if (shouldAssertPass) { + artifactView.artifacts.failures.map { it.stackTraceToString() } + } else { + emptyList() + } dependsOn(configurations.named("runtimeClasspath")) dependsOn(shadowModule) @@ -405,13 +412,21 @@ fun addMuzzleTask(muzzleDirective: MuzzleDirective, versionArtifact: Artifact?, val userUrls = (configFiles + muzzleBootstrapShadowJarFile.get().asFile).map { it.toURI().toURL() }.toTypedArray() val userCL = URLClassLoader(userUrls, ClassLoader.getPlatformClassLoader()) - MuzzleGradlePluginUtil.assertInstrumentationMuzzled( - instrumentationCL, - userCL, - excludedNames, - excludedModules, - shouldAssertPass - ) + try { + MuzzleGradlePluginUtil.assertInstrumentationMuzzled( + instrumentationCL, + userCL, + excludedNames, + excludedModules, + shouldAssertPass + ) + } catch (e: Exception) { + if (shouldAssertPass && resolutionFailures.isNotEmpty()) { + logger.error("Dependency resolution failures collected for $path (lenient resolution):") + resolutionFailures.forEach { logger.error(it) } + } + throw e + } } } diff --git a/gradle-plugins/src/test/java/io/opentelemetry/javaagent/muzzle/MuzzleResolutionDiagnosticsTest.java b/gradle-plugins/src/test/java/io/opentelemetry/javaagent/muzzle/MuzzleResolutionDiagnosticsTest.java new file mode 100644 index 000000000000..8daa1cf92ddd --- /dev/null +++ b/gradle-plugins/src/test/java/io/opentelemetry/javaagent/muzzle/MuzzleResolutionDiagnosticsTest.java @@ -0,0 +1,187 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.muzzle; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.jar.JarOutputStream; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.GradleRunner; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class MuzzleResolutionDiagnosticsTest { + + private static final String DIAGNOSTICS = "Dependency resolution failures collected for"; + + @TempDir Path projectDir; + + @Test + void failedPassReportsResolutionFailures() throws IOException { + prepareProject(true, false, true); + assertCachedBuilds( + true, + output -> + assertThat(output) + .contains( + "Instrumentation failed Muzzle validation", + DIAGNOSTICS, + "Could not find test:missing:1.0.", + "Searched in the following locations:", + "Required by:")); + } + + @Test + void failedPassReportsResolutionCauses() throws IOException { + prepareProject(true, false, true); + Path missingDir = projectDir.resolve("repo/test/missing/1.0"); + Files.createDirectories(missingDir); + Files.writeString(missingDir.resolve("missing-1.0.pom"), "not a pom"); + assertCachedBuilds( + true, + output -> + assertThat(output) + .contains( + "Instrumentation failed Muzzle validation", + DIAGNOSTICS, + "Could not parse POM", + "SAXParseException", + "Caused by:")); + } + + @Test + void failedPassWithoutResolutionFailures() throws IOException { + prepareProject(true, false, false); + assertCachedBuilds( + true, + output -> + assertThat(output) + .contains("Instrumentation failed Muzzle validation") + .doesNotContain(DIAGNOSTICS)); + } + + @Test + void successfulLenientPassIsQuiet() throws IOException { + prepareProject(true, true, true); + assertCachedBuilds(false, output -> assertThat(output).doesNotContain(DIAGNOSTICS)); + } + + @Test + void expectedFailIsQuiet() throws IOException { + prepareProject(false, false, true); + assertCachedBuilds(false, output -> assertThat(output).doesNotContain(DIAGNOSTICS)); + } + + @Test + void unexpectedPassPreservesAssertFailFailure() throws IOException { + prepareProject(false, true, true); + assertCachedBuilds( + true, + output -> + assertThat(output) + .contains("Instrumentation unexpectedly passed Muzzle validation") + .doesNotContain(DIAGNOSTICS)); + } + + private void assertCachedBuilds(boolean fails, java.util.function.Consumer assertions) { + GradleRunner runner = + GradleRunner.create() + .withProjectDir(projectDir.toFile()) + .withPluginClasspath() + .withArguments("muzzle", "--configuration-cache"); + BuildResult first = fails ? runner.buildAndFail() : runner.build(); + assertions.accept(first.getOutput()); + assertThat(first.getOutput()).contains("Configuration cache entry stored"); + BuildResult second = fails ? runner.buildAndFail() : runner.build(); + assertions.accept(second.getOutput()); + assertThat(second.getOutput()).contains("Reusing configuration cache"); + } + + private void prepareProject(boolean assertPass, boolean matches, boolean brokenDependency) + throws IOException { + Files.writeString(projectDir.resolve("settings.gradle"), "rootProject.name = 'muzzle-test'\n"); + Files.writeString( + projectDir.resolve("build.gradle"), + """ + plugins { + id 'io.opentelemetry.instrumentation.muzzle-check' + } + repositories { + maven { url = uri('repo') } + } + muzzle { + %s { + group.set('test') + module.set('library') + versions.set('1.0') + } + } + """ + .formatted(assertPass ? "pass" : "fail")); + + Path artifactDir = projectDir.resolve("repo/test/library/1.0"); + Files.createDirectories(artifactDir); + Files.writeString( + artifactDir.resolve("library-1.0.pom"), + """ + + 4.0.0 + test + library + 1.0 + %s + + """ + .formatted( + brokenDependency + ? """ + + + test + missing + 1.0 + + + """ + : "")); + try (JarOutputStream jar = + new JarOutputStream(Files.newOutputStream(artifactDir.resolve("library-1.0.jar")))) { + // A resource lets the matcher fixture verify the resolved user classpath. + if (matches) { + jar.putNextEntry(new java.util.jar.JarEntry("muzzle-match")); + jar.closeEntry(); + } + } + + // Exercise the plugin's reflective validation boundary without depending on the agent build. + Path matcher = + projectDir.resolve( + "src/main/java/io/opentelemetry/javaagent/tooling/muzzle/ClassLoaderMatcher.java"); + Files.createDirectories(matcher.getParent()); + Files.writeString( + matcher, + """ + package io.opentelemetry.javaagent.tooling.muzzle; + + import java.util.Collections; + import java.util.List; + import java.util.Map; + import java.util.Set; + + public class ClassLoaderMatcher { + public static Map> matchesAll( + ClassLoader loader, boolean assertPass, Set names, Set modules) { + return Collections.singletonMap("test-module", + loader.getResource("muzzle-match") != null + ? Collections.emptyList() : Collections.singletonList("missing library symbol")); + } + } + """); + } +} From 2c9b1d89a7673cef295c91a4e022d9da364b7328 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:31:20 +0000 Subject: [PATCH 3/3] Remove Muzzle resolution diagnostics functional tests as requested Co-authored-by: trask <218610+trask@users.noreply.github.com> --- gradle-plugins/build.gradle.kts | 1 - .../MuzzleResolutionDiagnosticsTest.java | 187 ------------------ 2 files changed, 188 deletions(-) delete mode 100644 gradle-plugins/src/test/java/io/opentelemetry/javaagent/muzzle/MuzzleResolutionDiagnosticsTest.java diff --git a/gradle-plugins/build.gradle.kts b/gradle-plugins/build.gradle.kts index 51535701ccd4..6736f916ecd8 100644 --- a/gradle-plugins/build.gradle.kts +++ b/gradle-plugins/build.gradle.kts @@ -48,7 +48,6 @@ dependencies { } testImplementation("org.assertj:assertj-core:3.27.7") - testImplementation(gradleTestKit()) testImplementation(enforcedPlatform("org.junit:junit-bom:5.14.4")) testImplementation("org.junit.jupiter:junit-jupiter-api") diff --git a/gradle-plugins/src/test/java/io/opentelemetry/javaagent/muzzle/MuzzleResolutionDiagnosticsTest.java b/gradle-plugins/src/test/java/io/opentelemetry/javaagent/muzzle/MuzzleResolutionDiagnosticsTest.java deleted file mode 100644 index 8daa1cf92ddd..000000000000 --- a/gradle-plugins/src/test/java/io/opentelemetry/javaagent/muzzle/MuzzleResolutionDiagnosticsTest.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.muzzle; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.jar.JarOutputStream; -import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.GradleRunner; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class MuzzleResolutionDiagnosticsTest { - - private static final String DIAGNOSTICS = "Dependency resolution failures collected for"; - - @TempDir Path projectDir; - - @Test - void failedPassReportsResolutionFailures() throws IOException { - prepareProject(true, false, true); - assertCachedBuilds( - true, - output -> - assertThat(output) - .contains( - "Instrumentation failed Muzzle validation", - DIAGNOSTICS, - "Could not find test:missing:1.0.", - "Searched in the following locations:", - "Required by:")); - } - - @Test - void failedPassReportsResolutionCauses() throws IOException { - prepareProject(true, false, true); - Path missingDir = projectDir.resolve("repo/test/missing/1.0"); - Files.createDirectories(missingDir); - Files.writeString(missingDir.resolve("missing-1.0.pom"), "not a pom"); - assertCachedBuilds( - true, - output -> - assertThat(output) - .contains( - "Instrumentation failed Muzzle validation", - DIAGNOSTICS, - "Could not parse POM", - "SAXParseException", - "Caused by:")); - } - - @Test - void failedPassWithoutResolutionFailures() throws IOException { - prepareProject(true, false, false); - assertCachedBuilds( - true, - output -> - assertThat(output) - .contains("Instrumentation failed Muzzle validation") - .doesNotContain(DIAGNOSTICS)); - } - - @Test - void successfulLenientPassIsQuiet() throws IOException { - prepareProject(true, true, true); - assertCachedBuilds(false, output -> assertThat(output).doesNotContain(DIAGNOSTICS)); - } - - @Test - void expectedFailIsQuiet() throws IOException { - prepareProject(false, false, true); - assertCachedBuilds(false, output -> assertThat(output).doesNotContain(DIAGNOSTICS)); - } - - @Test - void unexpectedPassPreservesAssertFailFailure() throws IOException { - prepareProject(false, true, true); - assertCachedBuilds( - true, - output -> - assertThat(output) - .contains("Instrumentation unexpectedly passed Muzzle validation") - .doesNotContain(DIAGNOSTICS)); - } - - private void assertCachedBuilds(boolean fails, java.util.function.Consumer assertions) { - GradleRunner runner = - GradleRunner.create() - .withProjectDir(projectDir.toFile()) - .withPluginClasspath() - .withArguments("muzzle", "--configuration-cache"); - BuildResult first = fails ? runner.buildAndFail() : runner.build(); - assertions.accept(first.getOutput()); - assertThat(first.getOutput()).contains("Configuration cache entry stored"); - BuildResult second = fails ? runner.buildAndFail() : runner.build(); - assertions.accept(second.getOutput()); - assertThat(second.getOutput()).contains("Reusing configuration cache"); - } - - private void prepareProject(boolean assertPass, boolean matches, boolean brokenDependency) - throws IOException { - Files.writeString(projectDir.resolve("settings.gradle"), "rootProject.name = 'muzzle-test'\n"); - Files.writeString( - projectDir.resolve("build.gradle"), - """ - plugins { - id 'io.opentelemetry.instrumentation.muzzle-check' - } - repositories { - maven { url = uri('repo') } - } - muzzle { - %s { - group.set('test') - module.set('library') - versions.set('1.0') - } - } - """ - .formatted(assertPass ? "pass" : "fail")); - - Path artifactDir = projectDir.resolve("repo/test/library/1.0"); - Files.createDirectories(artifactDir); - Files.writeString( - artifactDir.resolve("library-1.0.pom"), - """ - - 4.0.0 - test - library - 1.0 - %s - - """ - .formatted( - brokenDependency - ? """ - - - test - missing - 1.0 - - - """ - : "")); - try (JarOutputStream jar = - new JarOutputStream(Files.newOutputStream(artifactDir.resolve("library-1.0.jar")))) { - // A resource lets the matcher fixture verify the resolved user classpath. - if (matches) { - jar.putNextEntry(new java.util.jar.JarEntry("muzzle-match")); - jar.closeEntry(); - } - } - - // Exercise the plugin's reflective validation boundary without depending on the agent build. - Path matcher = - projectDir.resolve( - "src/main/java/io/opentelemetry/javaagent/tooling/muzzle/ClassLoaderMatcher.java"); - Files.createDirectories(matcher.getParent()); - Files.writeString( - matcher, - """ - package io.opentelemetry.javaagent.tooling.muzzle; - - import java.util.Collections; - import java.util.List; - import java.util.Map; - import java.util.Set; - - public class ClassLoaderMatcher { - public static Map> matchesAll( - ClassLoader loader, boolean assertPass, Set names, Set modules) { - return Collections.singletonMap("test-module", - loader.getResource("muzzle-match") != null - ? Collections.emptyList() : Collections.singletonList("missing library symbol")); - } - } - """); - } -}