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.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 import static org.apache.maven.surefire.util.internal.StringUtils.isBlank;
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
82 public class StatelessXmlReporter
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 private final String xsdSchemaLocation;
97
98
99
100 private final Map<String, Map<String, List<WrappedReportEntry>>> testClassMethodRunHistoryMap;
101
102 public StatelessXmlReporter( File reportsDirectory, String reportNameSuffix, boolean trimStackTrace,
103 int rerunFailingTestsCount,
104 Map<String, Map<String, List<WrappedReportEntry>>> testClassMethodRunHistoryMap,
105 String xsdSchemaLocation )
106 {
107 this.reportsDirectory = reportsDirectory;
108 this.reportNameSuffix = reportNameSuffix;
109 this.trimStackTrace = trimStackTrace;
110 this.rerunFailingTestsCount = rerunFailingTestsCount;
111 this.testClassMethodRunHistoryMap = testClassMethodRunHistoryMap;
112 this.xsdSchemaLocation = xsdSchemaLocation;
113 }
114
115 public void testSetCompleted( WrappedReportEntry testSetReportEntry, TestSetStats testSetStats )
116 {
117 String testClassName = testSetReportEntry.getName();
118
119 Map<String, List<WrappedReportEntry>> methodRunHistoryMap = getAddMethodRunHistoryMap( testClassName );
120
121
122 for ( WrappedReportEntry methodEntry : testSetStats.getReportEntries() )
123 {
124 getAddMethodEntryList( methodRunHistoryMap, methodEntry );
125 }
126
127 FileOutputStream outputStream = getOutputStream( testSetReportEntry );
128 OutputStreamWriter fw = getWriter( outputStream );
129 try
130 {
131 XMLWriter ppw = new PrettyPrintXMLWriter( fw );
132 ppw.setEncoding( ENCODING );
133
134 createTestSuiteElement( ppw, testSetReportEntry, testSetStats, testSetReportEntry.elapsedTimeAsString() );
135
136 showProperties( ppw );
137
138
139 for ( Map.Entry<String, List<WrappedReportEntry>> entry : methodRunHistoryMap.entrySet() )
140 {
141 List<WrappedReportEntry> methodEntryList = entry.getValue();
142 if ( methodEntryList == null )
143 {
144 throw new IllegalStateException( "Get null test method run history" );
145 }
146
147 if ( !methodEntryList.isEmpty() )
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 }
240 ppw.endElement();
241 }
242 finally
243 {
244 IOUtil.close( fw );
245 }
246 }
247
248
249
250
251 public void cleanTestHistoryMap()
252 {
253 testClassMethodRunHistoryMap.clear();
254 }
255
256
257
258
259
260
261
262 private TestResultType getTestResultType( List<WrappedReportEntry> methodEntryList )
263 {
264 List<ReportEntryType> testResultTypeList = new ArrayList<ReportEntryType>();
265 for ( WrappedReportEntry singleRunEntry : methodEntryList )
266 {
267 testResultTypeList.add( singleRunEntry.getReportEntryType() );
268 }
269
270 return DefaultReporterFactory.getTestResultType( testResultTypeList, rerunFailingTestsCount );
271 }
272
273 private Map<String, List<WrappedReportEntry>> getAddMethodRunHistoryMap( String testClassName )
274 {
275 Map<String, List<WrappedReportEntry>> methodRunHistoryMap = testClassMethodRunHistoryMap.get( testClassName );
276 if ( methodRunHistoryMap == null )
277 {
278 methodRunHistoryMap = Collections.synchronizedMap( new LinkedHashMap<String, List<WrappedReportEntry>>() );
279 testClassMethodRunHistoryMap.put( testClassName, methodRunHistoryMap );
280 }
281 return methodRunHistoryMap;
282 }
283
284 private FileOutputStream getOutputStream( WrappedReportEntry testSetReportEntry )
285 {
286 File reportFile = getReportFile( testSetReportEntry, reportsDirectory, reportNameSuffix );
287
288 File reportDir = reportFile.getParentFile();
289
290
291 reportDir.mkdirs();
292
293 try
294 {
295 return new FileOutputStream( reportFile );
296 }
297 catch ( Exception e )
298 {
299 throw new ReporterException( "When writing report", e );
300 }
301 }
302
303 private static OutputStreamWriter getWriter( FileOutputStream fos )
304 {
305 return new OutputStreamWriter( fos, ENCODING_CS );
306 }
307
308 private static void getAddMethodEntryList( Map<String, List<WrappedReportEntry>> methodRunHistoryMap,
309 WrappedReportEntry methodEntry )
310 {
311 List<WrappedReportEntry> methodEntryList = methodRunHistoryMap.get( methodEntry.getName() );
312 if ( methodEntryList == null )
313 {
314 methodEntryList = new ArrayList<WrappedReportEntry>();
315 methodRunHistoryMap.put( methodEntry.getName(), methodEntryList );
316 }
317 methodEntryList.add( methodEntry );
318 }
319
320 private static File getReportFile( ReportEntry report, File reportsDirectory, String reportNameSuffix )
321 {
322 String reportName = "TEST-" + report.getName();
323 String customizedReportName = isBlank( reportNameSuffix ) ? reportName : reportName + "-" + reportNameSuffix;
324 return new File( reportsDirectory, stripIllegalFilenameChars( customizedReportName + ".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 void createTestSuiteElement( XMLWriter ppw, WrappedReportEntry report, TestSetStats testSetStats,
351 String timeAsString )
352 {
353 ppw.startElement( "testsuite" );
354
355 ppw.addAttribute( "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance" );
356 ppw.addAttribute( "xsi:noNamespaceSchemaLocation", xsdSchemaLocation );
357
358 ppw.addAttribute( "name", report.getReportName( reportNameSuffix ) );
359
360 if ( report.getGroup() != null )
361 {
362 ppw.addAttribute( "group", report.getGroup() );
363 }
364
365 ppw.addAttribute( "time", timeAsString );
366
367 ppw.addAttribute( "tests", String.valueOf( testSetStats.getCompletedCount() ) );
368
369 ppw.addAttribute( "errors", String.valueOf( testSetStats.getErrors() ) );
370
371 ppw.addAttribute( "skipped", String.valueOf( testSetStats.getSkipped() ) );
372
373 ppw.addAttribute( "failures", String.valueOf( testSetStats.getFailures() ) );
374 }
375
376 private static void getTestProblems( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
377 WrappedReportEntry report, boolean trimStackTrace, FileOutputStream fw,
378 String testErrorType, boolean createOutErrElementsInside )
379 {
380 ppw.startElement( testErrorType );
381
382 String stackTrace = report.getStackTrace( trimStackTrace );
383
384 if ( report.getMessage() != null && report.getMessage().length() > 0 )
385 {
386 ppw.addAttribute( "message", extraEscape( report.getMessage(), true ) );
387 }
388
389 if ( report.getStackTraceWriter() != null )
390 {
391
392 SafeThrowable t = report.getStackTraceWriter().getThrowable();
393 if ( t != null )
394 {
395 if ( t.getMessage() != null )
396 {
397 ppw.addAttribute( "type", ( stackTrace.contains( ":" )
398 ? stackTrace.substring( 0, stackTrace.indexOf( ":" ) )
399 : stackTrace ) );
400 }
401 else
402 {
403 ppw.addAttribute( "type", new StringTokenizer( stackTrace ).nextToken() );
404 }
405 }
406 }
407
408 if ( stackTrace != null )
409 {
410 ppw.writeText( extraEscape( stackTrace, false ) );
411 }
412
413 if ( createOutErrElementsInside )
414 {
415 createOutErrElements( outputStreamWriter, ppw, report, fw );
416 }
417
418 ppw.endElement();
419 }
420
421
422 private static void createOutErrElements( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
423 WrappedReportEntry report, FileOutputStream fw )
424 {
425 EncodingOutputStream eos = new EncodingOutputStream( fw );
426 addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdout(), "system-out" );
427 addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdErr(), "system-err" );
428 }
429
430 private static void addOutputStreamElement( OutputStreamWriter outputStreamWriter,
431 EncodingOutputStream eos, XMLWriter xmlWriter,
432 Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
433 String name )
434 {
435 if ( utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0 )
436 {
437 xmlWriter.startElement( name );
438
439 try
440 {
441 xmlWriter.writeText( "" );
442 outputStreamWriter.flush();
443 utf8RecodingDeferredFileOutputStream.close();
444 eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES );
445 utf8RecodingDeferredFileOutputStream.writeTo( eos );
446 eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
447 eos.flush();
448 }
449 catch ( IOException e )
450 {
451 throw new ReporterException( "When writing xml report stdout/stderr", e );
452 }
453 xmlWriter.endElement();
454 }
455 }
456
457
458
459
460
461
462
463 private static void showProperties( XMLWriter xmlWriter )
464 {
465 xmlWriter.startElement( "properties" );
466
467 Properties systemProperties = System.getProperties();
468
469 if ( systemProperties != null )
470 {
471 for ( final String key : systemProperties.stringPropertyNames() )
472 {
473 String value = systemProperties.getProperty( key );
474
475 if ( value == null )
476 {
477 value = "null";
478 }
479
480 xmlWriter.startElement( "property" );
481
482 xmlWriter.addAttribute( "name", key );
483
484 xmlWriter.addAttribute( "value", extraEscape( value, true ) );
485
486 xmlWriter.endElement();
487 }
488 }
489 xmlWriter.endElement();
490 }
491
492
493
494
495
496
497
498
499 private static String extraEscape( String message, boolean attribute )
500 {
501
502 return containsEscapesIllegalXml10( message ) ? escapeXml( message, attribute ) : message;
503 }
504
505 private static final class EncodingOutputStream
506 extends FilterOutputStream
507 {
508 private int c1;
509
510 private int c2;
511
512 public EncodingOutputStream( OutputStream out )
513 {
514 super( out );
515 }
516
517 public OutputStream getUnderlying()
518 {
519 return out;
520 }
521
522 private boolean isCdataEndBlock( int c )
523 {
524 return c1 == ']' && c2 == ']' && c == '>';
525 }
526
527 @Override
528 public void write( int b )
529 throws IOException
530 {
531 if ( isCdataEndBlock( b ) )
532 {
533 out.write( ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES );
534 }
535 else if ( isIllegalEscape( b ) )
536 {
537
538
539
540
541
542 out.write( ByteConstantsHolder.AMP_BYTES );
543 out.write( String.valueOf( b ).getBytes( ENCODING ) );
544 out.write( ';' );
545 }
546 else
547 {
548 out.write( b );
549 }
550 c1 = c2;
551 c2 = b;
552 }
553 }
554
555 private static boolean containsEscapesIllegalXml10( String message )
556 {
557 int size = message.length();
558 for ( int i = 0; i < size; i++ )
559 {
560 if ( isIllegalEscape( message.charAt( i ) ) )
561 {
562 return true;
563 }
564
565 }
566 return false;
567 }
568
569 private static boolean isIllegalEscape( char c )
570 {
571 return isIllegalEscape( (int) c );
572 }
573
574 private static boolean isIllegalEscape( int c )
575 {
576 return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
577 }
578
579 private static String escapeXml( String text, boolean attribute )
580 {
581 StringBuilder sb = new StringBuilder( text.length() * 2 );
582 for ( int i = 0; i < text.length(); i++ )
583 {
584 char c = text.charAt( i );
585 if ( isIllegalEscape( c ) )
586 {
587
588
589
590
591
592 sb.append( attribute ? "&#" : "&#" ).append( (int) c ).append(
593 ';' );
594 }
595 else
596 {
597 sb.append( c );
598 }
599 }
600 return sb.toString();
601 }
602
603 private static final class ByteConstantsHolder
604 {
605 private static final byte[] CDATA_START_BYTES;
606
607 private static final byte[] CDATA_END_BYTES;
608
609 private static final byte[] CDATA_ESCAPE_STRING_BYTES;
610
611 private static final byte[] AMP_BYTES;
612
613 static
614 {
615 try
616 {
617 CDATA_START_BYTES = "<![CDATA[".getBytes( ENCODING );
618 CDATA_END_BYTES = "]]>".getBytes( ENCODING );
619 CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes( ENCODING );
620 AMP_BYTES = "&#".getBytes( ENCODING );
621 }
622 catch ( UnsupportedEncodingException e )
623 {
624 throw new RuntimeException( e );
625 }
626 }
627 }
628 }