diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/build.gradle b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/build.gradle index a6245277f19..b274e69ff26 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/build.gradle +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/build.gradle @@ -13,6 +13,8 @@ muzzle { } } +addTestSuiteForDir('cucumber723Test', 'test') +addTestSuiteForDir('cucumber76Test', 'test') addTestSuiteForDir('latestDepTest', 'test') dependencies { @@ -33,9 +35,15 @@ dependencies { latestDepTestImplementation group: 'io.cucumber', name: 'cucumber-java', version: '+' latestDepTestImplementation group: 'io.cucumber', name: 'cucumber-junit-platform-engine', version: '+' + + cucumber76TestImplementation group: 'io.cucumber', name: 'cucumber-java', version: '7.6.0' + cucumber76TestImplementation group: 'io.cucumber', name: 'cucumber-junit-platform-engine', version: '7.6.0' + + cucumber723TestImplementation group: 'io.cucumber', name: 'cucumber-java', version: '7.23.0' + cucumber723TestImplementation group: 'io.cucumber', name: 'cucumber-junit-platform-engine', version: '7.23.0' } -configurations.matching({ it.name.startsWith('test') }).configureEach({ +configurations.matching({ it.name.startsWith('test') || it.name.startsWith('cucumber') }).configureEach({ it.resolutionStrategy { force group: 'org.junit.platform', name: 'junit-platform-launcher', version: libs.versions.junit.platform.get() force group: 'org.junit.platform', name: 'junit-platform-suite', version: libs.versions.junit.platform.get() diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/CucumberRetryDescriptorFactory.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/CucumberRetryDescriptorFactory.java new file mode 100644 index 00000000000..449f3068ec2 --- /dev/null +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/CucumberRetryDescriptorFactory.java @@ -0,0 +1,109 @@ +package datadog.trace.instrumentation.junit5; + +import datadog.trace.instrumentation.junit5.execution.RetryDescriptorFactory; +import datadog.trace.util.MethodHandles; +import io.cucumber.core.gherkin.Pickle; +import java.lang.invoke.MethodHandle; +import java.util.function.UnaryOperator; +import org.junit.platform.commons.util.ClassLoaderUtils; +import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.UniqueId; + +/** + * Reconstructs the Cucumber retry descriptor ({@code PickleDescriptor}) through its own constructor + * with a transformed unique id to avoid final-field mutations (JEP 500). + */ +public final class CucumberRetryDescriptorFactory implements RetryDescriptorFactory { + + private static final MethodHandles METHOD_HANDLES = + new MethodHandles(ClassLoaderUtils.getDefaultClassLoader()); + + private static final String PACKAGE = "io.cucumber.junit.platform.engine."; + + private static final MethodHandle CONSTRUCTOR_7_24 = + METHOD_HANDLES.constructor( + PACKAGE + "CucumberTestDescriptor$PickleDescriptor", + JUnitPlatformUtils.loadClass(PACKAGE + "CucumberConfiguration"), + UniqueId.class, + String.class, + TestSource.class, + Pickle.class); + private static final MethodHandle CONSTRUCTOR_7_7 = + METHOD_HANDLES.constructor( + PACKAGE + "NodeDescriptor$PickleDescriptor", + ConfigurationParameters.class, + UniqueId.class, + String.class, + TestSource.class, + Pickle.class); + private static final MethodHandle CONSTRUCTOR_6_0 = + METHOD_HANDLES.constructor( + PACKAGE + "PickleDescriptor", + ConfigurationParameters.class, + UniqueId.class, + String.class, + TestSource.class, + Pickle.class); + private static final MethodHandle CONSTRUCTOR_5_4 = + METHOD_HANDLES.constructor( + PACKAGE + "PickleDescriptor", + UniqueId.class, + String.class, + TestSource.class, + Pickle.class); + + // 7.24+ stores the configuration on the descriptor, read it back for the reconstruction. + private static final MethodHandle CONFIGURATION_GETTER = + METHOD_HANDLES.privateFieldGetter( + PACKAGE + "CucumberTestDescriptor$PickleDescriptor", "configuration"); + + // The Pickle field was renamed pickleEvent -> pickle; resolved lazily off the descriptor's class. + private volatile MethodHandle pickleGetter; + + @Override + public TestDescriptor copy(TestDescriptor original, UnaryOperator idTransform) { + if (!"PickleDescriptor".equals(original.getClass().getSimpleName())) { + return null; // only the leaf scenario descriptor is retried; containers are filtered earlier + } + Object pickle = readPickle(original); + if (pickle == null) { + return null; + } + UniqueId newId = idTransform.apply(original.getUniqueId()); + String name = original.getDisplayName(); + TestSource source = original.getSource().orElse(null); + + if (CONSTRUCTOR_7_24 != null) { + Object configuration = METHOD_HANDLES.invoke(CONFIGURATION_GETTER, original); + return configuration == null + ? null + : METHOD_HANDLES.invoke(CONSTRUCTOR_7_24, configuration, newId, name, source, pickle); + } + if (CONSTRUCTOR_7_7 != null) { + return METHOD_HANDLES.invoke( + CONSTRUCTOR_7_7, new EmptyConfigurationParameters(), newId, name, source, pickle); + } + if (CONSTRUCTOR_6_0 != null) { + return METHOD_HANDLES.invoke( + CONSTRUCTOR_6_0, new EmptyConfigurationParameters(), newId, name, source, pickle); + } + if (CONSTRUCTOR_5_4 != null) { + return METHOD_HANDLES.invoke(CONSTRUCTOR_5_4, newId, name, source, pickle); + } + return null; // unknown cucumber version -> fall back to the generic clone + } + + private Object readPickle(TestDescriptor descriptor) { + MethodHandle getter = pickleGetter; + if (getter == null) { + getter = METHOD_HANDLES.privateFieldGetter(descriptor.getClass(), "pickle"); + if (getter == null) { + getter = METHOD_HANDLES.privateFieldGetter(descriptor.getClass(), "pickleEvent"); + } + pickleGetter = getter; + } + return getter != null ? METHOD_HANDLES.invoke(getter, descriptor) : null; + } +} diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/CucumberUtils.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/CucumberUtils.java index e0dd68f6c98..c199f4be4e1 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/CucumberUtils.java +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/CucumberUtils.java @@ -3,6 +3,7 @@ import datadog.trace.api.Pair; import datadog.trace.api.civisibility.config.TestIdentifier; import datadog.trace.api.civisibility.config.TestSourceData; +import datadog.trace.instrumentation.junit5.execution.RetryDescriptorFactories; import java.io.InputStream; import java.util.ArrayDeque; import java.util.Deque; @@ -25,6 +26,8 @@ public abstract class CucumberUtils { CucumberUtils::toTestIdentifier, d -> TestSourceData.UNKNOWN, null); + RetryDescriptorFactories.register( + JUnitPlatformUtils.ENGINE_ID_CUCUMBER, new CucumberRetryDescriptorFactory()); } public static @Nullable String getCucumberVersion(TestEngine cucumberEngine) { diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/EmptyConfigurationParameters.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/EmptyConfigurationParameters.java new file mode 100644 index 00000000000..17bda38f082 --- /dev/null +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/EmptyConfigurationParameters.java @@ -0,0 +1,34 @@ +package datadog.trace.instrumentation.junit5; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import org.junit.platform.engine.ConfigurationParameters; + +/** + * NO-OP {@link ConfigurationParameters}, used when reconstructing a Cucumber retry descriptor for + * engine versions (6.0–7.23) whose {@code PickleDescriptor} constructor consumes the configuration + * to compute exclusive resources but does not store it (so it cannot be read back). + */ +@SuppressWarnings("deprecation") // ConfigurationParameters#size() is deprecated in newer platforms +public final class EmptyConfigurationParameters implements ConfigurationParameters { + + @Override + public Optional get(String key) { + return Optional.empty(); + } + + @Override + public Optional getBoolean(String key) { + return Optional.empty(); + } + + @Override + public int size() { + return 0; + } + + public Set keySet() { + return Collections.emptySet(); + } +} diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberInstrumentation.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberInstrumentation.java index 2975e92d759..2ddb230ea86 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberInstrumentation.java +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberInstrumentation.java @@ -39,6 +39,10 @@ public String[] helperClassNames() { return new String[] { packageName + ".TestDataFactory", packageName + ".JUnitPlatformUtils", + packageName + ".execution.RetryDescriptorFactory", + packageName + ".execution.RetryDescriptorFactories", + packageName + ".EmptyConfigurationParameters", + packageName + ".CucumberRetryDescriptorFactory", packageName + ".CucumberUtils", packageName + ".TestEventsHandlerHolder", packageName + ".CucumberTracingListener", diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberSkipInstrumentation.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberSkipInstrumentation.java index 73af4a2d485..1833f955f40 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberSkipInstrumentation.java +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberSkipInstrumentation.java @@ -65,6 +65,10 @@ public String[] helperClassNames() { return new String[] { packageName + ".TestDataFactory", packageName + ".JUnitPlatformUtils", + packageName + ".execution.RetryDescriptorFactory", + packageName + ".execution.RetryDescriptorFactories", + packageName + ".EmptyConfigurationParameters", + packageName + ".CucumberRetryDescriptorFactory", packageName + ".CucumberUtils", packageName + ".TestEventsHandlerHolder", }; diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/groovy/CucumberTest.groovy b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/groovy/CucumberTest.groovy index 9aa909b8345..0b84103cbf4 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/groovy/CucumberTest.groovy +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/groovy/CucumberTest.groovy @@ -4,6 +4,7 @@ import datadog.trace.api.civisibility.config.TestIdentifier import datadog.trace.civisibility.CiVisibilityInstrumentationTest import datadog.trace.instrumentation.junit5.JUnitPlatformUtils import datadog.trace.instrumentation.junit5.TestEventsHandlerHolder +import datadog.trace.util.ComparableVersion import io.cucumber.core.api.TypeRegistry import io.cucumber.core.options.Constants import org.junit.platform.engine.DiscoverySelector @@ -31,10 +32,10 @@ class CucumberTest extends CiVisibilityInstrumentationTest { where: testcaseName | features | parallel "test-succeed" | ["org/example/cucumber/calculator/basic_arithmetic.feature"] | false - "test-scenario-outline-${version()}" | ["org/example/cucumber/calculator/basic_arithmetic_with_examples.feature"] | false + "test-scenario-outline-${fixtureVersion()}" | ["org/example/cucumber/calculator/basic_arithmetic_with_examples.feature"] | false "test-skipped" | ["org/example/cucumber/calculator/basic_arithmetic_skipped.feature"] | false "test-skipped-feature" | ["org/example/cucumber/calculator/basic_arithmetic_skipped_feature.feature"] | false - "test-skipped-scenario-outline-${version()}" | ["org/example/cucumber/calculator/basic_arithmetic_with_examples_skipped.feature"] | false + "test-skipped-scenario-outline-${fixtureVersion()}" | ["org/example/cucumber/calculator/basic_arithmetic_with_examples_skipped.feature"] | false "test-parallel" | [ "org/example/cucumber/calculator/basic_arithmetic.feature", "org/example/cucumber/calculator/basic_arithmetic_skipped.feature" @@ -78,7 +79,7 @@ class CucumberTest extends CiVisibilityInstrumentationTest { "test-failed-then-succeed" | true | ["org/example/cucumber/calculator/basic_arithmetic_failed_then_succeed.feature"] | [ new TestFQN("classpath:org/example/cucumber/calculator/basic_arithmetic_failed_then_succeed.feature:Basic Arithmetic", "Addition") ] - "test-retry-failed-scenario-outline-${version()}" | false | ["org/example/cucumber/calculator/basic_arithmetic_with_failed_examples.feature"] | [ + "test-retry-failed-scenario-outline-${fixtureVersion()}" | false | ["org/example/cucumber/calculator/basic_arithmetic_with_failed_examples.feature"] | [ new TestFQN("classpath:org/example/cucumber/calculator/basic_arithmetic_with_failed_examples.feature:Basic Arithmetic With Examples", "Many additions.Single digits.${parameterizedTestNameSuffix()}") ] } @@ -97,7 +98,7 @@ class CucumberTest extends CiVisibilityInstrumentationTest { new TestFQN("classpath:org/example/cucumber/calculator/basic_arithmetic.feature:Basic Arithmetic", "Addition") ] "test-efd-new-test" | ["org/example/cucumber/calculator/basic_arithmetic.feature"] | [] - "test-efd-new-scenario-outline-${version()}" | ["org/example/cucumber/calculator/basic_arithmetic_with_examples.feature"] | [] + "test-efd-new-scenario-outline-${fixtureVersion()}" | ["org/example/cucumber/calculator/basic_arithmetic_with_examples.feature"] | [] "test-efd-new-slow-test" | ["org/example/cucumber/calculator/basic_arithmetic_slow.feature"] | [] "test-efd-skip-new-test" | ["org/example/cucumber/calculator/basic_arithmetic_skip_efd.feature"] | [] } @@ -221,13 +222,22 @@ class CucumberTest extends CiVisibilityInstrumentationTest { } private String parameterizedTestNameSuffix() { - // older releases report different example names - version() == "5.4.0" ? "Example #1" : "Example #1.1" + // Cucumber 7.11.0 changed scenario-outline example naming from the flat "Example #" + // to "Example #.". + usesFlatExampleNaming() ? "Example #1" : "Example #1.1" } - private String version() { + private String fixtureVersion() { + // Scenario-outline fixtures only differ by the example naming scheme, so every release that + // still uses the flat naming shares the "legacy" fixture bucket; the rest use "latest". + usesFlatExampleNaming() ? "legacy" : "latest" + } + + private boolean usesFlatExampleNaming() { def version = TypeRegistry.package.getImplementationVersion() - return version != null ? "latest" : "5.4.0" // older releases do not have package version populated + // 5.4.0 does not populate the package version; cucumber 7.11.0 switched from the flat + // "Example #" naming to "Example #.". + return version == null || new ComparableVersion(version) < new ComparableVersion("7.11.0") } protected void runFeatures(List classpathFeatures, boolean parallel, boolean expectSuccess = true) { diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-efd-new-scenario-outline-5.4.0/coverages.ftl b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-efd-new-scenario-outline-legacy/coverages.ftl similarity index 100% rename from dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-efd-new-scenario-outline-5.4.0/coverages.ftl rename to dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-efd-new-scenario-outline-legacy/coverages.ftl diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-efd-new-scenario-outline-5.4.0/events.ftl b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-efd-new-scenario-outline-legacy/events.ftl similarity index 100% rename from dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-efd-new-scenario-outline-5.4.0/events.ftl rename to dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-efd-new-scenario-outline-legacy/events.ftl diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-retry-failed-scenario-outline-5.4.0/coverages.ftl b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-retry-failed-scenario-outline-legacy/coverages.ftl similarity index 100% rename from dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-retry-failed-scenario-outline-5.4.0/coverages.ftl rename to dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-retry-failed-scenario-outline-legacy/coverages.ftl diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-retry-failed-scenario-outline-5.4.0/events.ftl b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-retry-failed-scenario-outline-legacy/events.ftl similarity index 100% rename from dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-retry-failed-scenario-outline-5.4.0/events.ftl rename to dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-retry-failed-scenario-outline-legacy/events.ftl diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-scenario-outline-5.4.0/coverages.ftl b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-scenario-outline-legacy/coverages.ftl similarity index 100% rename from dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-scenario-outline-5.4.0/coverages.ftl rename to dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-scenario-outline-legacy/coverages.ftl diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-scenario-outline-5.4.0/events.ftl b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-scenario-outline-legacy/events.ftl similarity index 100% rename from dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-scenario-outline-5.4.0/events.ftl rename to dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-scenario-outline-legacy/events.ftl diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-skipped-scenario-outline-5.4.0/coverages.ftl b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-skipped-scenario-outline-legacy/coverages.ftl similarity index 100% rename from dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-skipped-scenario-outline-5.4.0/coverages.ftl rename to dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-skipped-scenario-outline-legacy/coverages.ftl diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-skipped-scenario-outline-5.4.0/events.ftl b/dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-skipped-scenario-outline-legacy/events.ftl similarity index 100% rename from dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-skipped-scenario-outline-5.4.0/events.ftl rename to dd-java-agent/instrumentation/junit/junit-5/junit-5-cucumber-5.4/src/test/resources/test-skipped-scenario-outline-legacy/events.ftl diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockInstrumentation.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockInstrumentation.java index 1960a6c254f..2871d8ff26d 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockInstrumentation.java +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockInstrumentation.java @@ -39,6 +39,9 @@ public String[] helperClassNames() { return new String[] { packageName + ".JUnitPlatformUtils", packageName + ".TestDataFactory", + packageName + ".execution.RetryDescriptorFactory", + packageName + ".execution.RetryDescriptorFactories", + packageName + ".SpockRetryDescriptorFactory", packageName + ".SpockUtils", packageName + ".TestEventsHandlerHolder", packageName + ".SpockTracingListener", diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockSkipInstrumentation.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockSkipInstrumentation.java index a93a401bfc8..023f0d53ac5 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockSkipInstrumentation.java +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockSkipInstrumentation.java @@ -59,6 +59,9 @@ public String[] helperClassNames() { return new String[] { packageName + ".JUnitPlatformUtils", packageName + ".TestDataFactory", + packageName + ".execution.RetryDescriptorFactory", + packageName + ".execution.RetryDescriptorFactories", + packageName + ".SpockRetryDescriptorFactory", packageName + ".SpockUtils", packageName + ".TestEventsHandlerHolder", }; diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockRetryDescriptorFactory.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockRetryDescriptorFactory.java new file mode 100644 index 00000000000..8c87bf0b775 --- /dev/null +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockRetryDescriptorFactory.java @@ -0,0 +1,99 @@ +package datadog.trace.instrumentation.junit5; + +import datadog.trace.instrumentation.junit5.execution.RetryDescriptorFactory; +import datadog.trace.util.MethodHandles; +import java.lang.invoke.MethodHandle; +import java.util.function.UnaryOperator; +import org.junit.platform.commons.util.ClassLoaderUtils; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; +import org.spockframework.runtime.IterationNode; +import org.spockframework.runtime.SimpleFeatureNode; +import org.spockframework.runtime.model.FeatureInfo; +import org.spockframework.runtime.model.IterationInfo; +import spock.config.RunnerConfiguration; + +/** + * Reconstructs the Spock retry descriptor through its own constructor (with a transformed unique + * id) instead of cloning + mutating the final {@code uniqueId} field to avoid JEP 500 warnings. + * + *

The leaf descriptor reaching the retry advice is either a {@link SimpleFeatureNode} + * (non-parametric feature, on every supported tag) or an {@link IterationNode} (a parametric {@code + * where:} row). + */ +public final class SpockRetryDescriptorFactory implements RetryDescriptorFactory { + + private static final MethodHandles METHOD_HANDLES = + new MethodHandles(ClassLoaderUtils.getDefaultClassLoader()); + + private static final MethodHandle SIMPLE_FEATURE_NODE_CONSTRUCTOR = + METHOD_HANDLES.constructor( + SimpleFeatureNode.class, + UniqueId.class, + RunnerConfiguration.class, + FeatureInfo.class, + IterationNode.class); + + private static final MethodHandle ITERATION_NODE_CONSTRUCTOR = + METHOD_HANDLES.constructor( + IterationNode.class, UniqueId.class, RunnerConfiguration.class, IterationInfo.class); + + private static final MethodHandle SIMPLE_FEATURE_NODE_DELEGATE = + METHOD_HANDLES.privateFieldGetter(SimpleFeatureNode.class, "delegate"); + + private static final MethodHandle ITERATION_NODE_INFO = + METHOD_HANDLES.privateFieldGetter(IterationNode.class, "iterationInfo"); + + @Override + public TestDescriptor copy(TestDescriptor original, UnaryOperator idTransform) { + if (original instanceof SimpleFeatureNode) { + return copySimpleFeatureNode((SimpleFeatureNode) original, idTransform); + } + if (original instanceof IterationNode) { + return copyIterationNode((IterationNode) original, idTransform); + } + return null; // unknown Spock node type -> fall back to the generic clone + } + + private static TestDescriptor copySimpleFeatureNode( + SimpleFeatureNode original, UnaryOperator idTransform) { + if (SIMPLE_FEATURE_NODE_CONSTRUCTOR == null || ITERATION_NODE_CONSTRUCTOR == null) { + return null; + } + RunnerConfiguration configuration = original.getConfiguration(); + FeatureInfo featureInfo = original.getNodeInfo(); + IterationNode originalDelegate = METHOD_HANDLES.invoke(SIMPLE_FEATURE_NODE_DELEGATE, original); + if (originalDelegate == null) { + return null; + } + IterationInfo iterationInfo = METHOD_HANDLES.invoke(ITERATION_NODE_INFO, originalDelegate); + + UniqueId newId = idTransform.apply(original.getUniqueId()); + // keep the delegate a proper child of the copy and distinct across attempts + UniqueId.Segment delegateSegment = originalDelegate.getUniqueId().getLastSegment(); + UniqueId newDelegateId = newId.append(delegateSegment.getType(), delegateSegment.getValue()); + + IterationNode delegate = + METHOD_HANDLES.invoke( + ITERATION_NODE_CONSTRUCTOR, newDelegateId, configuration, iterationInfo); + if (delegate == null) { + return null; + } + return METHOD_HANDLES.invoke( + SIMPLE_FEATURE_NODE_CONSTRUCTOR, newId, configuration, featureInfo, delegate); + } + + private static TestDescriptor copyIterationNode( + IterationNode original, UnaryOperator idTransform) { + if (ITERATION_NODE_CONSTRUCTOR == null) { + return null; + } + RunnerConfiguration configuration = original.getConfiguration(); + IterationInfo iterationInfo = METHOD_HANDLES.invoke(ITERATION_NODE_INFO, original); + if (iterationInfo == null) { + return null; + } + UniqueId newId = idTransform.apply(original.getUniqueId()); + return METHOD_HANDLES.invoke(ITERATION_NODE_CONSTRUCTOR, newId, configuration, iterationInfo); + } +} diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java index 8cd89350722..19796973732 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java @@ -3,6 +3,7 @@ import datadog.trace.api.civisibility.CIConstants; import datadog.trace.api.civisibility.config.TestIdentifier; import datadog.trace.api.civisibility.config.TestSourceData; +import datadog.trace.instrumentation.junit5.execution.RetryDescriptorFactories; import java.lang.invoke.MethodHandle; import java.lang.reflect.Method; import java.util.ArrayList; @@ -43,6 +44,8 @@ public class SpockUtils { SpockUtils::toTestIdentifier, SpockUtils::toTestSourceData, SpockUtils::shouldBeTraced); + RetryDescriptorFactories.register( + JUnitPlatformUtils.ENGINE_ID_SPOCK, new SpockRetryDescriptorFactory()); } /* diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/JUnit5ExecutionInstrumentation.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/JUnit5ExecutionInstrumentation.java index e977d70628d..2961aed44e6 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/JUnit5ExecutionInstrumentation.java +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/JUnit5ExecutionInstrumentation.java @@ -58,6 +58,8 @@ public String[] helperClassNames() { return new String[] { packageName + ".TestTaskHandle", packageName + ".TestDescriptorHandle", + packageName + ".RetryDescriptorFactory", + packageName + ".RetryDescriptorFactories", packageName + ".ThrowableCollectorFactoryWrapper", parentPackageName + ".JUnitPlatformUtils", parentPackageName + ".TestDataFactory", diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/RetryDescriptorFactories.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/RetryDescriptorFactories.java new file mode 100644 index 00000000000..4d4eb3885ab --- /dev/null +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/RetryDescriptorFactories.java @@ -0,0 +1,24 @@ +package datadog.trace.instrumentation.junit5.execution; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Registry of per-engine {@link RetryDescriptorFactory} keyed by leaf engine id (e.g. {@code + * spock}, {@code cucumber}). Used by the engine-agnostic retry advice in {@code + * TestDescriptorHandle}. + */ +public final class RetryDescriptorFactories { + + private static final Map BY_ENGINE_ID = new ConcurrentHashMap<>(); + + private RetryDescriptorFactories() {} + + public static void register(String engineId, RetryDescriptorFactory factory) { + BY_ENGINE_ID.put(engineId, factory); + } + + public static RetryDescriptorFactory forEngine(String engineId) { + return engineId != null ? BY_ENGINE_ID.get(engineId) : null; + } +} diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/RetryDescriptorFactory.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/RetryDescriptorFactory.java new file mode 100644 index 00000000000..e1fc99fa8a1 --- /dev/null +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/RetryDescriptorFactory.java @@ -0,0 +1,18 @@ +package datadog.trace.instrumentation.junit5.execution; + +import java.util.function.UnaryOperator; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; + +/** + * Builds a re-executable copy of a leaf test descriptor carrying a transformed unique id, + * without mutating final fields (JEP 500). + */ +public interface RetryDescriptorFactory { + + /** + * @return a reconstructed, re-executable copy with the transformed id, or {@code null} to fall + * back to the generic (Unsafe/reflection) clone. + */ + TestDescriptor copy(TestDescriptor original, UnaryOperator idTransform); +} diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/TestDescriptorHandle.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/TestDescriptorHandle.java index 182171776ac..03dbf504fb6 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/TestDescriptorHandle.java +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/execution/TestDescriptorHandle.java @@ -92,6 +92,21 @@ private static TestDescriptor copy( return copy; } } + + // per-engine reconstruction (Spock, Cucumber) + RetryDescriptorFactory factory = + RetryDescriptorFactories.forEngine(JUnitPlatformUtils.getEngineId(testDescriptor)); + if (factory != null) { + TestDescriptor copy = factory.copy(testDescriptor, idTransform); + if (copy != null) { + // reconstructed descriptors are detached, so we link them back to the original's suite + if (copy instanceof AbstractTestDescriptor) { + ((AbstractTestDescriptor) copy).setParent(testDescriptor.getParent().orElse(null)); + } + return copy; + } + } + return legacyCopy(testDescriptor, idTransform); }