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.shared.utils.io.IOUtil;
23 import org.apache.maven.shared.utils.xml.PrettyPrintXMLWriter;
24 import org.apache.maven.shared.utils.xml.XMLWriter;
25 import org.apache.maven.surefire.report.ReportEntry;
26 import org.apache.maven.surefire.report.ReporterException;
27 import org.apache.maven.surefire.report.SafeThrowable;
28
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.UnsupportedEncodingException;
36 import java.nio.charset.Charset;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.Enumeration;
40 import java.util.LinkedHashMap;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Properties;
44 import java.util.StringTokenizer;
45
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.surefire.util.internal.StringUtils.isNotBlank;
49
50
51
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 public class StatelessXmlReporter
84 {
85 private static final String ENCODING = "UTF-8";
86
87 private static final Charset ENCODING_CS = Charset.forName( ENCODING );
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
98
99 private final Map<String, Map<String, List<WrappedReportEntry>>> testClassMethodRunHistoryMap;
100
101 public StatelessXmlReporter( File reportsDirectory, String reportNameSuffix, boolean trimStackTrace,
102 int rerunFailingTestsCount,
103 Map<String, Map<String, List<WrappedReportEntry>>> testClassMethodRunHistoryMap )
104 {
105 this.reportsDirectory = reportsDirectory;
106 this.reportNameSuffix = reportNameSuffix;
107 this.trimStackTrace = trimStackTrace;
108 this.rerunFailingTestsCount = rerunFailingTestsCount;
109 this.testClassMethodRunHistoryMap = testClassMethodRunHistoryMap;
110 }
111
112 public void testSetCompleted( WrappedReportEntry testSetReportEntry, TestSetStats testSetStats )
113 {
114 String testClassName = testSetReportEntry.getName();
115
116 Map<String, List<WrappedReportEntry>> methodRunHistoryMap = getAddMethodRunHistoryMap( testClassName );
117
118
119 for ( WrappedReportEntry methodEntry : testSetStats.getReportEntries() )
120 {
121 getAddMethodEntryList( methodRunHistoryMap, methodEntry );
122 }
123
124 FileOutputStream outputStream = getOutputStream( testSetReportEntry );
125 OutputStreamWriter fw = getWriter( outputStream );
126 try
127 {
128 XMLWriter ppw = new PrettyPrintXMLWriter( fw );
129 ppw.setEncoding( ENCODING );
130
131 createTestSuiteElement( ppw, testSetReportEntry, testSetStats, reportNameSuffix,
132 testSetReportEntry.elapsedTimeAsString() );
133
134 showProperties( ppw );
135
136
137 for ( Map.Entry<String, List<WrappedReportEntry>> entry : methodRunHistoryMap.entrySet() )
138 {
139 List<WrappedReportEntry> methodEntryList = entry.getValue();
140 if ( methodEntryList == null )
141 {
142 throw new IllegalStateException( "Get null test method run history" );
143 }
144
145 if ( !methodEntryList.isEmpty() )
146 {
147 if ( rerunFailingTestsCount > 0 )
148 {
149 TestResultType resultType = getTestResultType( methodEntryList );
150 switch ( resultType )
151 {
152 case success:
153 for ( WrappedReportEntry methodEntry : methodEntryList )
154 {
155 if ( methodEntry.getReportEntryType() == ReportEntryType.SUCCESS )
156 {
157 startTestElement( ppw, methodEntry, reportNameSuffix,
158 methodEntryList.get( 0 ).elapsedTimeAsString() );
159 ppw.endElement();
160 }
161 }
162 break;
163 case error:
164 case failure:
165
166 startTestElement( ppw, methodEntryList.get( 0 ), reportNameSuffix,
167 methodEntryList.get( 0 ).elapsedTimeAsString() );
168 boolean firstRun = true;
169 for ( WrappedReportEntry singleRunEntry : methodEntryList )
170 {
171 if ( firstRun )
172 {
173 firstRun = false;
174 getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream,
175 singleRunEntry.getReportEntryType().getXmlTag(), false );
176 createOutErrElements( fw, ppw, singleRunEntry, outputStream );
177 }
178 else
179 {
180 getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream,
181 singleRunEntry.getReportEntryType().getRerunXmlTag(), true );
182 }
183 }
184 ppw.endElement();
185 break;
186 case flake:
187 String runtime = "";
188
189 for ( WrappedReportEntry singleRunEntry : methodEntryList )
190 {
191 if ( singleRunEntry.getReportEntryType() == ReportEntryType.SUCCESS )
192 {
193 runtime = singleRunEntry.elapsedTimeAsString();
194 break;
195 }
196 }
197 startTestElement( ppw, methodEntryList.get( 0 ), reportNameSuffix, runtime );
198 for ( WrappedReportEntry singleRunEntry : methodEntryList )
199 {
200 if ( singleRunEntry.getReportEntryType() != ReportEntryType.SUCCESS )
201 {
202 getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream,
203 singleRunEntry.getReportEntryType().getFlakyXmlTag(), true );
204 }
205 }
206 ppw.endElement();
207
208 break;
209 case skipped:
210 startTestElement( ppw, methodEntryList.get( 0 ), reportNameSuffix,
211 methodEntryList.get( 0 ).elapsedTimeAsString() );
212 getTestProblems( fw, ppw, methodEntryList.get( 0 ), trimStackTrace, outputStream,
213 methodEntryList.get( 0 ).getReportEntryType().getXmlTag(), false );
214 ppw.endElement();
215 break;
216 default:
217 throw new IllegalStateException( "Get unknown test result type" );
218 }
219 }
220 else
221 {
222
223
224 for ( WrappedReportEntry methodEntry : methodEntryList )
225 {
226 startTestElement( ppw, methodEntry, reportNameSuffix, methodEntry.elapsedTimeAsString() );
227 if ( methodEntry.getReportEntryType() != ReportEntryType.SUCCESS )
228 {
229 getTestProblems( fw, ppw, methodEntry, trimStackTrace, outputStream,
230 methodEntry.getReportEntryType().getXmlTag(), false );
231 createOutErrElements( fw, ppw, methodEntry, outputStream );
232 }
233 ppw.endElement();
234 }
235 }
236 }
237 }
238 ppw.endElement();
239 }
240 finally
241 {
242 IOUtil.close( fw );
243 }
244 }
245
246
247
248
249 public void cleanTestHistoryMap()
250 {
251 testClassMethodRunHistoryMap.clear();
252 }
253
254
255
256
257
258
259
260 private TestResultType getTestResultType( List<WrappedReportEntry> methodEntryList )
261 {
262 List<ReportEntryType> testResultTypeList = new ArrayList<ReportEntryType>();
263 for ( WrappedReportEntry singleRunEntry : methodEntryList )
264 {
265 testResultTypeList.add( singleRunEntry.getReportEntryType() );
266 }
267
268 return DefaultReporterFactory.getTestResultType( testResultTypeList, rerunFailingTestsCount );
269 }
270
271 private Map<String, List<WrappedReportEntry>> getAddMethodRunHistoryMap( String testClassName )
272 {
273 Map<String, List<WrappedReportEntry>> methodRunHistoryMap = testClassMethodRunHistoryMap.get( testClassName );
274 if ( methodRunHistoryMap == null )
275 {
276 methodRunHistoryMap = Collections.synchronizedMap( new LinkedHashMap<String, List<WrappedReportEntry>>() );
277 testClassMethodRunHistoryMap.put( testClassName, methodRunHistoryMap );
278 }
279 return methodRunHistoryMap;
280 }
281
282 private FileOutputStream getOutputStream( WrappedReportEntry testSetReportEntry )
283 {
284 File reportFile = getReportFile( testSetReportEntry, reportsDirectory, reportNameSuffix );
285
286 File reportDir = reportFile.getParentFile();
287
288
289 reportDir.mkdirs();
290
291 try
292 {
293
294 return new FileOutputStream( reportFile );
295 }
296 catch ( Exception e )
297 {
298 throw new ReporterException( "When writing report", e );
299 }
300 }
301
302 private static OutputStreamWriter getWriter( FileOutputStream fos )
303 {
304 return new OutputStreamWriter( fos, ENCODING_CS );
305 }
306
307 private static void getAddMethodEntryList( Map<String, List<WrappedReportEntry>> methodRunHistoryMap,
308 WrappedReportEntry methodEntry )
309 {
310 List<WrappedReportEntry> methodEntryList = methodRunHistoryMap.get( methodEntry.getName() );
311 if ( methodEntryList == null )
312 {
313 methodEntryList = new ArrayList<WrappedReportEntry>();
314 methodRunHistoryMap.put( methodEntry.getName(), methodEntryList );
315 }
316 methodEntryList.add( methodEntry );
317 }
318
319 private static File getReportFile( ReportEntry report, File reportsDirectory, String reportNameSuffix )
320 {
321 String reportName = "TEST-" + report.getName();
322 return isNotBlank( reportNameSuffix )
323 ? new File( reportsDirectory, stripIllegalFilenameChars( reportName + "-" + reportNameSuffix + ".xml" ) )
324 : new File( reportsDirectory, stripIllegalFilenameChars( reportName + ".xml" ) );
325 }
326
327 private static void startTestElement( XMLWriter ppw, WrappedReportEntry report, String reportNameSuffix,
328 String timeAsString )
329 {
330 ppw.startElement( "testcase" );
331 ppw.addAttribute( "name", report.getReportName() );
332 if ( report.getGroup() != null )
333 {
334 ppw.addAttribute( "group", report.getGroup() );
335 }
336 if ( report.getSourceName() != null )
337 {
338 if ( reportNameSuffix != null && reportNameSuffix.length() > 0 )
339 {
340 ppw.addAttribute( "classname", report.getSourceName() + "(" + reportNameSuffix + ")" );
341 }
342 else
343 {
344 ppw.addAttribute( "classname", report.getSourceName() );
345 }
346 }
347 ppw.addAttribute( "time", timeAsString );
348 }
349
350 private static void createTestSuiteElement( XMLWriter ppw, WrappedReportEntry report, TestSetStats testSetStats,
351 String reportNameSuffix1, String timeAsString )
352 {
353 ppw.startElement( "testsuite" );
354
355 ppw.addAttribute( "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance" );
356
357 ppw.addAttribute( "xsi:schemaLocation",
358 "https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report.xsd" );
359
360 ppw.addAttribute( "name", report.getReportName( reportNameSuffix1 ) );
361
362 if ( report.getGroup() != null )
363 {
364 ppw.addAttribute( "group", report.getGroup() );
365 }
366
367 ppw.addAttribute( "time", timeAsString );
368
369 ppw.addAttribute( "tests", String.valueOf( testSetStats.getCompletedCount() ) );
370
371 ppw.addAttribute( "errors", String.valueOf( testSetStats.getErrors() ) );
372
373 ppw.addAttribute( "skipped", String.valueOf( testSetStats.getSkipped() ) );
374
375 ppw.addAttribute( "failures", String.valueOf( testSetStats.getFailures() ) );
376 }
377
378 private static void getTestProblems( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
379 WrappedReportEntry report, boolean trimStackTrace, FileOutputStream fw,
380 String testErrorType, boolean createOutErrElementsInside )
381 {
382 ppw.startElement( testErrorType );
383
384 String stackTrace = report.getStackTrace( trimStackTrace );
385
386 if ( report.getMessage() != null && report.getMessage().length() > 0 )
387 {
388 ppw.addAttribute( "message", extraEscape( report.getMessage(), true ) );
389 }
390
391 if ( report.getStackTraceWriter() != null )
392 {
393
394 SafeThrowable t = report.getStackTraceWriter().getThrowable();
395 if ( t != null )
396 {
397 if ( t.getMessage() != null )
398 {
399 ppw.addAttribute( "type", ( stackTrace.contains( ":" )
400 ? stackTrace.substring( 0, stackTrace.indexOf( ":" ) )
401 : stackTrace ) );
402 }
403 else
404 {
405 ppw.addAttribute( "type", new StringTokenizer( stackTrace ).nextToken() );
406 }
407 }
408 }
409
410 if ( stackTrace != null )
411 {
412 ppw.writeText( extraEscape( stackTrace, false ) );
413 }
414
415 if ( createOutErrElementsInside )
416 {
417 createOutErrElements( outputStreamWriter, ppw, report, fw );
418 }
419
420 ppw.endElement();
421 }
422
423
424 private static void createOutErrElements( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
425 WrappedReportEntry report, FileOutputStream fw )
426 {
427 EncodingOutputStream eos = new EncodingOutputStream( fw );
428 addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdout(), "system-out" );
429 addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdErr(), "system-err" );
430 }
431
432 private static void addOutputStreamElement( OutputStreamWriter outputStreamWriter,
433 EncodingOutputStream eos, XMLWriter xmlWriter,
434 Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
435 String name )
436 {
437 if ( utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0 )
438 {
439 xmlWriter.startElement( name );
440
441 try
442 {
443 xmlWriter.writeText( "" );
444 outputStreamWriter.flush();
445 utf8RecodingDeferredFileOutputStream.close();
446 eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES );
447 utf8RecodingDeferredFileOutputStream.writeTo( eos );
448 eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
449 eos.flush();
450 }
451 catch ( IOException e )
452 {
453 throw new ReporterException( "When writing xml report stdout/stderr", e );
454 }
455 xmlWriter.endElement();
456 }
457 }
458
459
460
461
462
463
464
465 private static void showProperties( XMLWriter xmlWriter )
466 {
467 xmlWriter.startElement( "properties" );
468
469 Properties systemProperties = System.getProperties();
470
471 if ( systemProperties != null )
472 {
473 Enumeration<?> propertyKeys = systemProperties.propertyNames();
474
475 while ( propertyKeys.hasMoreElements() )
476 {
477 String key = (String) propertyKeys.nextElement();
478
479 String value = systemProperties.getProperty( key );
480
481 if ( value == null )
482 {
483 value = "null";
484 }
485
486 xmlWriter.startElement( "property" );
487
488 xmlWriter.addAttribute( "name", key );
489
490 xmlWriter.addAttribute( "value", extraEscape( value, true ) );
491
492 xmlWriter.endElement();
493
494 }
495 }
496 xmlWriter.endElement();
497 }
498
499
500
501
502
503
504
505
506 private static String extraEscape( String message, boolean attribute )
507 {
508
509 if ( !containsEscapesIllegalnXml10( message ) )
510 {
511 return message;
512 }
513 return escapeXml( message, attribute );
514 }
515
516 private static class EncodingOutputStream
517 extends FilterOutputStream
518 {
519 private int c1;
520
521 private int c2;
522
523 public EncodingOutputStream( OutputStream out )
524 {
525 super( out );
526 }
527
528 public OutputStream getUnderlying()
529 {
530 return out;
531 }
532
533 private boolean isCdataEndBlock( int c )
534 {
535 return c1 == ']' && c2 == ']' && c == '>';
536 }
537
538 @Override
539 public void write( int b )
540 throws IOException
541 {
542 if ( isCdataEndBlock( b ) )
543 {
544 out.write( ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES );
545 }
546 else if ( isIllegalEscape( b ) )
547 {
548
549
550
551
552
553 out.write( ByteConstantsHolder.AMP_BYTES );
554 out.write( String.valueOf( b ).getBytes( ENCODING ) );
555 out.write( ';' );
556 }
557 else
558 {
559 out.write( b );
560 }
561 c1 = c2;
562 c2 = b;
563 }
564 }
565
566 private static boolean containsEscapesIllegalnXml10( String message )
567 {
568 int size = message.length();
569 for ( int i = 0; i < size; i++ )
570 {
571 if ( isIllegalEscape( message.charAt( i ) ) )
572 {
573 return true;
574 }
575
576 }
577 return false;
578 }
579
580 private static boolean isIllegalEscape( char c )
581 {
582 return isIllegalEscape( (int) c );
583 }
584
585 private static boolean isIllegalEscape( int c )
586 {
587 return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
588 }
589
590 private static String escapeXml( String text, boolean attribute )
591 {
592 StringBuilder sb = new StringBuilder( text.length() * 2 );
593 for ( int i = 0; i < text.length(); i++ )
594 {
595 char c = text.charAt( i );
596 if ( isIllegalEscape( c ) )
597 {
598
599
600
601
602
603 sb.append( attribute ? "&#" : "&#" ).append( (int) c ).append(
604 ';' );
605 }
606 else
607 {
608 sb.append( c );
609 }
610 }
611 return sb.toString();
612 }
613
614 private static class ByteConstantsHolder
615 {
616 private static final byte[] CDATA_START_BYTES;
617
618 private static final byte[] CDATA_END_BYTES;
619
620 private static final byte[] CDATA_ESCAPE_STRING_BYTES;
621
622 private static final byte[] AMP_BYTES;
623
624 static
625 {
626 try
627 {
628 CDATA_START_BYTES = "<![CDATA[".getBytes( ENCODING );
629 CDATA_END_BYTES = "]]>".getBytes( ENCODING );
630 CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes( ENCODING );
631 AMP_BYTES = "&#".getBytes( ENCODING );
632 }
633 catch ( UnsupportedEncodingException e )
634 {
635 throw new RuntimeException( e );
636 }
637 }
638 }
639 }