11package datadog .trace .instrumentation .junit5 .execution ;
22
33import datadog .trace .agent .tooling .muzzle .Reference ;
4+ import datadog .trace .instrumentation .junit5 .JUnitPlatformUtils ;
45import datadog .trace .util .MethodHandles ;
56import datadog .trace .util .UnsafeUtils ;
67import java .lang .invoke .MethodHandle ;
78import java .util .Collection ;
89import java .util .Collections ;
910import java .util .Map ;
11+ import java .util .function .UnaryOperator ;
1012import org .junit .platform .commons .util .ClassLoaderUtils ;
1113import org .junit .platform .engine .TestDescriptor ;
1214import org .junit .platform .engine .UniqueId ;
@@ -17,8 +19,28 @@ public class TestDescriptorHandle {
1719 private static final MethodHandles METHOD_HANDLES =
1820 new MethodHandles (ClassLoaderUtils .getDefaultClassLoader ());
1921
20- private static final MethodHandle UNIQUE_ID_SETTER =
21- METHOD_HANDLES .privateFieldSetter (AbstractTestDescriptor .class , "uniqueId" );
22+ private static final String JUPITER_TEST_DESCRIPTOR =
23+ "org.junit.jupiter.engine.descriptor.JupiterTestDescriptor" ;
24+ private static final Class <?> JUPITER_TEST_DESCRIPTOR_CLASS =
25+ JUnitPlatformUtils .loadClass (JUPITER_TEST_DESCRIPTOR );
26+
27+ /** {@code JupiterTestDescriptor#copyIncludingDescendants(UnaryOperator<UniqueId>)} (5.13+) */
28+ private static final MethodHandle COPY_INCLUDING_DESCENDANTS =
29+ METHOD_HANDLES .method (
30+ JUPITER_TEST_DESCRIPTOR , "copyIncludingDescendants" , UnaryOperator .class );
31+
32+ // Legacy fallback used when copyIncludingDescendants is unavailable.
33+ // Overwrites the final unique ID field by reflection. Lazily created to avoid JEP 500 warnings.
34+ private static volatile MethodHandle uniqueIdSetter ;
35+
36+ private static MethodHandle uniqueIdSetter () {
37+ MethodHandle handle = uniqueIdSetter ;
38+ if (handle == null ) {
39+ handle = METHOD_HANDLES .privateFieldSetter (AbstractTestDescriptor .class , "uniqueId" );
40+ uniqueIdSetter = handle ;
41+ }
42+ return handle ;
43+ }
2244
2345 public static final class MuzzleHelper {
2446 public static Collection <? extends Reference > compileReferences () {
@@ -33,24 +55,57 @@ public static Collection<? extends Reference> compileReferences() {
3355
3456 public TestDescriptorHandle (TestDescriptor testDescriptor ) {
3557 /*
36- * We're cloning the descriptor to preserve its original state:
58+ * We're copying the descriptor to preserve its original state:
3759 * JUnit will modify some of its fields during and after test execution
3860 * (one example is parameterized test descriptor,
3961 * whose invocation context is overwritten with null).
40- * Cloning has to be done before each test retry to
41- * compensate for the state modifications .
62+ * The snapshot is taken before the first execution so that every retry
63+ * can be derived from the pristine state .
4264 */
43- this .testDescriptor = UnsafeUtils . tryShallowClone (testDescriptor );
65+ this .testDescriptor = copy (testDescriptor , UnaryOperator . identity () );
4466 }
4567
4668 public TestDescriptor withIdSuffix (Map <String , Object > suffices ) {
47- UniqueId updatedId = testDescriptor .getUniqueId ();
48- for (Map .Entry <String , Object > e : suffices .entrySet ()) {
49- updatedId = updatedId .append (e .getKey (), String .valueOf (e .getValue ()));
69+ return copy (
70+ testDescriptor ,
71+ id -> {
72+ UniqueId updatedId = id ;
73+ for (Map .Entry <String , Object > e : suffices .entrySet ()) {
74+ updatedId = updatedId .append (e .getKey (), String .valueOf (e .getValue ()));
75+ }
76+ return updatedId ;
77+ });
78+ }
79+
80+ private static TestDescriptor copy (
81+ TestDescriptor testDescriptor , UnaryOperator <UniqueId > idTransform ) {
82+ if (COPY_INCLUDING_DESCENDANTS != null
83+ && JUPITER_TEST_DESCRIPTOR_CLASS != null
84+ && JUPITER_TEST_DESCRIPTOR_CLASS .isInstance (testDescriptor )) {
85+ TestDescriptor copy =
86+ METHOD_HANDLES .invoke (COPY_INCLUDING_DESCENDANTS , testDescriptor , idTransform );
87+ if (copy != null ) {
88+ // copyIncludingDescendants returns a detached copy so we link it back to its suite
89+ if (copy instanceof AbstractTestDescriptor ) {
90+ ((AbstractTestDescriptor ) copy ).setParent (testDescriptor .getParent ().orElse (null ));
91+ }
92+ return copy ;
93+ }
5094 }
95+ return legacyCopy (testDescriptor , idTransform );
96+ }
5197
98+ /**
99+ * Fallback for engines without {@code copyIncludingDescendants}: shallow-clone the descriptor and
100+ * overwrite the cloned unique ID field by reflection. Not JEP 500 compliant.
101+ */
102+ private static TestDescriptor legacyCopy (
103+ TestDescriptor testDescriptor , UnaryOperator <UniqueId > idTransform ) {
52104 TestDescriptor descriptorClone = UnsafeUtils .tryShallowClone (testDescriptor );
53- METHOD_HANDLES .invoke (UNIQUE_ID_SETTER , descriptorClone , updatedId );
105+ UniqueId updatedId = idTransform .apply (testDescriptor .getUniqueId ());
106+ if (descriptorClone != testDescriptor && !updatedId .equals (testDescriptor .getUniqueId ())) {
107+ METHOD_HANDLES .invoke (uniqueIdSetter (), descriptorClone , updatedId );
108+ }
54109 return descriptorClone ;
55110 }
56111}
0 commit comments