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 eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
515 eos.flush();
516 xmlWriter.endElement();
517 }
518 }
519
520
521
522
523
524
525
526 private static void showProperties( XMLWriter xmlWriter, Map<String, String> systemProperties )
527 throws IOException
528 {
529 xmlWriter.startElement( "properties" );
530 for ( final Entry<String, String> entry : systemProperties.entrySet() )
531 {
532 final String key = entry.getKey();
533 String value = entry.getValue();
534
535 if ( value == null )
536 {
537 value = "null";
538 }
539
540 xmlWriter.startElement( "property" );
541
542 xmlWriter.addAttribute( "name", key );
543
544 xmlWriter.addAttribute( "value", extraEscapeAttribute( value ) );
545
546 xmlWriter.endElement();
547 }
548 xmlWriter.endElement();
549 }
550
551
552
553
554
555
556
557 private static String extraEscapeAttribute( String message )
558 {
559
560 return containsEscapesIllegalXml10( message ) ? escapeXml( message, true ) : message;
561 }
562
563
564
565
566
567
568 private static void extraEscapeElementValue( String message, OutputStreamWriter outputStreamWriter,
569 XMLWriter xmlWriter, OutputStream fw )
570 throws IOException
571 {
572
573 if ( containsEscapesIllegalXml10( message ) )
574 {
575 xmlWriter.writeText( escapeXml( message, false ) );
576 }
577 else
578 {
579 EncodingOutputStream eos = new EncodingOutputStream( fw );
580 xmlWriter.writeText( "" );
581 outputStreamWriter.flush();
582 eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES );
583 eos.write( message.getBytes( UTF_8 ) );
584 eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
585 eos.flush();
586 }
587 }
588
589
590 private static void addCommentElementTestCase( String comment, OutputStreamWriter outputStreamWriter,
591 XMLWriter xmlWriter, OutputStream fw )
592 throws IOException
593 {
594 xmlWriter.writeText( "" );
595 outputStreamWriter.flush();
596 fw.write( XML_NL.getBytes( UTF_8 ) );
597 fw.write( XML_INDENT.getBytes( UTF_8 ) );
598 fw.write( XML_INDENT.getBytes( UTF_8 ) );
599 fw.write( ByteConstantsHolder.COMMENT_START );
600 fw.write( comment.getBytes( UTF_8 ) );
601 fw.write( ByteConstantsHolder.COMMENT_END );
602 fw.write( XML_NL.getBytes( UTF_8 ) );
603 fw.write( XML_INDENT.getBytes( UTF_8 ) );
604 fw.flush();
605 }
606
607 private static final class EncodingOutputStream
608 extends FilterOutputStream
609 {
610 private int c1;
611
612 private int c2;
613
614 EncodingOutputStream( OutputStream out )
615 {
616 super( out );
617 }
618
619 OutputStream getUnderlying()
620 {
621 return out;
622 }
623
624 private boolean isCdataEndBlock( int c )
625 {
626 return c1 == ']' && c2 == ']' && c == '>';
627 }
628
629 @Override
630 public void write( int b )
631 throws IOException
632 {
633 if ( isCdataEndBlock( b ) )
634 {
635 out.write( ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES );
636 }
637 else if ( isIllegalEscape( b ) )
638 {
639
640
641
642
643
644 out.write( ByteConstantsHolder.AMP_BYTES );
645 out.write( String.valueOf( b ).getBytes( UTF_8 ) );
646 out.write( ';' );
647 }
648 else
649 {
650 out.write( b );
651 }
652 c1 = c2;
653 c2 = b;
654 }
655 }
656
657 private static boolean containsEscapesIllegalXml10( String message )
658 {
659 int size = message.length();
660 for ( int i = 0; i < size; i++ )
661 {
662 if ( isIllegalEscape( message.charAt( i ) ) )
663 {
664 return true;
665 }
666
667 }
668 return false;
669 }
670
671 private static boolean isIllegalEscape( char c )
672 {
673 return isIllegalEscape( (int) c );
674 }
675
676 private static boolean isIllegalEscape( int c )
677 {
678 return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
679 }
680
681
682
683
684
685
686
687
688 private static String escapeXml( String text, boolean attribute )
689 {
690 StringBuilder sb = new StringBuilder( text.length() * 2 );
691 for ( int i = 0; i < text.length(); i++ )
692 {
693 char c = text.charAt( i );
694 if ( isIllegalEscape( c ) )
695 {
696
697
698
699
700
701 sb.append( attribute ? "&#" : "&#" ).append( (int) c ).append(
702 ';' );
703 }
704 else
705 {
706 sb.append( c );
707 }
708 }
709 return sb.toString();
710 }
711
712 private static final class ByteConstantsHolder
713 {
714 private static final byte[] CDATA_START_BYTES = "<![CDATA[".getBytes( UTF_8 );
715
716 private static final byte[] CDATA_END_BYTES = "]]>".getBytes( UTF_8 );
717
718 private static final byte[] CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes( UTF_8 );
719
720 private static final byte[] AMP_BYTES = "&#".getBytes( UTF_8 );
721
722 private static final byte[] COMMENT_START = "<!-- ".getBytes( UTF_8 );
723
724 private static final byte[] COMMENT_END = " --> ".getBytes( UTF_8 );
725 }
726 }