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 @Deprecated
86 public class StatelessXmlReporter
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 OutputStream outputStream = getOutputStream( testSetReportEntry );
128 OutputStreamWriter fw = getWriter( outputStream );
129 try
130 {
131 XMLWriter ppw = new PrettyPrintXMLWriter( fw );
132 ppw.setEncoding( StringUtils.UTF_8.name() );
133
134 createTestSuiteElement( ppw, testSetReportEntry, testSetStats, testSetReportEntry.elapsedTimeAsString() );
135
136 showProperties( ppw, testSetReportEntry.getSystemProperties() );
137
138
139 for ( 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 switch ( getTestResultType( methodEntryList ) )
152 {
153 case success:
154 for ( WrappedReportEntry methodEntry : methodEntryList )
155 {
156 if ( methodEntry.getReportEntryType() == SUCCESS )
157 {
158 startTestElement( ppw, methodEntry, reportNameSuffix,
159 methodEntryList.get( 0 ).elapsedTimeAsString() );
160 ppw.endElement();
161 }
162 }
163 break;
164 case error:
165 case failure:
166
167 startTestElement( ppw, methodEntryList.get( 0 ), reportNameSuffix,
168 methodEntryList.get( 0 ).elapsedTimeAsString() );
169 boolean firstRun = true;
170 for ( WrappedReportEntry singleRunEntry : methodEntryList )
171 {
172 if ( firstRun )
173 {
174 firstRun = false;
175 getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream,
176 singleRunEntry.getReportEntryType().getXmlTag(), false );
177 createOutErrElements( fw, ppw, singleRunEntry, outputStream );
178 }
179 else
180 {
181 getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream,
182 singleRunEntry.getReportEntryType().getRerunXmlTag(), true );
183 }
184 }
185 ppw.endElement();
186 break;
187 case flake:
188 String runtime = "";
189
190 for ( WrappedReportEntry singleRunEntry : methodEntryList )
191 {
192 if ( singleRunEntry.getReportEntryType() == SUCCESS )
193 {
194 runtime = singleRunEntry.elapsedTimeAsString();
195 break;
196 }
197 }
198 startTestElement( ppw, methodEntryList.get( 0 ), reportNameSuffix, runtime );
199 for ( WrappedReportEntry singleRunEntry : methodEntryList )
200 {
201 if ( singleRunEntry.getReportEntryType() != SUCCESS )
202 {
203 getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream,
204 singleRunEntry.getReportEntryType().getFlakyXmlTag(), true );
205 }
206 }
207 ppw.endElement();
208
209 break;
210 case skipped:
211 startTestElement( ppw, methodEntryList.get( 0 ), reportNameSuffix,
212 methodEntryList.get( 0 ).elapsedTimeAsString() );
213 getTestProblems( fw, ppw, methodEntryList.get( 0 ), trimStackTrace, outputStream,
214 methodEntryList.get( 0 ).getReportEntryType().getXmlTag(), false );
215 ppw.endElement();
216 break;
217 default:
218 throw new IllegalStateException( "Get unknown test result type" );
219 }
220 }
221 else
222 {
223
224
225 for ( WrappedReportEntry methodEntry : methodEntryList )
226 {
227 startTestElement( ppw, methodEntry, reportNameSuffix, methodEntry.elapsedTimeAsString() );
228 if ( methodEntry.getReportEntryType() != SUCCESS )
229 {
230 getTestProblems( fw, ppw, methodEntry, trimStackTrace, outputStream,
231 methodEntry.getReportEntryType().getXmlTag(), false );
232 createOutErrElements( fw, ppw, methodEntry, outputStream );
233 }
234 ppw.endElement();
235 }
236 }
237 }
238 }
239 ppw.endElement();
240 }
241 finally
242 {
243 closeQuietly( 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 private Map<String, List<WrappedReportEntry>> getAddMethodRunHistoryMap( String testClassName )
273 {
274 Map<String, List<WrappedReportEntry>> methodRunHistoryMap = testClassMethodRunHistoryMap.get( testClassName );
275 if ( methodRunHistoryMap == null )
276 {
277 methodRunHistoryMap = Collections.synchronizedMap( new LinkedHashMap<String, List<WrappedReportEntry>>() );
278 testClassMethodRunHistoryMap.put( testClassName, methodRunHistoryMap );
279 }
280 return methodRunHistoryMap;
281 }
282
283 private OutputStream getOutputStream( WrappedReportEntry testSetReportEntry )
284 {
285 File reportFile = getReportFile( testSetReportEntry, reportsDirectory, reportNameSuffix );
286
287 File reportDir = reportFile.getParentFile();
288
289
290 reportDir.mkdirs();
291
292 try
293 {
294 return new BufferedOutputStream( new FileOutputStream( reportFile ), 16 * 1024 );
295 }
296 catch ( Exception e )
297 {
298 throw new ReporterException( "When writing report", e );
299 }
300 }
301
302 private static OutputStreamWriter getWriter( OutputStream fos )
303 {
304 return new OutputStreamWriter( fos, UTF_8 );
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 String customizedReportName = isBlank( reportNameSuffix ) ? reportName : reportName + "-" + reportNameSuffix;
323 return new File( reportsDirectory, stripIllegalFilenameChars( customizedReportName + ".xml" ) );
324 }
325
326 private static void startTestElement( XMLWriter ppw, WrappedReportEntry report, String reportNameSuffix,
327 String timeAsString )
328 {
329 ppw.startElement( "testcase" );
330 ppw.addAttribute( "name", report.getReportName() );
331 if ( report.getGroup() != null )
332 {
333 ppw.addAttribute( "group", report.getGroup() );
334 }
335 if ( report.getSourceName() != null )
336 {
337 if ( reportNameSuffix != null && !reportNameSuffix.isEmpty() )
338 {
339 ppw.addAttribute( "classname", report.getSourceName() + "(" + reportNameSuffix + ")" );
340 }
341 else
342 {
343 ppw.addAttribute( "classname", report.getSourceName() );
344 }
345 }
346 ppw.addAttribute( "time", timeAsString );
347 }
348
349 private void createTestSuiteElement( XMLWriter ppw, WrappedReportEntry report, TestSetStats testSetStats,
350 String timeAsString )
351 {
352 ppw.startElement( "testsuite" );
353
354 ppw.addAttribute( "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance" );
355 ppw.addAttribute( "xsi:noNamespaceSchemaLocation", xsdSchemaLocation );
356
357 ppw.addAttribute( "name", report.getReportName( reportNameSuffix ) );
358
359 if ( report.getGroup() != null )
360 {
361 ppw.addAttribute( "group", report.getGroup() );
362 }
363
364 ppw.addAttribute( "time", timeAsString );
365
366 ppw.addAttribute( "tests", String.valueOf( testSetStats.getCompletedCount() ) );
367
368 ppw.addAttribute( "errors", String.valueOf( testSetStats.getErrors() ) );
369
370 ppw.addAttribute( "skipped", String.valueOf( testSetStats.getSkipped() ) );
371
372 ppw.addAttribute( "failures", String.valueOf( testSetStats.getFailures() ) );
373 }
374
375 private static void getTestProblems( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
376 WrappedReportEntry report, boolean trimStackTrace, OutputStream fw,
377 String testErrorType, boolean createOutErrElementsInside )
378 {
379 ppw.startElement( testErrorType );
380
381 String stackTrace = report.getStackTrace( trimStackTrace );
382
383 if ( report.getMessage() != null && !report.getMessage().isEmpty() )
384 {
385 ppw.addAttribute( "message", extraEscape( report.getMessage(), true ) );
386 }
387
388 if ( report.getStackTraceWriter() != null )
389 {
390
391 SafeThrowable t = report.getStackTraceWriter().getThrowable();
392 if ( t != null )
393 {
394 if ( t.getMessage() != null )
395 {
396 int delimiter = stackTrace.indexOf( ":" );
397 String type = delimiter == -1 ? stackTrace : stackTrace.substring( 0, delimiter );
398 ppw.addAttribute( "type", type );
399 }
400 else
401 {
402 ppw.addAttribute( "type", new StringTokenizer( stackTrace ).nextToken() );
403 }
404 }
405 }
406
407 boolean hasNestedElements = createOutErrElementsInside & stackTrace != null;
408
409 if ( stackTrace != null )
410 {
411 if ( hasNestedElements )
412 {
413 ppw.startElement( "stackTrace" );
414 }
415
416 ppw.writeText( extraEscape( stackTrace, false ) );
417
418 if ( hasNestedElements )
419 {
420 ppw.endElement();
421 }
422 }
423
424 if ( createOutErrElementsInside )
425 {
426 createOutErrElements( outputStreamWriter, ppw, report, fw );
427 }
428
429 ppw.endElement();
430 }
431
432
433 private static void createOutErrElements( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
434 WrappedReportEntry report, OutputStream fw )
435 {
436 EncodingOutputStream eos = new EncodingOutputStream( fw );
437 addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdout(), "system-out" );
438 addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdErr(), "system-err" );
439 }
440
441 private static void addOutputStreamElement( OutputStreamWriter outputStreamWriter,
442 EncodingOutputStream eos, XMLWriter xmlWriter,
443 Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
444 String name )
445 {
446 if ( utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0 )
447 {
448 xmlWriter.startElement( name );
449
450 try
451 {
452 xmlWriter.writeText( "" );
453 outputStreamWriter.flush();
454 utf8RecodingDeferredFileOutputStream.close();
455 eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES );
456 utf8RecodingDeferredFileOutputStream.writeTo( eos );
457 eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
458 eos.flush();
459 }
460 catch ( IOException e )
461 {
462 throw new ReporterException( "When writing xml report stdout/stderr", e );
463 }
464 xmlWriter.endElement();
465 }
466 }
467
468
469
470
471
472
473
474 private static void showProperties( XMLWriter xmlWriter, Map<String, String> systemProperties )
475 {
476 xmlWriter.startElement( "properties" );
477 for ( final Entry<String, String> entry : systemProperties.entrySet() )
478 {
479 final String key = entry.getKey();
480 String value = entry.getValue();
481
482 if ( value == null )
483 {
484 value = "null";
485 }
486
487 xmlWriter.startElement( "property" );
488
489 xmlWriter.addAttribute( "name", key );
490
491 xmlWriter.addAttribute( "value", extraEscape( value, true ) );
492
493 xmlWriter.endElement();
494 }
495 xmlWriter.endElement();
496 }
497
498
499
500
501
502
503
504
505 private static String extraEscape( String message, boolean attribute )
506 {
507
508 return containsEscapesIllegalXml10( message ) ? escapeXml( message, attribute ) : message;
509 }
510
511 private static final class EncodingOutputStream
512 extends FilterOutputStream
513 {
514 private int c1;
515
516 private int c2;
517
518 EncodingOutputStream( OutputStream out )
519 {
520 super( out );
521 }
522
523 OutputStream getUnderlying()
524 {
525 return out;
526 }
527
528 private boolean isCdataEndBlock( int c )
529 {
530 return c1 == ']' && c2 == ']' && c == '>';
531 }
532
533 @Override
534 public void write( int b )
535 throws IOException
536 {
537 if ( isCdataEndBlock( b ) )
538 {
539 out.write( ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES );
540 }
541 else if ( isIllegalEscape( b ) )
542 {
543
544
545
546
547
548 out.write( ByteConstantsHolder.AMP_BYTES );
549 out.write( String.valueOf( b ).getBytes( UTF_8 ) );
550 out.write( ';' );
551 }
552 else
553 {
554 out.write( b );
555 }
556 c1 = c2;
557 c2 = b;
558 }
559 }
560
561 private static boolean containsEscapesIllegalXml10( String message )
562 {
563 int size = message.length();
564 for ( int i = 0; i < size; i++ )
565 {
566 if ( isIllegalEscape( message.charAt( i ) ) )
567 {
568 return true;
569 }
570
571 }
572 return false;
573 }
574
575 private static boolean isIllegalEscape( char c )
576 {
577 return isIllegalEscape( (int) c );
578 }
579
580 private static boolean isIllegalEscape( int c )
581 {
582 return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
583 }
584
585 private static String escapeXml( String text, boolean attribute )
586 {
587 StringBuilder sb = new StringBuilder( text.length() * 2 );
588 for ( int i = 0; i < text.length(); i++ )
589 {
590 char c = text.charAt( i );
591 if ( isIllegalEscape( c ) )
592 {
593
594
595
596
597
598 sb.append( attribute ? "&#" : "&#" ).append( (int) c ).append(
599 ';' );
600 }
601 else
602 {
603 sb.append( c );
604 }
605 }
606 return sb.toString();
607 }
608
609 private static final class ByteConstantsHolder
610 {
611 private static final byte[] CDATA_START_BYTES;
612
613 private static final byte[] CDATA_END_BYTES;
614
615 private static final byte[] CDATA_ESCAPE_STRING_BYTES;
616
617 private static final byte[] AMP_BYTES;
618
619 static
620 {
621 CDATA_START_BYTES = "<![CDATA[".getBytes( UTF_8 );
622 CDATA_END_BYTES = "]]>".getBytes( UTF_8 );
623 CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes( UTF_8 );
624 AMP_BYTES = "&#".getBytes( UTF_8 );
625 }
626 }
627 }