55import java .io .BufferedWriter ;
66import java .io .Closeable ;
77import java .io .IOException ;
8+ import java .io .Writer ;
89import java .nio .file .Files ;
910import java .nio .file .Path ;
1011import java .util .ArrayList ;
@@ -50,8 +51,12 @@ public class BazelJUnitOutputListener implements TestExecutionListener, Closeabl
5051 public BazelJUnitOutputListener (Path xmlOut ) {
5152 try {
5253 Files .createDirectories (xmlOut .getParent ());
53- BufferedWriter writer = Files .newBufferedWriter (xmlOut , UTF_8 );
54- xml = XMLOutputFactory .newFactory ().createXMLStreamWriter (writer );
54+ // Use LazyFileWriter to defer file creation until the first write operation.
55+ // This prevents premature file creation in cases where the JVM terminates abruptly
56+ // before any content is written. If no output is generated, Bazel has custom logic
57+ // to create the XML file from test.log, but this logic only activates if the
58+ // output file does not already exist.
59+ xml = XMLOutputFactory .newFactory ().createXMLStreamWriter (new LazyFileWriter (xmlOut ));
5560 xml .writeStartDocument ("UTF-8" , "1.0" );
5661 } catch (IOException | XMLStreamException e ) {
5762 throw new IllegalStateException ("Unable to create output file" , e );
@@ -140,8 +145,9 @@ private Map<TestData, List<TestData>> matchTestCasesToSuites_locked(
140145 throw new IllegalStateException (
141146 "Unexpected test organization for test Case: " + testCase .getId ());
142147 }
143- if (includeIncompleteTests || testCase .getDuration () != null ) {
144- knownSuites .computeIfAbsent (parent , id -> new ArrayList <>()).add (testCase );
148+ knownSuites .computeIfAbsent (parent , id -> new ArrayList <>());
149+ if (testCase .getId ().isTest () && (includeIncompleteTests || testCase .getDuration () != null )) {
150+ knownSuites .get (parent ).add (testCase );
145151 }
146152 }
147153
@@ -153,18 +159,19 @@ private Map<TestData, List<TestData>> matchTestCasesToSuites_locked(
153159 // This is really just documentation until someone actually turns on a static analyser.
154160 // If they do, we can decide whether we want to pick up the dependency.
155161 // @GuardedBy("resultsLock")
156- private List <TestData > findTestCases_locked () {
162+ private List <TestData > findTestCases_locked (String engineId ) {
157163 return results .values ().stream ()
158164 // Ignore test plan roots. These are always the engine being used.
159165 .filter (result -> !testPlan .getRoots ().contains (result .getId ()))
160166 .filter (
161- result -> {
162- // Find the test results we will convert to `testcase` entries. These
163- // are identified by the fact that they have no child test cases in the
164- // test plan, or they are marked as tests.
165- TestIdentifier id = result .getId ();
166- return id .getSource () != null || id .isTest () || testPlan .getChildren (id ).isEmpty ();
167- })
167+ result ->
168+ engineId == null
169+ || result
170+ .getId ()
171+ .getUniqueIdObject ()
172+ .getEngineId ()
173+ .map (engineId ::equals )
174+ .orElse (false ))
168175 .collect (Collectors .toList ());
169176 }
170177
@@ -196,20 +203,38 @@ private void outputIfTestRootIsComplete(TestIdentifier testIdentifier) {
196203 return ;
197204 }
198205
199- output (false );
206+ output (false , testIdentifier . getUniqueIdObject (). getEngineId (). orElse ( null ) );
200207 }
201208
202- private void output (boolean includeIncompleteTests ) {
209+ private void output (boolean includeIncompleteTests , /*@Nullable*/ String engineId ) {
203210 synchronized (this .resultsLock ) {
204- List <TestData > testCases = findTestCases_locked ();
211+ List <TestData > testCases = findTestCases_locked (engineId );
205212 Map <TestData , List <TestData >> testSuites =
206213 matchTestCasesToSuites_locked (testCases , includeIncompleteTests );
207214
208215 // Write the results
209216 try {
210217 for (Map .Entry <TestData , List <TestData >> suiteAndTests : testSuites .entrySet ()) {
211- new TestSuiteXmlRenderer (testPlan )
212- .toXml (xml , suiteAndTests .getKey (), suiteAndTests .getValue ());
218+ TestData suite = suiteAndTests .getKey ();
219+ List <TestData > tests = suiteAndTests .getValue ();
220+ if (suite .getResult () != null
221+ && suite .getResult ().getStatus () != TestExecutionResult .Status .SUCCESSFUL ) {
222+ // If a test suite fails or is skipped, all its tests must be included in the XML output
223+ // with the same result as the suite, since the XML format does not support marking a
224+ // suite as failed or skipped. This aligns with Bazel's XmlWriter for JUnitRunner.
225+ getTestsFromSuite (suite .getId ())
226+ .forEach (
227+ testIdentifier -> {
228+ TestData test = results .get (testIdentifier .getUniqueIdObject ());
229+ if (test == null ) {
230+ // add test to results.
231+ test = getResult (testIdentifier );
232+ tests .add (test );
233+ }
234+ test .mark (suite .getResult ()).skipReason (suite .getSkipReason ());
235+ });
236+ }
237+ new TestSuiteXmlRenderer (testPlan ).toXml (xml , suite , tests );
213238 }
214239 } catch (XMLStreamException e ) {
215240 throw new RuntimeException (e );
@@ -227,6 +252,18 @@ private void output(boolean includeIncompleteTests) {
227252 }
228253 }
229254
255+ private List <TestIdentifier > getTestsFromSuite (TestIdentifier suiteIdentifier ) {
256+ return testPlan .getChildren (suiteIdentifier ).stream ()
257+ .flatMap (
258+ testIdentifier -> {
259+ if (testIdentifier .isContainer ()) {
260+ return getTestsFromSuite (testIdentifier ).stream ();
261+ }
262+ return Stream .of (testIdentifier );
263+ })
264+ .collect (Collectors .toList ());
265+ }
266+
230267 @ Override
231268 public void reportingEntryPublished (TestIdentifier testIdentifier , ReportEntry entry ) {
232269 getResult (testIdentifier ).addReportEntry (entry );
@@ -248,7 +285,7 @@ public void close() {
248285 return ;
249286 }
250287 if (wasInterrupted .get ()) {
251- output (true );
288+ output (true , null );
252289 }
253290 try {
254291 xml .writeEndDocument ();
@@ -257,4 +294,41 @@ public void close() {
257294 LOG .log (Level .SEVERE , "Unable to close xml output" , e );
258295 }
259296 }
297+
298+ static class LazyFileWriter extends Writer {
299+ private final Path path ;
300+ private BufferedWriter delegate ;
301+ private boolean isCreated = false ;
302+
303+ public LazyFileWriter (Path path ) {
304+ this .path = path ;
305+ }
306+
307+ private void createWriterIfNeeded () throws IOException {
308+ if (!isCreated ) {
309+ delegate = Files .newBufferedWriter (path , UTF_8 );
310+ isCreated = true ;
311+ }
312+ }
313+
314+ @ Override
315+ public void write (char [] cbuf , int off , int len ) throws IOException {
316+ createWriterIfNeeded ();
317+ delegate .write (cbuf , off , len );
318+ }
319+
320+ @ Override
321+ public void flush () throws IOException {
322+ if (isCreated ) {
323+ delegate .flush ();
324+ }
325+ }
326+
327+ @ Override
328+ public void close () throws IOException {
329+ if (isCreated ) {
330+ delegate .close ();
331+ }
332+ }
333+ }
260334}
0 commit comments