Skip to content

Commit 5e64fba

Browse files
fix: proper handling of bazel test runner suite events
1 parent 4ee2247 commit 5e64fba

3 files changed

Lines changed: 96 additions & 78 deletions

File tree

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

Lines changed: 0 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,8 @@
88
import datadog.trace.bootstrap.ContextStore;
99
import java.lang.reflect.Method;
1010
import java.util.List;
11-
import java.util.Set;
12-
import java.util.concurrent.ConcurrentHashMap;
1311
import org.junit.Ignore;
1412
import org.junit.runner.Description;
15-
import org.junit.runner.Result;
1613
import org.junit.runner.notification.Failure;
1714

1815
public class JUnit4TracingListener extends TracingListener {
@@ -22,26 +19,6 @@ public class JUnit4TracingListener extends TracingListener {
2219

2320
private final ContextStore<Description, TestExecutionTracker> executionTrackers;
2421

25-
/**
26-
* Suites for which {@code onTestSuiteStart} has been fired (from either the normal
27-
* ParentRunner-based flow or via lazy-registration in {@link #testStarted}). Used to keep
28-
* lifecycle events idempotent and to know which auto-started suite still needs closing.
29-
*/
30-
private final Set<TestSuiteDescriptor> startedSuites = ConcurrentHashMap.newKeySet();
31-
32-
/**
33-
* Last suite lazy-started from {@link #testStarted} because no {@link #testSuiteStarted} event
34-
* was observed for it first. This has been seen under {@code
35-
* com.google.testing.junit.runner.BazelTestRunner}, where the suite-start advice in {@code
36-
* JUnit4SuiteEventsInstrumentation} does not fire for reasons still to be pinpointed (likely a
37-
* classloader or runner-wrapping quirk specific to the Bazel test launcher). Closed when the next
38-
* test belongs to a different suite, or when the whole test run finishes.
39-
*
40-
* <p>TODO: investigate the exact cause under {@code BazelTestRunner} and add a dedicated
41-
* instrumentation that emits proper suite-lifecycle events instead of relying on this fallback.
42-
*/
43-
private volatile TestSuiteDescriptor autoStartedSuite;
44-
4522
public JUnit4TracingListener(ContextStore<Description, TestExecutionTracker> executionTrackers) {
4623
this.executionTrackers = executionTrackers;
4724
}
@@ -55,9 +32,6 @@ public void testSuiteStarted(final Description description) {
5532
}
5633

5734
TestSuiteDescriptor suiteDescriptor = JUnit4Utils.toSuiteDescriptor(description);
58-
if (!startedSuites.add(suiteDescriptor)) {
59-
return; // already started (idempotent vs. lazy-registration or duplicate events)
60-
}
6135
Class<?> testClass = description.getTestClass();
6236
String testSuiteName = JUnit4Utils.getSuiteName(testClass, description);
6337
List<String> categories = JUnit4Utils.getCategories(testClass, null);
@@ -84,9 +58,6 @@ public void testSuiteFinished(final Description description) {
8458
}
8559

8660
TestSuiteDescriptor suiteDescriptor = JUnit4Utils.toSuiteDescriptor(description);
87-
if (!startedSuites.remove(suiteDescriptor)) {
88-
return; // never started
89-
}
9061
TestEventsHandlerHolder.HANDLERS
9162
.get(TestFrameworkInstrumentation.JUNIT4)
9263
.onTestSuiteFinish(suiteDescriptor, null);
@@ -102,8 +73,6 @@ public void testStarted(final Description description) {
10273
TestDescriptor testDescriptor = JUnit4Utils.toTestDescriptor(description);
10374
TestSourceData testSourceData = JUnit4Utils.toTestSourceData(description);
10475

105-
lazyStartSuiteIfNeeded(suiteDescriptor, description, testSourceData);
106-
10776
String testName = JUnit4Utils.getTestName(description, testSourceData.getTestMethod());
10877
String testParameters = JUnit4Utils.getParameters(description);
10978
List<String> categories =
@@ -124,50 +93,6 @@ public void testStarted(final Description description) {
12493
executionTrackers.get(description));
12594
}
12695

127-
@Override
128-
public void testRunFinished(Result result) {
129-
closeAutoStartedSuite();
130-
}
131-
132-
private void lazyStartSuiteIfNeeded(
133-
TestSuiteDescriptor newSuite, Description description, TestSourceData testSourceData) {
134-
if (startedSuites.contains(newSuite)) {
135-
return;
136-
}
137-
closeAutoStartedSuite();
138-
139-
Class<?> testClass = testSourceData.getTestClass();
140-
String testSuiteName = JUnit4Utils.getSuiteName(testClass, description);
141-
List<String> categories = JUnit4Utils.getCategories(testClass, null);
142-
TestEventsHandlerHolder.HANDLERS
143-
.get(TestFrameworkInstrumentation.JUNIT4)
144-
.onTestSuiteStart(
145-
newSuite,
146-
testSuiteName,
147-
FRAMEWORK_NAME,
148-
FRAMEWORK_VERSION,
149-
testClass,
150-
categories,
151-
false,
152-
TestFrameworkInstrumentation.JUNIT4,
153-
null);
154-
startedSuites.add(newSuite);
155-
autoStartedSuite = newSuite;
156-
}
157-
158-
private void closeAutoStartedSuite() {
159-
TestSuiteDescriptor suite = autoStartedSuite;
160-
if (suite == null) {
161-
return;
162-
}
163-
autoStartedSuite = null;
164-
if (startedSuites.remove(suite)) {
165-
TestEventsHandlerHolder.HANDLERS
166-
.get(TestFrameworkInstrumentation.JUNIT4)
167-
.onTestSuiteFinish(suite, null);
168-
}
169-
}
170-
17196
@Override
17297
public void testFinished(final Description description) {
17398
if (JUnit4Utils.isJUnitPlatformRunnerTest(description)) {

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,11 @@ public static List<RunListener> runListenersFromRunNotifier(final RunNotifier ru
117117
}
118118

119119
/**
120-
* Walks through {@link RunNotifier} wrappers (e.g. Bazel's {@code RunNotifierWrapper}) so the
121-
* effective {@code listeners} field is read, not the wrapper's own (forwarded) one.
120+
* Walks through {@link RunNotifier} wrappers (e.g. Bazel's {@code RunNotifierWrapper}) and
121+
* returns the inner notifier whose {@code listeners} field actually receives {@code addListener}
122+
* calls. Returns the input untouched when it is not a known wrapper.
122123
*/
123-
private static RunNotifier unwrapRunNotifier(RunNotifier notifier) {
124+
public static RunNotifier unwrapRunNotifier(RunNotifier notifier) {
124125
RunNotifier current = notifier;
125126
for (int i = 0; i < 8 && current != null; i++) {
126127
if (!isBazelRunNotifierWrapper(current.getClass())) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 datadog.trace.agent.tooling.InstrumenterModule;
9+
import net.bytebuddy.asm.Advice;
10+
import org.junit.runner.Description;
11+
import org.junit.runner.notification.RunNotifier;
12+
13+
/**
14+
* Restores suite lifecycle events when JUnit 4.13+ tests run under Bazel's {@code
15+
* com.google.testing.junit.runner.BazelTestRunner}.
16+
*
17+
* <p>Bazel's {@code com.google.testing.junit.junit4.runner.RunNotifierWrapper} (last touched in
18+
* 2015) explicitly delegates {@code addListener}, {@code fireTestStarted}, etc. to the inner
19+
* notifier, but does not override {@link RunNotifier#fireTestSuiteStarted(Description)} and {@link
20+
* RunNotifier#fireTestSuiteFinished(Description)} — those were added in JUnit 4.13. {@link
21+
* org.junit.runners.ParentRunner#run} therefore fires the suite-lifecycle events on the wrapper's
22+
* own (always empty) listener list, and our tracing listener — installed on the inner notifier via
23+
* the wrapper's delegating {@code addListener} — never sees them.
24+
*
25+
* <p>This advice intercepts {@link RunNotifier#fireTestSuiteStarted(Description)} and {@link
26+
* RunNotifier#fireTestSuiteFinished(Description)} on the wrapper instance and re-fires the event on
27+
* the inner notifier, where the listener actually lives. Calls on a non-wrapper notifier are a
28+
* no-op.
29+
*/
30+
@AutoService(InstrumenterModule.class)
31+
public class BazelRunNotifierWrapperInstrumentation extends InstrumenterModule.CiVisibility
32+
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
33+
34+
public BazelRunNotifierWrapperInstrumentation() {
35+
super("ci-visibility", "junit-4");
36+
}
37+
38+
@Override
39+
public String instrumentedType() {
40+
return "org.junit.runner.notification.RunNotifier";
41+
}
42+
43+
@Override
44+
public String[] helperClassNames() {
45+
return new String[] {
46+
packageName + ".JUnit4Utils",
47+
packageName + ".TracingListener",
48+
packageName + ".SkippedByDatadog",
49+
};
50+
}
51+
52+
@Override
53+
public void methodAdvice(MethodTransformer transformer) {
54+
transformer.applyAdvice(
55+
named("fireTestSuiteStarted").and(takesArgument(0, named("org.junit.runner.Description"))),
56+
BazelRunNotifierWrapperInstrumentation.class.getName() + "$FireSuiteStartedAdvice");
57+
transformer.applyAdvice(
58+
named("fireTestSuiteFinished").and(takesArgument(0, named("org.junit.runner.Description"))),
59+
BazelRunNotifierWrapperInstrumentation.class.getName() + "$FireSuiteFinishedAdvice");
60+
}
61+
62+
public static class FireSuiteStartedAdvice {
63+
@Advice.OnMethodEnter(suppress = Throwable.class)
64+
public static void fireOnInnerNotifier(
65+
@Advice.This final RunNotifier self, @Advice.Argument(0) final Description description) {
66+
RunNotifier inner = JUnit4Utils.unwrapRunNotifier(self);
67+
if (inner != null && inner != self) {
68+
inner.fireTestSuiteStarted(description);
69+
}
70+
}
71+
72+
// JUnit 4.13 muzzle marker: fireTestSuiteStarted exists from 4.13.
73+
public static void muzzleCheck(final RunNotifier notifier) {
74+
notifier.fireTestSuiteStarted(null);
75+
}
76+
}
77+
78+
public static class FireSuiteFinishedAdvice {
79+
@Advice.OnMethodEnter(suppress = Throwable.class)
80+
public static void fireOnInnerNotifier(
81+
@Advice.This final RunNotifier self, @Advice.Argument(0) final Description description) {
82+
RunNotifier inner = JUnit4Utils.unwrapRunNotifier(self);
83+
if (inner != null && inner != self) {
84+
inner.fireTestSuiteFinished(description);
85+
}
86+
}
87+
88+
public static void muzzleCheck(final RunNotifier notifier) {
89+
notifier.fireTestSuiteFinished(null);
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)