From 8aacf2e7259614d7f53733cf9d5c2f146b97cfa0 Mon Sep 17 00:00:00 2001 From: Daniel Mohedano Date: Wed, 29 Jul 2026 11:47:29 +0200 Subject: [PATCH 1/2] fix: restore junit4 test ordering on gradle 9.3+ --- ...estDefinitionProcessorInstrumentation.java | 74 ++++++++++++++++++ .../DDCollectAllTestDefinitionsExecutor.java | 59 +++++++++++++++ ...estDefinitionProcessorInstrumentation.java | 75 +++++++++++++++++++ .../smoketest/GradleDaemonSmokeTest.java | 10 ++- 4 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 dd-java-agent/instrumentation/gradle-testing-5.1/src/main/groovy/datadog/trace/instrumentation/gradle/junit4/AbstractJUnitTestDefinitionProcessorInstrumentation.java create mode 100644 dd-java-agent/instrumentation/gradle-testing-5.1/src/main/groovy/datadog/trace/instrumentation/gradle/junit4/DDCollectAllTestDefinitionsExecutor.java create mode 100644 dd-java-agent/instrumentation/gradle-testing-5.1/src/main/groovy/datadog/trace/instrumentation/gradle/junit4/JUnitTestDefinitionProcessorInstrumentation.java diff --git a/dd-java-agent/instrumentation/gradle-testing-5.1/src/main/groovy/datadog/trace/instrumentation/gradle/junit4/AbstractJUnitTestDefinitionProcessorInstrumentation.java b/dd-java-agent/instrumentation/gradle-testing-5.1/src/main/groovy/datadog/trace/instrumentation/gradle/junit4/AbstractJUnitTestDefinitionProcessorInstrumentation.java new file mode 100644 index 00000000000..04d8677d4d7 --- /dev/null +++ b/dd-java-agent/instrumentation/gradle-testing-5.1/src/main/groovy/datadog/trace/instrumentation/gradle/junit4/AbstractJUnitTestDefinitionProcessorInstrumentation.java @@ -0,0 +1,74 @@ +package datadog.trace.instrumentation.gradle.junit4; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.Config; +import datadog.trace.api.civisibility.CIConstants; +import datadog.trace.instrumentation.junit4.JUnit4Instrumentation; +import net.bytebuddy.asm.Advice; +import org.gradle.api.internal.tasks.testing.ClassTestDefinition; +import org.gradle.api.internal.tasks.testing.TestDefinitionConsumer; + +@AutoService(InstrumenterModule.class) +public class AbstractJUnitTestDefinitionProcessorInstrumentation + extends InstrumenterModule.CiVisibility + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + public AbstractJUnitTestDefinitionProcessorInstrumentation() { + super("ci-visibility", "gradle", "junit4"); + } + + @Override + public boolean isEnabled() { + return super.isEnabled() && Config.get().getCiVisibilityTestOrder() != null; + } + + @Override + public String instrumentedType() { + return "org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + JUnit4Instrumentation.class.getPackage().getName() + ".SkippedByDatadog", + JUnit4Instrumentation.class.getPackage().getName() + ".JUnit4Utils", + JUnit4Instrumentation.class.getPackage().getName() + ".TestEventsHandlerHolder", + JUnit4Instrumentation.class.getPackage().getName() + ".TracingListener", + JUnit4Instrumentation.class.getPackage().getName() + ".order.JUnit4FailFastClassOrderer", + packageName + ".DDCollectAllTestDefinitionsExecutor", + }; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("stop"), + AbstractJUnitTestDefinitionProcessorInstrumentation.class.getName() + + "$ProcessAllTestClassesAdvice"); + } + + @Override + public String muzzleDirective() { + return "skipMuzzle"; + } + + public static class ProcessAllTestClassesAdvice { + @SuppressWarnings("bytebuddy-exception-suppression") + @Advice.OnMethodEnter + public static void onStop( + @Advice.FieldValue(value = "executor") + final TestDefinitionConsumer executor) { + String testOrder = Config.get().getCiVisibilityTestOrder(); + if (!CIConstants.FAIL_FAST_TEST_ORDER.equalsIgnoreCase(testOrder)) { + throw new IllegalArgumentException("Unknown test order: " + testOrder); + } + + if (executor instanceof DDCollectAllTestDefinitionsExecutor) { + ((DDCollectAllTestDefinitionsExecutor) executor).processAllTestClasses(); + } + } + } +} diff --git a/dd-java-agent/instrumentation/gradle-testing-5.1/src/main/groovy/datadog/trace/instrumentation/gradle/junit4/DDCollectAllTestDefinitionsExecutor.java b/dd-java-agent/instrumentation/gradle-testing-5.1/src/main/groovy/datadog/trace/instrumentation/gradle/junit4/DDCollectAllTestDefinitionsExecutor.java new file mode 100644 index 00000000000..96b3803e49e --- /dev/null +++ b/dd-java-agent/instrumentation/gradle-testing-5.1/src/main/groovy/datadog/trace/instrumentation/gradle/junit4/DDCollectAllTestDefinitionsExecutor.java @@ -0,0 +1,59 @@ +package datadog.trace.instrumentation.gradle.junit4; + +import datadog.trace.api.civisibility.telemetry.tag.TestFrameworkInstrumentation; +import datadog.trace.instrumentation.junit4.JUnit4Utils; +import datadog.trace.instrumentation.junit4.TestEventsHandlerHolder; +import datadog.trace.instrumentation.junit4.order.JUnit4FailFastClassOrderer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.gradle.api.internal.tasks.testing.ClassTestDefinition; +import org.gradle.api.internal.tasks.testing.TestDefinitionConsumer; +import org.gradle.internal.UncheckedException; + +public class DDCollectAllTestDefinitionsExecutor + implements TestDefinitionConsumer { + private final List> testClasses = new ArrayList<>(); + private final Map testDefinitions = new HashMap<>(); + private final TestDefinitionConsumer delegate; + private final ClassLoader classLoader; + + public DDCollectAllTestDefinitionsExecutor( + TestDefinitionConsumer delegate, ClassLoader junitClassLoader) { + this.delegate = delegate; + this.classLoader = junitClassLoader; + } + + @Override + public void accept(ClassTestDefinition testDefinition) { + Class clazz = loadClass(testDefinition.getTestClassName()); + + TestFrameworkInstrumentation framework = JUnit4Utils.classToFramework(clazz); + if (framework == TestFrameworkInstrumentation.JUNIT4) { + TestEventsHandlerHolder.start( + TestFrameworkInstrumentation.JUNIT4, JUnit4Utils.capabilities(true)); + } + + testClasses.add(clazz); + testDefinitions.put(testDefinition.getTestClassName(), testDefinition); + } + + public void processAllTestClasses() { + testClasses.sort( + new JUnit4FailFastClassOrderer( + TestEventsHandlerHolder.HANDLERS.get(TestFrameworkInstrumentation.JUNIT4))); + + for (Class clazz : testClasses) { + delegate.accept(testDefinitions.get(clazz.getName())); + } + } + + private Class loadClass(String testClassName) { + try { + return Class.forName(testClassName, false, classLoader); + } catch (ClassNotFoundException e) { + throw UncheckedException.throwAsUncheckedException(e); + } + } +} diff --git a/dd-java-agent/instrumentation/gradle-testing-5.1/src/main/groovy/datadog/trace/instrumentation/gradle/junit4/JUnitTestDefinitionProcessorInstrumentation.java b/dd-java-agent/instrumentation/gradle-testing-5.1/src/main/groovy/datadog/trace/instrumentation/gradle/junit4/JUnitTestDefinitionProcessorInstrumentation.java new file mode 100644 index 00000000000..9963637cea3 --- /dev/null +++ b/dd-java-agent/instrumentation/gradle-testing-5.1/src/main/groovy/datadog/trace/instrumentation/gradle/junit4/JUnitTestDefinitionProcessorInstrumentation.java @@ -0,0 +1,75 @@ +package datadog.trace.instrumentation.gradle.junit4; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.Config; +import datadog.trace.api.civisibility.CIConstants; +import datadog.trace.instrumentation.junit4.JUnit4Instrumentation; +import net.bytebuddy.asm.Advice; +import org.gradle.api.internal.tasks.testing.ClassTestDefinition; +import org.gradle.api.internal.tasks.testing.TestDefinitionConsumer; + +@AutoService(InstrumenterModule.class) +public class JUnitTestDefinitionProcessorInstrumentation extends InstrumenterModule.CiVisibility + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + public JUnitTestDefinitionProcessorInstrumentation() { + super("ci-visibility", "gradle", "junit4"); + } + + @Override + public boolean isEnabled() { + return super.isEnabled() && Config.get().getCiVisibilityTestOrder() != null; + } + + @Override + public String instrumentedType() { + return "org.gradle.api.internal.tasks.testing.junit.JUnitTestDefinitionProcessor"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + JUnit4Instrumentation.class.getPackage().getName() + ".JUnit4Utils", + JUnit4Instrumentation.class.getPackage().getName() + ".TestEventsHandlerHolder", + JUnit4Instrumentation.class.getPackage().getName() + ".SkippedByDatadog", + JUnit4Instrumentation.class.getPackage().getName() + ".TracingListener", + JUnit4Instrumentation.class.getPackage().getName() + ".order.JUnit4FailFastClassOrderer", + packageName + ".DDCollectAllTestDefinitionsExecutor", + }; + } + + @Override + public String muzzleDirective() { + return "skipMuzzle"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("createTestExecutor") + .and(takesArgument(0, named("org.gradle.internal.actor.Actor"))) + .and(returns(named("org.gradle.api.internal.tasks.testing.TestDefinitionConsumer"))), + JUnitTestDefinitionProcessorInstrumentation.class.getName() + "$TestExecutorAdvice"); + } + + public static class TestExecutorAdvice { + @SuppressWarnings("bytebuddy-exception-suppression") + @Advice.OnMethodExit + public static void onTestExecutorCreation( + @Advice.Return(readOnly = false) TestDefinitionConsumer executor) { + String testOrder = Config.get().getCiVisibilityTestOrder(); + if (!CIConstants.FAIL_FAST_TEST_ORDER.equalsIgnoreCase(testOrder)) { + throw new IllegalArgumentException("Unknown test order: " + testOrder); + } + + executor = + new DDCollectAllTestDefinitionsExecutor( + executor, Thread.currentThread().getContextClassLoader()); + } + } +} diff --git a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java index 5b21058b906..bf7bfe95f60 100644 --- a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java +++ b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java @@ -161,11 +161,12 @@ void testRobolectric(String gradleVersion, String projectName, int expectedTrace mockBackend.waitForCoverages(0)); } - // TODO: add back LATEST_GRADLE_VERSION after fixing ordering on Gradle 9.3.0 @TableTest({ - "scenario | gradleVersion | projectName | flakyTests | expectedOrder | eventsNumber", - "junit4-ordering-7.6.4 | 7.6.4 | test-succeed-junit-4-class-ordering | ['datadog.smoke.TestSucceedB:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed'] | ['datadog.smoke.TestSucceedC:test_succeed', 'datadog.smoke.TestSucceedC:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed_another'] | 15 ", - "junit4-ordering-9.2.1 | 9.2.1 | test-succeed-junit-4-class-ordering | ['datadog.smoke.TestSucceedB:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed'] | ['datadog.smoke.TestSucceedC:test_succeed', 'datadog.smoke.TestSucceedC:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed_another'] | 15 " + "scenario | gradleVersion | projectName | flakyTests | expectedOrder | eventsNumber", + "junit4-ordering-7.6.4 | 7.6.4 | test-succeed-junit-4-class-ordering | ['datadog.smoke.TestSucceedB:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed'] | ['datadog.smoke.TestSucceedC:test_succeed', 'datadog.smoke.TestSucceedC:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed_another'] | 15 ", + "junit4-ordering-9.2.1 | 9.2.1 | test-succeed-junit-4-class-ordering | ['datadog.smoke.TestSucceedB:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed'] | ['datadog.smoke.TestSucceedC:test_succeed', 'datadog.smoke.TestSucceedC:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed_another'] | 15 ", + "junit4-ordering-9.3.0 | 9.3.0 | test-succeed-junit-4-class-ordering | ['datadog.smoke.TestSucceedB:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed'] | ['datadog.smoke.TestSucceedC:test_succeed', 'datadog.smoke.TestSucceedC:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed_another'] | 15 ", + "junit4-ordering-latest | latest | test-succeed-junit-4-class-ordering | ['datadog.smoke.TestSucceedB:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed'] | ['datadog.smoke.TestSucceedC:test_succeed', 'datadog.smoke.TestSucceedC:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed_another', 'datadog.smoke.TestSucceedA:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed', 'datadog.smoke.TestSucceedB:test_succeed_another'] | 15 " }) @ParameterizedTest void testJunit4ClassOrdering( @@ -175,6 +176,7 @@ void testJunit4ClassOrdering( List expectedOrder, int eventsNumber) throws IOException { + gradleVersion = resolveVersion(gradleVersion); givenGradleVersionIsCompatibleWithCurrentJvm(gradleVersion); givenGradleProjectFiles(projectName); givenGradleProjectProperties(); From ff897545d7b97e9df3b4262eff2f3d3265f52d75 Mon Sep 17 00:00:00 2001 From: Daniel Mohedano Date: Thu, 30 Jul 2026 09:30:14 +0200 Subject: [PATCH 2/2] fix: skip all gradle smoke tests under JDK27 --- .../src/test/java/datadog/smoketest/AbstractGradleTest.java | 5 ++++- .../test/java/datadog/smoketest/GradleDaemonSmokeTest.java | 2 -- .../test/java/datadog/smoketest/GradleLauncherSmokeTest.java | 4 ---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/AbstractGradleTest.java b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/AbstractGradleTest.java index 623184dead6..6cb475766af 100644 --- a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/AbstractGradleTest.java +++ b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/AbstractGradleTest.java @@ -195,7 +195,10 @@ protected void givenGradleVersionIsCompatibleWithCurrentJvm(String gradleVersion private static boolean isSupported(ComparableVersion gradleVersion) { // https://docs.gradle.org/current/userguide/compatibility.html - if (JavaVirtualMachine.isJavaVersionAtLeast(26)) { + // TODO: remove once Gradle supports running on JDK 27. + if (JavaVirtualMachine.isJavaVersionAtLeast(27)) { + return false; + } else if (JavaVirtualMachine.isJavaVersionAtLeast(26)) { return gradleVersion.compareTo(new ComparableVersion("9.4")) >= 0; } else if (JavaVirtualMachine.isJavaVersionAtLeast(25)) { return gradleVersion.compareTo(new ComparableVersion("9.1")) >= 0; diff --git a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java index 24ab0ec135d..3aaf72fac1a 100644 --- a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java +++ b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleDaemonSmokeTest.java @@ -119,8 +119,6 @@ void testNew( int expectedTraces, int expectedCoverages) throws IOException { - Assumptions.assumeFalse( - JavaVirtualMachine.isJavaVersion(27), "JDK 27 TODO: address failing test"); runGradleTest( gradleVersion, projectName, diff --git a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleLauncherSmokeTest.java b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleLauncherSmokeTest.java index 5d4c827a17a..199e21bfe70 100644 --- a/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleLauncherSmokeTest.java +++ b/dd-smoke-tests/gradle/src/test/java/datadog/smoketest/GradleLauncherSmokeTest.java @@ -1,7 +1,6 @@ package datadog.smoketest; import datadog.communication.util.IOUtils; -import datadog.environment.JavaVirtualMachine; import datadog.trace.civisibility.utils.ShellCommandExecutor; import java.nio.file.Files; import java.nio.file.Path; @@ -10,7 +9,6 @@ import java.util.List; import java.util.Map; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.opentest4j.AssertionFailedError; @@ -48,8 +46,6 @@ class GradleLauncherSmokeTest extends AbstractGradleTest { @ParameterizedTest void testGradleLauncherInjectsTracerIntoGradleDaemon( String gradleVersion, String gradleDaemonCmdLineParams) throws Exception { - Assumptions.assumeFalse( - JavaVirtualMachine.isJavaVersion(27), "JDK 27 TODO: address failing test"); String resolvedGradleVersion = "latest".equals(gradleVersion) ? LATEST_GRADLE_VERSION : gradleVersion; String cmdLineParams =