Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ muzzle {
}
}

addTestSuiteForDir('cucumber723Test', 'test')
addTestSuiteForDir('cucumber76Test', 'test')
addTestSuiteForDir('latestDepTest', 'test')

dependencies {
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UniqueId> 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);
Comment on lines +86 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Cucumber parameters for retry descriptors

For Cucumber 6.0–7.23, PickleDescriptor derives its execution mode and tag-based exclusive resources from the ConfigurationParameters passed to this constructor (for example, the 7.23 source computes both from parameters: https://github.com/cucumber/cucumber-jvm/blob/v7.23.0/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/NodeDescriptor.java). Passing EmptyConfigurationParameters here means retries for suites that configure non-default feature execution mode or cucumber.execution.exclusive-resources.* are rebuilt with default concurrent mode and no resource locks, while the previous shallow clone preserved those fields; those retries can therefore be scheduled differently from the original scenario.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the execution mode and exclusive resources are read by the HierarchicalTestExecutorService when planning on how to run a test task. But the retry descriptors are only ever executed by us manually in the execution instrumentation through currentTask.execute() without scheduling, so the fields are never used.

}
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> get(String key) {
return Optional.empty();
}

@Override
public Optional<Boolean> getBoolean(String key) {
return Optional.empty();
}

@Override
public int size() {
return 0;
}

public Set<String> keySet() {
return Collections.emptySet();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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()}")
]
}
Expand All @@ -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"] | []
}
Expand Down Expand Up @@ -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 #<row>"
// to "Example #<examplesBlock>.<row>".
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 #<row>" naming to "Example #<examplesBlock>.<row>".
return version == null || new ComparableVersion(version) < new ComparableVersion("7.11.0")
}

protected void runFeatures(List<String> classpathFeatures, boolean parallel, boolean expectSuccess = true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<UniqueId> 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<UniqueId> 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<UniqueId> 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);
}
}
Loading
Loading