Skip to content

Commit 3fe1b2d

Browse files
Added test suite level visibility for JUnit 3.8 test cases (DataDog#6320)
1 parent 4d0b113 commit 3fe1b2d

File tree

7 files changed

+322
-109
lines changed

7 files changed

+322
-109
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package datadog.trace.instrumentation.junit4;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
4+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
5+
6+
import com.google.auto.service.AutoService;
7+
import datadog.trace.agent.tooling.Instrumenter;
8+
import java.util.List;
9+
import junit.framework.TestCase;
10+
import net.bytebuddy.asm.Advice;
11+
import org.junit.rules.RuleChain;
12+
import org.junit.runner.Runner;
13+
import org.junit.runner.notification.RunListener;
14+
import org.junit.runner.notification.RunNotifier;
15+
16+
/** Supports suite started/finished events for {@link TestCase} subclasses. */
17+
@AutoService(Instrumenter.class)
18+
public class JUnit38SuiteEventsInstrumentation extends Instrumenter.CiVisibility
19+
implements Instrumenter.ForSingleType {
20+
21+
public JUnit38SuiteEventsInstrumentation() {
22+
super("ci-visibility", "junit-4", "junit-38");
23+
}
24+
25+
@Override
26+
public String instrumentedType() {
27+
return "org.junit.internal.runners.JUnit38ClassRunner";
28+
}
29+
30+
@Override
31+
public String[] helperClassNames() {
32+
return new String[] {
33+
packageName + ".TestEventsHandlerHolder",
34+
packageName + ".SkippedByItr",
35+
packageName + ".JUnit4Utils",
36+
packageName + ".TracingListener",
37+
packageName + ".JUnit4TracingListener",
38+
};
39+
}
40+
41+
@Override
42+
public void adviceTransformations(AdviceTransformation transformation) {
43+
transformation.applyAdvice(
44+
named("run").and(takesArgument(0, named("org.junit.runner.notification.RunNotifier"))),
45+
JUnit38SuiteEventsInstrumentation.class.getName() + "$JUnit38SuiteEventsAdvice");
46+
}
47+
48+
public static class JUnit38SuiteEventsAdvice {
49+
@Advice.OnMethodEnter(suppress = Throwable.class)
50+
public static void fireSuiteStartedEvent(
51+
@Advice.Argument(0) final RunNotifier runNotifier, @Advice.This final Runner runner) {
52+
final List<RunListener> runListeners = JUnit4Utils.runListenersFromRunNotifier(runNotifier);
53+
if (runListeners == null) {
54+
return;
55+
}
56+
57+
for (final RunListener listener : runListeners) {
58+
TracingListener tracingListener = JUnit4Utils.toTracingListener(listener);
59+
if (tracingListener != null) {
60+
tracingListener.testSuiteStarted(runner.getDescription());
61+
}
62+
}
63+
}
64+
65+
@Advice.OnMethodExit(suppress = Throwable.class)
66+
public static void fireSuiteFinishedEvent(
67+
@Advice.Argument(0) final RunNotifier runNotifier, @Advice.This final Runner runner) {
68+
final List<RunListener> runListeners = JUnit4Utils.runListenersFromRunNotifier(runNotifier);
69+
if (runListeners == null) {
70+
return;
71+
}
72+
73+
for (final RunListener listener : runListeners) {
74+
TracingListener tracingListener = JUnit4Utils.toTracingListener(listener);
75+
if (tracingListener != null) {
76+
tracingListener.testSuiteFinished(runner.getDescription());
77+
}
78+
}
79+
}
80+
81+
// JUnit 4.10 and above
82+
public static void muzzleCheck(final RuleChain ruleChain) {
83+
ruleChain.apply(null, null);
84+
}
85+
}
86+
}

dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Utils.java

Lines changed: 30 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,27 @@
11
package datadog.trace.instrumentation.junit4;
22

33
import datadog.trace.api.civisibility.config.SkippableTest;
4+
import datadog.trace.util.MethodHandles;
45
import datadog.trace.util.Strings;
56
import java.lang.annotation.Annotation;
67
import java.lang.invoke.MethodHandle;
7-
import java.lang.invoke.MethodHandles;
8-
import java.lang.reflect.Field;
98
import java.lang.reflect.Method;
109
import java.util.ArrayList;
1110
import java.util.Collection;
1211
import java.util.List;
1312
import java.util.regex.Matcher;
1413
import java.util.regex.Pattern;
1514
import javax.annotation.Nullable;
15+
import junit.framework.TestCase;
1616
import org.junit.Test;
1717
import org.junit.experimental.categories.Category;
1818
import org.junit.runner.Description;
1919
import org.junit.runner.notification.RunListener;
2020
import org.junit.runner.notification.RunNotifier;
2121
import org.junit.runners.ParentRunner;
22-
import org.slf4j.Logger;
23-
import org.slf4j.LoggerFactory;
2422

2523
public abstract class JUnit4Utils {
2624

27-
private static final Logger log = LoggerFactory.getLogger(JUnit4Utils.class);
2825
private static final String SYNCHRONIZED_LISTENER =
2926
"org.junit.runner.notification.SynchronizedRunListener";
3027

@@ -34,91 +31,37 @@ public abstract class JUnit4Utils {
3431
private static final Pattern METHOD_AND_CLASS_NAME_PATTERN =
3532
Pattern.compile("([\\s\\S]*)\\((.*)\\)");
3633

37-
private static final MethodHandle PARENT_RUNNER_DESCRIBE_CHILD;
38-
private static final MethodHandle RUN_NOTIFIER_LISTENERS;
39-
private static final MethodHandle INNER_SYNCHRONIZED_LISTENER;
40-
private static final MethodHandle DESCRIPTION_UNIQUE_ID;
41-
42-
static {
43-
MethodHandles.Lookup lookup = MethodHandles.lookup();
44-
PARENT_RUNNER_DESCRIBE_CHILD = accessDescribeChildMethodInParentRunner(lookup);
45-
RUN_NOTIFIER_LISTENERS = accessListenersFieldInRunNotifier(lookup);
46-
INNER_SYNCHRONIZED_LISTENER = accessListenerFieldInSynchronizedListener(lookup);
47-
DESCRIPTION_UNIQUE_ID = accessUniqueIdInDescription(lookup);
48-
}
49-
50-
private static MethodHandle accessDescribeChildMethodInParentRunner(MethodHandles.Lookup lookup) {
51-
try {
52-
Method describeChild = ParentRunner.class.getDeclaredMethod("describeChild", Object.class);
53-
describeChild.setAccessible(true);
54-
return lookup.unreflect(describeChild);
55-
} catch (Exception e) {
56-
return null;
57-
}
58-
}
59-
60-
private static MethodHandle accessListenersFieldInRunNotifier(MethodHandles.Lookup lookup) {
61-
try {
62-
Field listeners;
63-
try {
64-
// Since JUnit 4.12, the field is called "listeners"
65-
listeners = RunNotifier.class.getDeclaredField("listeners");
66-
} catch (final NoSuchFieldException e) {
67-
// Before JUnit 4.12, the field is called "fListeners"
68-
listeners = RunNotifier.class.getDeclaredField("fListeners");
69-
}
70-
71-
listeners.setAccessible(true);
72-
return lookup.unreflectGetter(listeners);
73-
} catch (final Throwable e) {
74-
log.debug("Could not get runListeners for JUnit4Advice", e);
75-
return null;
34+
private static final MethodHandles METHOD_HANDLES =
35+
new MethodHandles(ParentRunner.class.getClassLoader());
36+
private static final MethodHandle PARENT_RUNNER_DESCRIBE_CHILD =
37+
METHOD_HANDLES.method(ParentRunner.class, "describeChild", Object.class);
38+
private static final MethodHandle RUN_NOTIFIER_LISTENERS = accessListenersFieldInRunNotifier();
39+
private static final MethodHandle INNER_SYNCHRONIZED_LISTENER =
40+
accessListenerFieldInSynchronizedListener();
41+
private static final MethodHandle DESCRIPTION_UNIQUE_ID =
42+
METHOD_HANDLES.privateFieldGetter(Description.class, "fUniqueId");
43+
44+
private static MethodHandle accessListenersFieldInRunNotifier() {
45+
MethodHandle listeners = METHOD_HANDLES.privateFieldGetter(RunNotifier.class, "listeners");
46+
if (listeners != null) {
47+
return listeners;
7648
}
49+
// Before JUnit 4.12, the field is called "fListeners"
50+
return METHOD_HANDLES.privateFieldGetter(RunNotifier.class, "fListeners");
7751
}
7852

79-
private static MethodHandle accessListenerFieldInSynchronizedListener(
80-
MethodHandles.Lookup lookup) {
81-
ClassLoader classLoader = RunListener.class.getClassLoader();
82-
MethodHandle handle = accessListenerFieldInSynchronizedListener(lookup, classLoader);
53+
private static MethodHandle accessListenerFieldInSynchronizedListener() {
54+
MethodHandle handle = METHOD_HANDLES.privateFieldGetter(SYNCHRONIZED_LISTENER, "listener");
8355
if (handle != null) {
8456
return handle;
85-
} else {
86-
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
87-
return accessListenerFieldInSynchronizedListener(lookup, contextClassLoader);
88-
}
89-
}
90-
91-
private static MethodHandle accessListenerFieldInSynchronizedListener(
92-
MethodHandles.Lookup lookup, ClassLoader classLoader) {
93-
try {
94-
Class<?> synchronizedListenerClass = classLoader.loadClass(SYNCHRONIZED_LISTENER);
95-
final Field innerListenerField = synchronizedListenerClass.getDeclaredField("listener");
96-
innerListenerField.setAccessible(true);
97-
return lookup.unreflectGetter(innerListenerField);
98-
} catch (Exception e) {
99-
return null;
100-
}
101-
}
102-
103-
private static MethodHandle accessUniqueIdInDescription(MethodHandles.Lookup lookup) {
104-
try {
105-
final Field uniqueIdField = Description.class.getDeclaredField("fUniqueId");
106-
uniqueIdField.setAccessible(true);
107-
return lookup.unreflectGetter(uniqueIdField);
108-
} catch (Throwable throwable) {
109-
return null;
11057
}
58+
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
59+
return new MethodHandles(contextClassLoader)
60+
.privateFieldGetter(SYNCHRONIZED_LISTENER, "listeners");
11161
}
11262

11363
public static List<RunListener> runListenersFromRunNotifier(final RunNotifier runNotifier) {
114-
try {
115-
if (RUN_NOTIFIER_LISTENERS != null) {
116-
return (List<RunListener>) RUN_NOTIFIER_LISTENERS.invoke(runNotifier);
117-
}
118-
} catch (final Throwable e) {
119-
log.debug("Could not get runListeners for JUnit4Advice", e);
120-
}
121-
return null;
64+
return METHOD_HANDLES.invoke(RUN_NOTIFIER_LISTENERS, runNotifier);
12265
}
12366

12467
public static TracingListener toTracingListener(final RunListener listener) {
@@ -128,15 +71,9 @@ public static TracingListener toTracingListener(final RunListener listener) {
12871

12972
// Since JUnit 4.12, the RunListener are wrapped by a SynchronizedRunListener object.
13073
if (SYNCHRONIZED_LISTENER.equals(listener.getClass().getName())) {
131-
try {
132-
if (INNER_SYNCHRONIZED_LISTENER != null) {
133-
Object innerListener = INNER_SYNCHRONIZED_LISTENER.invoke(listener);
134-
if (innerListener instanceof TracingListener) {
135-
return (TracingListener) innerListener;
136-
}
137-
}
138-
} catch (final Throwable e) {
139-
log.debug("Could not get inner listener from SynchronizedRunListener", e);
74+
RunListener innerListener = METHOD_HANDLES.invoke(INNER_SYNCHRONIZED_LISTENER, listener);
75+
if (innerListener instanceof TracingListener) {
76+
return (TracingListener) innerListener;
14077
}
14178
}
14279
return null;
@@ -301,18 +238,11 @@ public static boolean isSuiteContainingChildren(final Description description) {
301238
return true;
302239
}
303240
}
304-
return false;
241+
return TestCase.class.isAssignableFrom(testClass);
305242
}
306243

307244
public static Object getUniqueId(final Description description) {
308-
try {
309-
if (DESCRIPTION_UNIQUE_ID != null) {
310-
return DESCRIPTION_UNIQUE_ID.invoke(description);
311-
}
312-
} catch (Throwable e) {
313-
log.error("Could not get unique ID from descriptions: " + description, e);
314-
}
315-
return null;
245+
return METHOD_HANDLES.invoke(DESCRIPTION_UNIQUE_ID, description);
316246
}
317247

318248
public static String getSuiteName(final Class<?> testClass, final Description description) {
@@ -332,14 +262,7 @@ public static List<Method> getTestMethods(final Class<?> testClass) {
332262
}
333263

334264
public static Description getDescription(ParentRunner<?> runner, Object child) {
335-
try {
336-
if (PARENT_RUNNER_DESCRIBE_CHILD != null) {
337-
return (Description) PARENT_RUNNER_DESCRIBE_CHILD.invokeWithArguments(runner, child);
338-
}
339-
} catch (Throwable e) {
340-
log.error("Could not describe child: " + child, e);
341-
}
342-
return null;
265+
return METHOD_HANDLES.invoke(PARENT_RUNNER_DESCRIBE_CHILD, runner, child);
343266
}
344267

345268
public static Description getSkippedDescription(Description description) {

dd-java-agent/instrumentation/junit-4.10/src/test/groovy/JUnit4Test.groovy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import org.example.TestSkipped
1717
import org.example.TestSkippedClass
1818
import org.example.TestSucceed
1919
import org.example.TestSucceedAndSkipped
20+
import org.example.TestSucceedLegacy
2021
import org.example.TestSucceedSuite
2122
import org.example.TestSucceedUnskippable
2223
import org.example.TestSucceedUnskippableSuite
@@ -66,6 +67,7 @@ class JUnit4Test extends CiVisibilityInstrumentationTest {
6667
"test-itr-unskippable" | [TestSucceedUnskippable] | 2 | [new SkippableTest("org.example.TestSucceedUnskippable", "test_succeed", null, null)]
6768
"test-itr-unskippable-suite" | [TestSucceedUnskippableSuite] | 2 | [new SkippableTest("org.example.TestSucceedUnskippableSuite", "test_succeed", null, null)]
6869
"test-itr-unskippable-not-skipped" | [TestSucceedUnskippable] | 2 | []
70+
"test-legacy" | [TestSucceedLegacy] | 2 | []
6971
}
7072

7173
private void runTests(Collection<Class<?>> tests) {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.example;
2+
3+
import junit.framework.TestCase;
4+
5+
public class TestSucceedLegacy extends TestCase {
6+
7+
public void test_succeed() {
8+
assertTrue(true);
9+
}
10+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[ ]

0 commit comments

Comments
 (0)