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