1 package org.apache.maven.plugin.surefire.report;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
23 import org.apache.maven.surefire.shared.utils.xml.PrettyPrintXMLWriter;
24 import org.apache.maven.surefire.shared.utils.xml.XMLWriter;
25 import org.apache.maven.surefire.extensions.StatelessReportEventListener;
26 import org.apache.maven.surefire.api.report.SafeThrowable;
27
28 import java.io.BufferedOutputStream;
29 import java.io.File;
30 import java.io.FileOutputStream;
31 import java.io.FilterOutputStream;
32 import java.io.IOException;
33 import java.io.OutputStream;
34 import java.io.OutputStreamWriter;
35 import java.io.PrintWriter;
36 import java.util.ArrayList;
37 import java.util.Deque;
38 import java.util.LinkedHashMap;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Map.Entry;
42 import java.util.StringTokenizer;
43 import java.util.concurrent.ConcurrentLinkedDeque;
44
45 import static java.nio.charset.StandardCharsets.UTF_8;
46 import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType;
47 import static org.apache.maven.plugin.surefire.report.FileReporterUtils.stripIllegalFilenameChars;
48 import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED;
49 import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS;
50 import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank;
51
52 @SuppressWarnings( { "javadoc", "checkstyle:javadoctype" } )
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87 public class StatelessXmlReporter
88 implements StatelessReportEventListener<WrappedReportEntry, TestSetStats>
89 {
90 private static final String XML_INDENT = " ";
91
92 private static final String XML_NL = "\n";
93
94 private final File reportsDirectory;
95
96 private final String reportNameSuffix;
97
98 private final boolean trimStackTrace;
99
100 private final int rerunFailingTestsCount;
101
102 private final String xsdSchemaLocation;
103
104 private final String xsdVersion;
105
106
107
108 private final Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap;
109
110 private final boolean phrasedFileName;
111
112 private final boolean phrasedSuiteName;
113
114 private final boolean phrasedClassName;
115
116 private final boolean phrasedMethodName;
117
118 public StatelessXmlReporter( File reportsDirectory, String reportNameSuffix, boolean trimStackTrace,
119 int rerunFailingTestsCount,
120 Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap,
121 String xsdSchemaLocation, String xsdVersion, boolean phrasedFileName,
122 boolean phrasedSuiteName, boolean phrasedClassName, boolean phrasedMethodName )
123 {
124 this.reportsDirectory = reportsDirectory;
125 this.reportNameSuffix = reportNameSuffix;
126 this.trimStackTrace = trimStackTrace;
127 this.rerunFailingTestsCount = rerunFailingTestsCount;
128 this.testClassMethodRunHistoryMap = testClassMethodRunHistoryMap;
129 this.xsdSchemaLocation = xsdSchemaLocation;
130 this.xsdVersion = xsdVersion;
131 this.phrasedFileName = phrasedFileName;
132 this.phrasedSuiteName = phrasedSuiteName;
133 this.phrasedClassName = phrasedClassName;
134 this.phrasedMethodName = phrasedMethodName;
135 }
136
137 @Override
138 public void testSetCompleted( WrappedReportEntry testSetReportEntry, TestSetStats testSetStats )
139 {
140 Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics =
141 arrangeMethodStatistics( testSetReportEntry, testSetStats );
142
143
144
145 try ( OutputStream outputStream = getOutputStream( testSetReportEntry );
146 OutputStreamWriter fw = getWriter( outputStream ) )
147 {
148 XMLWriter ppw = new PrettyPrintXMLWriter( new PrintWriter( fw ), XML_INDENT, XML_NL, UTF_8.name(), null );
149
150 createTestSuiteElement( ppw, testSetReportEntry, testSetStats );
151
152 showProperties( ppw, testSetReportEntry.getSystemProperties() );
153
154 for ( Entry<String, Map<String, List<WrappedReportEntry>>> statistics : classMethodStatistics.entrySet() )
155 {
156 for ( Entry<String, List<WrappedReportEntry>> thisMethodRuns : statistics.getValue().entrySet() )
157 {
158 serializeTestClass( outputStream, fw, ppw, thisMethodRuns.getValue() );
159 }
160 }
161
162 ppw.endElement();
163 }
164 catch ( IOException e )
165 {
166
167
168
169 InPluginProcessDumpSingleton.getSingleton()
170 .dumpException( e, e.getLocalizedMessage(), reportsDirectory );
171 }
172 }
173
174 private Map<String, Map<String, List<WrappedReportEntry>>> arrangeMethodStatistics(
175 WrappedReportEntry testSetReportEntry, TestSetStats testSetStats )
176 {
177 Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics = new LinkedHashMap<>();
178 for ( WrappedReportEntry methodEntry : aggregateCacheFromMultipleReruns( testSetReportEntry, testSetStats ) )
179 {
180 String testClassName = methodEntry.getSourceName();
181 Map<String, List<WrappedReportEntry>> stats = classMethodStatistics.get( testClassName );
182 if ( stats == null )
183 {
184 stats = new LinkedHashMap<>();
185 classMethodStatistics.put( testClassName, stats );
186 }
187 String methodName = methodEntry.getName();
188 List<WrappedReportEntry> methodRuns = stats.get( methodName );
189 if ( methodRuns == null )
190 {
191 methodRuns = new ArrayList<>();
192 stats.put( methodName, methodRuns );
193 }
194 methodRuns.add( methodEntry );
195 }
196 return classMethodStatistics;
197 }
198
199 private Deque<WrappedReportEntry> aggregateCacheFromMultipleReruns( WrappedReportEntry testSetReportEntry,
200 TestSetStats testSetStats )
201 {
202 String suiteClassName = testSetReportEntry.getSourceName();
203 Deque<WrappedReportEntry> methodRunHistory = getAddMethodRunHistoryMap( suiteClassName );
204 methodRunHistory.addAll( testSetStats.getReportEntries() );
205 return methodRunHistory;
206 }
207
208 private void serializeTestClass( OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw,
209 List<WrappedReportEntry> methodEntries )
210 throws IOException
211 {
212 if ( rerunFailingTestsCount > 0 )
213 {
214 serializeTestClassWithRerun( outputStream, fw, ppw, methodEntries );
215 }
216 else
217 {
218
219
220 serializeTestClassWithoutRerun( outputStream, fw, ppw, methodEntries );
221 }
222 }
223
224 private void serializeTestClassWithoutRerun( OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw,
225 List<WrappedReportEntry> methodEntries )
226 throws IOException
227 {
228 for ( WrappedReportEntry methodEntry : methodEntries )
229 {
230 startTestElement( ppw, methodEntry );
231 if ( methodEntry.getReportEntryType() != SUCCESS )
232 {
233 getTestProblems( fw, ppw, methodEntry, trimStackTrace, outputStream,
234 methodEntry.getReportEntryType().getXmlTag(), false );
235 }
236 createOutErrElements( fw, ppw, methodEntry, outputStream );
237 ppw.endElement();
238 }
239 }
240
241 private void serializeTestClassWithRerun( OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw,
242 List<WrappedReportEntry> methodEntries )
243 throws IOException
244 {
245 WrappedReportEntry firstMethodEntry = methodEntries.get( 0 );
246 switch ( getTestResultType( methodEntries ) )
247 {
248 case success:
249 for ( WrappedReportEntry methodEntry : methodEntries )
250 {
251 if ( methodEntry.getReportEntryType() == SUCCESS )
252 {
253 startTestElement( ppw, methodEntry );
254 ppw.endElement();
255 }
256 }
257 break;
258 case error:
259 case failure:
260
261 startTestElement( ppw, firstMethodEntry );
262 boolean firstRun = true;
263 for ( WrappedReportEntry singleRunEntry : methodEntries )
264 {
265 if ( firstRun )
266 {
267 firstRun = false;
268 getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream,
269 singleRunEntry.getReportEntryType().getXmlTag(), false );
270 createOutErrElements( fw, ppw, singleRunEntry, outputStream );
271 }
272 else if ( singleRunEntry.getReportEntryType() == SKIPPED )
273 {
274
275
276
277
278 addCommentElementTestCase( "a skipped test execution in re-run phase", fw, ppw, outputStream );
279 }
280 else
281 {
282 getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream,
283 singleRunEntry.getReportEntryType().getRerunXmlTag(), true );
284 }
285 }
286 ppw.endElement();
287 break;
288 case flake:
289 WrappedReportEntry successful = null;
290
291 for ( WrappedReportEntry singleRunEntry : methodEntries )
292 {
293 if ( singleRunEntry.getReportEntryType() == SUCCESS )
294 {
295 successful = singleRunEntry;
296 break;
297 }
298 }
299 WrappedReportEntry firstOrSuccessful = successful == null ? methodEntries.get( 0 ) : successful;
300 startTestElement( ppw, firstOrSuccessful );
301 for ( WrappedReportEntry singleRunEntry : methodEntries )
302 {
303 if ( singleRunEntry.getReportEntryType() != SUCCESS )
304 {
305 getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream,
306 singleRunEntry.getReportEntryType().getFlakyXmlTag(), true );
307 }
308 }
309 ppw.endElement();
310 break;
311 case skipped:
312 startTestElement( ppw, firstMethodEntry );
313 getTestProblems( fw, ppw, firstMethodEntry, trimStackTrace, outputStream,
314 firstMethodEntry.getReportEntryType().getXmlTag(), false );
315 ppw.endElement();
316 break;
317 default:
318 throw new IllegalStateException( "Get unknown test result type" );
319 }
320 }
321
322
323
324
325 public void cleanTestHistoryMap()
326 {
327 testClassMethodRunHistoryMap.clear();
328 }
329
330
331
332
333
334
335
336 private TestResultType getTestResultType( List<WrappedReportEntry> methodEntryList )
337 {
338 List<ReportEntryType> testResultTypeList = new ArrayList<>();
339 for ( WrappedReportEntry singleRunEntry : methodEntryList )
340 {
341 testResultTypeList.add( singleRunEntry.getReportEntryType() );
342 }
343
344 return DefaultReporterFactory.getTestResultType( testResultTypeList, rerunFailingTestsCount );
345 }
346
347 private Deque<WrappedReportEntry> getAddMethodRunHistoryMap( String testClassName )
348 {
349 Deque<WrappedReportEntry> methodRunHistory = testClassMethodRunHistoryMap.get( testClassName );
350 if ( methodRunHistory == null )
351 {
352 methodRunHistory = new ConcurrentLinkedDeque<>();
353 testClassMethodRunHistoryMap.put( testClassName == null ? "null" : testClassName, methodRunHistory );
354 }
355 return methodRunHistory;
356 }
357
358 private OutputStream getOutputStream( WrappedReportEntry testSetReportEntry )
359 throws IOException
360 {
361 File reportFile = getReportFile( testSetReportEntry );
362 File reportDir = reportFile.getParentFile();
363
364 reportFile.delete();
365
366 reportDir.mkdirs();
367 return new BufferedOutputStream( new FileOutputStream( reportFile ), 64 * 1024 );
368 }
369
370 private static OutputStreamWriter getWriter( OutputStream fos )
371 {
372 return new OutputStreamWriter( fos, UTF_8 );
373 }
374
375 private File getReportFile( WrappedReportEntry report )
376 {
377 String reportName = "TEST-" + ( phrasedFileName ? report.getReportSourceName() : report.getSourceName() );
378 String customizedReportName = isBlank( reportNameSuffix ) ? reportName : reportName + "-" + reportNameSuffix;
379 return new File( reportsDirectory, stripIllegalFilenameChars( customizedReportName + ".xml" ) );
380 }
381
382 private void startTestElement( XMLWriter ppw, WrappedReportEntry report )
383 throws IOException
384 {
385 ppw.startElement( "testcase" );
386 String name = phrasedMethodName ? report.getReportName() : report.getName();
387 ppw.addAttribute( "name", name == null ? "" : extraEscapeAttribute( name ) );
388
389 if ( report.getGroup() != null )
390 {
391 ppw.addAttribute( "group", report.getGroup() );
392 }
393
394 String className = phrasedClassName ? report.getReportSourceName( reportNameSuffix )
395 : report.getSourceName( reportNameSuffix );
396 if ( className != null )
397 {
398 ppw.addAttribute( "classname", extraEscapeAttribute( className ) );
399 }
400
401 ppw.addAttribute( "time", report.elapsedTimeAsString() );
402 }
403
404 private void createTestSuiteElement( XMLWriter ppw, WrappedReportEntry report, TestSetStats testSetStats )
405 throws IOException
406 {
407 ppw.startElement( "testsuite" );
408
409 ppw.addAttribute( "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance" );
410 ppw.addAttribute( "xsi:noNamespaceSchemaLocation", xsdSchemaLocation );
411 ppw.addAttribute( "version", xsdVersion );
412
413 String reportName = phrasedSuiteName ? report.getReportSourceName( reportNameSuffix )
414 : report.getSourceName( reportNameSuffix );
415 ppw.addAttribute( "name", reportName == null ? "" : extraEscapeAttribute( reportName ) );
416
417 if ( report.getGroup() != null )
418 {
419 ppw.addAttribute( "group", report.getGroup() );
420 }
421
422 ppw.addAttribute( "time", report.elapsedTimeAsString() );
423
424 ppw.addAttribute( "tests", String.valueOf( testSetStats.getCompletedCount() ) );
425
426 ppw.addAttribute( "errors", String.valueOf( testSetStats.getErrors() ) );
427
428 ppw.addAttribute( "skipped", String.valueOf( testSetStats.getSkipped() ) );
429
430 ppw.addAttribute( "failures", String.valueOf( testSetStats.getFailures() ) );
431 }
432
433 private static void getTestProblems( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
434 WrappedReportEntry report, boolean trimStackTrace, OutputStream fw,
435 String testErrorType, boolean createOutErrElementsInside )
436 throws IOException
437 {
438 ppw.startElement( testErrorType );
439
440 String stackTrace = report.getStackTrace( trimStackTrace );
441
442 if ( report.getMessage() != null && !report.getMessage().isEmpty() )
443 {
444 ppw.addAttribute( "message", extraEscapeAttribute( report.getMessage() ) );
445 }
446
447 if ( report.getStackTraceWriter() != null )
448 {
449
450 SafeThrowable t = report.getStackTraceWriter().getThrowable();
451 if ( t != null )
452 {
453 if ( t.getMessage() != null )
454 {
455 int delimiter = stackTrace.indexOf( ":" );
456 String type = delimiter == -1 ? stackTrace : stackTrace.substring( 0, delimiter );
457 ppw.addAttribute( "type", type );
458 }
459 else
460 {
461 ppw.addAttribute( "type", new StringTokenizer( stackTrace ).nextToken() );
462 }
463 }
464 }
465
466 boolean hasNestedElements = createOutErrElementsInside & stackTrace != null;
467
468 if ( stackTrace != null )
469 {
470 if ( hasNestedElements )
471 {
472 ppw.startElement( "stackTrace" );
473 }
474
475 extraEscapeElementValue( stackTrace, outputStreamWriter, ppw, fw );
476
477 if ( hasNestedElements )
478 {
479 ppw.endElement();
480 }
481 }
482
483 if ( createOutErrElementsInside )
484 {
485 createOutErrElements( outputStreamWriter, ppw, report, fw );
486 }
487
488 ppw.endElement();
489 }
490
491
492 private static void createOutErrElements( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
493 WrappedReportEntry report, OutputStream fw )
494 throws IOException
495 {
496 EncodingOutputStream eos = new EncodingOutputStream( fw );
497 addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdout(), "system-out" );
498 addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdErr(), "system-err" );
499 }
500
501 private static void addOutputStreamElement( OutputStreamWriter outputStreamWriter,
502 EncodingOutputStream eos, XMLWriter xmlWriter,
503 Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
504 String name )
505 throws IOException
506 {
507 if ( utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0 )
508 {
509 xmlWriter.startElement( name );
510 xmlWriter.writeText( "" );
511 outputStreamWriter.flush();
512 eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES );
513 utf8RecodingDeferredFileOutputStream.writeTo( eos );
514 utf8RecodingDeferredFileOutputStream.free();
515 eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
516 eos.flush();
517 xmlWriter.endElement();
518 }
519 }
520
521
522
523
524
525
526
527 private static void showProperties( XMLWriter xmlWriter, Map<String, String> systemProperties )
528 throws IOException
529 {
530 xmlWriter.startElement( "properties" );
531 for ( final Entry<String, String> entry : systemProperties.entrySet() )
532 {
533 final String key = entry.getKey();
534 String value = entry.getValue();
535
536 if ( value == null )
537 {
538 value = "null";
539 }
540
541 xmlWriter.startElement( "property" );
542
543 xmlWriter.addAttribute( "name", key );
544
545 xmlWriter.addAttribute( "value", extraEscapeAttribute( value ) );
546
547 xmlWriter.endElement();
548 }
549 xmlWriter.endElement();
550 }
551
552
553
554
555
556
557
558 private static String extraEscapeAttribute( String message )
559 {
560
561 return containsEscapesIllegalXml10( message ) ? escapeXml( message, true ) : message;
562 }
563
564
565
566
567
568
569 private static void extraEscapeElementValue( String message, OutputStreamWriter outputStreamWriter,
570 XMLWriter xmlWriter, OutputStream fw )
571 throws IOException
572 {
573
574 if ( containsEscapesIllegalXml10( message ) )
575 {
576 xmlWriter.writeText( escapeXml( message, false ) );
577 }
578 else
579 {
580 EncodingOutputStream eos = new EncodingOutputStream( fw );
581 xmlWriter.writeText( "" );
582 outputStreamWriter.flush();
583 eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES );
584 eos.write( message.getBytes( UTF_8 ) );
585 eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
586 eos.flush();
587 }
588 }
589
590
591 private static void addCommentElementTestCase( String comment, OutputStreamWriter outputStreamWriter,
592 XMLWriter xmlWriter, OutputStream fw )
593 throws IOException
594 {
595 xmlWriter.writeText( "" );
596 outputStreamWriter.flush();
597 fw.write( XML_NL.getBytes( UTF_8 ) );
598 fw.write( XML_INDENT.getBytes( UTF_8 ) );
599 fw.write( XML_INDENT.getBytes( UTF_8 ) );
600 fw.write( ByteConstantsHolder.COMMENT_START );
601 fw.write( comment.getBytes( UTF_8 ) );
602 fw.write( ByteConstantsHolder.COMMENT_END );
603 fw.write( XML_NL.getBytes( UTF_8 ) );
604 fw.write( XML_INDENT.getBytes( UTF_8 ) );
605 fw.flush();
606 }
607
608 private static final class EncodingOutputStream
609 extends FilterOutputStream
610 {
611 private int c1;
612
613 private int c2;
614
615 EncodingOutputStream( OutputStream out )
616 {
617 super( out );
618 }
619
620 OutputStream getUnderlying()
621 {
622 return out;
623 }
624
625 private boolean isCdataEndBlock( int c )
626 {
627 return c1 == ']' && c2 == ']' && c == '>';
628 }
629
630 @Override
631 public void write( int b )
632 throws IOException
633 {
634 if ( isCdataEndBlock( b ) )
635 {
636 out.write( ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES );
637 }
638 else if ( isIllegalEscape( b ) )
639 {
640
641
642
643
644
645 out.write( ByteConstantsHolder.AMP_BYTES );
646 out.write( String.valueOf( b ).getBytes( UTF_8 ) );
647 out.write( ';' );
648 }
649 else
650 {
651 out.write( b );
652 }
653 c1 = c2;
654 c2 = b;
655 }
656 }
657
658 private static boolean containsEscapesIllegalXml10( String message )
659 {
660 int size = message.length();
661 for ( int i = 0; i < size; i++ )
662 {
663 if ( isIllegalEscape( message.charAt( i ) ) )
664 {
665 return true;
666 }
667
668 }
669 return false;
670 }
671
672 private static boolean isIllegalEscape( char c )
673 {
674 return isIllegalEscape( (int) c );
675 }
676
677 private static boolean isIllegalEscape( int c )
678 {
679 return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
680 }
681
682
683
684
685
686
687
688
689 private static String escapeXml( String text, boolean attribute )
690 {
691 StringBuilder sb = new StringBuilder( text.length() * 2 );
692 for ( int i = 0; i < text.length(); i++ )
693 {
694 char c = text.charAt( i );
695 if ( isIllegalEscape( c ) )
696 {
697
698
699
700
701
702 sb.append( attribute ? "&#" : "&#" ).append( (int) c ).append(
703 ';' );
704 }
705 else
706 {
707 sb.append( c );
708 }
709 }
710 return sb.toString();
711 }
712
713 private static final class ByteConstantsHolder
714 {
715 private static final byte[] CDATA_START_BYTES = "<![CDATA[".getBytes( UTF_8 );
716
717 private static final byte[] CDATA_END_BYTES = "]]>".getBytes( UTF_8 );
718
719 private static final byte[] CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes( UTF_8 );
720
721 private static final byte[] AMP_BYTES = "&#".getBytes( UTF_8 );
722
723 private static final byte[] COMMENT_START = "<!-- ".getBytes( UTF_8 );
724
725 private static final byte[] COMMENT_END = " --> ".getBytes( UTF_8 );
726 }
727 }