13
13
import java .util .Map ;
14
14
import java .util .NoSuchElementException ;
15
15
import java .util .concurrent .ConcurrentHashMap ;
16
+ import java .util .concurrent .atomic .AtomicBoolean ;
16
17
import java .util .logging .Level ;
17
18
import java .util .logging .Logger ;
18
19
import java .util .stream .Collectors ;
@@ -31,9 +32,21 @@ public class BazelJUnitOutputListener implements TestExecutionListener, Closeabl
31
32
public static final Logger LOG = Logger .getLogger (BazelJUnitOutputListener .class .getName ());
32
33
private final XMLStreamWriter xml ;
33
34
35
+ private final Object resultsLock = new Object ();
36
+ // Commented out to avoid adding a dependency to building the test runner.
37
+ // This is really just documentation until someone actually turns on a static analyser.
38
+ // If they do, we can decide whether we want to pick up the dependency.
39
+ // @GuardedBy("resultsLock")
34
40
private final Map <UniqueId , TestData > results = new ConcurrentHashMap <>();
35
41
private TestPlan testPlan ;
36
42
43
+ // If we have already closed this listener, we shouldn't write any more XML.
44
+ private final AtomicBoolean hasClosed = new AtomicBoolean ();
45
+ // Whether test-running was interrupted (e.g. because our tests timed out and we got SIGTERM'd)
46
+ // and when writing results we want to flush any pending tests as interrupted,
47
+ // rather than ignoring them because they're incomplete.
48
+ private final AtomicBoolean wasInterrupted = new AtomicBoolean ();
49
+
37
50
public BazelJUnitOutputListener (Path xmlOut ) {
38
51
try {
39
52
Files .createDirectories (xmlOut .getParent ());
@@ -73,12 +86,17 @@ public void testPlanExecutionFinished(TestPlan testPlan) {
73
86
this .testPlan = null ;
74
87
}
75
88
76
- private Map <TestData , List <TestData >> matchTestCasesToSuites (List <TestData > testCases ) {
89
+ // Requires the caller to have acquired resultsLock.
90
+ // Commented out to avoid adding a dependency to building the test runner.
91
+ // This is really just documentation until someone actually turns on a static analyser.
92
+ // If they do, we can decide whether we want to pick up the dependency.
93
+ // @GuardedBy("resultsLock")
94
+ private Map <TestData , List <TestData >> matchTestCasesToSuites_locked (
95
+ List <TestData > testCases , boolean includeIncompleteTests ) {
77
96
Map <TestData , List <TestData >> knownSuites = new HashMap <>();
78
97
79
98
// Find the containing test suites for the test cases.
80
99
for (TestData testCase : testCases ) {
81
-
82
100
TestData parent ;
83
101
84
102
// The number of segments in the test case Unique ID depends upon the nature of the test:
@@ -120,13 +138,20 @@ private Map<TestData, List<TestData>> matchTestCasesToSuites(List<TestData> test
120
138
throw new IllegalStateException (
121
139
"Unexpected test organization for test Case: " + testCase .getId ());
122
140
}
123
- knownSuites .computeIfAbsent (parent , id -> new ArrayList <>()).add (testCase );
141
+ if (includeIncompleteTests || testCase .getDuration () != null ) {
142
+ knownSuites .computeIfAbsent (parent , id -> new ArrayList <>()).add (testCase );
143
+ }
124
144
}
125
145
126
146
return knownSuites ;
127
147
}
128
148
129
- private List <TestData > findTestCases () {
149
+ // Requires the caller to have acquired resultsLock.
150
+ // Commented out to avoid adding a dependency to building the test runner.
151
+ // This is really just documentation until someone actually turns on a static analyser.
152
+ // If they do, we can decide whether we want to pick up the dependency.
153
+ // @GuardedBy("resultsLock")
154
+ private List <TestData > findTestCases_locked () {
130
155
return results .values ().stream ()
131
156
// Ignore test plan roots. These are always the engine being used.
132
157
.filter (result -> !testPlan .getRoots ().contains (result .getId ()))
@@ -169,28 +194,35 @@ private void outputIfTestRootIsComplete(TestIdentifier testIdentifier) {
169
194
return ;
170
195
}
171
196
172
- List <TestData > testCases = findTestCases ();
173
- Map <TestData , List <TestData >> testSuites = matchTestCasesToSuites (testCases );
197
+ output (false );
198
+ }
199
+
200
+ private void output (boolean includeIncompleteTests ) {
201
+ synchronized (this .resultsLock ) {
202
+ List <TestData > testCases = findTestCases_locked ();
203
+ Map <TestData , List <TestData >> testSuites =
204
+ matchTestCasesToSuites_locked (testCases , includeIncompleteTests );
174
205
175
- // Write the results
176
- try {
177
- for (Map .Entry <TestData , List <TestData >> suiteAndTests : testSuites .entrySet ()) {
178
- new TestSuiteXmlRenderer (testPlan )
179
- .toXml (xml , suiteAndTests .getKey (), suiteAndTests .getValue ());
206
+ // Write the results
207
+ try {
208
+ for (Map .Entry <TestData , List <TestData >> suiteAndTests : testSuites .entrySet ()) {
209
+ new TestSuiteXmlRenderer (testPlan )
210
+ .toXml (xml , suiteAndTests .getKey (), suiteAndTests .getValue ());
211
+ }
212
+ } catch (XMLStreamException e ) {
213
+ throw new RuntimeException (e );
180
214
}
181
- } catch (XMLStreamException e ) {
182
- throw new RuntimeException (e );
183
- }
184
215
185
- // Delete the results we've used to conserve memory. This is safe to do
186
- // since we only do this when the test root is complete, so we know that
187
- // we won't be adding to the list of suites and test cases for that root
188
- // (because tests and containers are arranged in a hierarchy --- the
189
- // containers only complete when all the things they contain are
190
- // finished. We are leaving all the test data that we have _not_ written
191
- // to the XML file.
192
- Stream .concat (testCases .stream (), testSuites .keySet ().stream ())
193
- .forEach (data -> results .remove (data .getId ().getUniqueIdObject ()));
216
+ // Delete the results we've used to conserve memory. This is safe to do
217
+ // since we only do this when the test root is complete, so we know that
218
+ // we won't be adding to the list of suites and test cases for that root
219
+ // (because tests and containers are arranged in a hierarchy --- the
220
+ // containers only complete when all the things they contain are
221
+ // finished. We are leaving all the test data that we have _not_ written
222
+ // to the XML file.
223
+ Stream .concat (testCases .stream (), testSuites .keySet ().stream ())
224
+ .forEach (data -> results .remove (data .getId ().getUniqueIdObject ()));
225
+ }
194
226
}
195
227
196
228
@ Override
@@ -199,10 +231,23 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e
199
231
}
200
232
201
233
private TestData getResult (TestIdentifier id ) {
202
- return results .computeIfAbsent (id .getUniqueIdObject (), ignored -> new TestData (id ));
234
+ synchronized (resultsLock ) {
235
+ return results .computeIfAbsent (id .getUniqueIdObject (), ignored -> new TestData (id ));
236
+ }
237
+ }
238
+
239
+ public void closeForInterrupt () {
240
+ wasInterrupted .set (true );
241
+ close ();
203
242
}
204
243
205
244
public void close () {
245
+ if (hasClosed .getAndSet (true )) {
246
+ return ;
247
+ }
248
+ if (wasInterrupted .get ()) {
249
+ output (true );
250
+ }
206
251
try {
207
252
xml .writeEndDocument ();
208
253
xml .close ();
0 commit comments