88import datadog .trace .bootstrap .ContextStore ;
99import java .lang .reflect .Method ;
1010import java .util .List ;
11+ import java .util .Set ;
12+ import java .util .concurrent .ConcurrentHashMap ;
1113import org .junit .Ignore ;
1214import org .junit .runner .Description ;
15+ import org .junit .runner .Result ;
1316import org .junit .runner .notification .Failure ;
1417
1518public class JUnit4TracingListener extends TracingListener {
@@ -19,6 +22,26 @@ public class JUnit4TracingListener extends TracingListener {
1922
2023 private final ContextStore <Description , TestExecutionTracker > executionTrackers ;
2124
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+
2245 public JUnit4TracingListener (ContextStore <Description , TestExecutionTracker > executionTrackers ) {
2346 this .executionTrackers = executionTrackers ;
2447 }
@@ -32,6 +55,9 @@ public void testSuiteStarted(final Description description) {
3255 }
3356
3457 TestSuiteDescriptor suiteDescriptor = JUnit4Utils .toSuiteDescriptor (description );
58+ if (!startedSuites .add (suiteDescriptor )) {
59+ return ; // already started (idempotent vs. lazy-registration or duplicate events)
60+ }
3561 Class <?> testClass = description .getTestClass ();
3662 String testSuiteName = JUnit4Utils .getSuiteName (testClass , description );
3763 List <String > categories = JUnit4Utils .getCategories (testClass , null );
@@ -58,6 +84,9 @@ public void testSuiteFinished(final Description description) {
5884 }
5985
6086 TestSuiteDescriptor suiteDescriptor = JUnit4Utils .toSuiteDescriptor (description );
87+ if (!startedSuites .remove (suiteDescriptor )) {
88+ return ; // never started
89+ }
6190 TestEventsHandlerHolder .HANDLERS
6291 .get (TestFrameworkInstrumentation .JUNIT4 )
6392 .onTestSuiteFinish (suiteDescriptor , null );
@@ -73,6 +102,8 @@ public void testStarted(final Description description) {
73102 TestDescriptor testDescriptor = JUnit4Utils .toTestDescriptor (description );
74103 TestSourceData testSourceData = JUnit4Utils .toTestSourceData (description );
75104
105+ lazyStartSuiteIfNeeded (suiteDescriptor , description , testSourceData );
106+
76107 String testName = JUnit4Utils .getTestName (description , testSourceData .getTestMethod ());
77108 String testParameters = JUnit4Utils .getParameters (description );
78109 List <String > categories =
@@ -93,6 +124,51 @@ public void testStarted(final Description description) {
93124 executionTrackers .get (description ));
94125 }
95126
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 ; // suite already started normally or by a prior lazy-registration for this same suite
136+ }
137+ // Close any previous auto-started suite (typical when Bazel runs multiple classes in one JVM).
138+ closeAutoStartedSuite ();
139+
140+ Class <?> testClass = testSourceData .getTestClass ();
141+ String testSuiteName = JUnit4Utils .getSuiteName (testClass , description );
142+ List <String > categories = JUnit4Utils .getCategories (testClass , null );
143+ TestEventsHandlerHolder .HANDLERS
144+ .get (TestFrameworkInstrumentation .JUNIT4 )
145+ .onTestSuiteStart (
146+ newSuite ,
147+ testSuiteName ,
148+ FRAMEWORK_NAME ,
149+ FRAMEWORK_VERSION ,
150+ testClass ,
151+ categories ,
152+ false ,
153+ TestFrameworkInstrumentation .JUNIT4 ,
154+ null );
155+ startedSuites .add (newSuite );
156+ autoStartedSuite = newSuite ;
157+ }
158+
159+ private void closeAutoStartedSuite () {
160+ TestSuiteDescriptor suite = autoStartedSuite ;
161+ if (suite == null ) {
162+ return ;
163+ }
164+ autoStartedSuite = null ;
165+ if (startedSuites .remove (suite )) {
166+ TestEventsHandlerHolder .HANDLERS
167+ .get (TestFrameworkInstrumentation .JUNIT4 )
168+ .onTestSuiteFinish (suite , null );
169+ }
170+ }
171+
96172 @ Override
97173 public void testFinished (final Description description ) {
98174 if (JUnit4Utils .isJUnitPlatformRunnerTest (description )) {
0 commit comments