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