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