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.xml.PrettyPrintXMLWriter;
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 import org.apache.maven.surefire.util.internal.StringUtils;
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.Collections;
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
44 import static org.apache.commons.io.IOUtils.closeQuietly;
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.plugin.surefire.report.ReportEntryType.SUCCESS;
48 import static org.apache.maven.surefire.util.internal.StringUtils.UTF_8;
49 import static org.apache.maven.surefire.util.internal.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 public class StatelessXmlReporter
86 {
87 private final File reportsDirectory;
88
89 private final String reportNameSuffix;
90
91 private final boolean trimStackTrace;
92
93 private final int rerunFailingTestsCount;
94
95 private final String xsdSchemaLocation;
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 String xsdSchemaLocation )
105 {
106 this.reportsDirectory = reportsDirectory;
107 this.reportNameSuffix = reportNameSuffix;
108 this.trimStackTrace = trimStackTrace;
109 this.rerunFailingTestsCount = rerunFailingTestsCount;
110 this.testClassMethodRunHistoryMap = testClassMethodRunHistoryMap;
111 this.xsdSchemaLocation = xsdSchemaLocation;
112 }
113
114 public void testSetCompleted( WrappedReportEntry testSetReportEntry, TestSetStats testSetStats )
115 {
116 String testClassName = testSetReportEntry.getName();
117
118 Map<String, List<WrappedReportEntry>> methodRunHistoryMap = getAddMethodRunHistoryMap( testClassName );
119
120
121 for ( WrappedReportEntry methodEntry : testSetStats.getReportEntries() )
122 {
123 getAddMethodEntryList( methodRunHistoryMap, methodEntry );
124 }
125
126 OutputStream outputStream = getOutputStream( testSetReportEntry );
127 OutputStreamWriter fw = getWriter( outputStream );
128 try
129 {
130 XMLWriter ppw = new PrettyPrintXMLWriter( fw );
131 ppw.setEncoding( StringUtils.UTF_8.name() );
132
133 createTestSuiteElement( ppw, testSetReportEntry, testSetStats, testSetReportEntry.elapsedTimeAsString() );
134
135 showProperties( ppw, testSetReportEntry.getSystemProperties() );
136
137
138 for ( Entry<String, List<WrappedReportEntry>> entry : methodRunHistoryMap.entrySet() )
139 {
140 List<WrappedReportEntry> methodEntryList = entry.getValue();
141 if ( methodEntryList == null )
142 {
143 throw new IllegalStateException( "Get null test method run history" );
144 }
145
146 if ( !methodEntryList.isEmpty() )
147 {
148 if ( rerunFailingTestsCount > 0 )
149 {
150 switch ( getTestResultType( methodEntryList ) )
151 {
152 case success:
153 for ( WrappedReportEntry methodEntry : methodEntryList )
154 {
155 if ( methodEntry.getReportEntryType() == 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() == 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() != 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() != 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 closeQuietly( 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 OutputStream 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 return new BufferedOutputStream( new FileOutputStream( reportFile ), 16 * 1024 );
294 }
295 catch ( Exception e )
296 {
297 throw new ReporterException( "When writing report", e );
298 }
299 }
300
301 private static OutputStreamWriter getWriter( OutputStream fos )
302 {
303 return new OutputStreamWriter( fos, UTF_8 );
304 }
305
306 private static void getAddMethodEntryList( Map<String, List<WrappedReportEntry>> methodRunHistoryMap,
307 WrappedReportEntry methodEntry )
308 {
309 List<WrappedReportEntry> methodEntryList = methodRunHistoryMap.get( methodEntry.getName() );
310 if ( methodEntryList == null )
311 {
312 methodEntryList = new ArrayList<WrappedReportEntry>();
313 methodRunHistoryMap.put( methodEntry.getName(), methodEntryList );
314 }
315 methodEntryList.add( methodEntry );
316 }
317
318 private static File getReportFile( ReportEntry report, File reportsDirectory, String reportNameSuffix )
319 {
320 String reportName = "TEST-" + report.getName();
321 String customizedReportName = isBlank( reportNameSuffix ) ? reportName : reportName + "-" + reportNameSuffix;
322 return new File( reportsDirectory, stripIllegalFilenameChars( customizedReportName + ".xml" ) );
323 }
324
325 private static void startTestElement( XMLWriter ppw, WrappedReportEntry report, String reportNameSuffix,
326 String timeAsString )
327 {
328 ppw.startElement( "testcase" );
329 ppw.addAttribute( "name", report.getReportName() );
330 if ( report.getGroup() != null )
331 {
332 ppw.addAttribute( "group", report.getGroup() );
333 }
334 if ( report.getSourceName() != null )
335 {
336 if ( reportNameSuffix != null && !reportNameSuffix.isEmpty() )
337 {
338 ppw.addAttribute( "classname", report.getSourceName() + "(" + reportNameSuffix + ")" );
339 }
340 else
341 {
342 ppw.addAttribute( "classname", report.getSourceName() );
343 }
344 }
345 ppw.addAttribute( "time", timeAsString );
346 }
347
348 private void createTestSuiteElement( XMLWriter ppw, WrappedReportEntry report, TestSetStats testSetStats,
349 String timeAsString )
350 {
351 ppw.startElement( "testsuite" );
352
353 ppw.addAttribute( "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance" );
354 ppw.addAttribute( "xsi:noNamespaceSchemaLocation", xsdSchemaLocation );
355
356 ppw.addAttribute( "name", report.getReportName( reportNameSuffix ) );
357
358 if ( report.getGroup() != null )
359 {
360 ppw.addAttribute( "group", report.getGroup() );
361 }
362
363 ppw.addAttribute( "time", timeAsString );
364
365 ppw.addAttribute( "tests", String.valueOf( testSetStats.getCompletedCount() ) );
366
367 ppw.addAttribute( "errors", String.valueOf( testSetStats.getErrors() ) );
368
369 ppw.addAttribute( "skipped", String.valueOf( testSetStats.getSkipped() ) );
370
371 ppw.addAttribute( "failures", String.valueOf( testSetStats.getFailures() ) );
372 }
373
374 private static void getTestProblems( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
375 WrappedReportEntry report, boolean trimStackTrace, OutputStream fw,
376 String testErrorType, boolean createOutErrElementsInside )
377 {
378 ppw.startElement( testErrorType );
379
380 String stackTrace = report.getStackTrace( trimStackTrace );
381
382 if ( report.getMessage() != null && !report.getMessage().isEmpty() )
383 {
384 ppw.addAttribute( "message", extraEscape( report.getMessage(), true ) );
385 }
386
387 if ( report.getStackTraceWriter() != null )
388 {
389
390 SafeThrowable t = report.getStackTraceWriter().getThrowable();
391 if ( t != null )
392 {
393 if ( t.getMessage() != null )
394 {
395 int delimiter = stackTrace.indexOf( ":" );
396 String type = delimiter == -1 ? stackTrace : stackTrace.substring( 0, delimiter );
397 ppw.addAttribute( "type", type );
398 }
399 else
400 {
401 ppw.addAttribute( "type", new StringTokenizer( stackTrace ).nextToken() );
402 }
403 }
404 }
405
406 if ( stackTrace != null )
407 {
408 ppw.writeText( extraEscape( stackTrace, false ) );
409 }
410
411 if ( createOutErrElementsInside )
412 {
413 createOutErrElements( outputStreamWriter, ppw, report, fw );
414 }
415
416 ppw.endElement();
417 }
418
419
420 private static void createOutErrElements( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
421 WrappedReportEntry report, OutputStream fw )
422 {
423 EncodingOutputStream eos = new EncodingOutputStream( fw );
424 addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdout(), "system-out" );
425 addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdErr(), "system-err" );
426 }
427
428 private static void addOutputStreamElement( OutputStreamWriter outputStreamWriter,
429 EncodingOutputStream eos, XMLWriter xmlWriter,
430 Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
431 String name )
432 {
433 if ( utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0 )
434 {
435 xmlWriter.startElement( name );
436
437 try
438 {
439 xmlWriter.writeText( "" );
440 outputStreamWriter.flush();
441 utf8RecodingDeferredFileOutputStream.close();
442 eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES );
443 utf8RecodingDeferredFileOutputStream.writeTo( eos );
444 eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
445 eos.flush();
446 }
447 catch ( IOException e )
448 {
449 throw new ReporterException( "When writing xml report stdout/stderr", e );
450 }
451 xmlWriter.endElement();
452 }
453 }
454
455
456
457
458
459
460
461 private static void showProperties( XMLWriter xmlWriter, Map<String, String> systemProperties )
462 {
463 xmlWriter.startElement( "properties" );
464 for ( final Entry<String, String> entry : systemProperties.entrySet() )
465 {
466 final String key = entry.getKey();
467 String value = entry.getValue();
468
469 if ( value == null )
470 {
471 value = "null";
472 }
473
474 xmlWriter.startElement( "property" );
475
476 xmlWriter.addAttribute( "name", key );
477
478 xmlWriter.addAttribute( "value", extraEscape( value, true ) );
479
480 xmlWriter.endElement();
481 }
482 xmlWriter.endElement();
483 }
484
485
486
487
488
489
490
491
492 private static String extraEscape( String message, boolean attribute )
493 {
494
495 return containsEscapesIllegalXml10( message ) ? escapeXml( message, attribute ) : message;
496 }
497
498 private static final class EncodingOutputStream
499 extends FilterOutputStream
500 {
501 private int c1;
502
503 private int c2;
504
505 EncodingOutputStream( OutputStream out )
506 {
507 super( out );
508 }
509
510 OutputStream getUnderlying()
511 {
512 return out;
513 }
514
515 private boolean isCdataEndBlock( int c )
516 {
517 return c1 == ']' && c2 == ']' && c == '>';
518 }
519
520 @Override
521 public void write( int b )
522 throws IOException
523 {
524 if ( isCdataEndBlock( b ) )
525 {
526 out.write( ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES );
527 }
528 else if ( isIllegalEscape( b ) )
529 {
530
531
532
533
534
535 out.write( ByteConstantsHolder.AMP_BYTES );
536 out.write( String.valueOf( b ).getBytes( UTF_8 ) );
537 out.write( ';' );
538 }
539 else
540 {
541 out.write( b );
542 }
543 c1 = c2;
544 c2 = b;
545 }
546 }
547
548 private static boolean containsEscapesIllegalXml10( String message )
549 {
550 int size = message.length();
551 for ( int i = 0; i < size; i++ )
552 {
553 if ( isIllegalEscape( message.charAt( i ) ) )
554 {
555 return true;
556 }
557
558 }
559 return false;
560 }
561
562 private static boolean isIllegalEscape( char c )
563 {
564 return isIllegalEscape( (int) c );
565 }
566
567 private static boolean isIllegalEscape( int c )
568 {
569 return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
570 }
571
572 private static String escapeXml( String text, boolean attribute )
573 {
574 StringBuilder sb = new StringBuilder( text.length() * 2 );
575 for ( int i = 0; i < text.length(); i++ )
576 {
577 char c = text.charAt( i );
578 if ( isIllegalEscape( c ) )
579 {
580
581
582
583
584
585 sb.append( attribute ? "&#" : "&#" ).append( (int) c ).append(
586 ';' );
587 }
588 else
589 {
590 sb.append( c );
591 }
592 }
593 return sb.toString();
594 }
595
596 private static final class ByteConstantsHolder
597 {
598 private static final byte[] CDATA_START_BYTES;
599
600 private static final byte[] CDATA_END_BYTES;
601
602 private static final byte[] CDATA_ESCAPE_STRING_BYTES;
603
604 private static final byte[] AMP_BYTES;
605
606 static
607 {
608 CDATA_START_BYTES = "<![CDATA[".getBytes( UTF_8 );
609 CDATA_END_BYTES = "]]>".getBytes( UTF_8 );
610 CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes( UTF_8 );
611 AMP_BYTES = "&#".getBytes( UTF_8 );
612 }
613 }
614 }