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 java.io.File;
23 import java.io.FileOutputStream;
24 import java.io.FilterOutputStream;
25 import java.io.IOException;
26 import java.io.OutputStream;
27 import java.io.OutputStreamWriter;
28 import java.io.UnsupportedEncodingException;
29 import java.nio.charset.Charset;
30 import java.util.Enumeration;
31 import java.util.Properties;
32 import java.util.StringTokenizer;
33
34 import org.apache.maven.shared.utils.io.IOUtil;
35 import org.apache.maven.shared.utils.xml.XMLWriter;
36 import org.apache.maven.surefire.report.ReportEntry;
37 import org.apache.maven.surefire.report.ReporterException;
38 import org.apache.maven.surefire.report.SafeThrowable;
39
40 import static org.apache.maven.plugin.surefire.report.FileReporterUtils.stripIllegalFilenameChars;
41
42
43
44
45
46
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 public class StatelessXmlReporter
75 {
76
77 private static final String ENCODING = "UTF-8";
78
79 private static final Charset ENCODING_CS = Charset.forName( ENCODING );
80
81 private final File reportsDirectory;
82
83 private final String reportNameSuffix;
84
85 private final boolean trimStackTrace;
86
87
88 public StatelessXmlReporter( File reportsDirectory, String reportNameSuffix, boolean trimStackTrace )
89 {
90 this.reportsDirectory = reportsDirectory;
91 this.reportNameSuffix = reportNameSuffix;
92 this.trimStackTrace = trimStackTrace;
93 }
94
95 public void testSetCompleted( WrappedReportEntry testSetReportEntry, TestSetStats testSetStats )
96 throws ReporterException
97 {
98
99 FileOutputStream outputStream = getOutputStream( testSetReportEntry );
100 OutputStreamWriter fw = getWriter( outputStream );
101 try
102 {
103
104 org.apache.maven.shared.utils.xml.XMLWriter ppw =
105 new org.apache.maven.shared.utils.xml.PrettyPrintXMLWriter( fw );
106 ppw.setEncoding( ENCODING );
107
108 createTestSuiteElement( ppw, testSetReportEntry, testSetStats, reportNameSuffix );
109
110 showProperties( ppw );
111
112 for ( WrappedReportEntry entry : testSetStats.getReportEntries() )
113 {
114 if ( ReportEntryType.success.equals( entry.getReportEntryType() ) )
115 {
116 startTestElement( ppw, entry, reportNameSuffix );
117 ppw.endElement();
118 }
119 else
120 {
121 getTestProblems( fw, ppw, entry, trimStackTrace, reportNameSuffix, outputStream );
122 }
123
124 }
125 ppw.endElement();
126
127 }
128 finally
129 {
130 IOUtil.close( fw );
131 }
132 }
133
134 private OutputStreamWriter getWriter( FileOutputStream fos )
135 {
136 return new OutputStreamWriter( fos, ENCODING_CS );
137 }
138
139 private FileOutputStream getOutputStream( WrappedReportEntry testSetReportEntry )
140 {
141 File reportFile = getReportFile( testSetReportEntry, reportsDirectory, reportNameSuffix );
142
143 File reportDir = reportFile.getParentFile();
144
145
146 reportDir.mkdirs();
147
148 try
149 {
150
151 return new FileOutputStream( reportFile );
152 }
153 catch ( Exception e )
154 {
155 throw new ReporterException( "When writing report", e );
156 }
157 }
158
159 private File getReportFile( ReportEntry report, File reportsDirectory, String reportNameSuffix )
160 {
161 File reportFile;
162
163 if ( reportNameSuffix != null && reportNameSuffix.length() > 0 )
164 {
165 reportFile = new File( reportsDirectory, stripIllegalFilenameChars(
166 "TEST-" + report.getName() + "-" + reportNameSuffix + ".xml" ) );
167 }
168 else
169 {
170 reportFile = new File( reportsDirectory, stripIllegalFilenameChars( "TEST-" + report.getName() + ".xml" ) );
171 }
172
173 return reportFile;
174 }
175
176 private static void startTestElement( XMLWriter ppw, WrappedReportEntry report, String reportNameSuffix )
177 {
178 ppw.startElement( "testcase" );
179 ppw.addAttribute( "name", report.getReportName() );
180 if ( report.getGroup() != null )
181 {
182 ppw.addAttribute( "group", report.getGroup() );
183 }
184 if ( report.getSourceName() != null )
185 {
186 if ( reportNameSuffix != null && reportNameSuffix.length() > 0 )
187 {
188 ppw.addAttribute( "classname", report.getSourceName() + "(" + reportNameSuffix + ")" );
189 }
190 else
191 {
192 ppw.addAttribute( "classname", report.getSourceName() );
193 }
194 }
195 ppw.addAttribute( "time", report.elapsedTimeAsString() );
196 }
197
198 private static void createTestSuiteElement( XMLWriter ppw, WrappedReportEntry report, TestSetStats testSetStats,
199 String reportNameSuffix1 )
200 {
201 ppw.startElement( "testsuite" );
202
203 ppw.addAttribute( "name", report.getReportName( reportNameSuffix1 ) );
204
205 if ( report.getGroup() != null )
206 {
207 ppw.addAttribute( "group", report.getGroup() );
208 }
209
210 ppw.addAttribute( "time", testSetStats.getElapsedForTestSet() );
211
212 ppw.addAttribute( "tests", String.valueOf( testSetStats.getCompletedCount() ) );
213
214 ppw.addAttribute( "errors", String.valueOf( testSetStats.getErrors() ) );
215
216 ppw.addAttribute( "skipped", String.valueOf( testSetStats.getSkipped() ) );
217
218 ppw.addAttribute( "failures", String.valueOf( testSetStats.getFailures() ) );
219
220 }
221
222
223 private void getTestProblems( OutputStreamWriter outputStreamWriter, XMLWriter ppw, WrappedReportEntry report,
224 boolean trimStackTrace, String reportNameSuffix, FileOutputStream fw )
225 {
226
227 startTestElement( ppw, report, reportNameSuffix );
228
229 ppw.startElement( report.getReportEntryType().name() );
230
231 String stackTrace = report.getStackTrace( trimStackTrace );
232
233 if ( report.getMessage() != null && report.getMessage().length() > 0 )
234 {
235 ppw.addAttribute( "message", extraEscape( report.getMessage(), true ) );
236 }
237
238 if ( report.getStackTraceWriter() != null )
239 {
240
241 SafeThrowable t = report.getStackTraceWriter().getThrowable();
242 if ( t != null )
243 {
244 if ( t.getMessage() != null )
245 {
246 ppw.addAttribute( "type", ( stackTrace.contains( ":" )
247 ? stackTrace.substring( 0, stackTrace.indexOf( ":" ) )
248 : stackTrace ) );
249 }
250 else
251 {
252 ppw.addAttribute( "type", new StringTokenizer( stackTrace ).nextToken() );
253 }
254 }
255 }
256
257 if ( stackTrace != null )
258 {
259 ppw.writeText( extraEscape( stackTrace, false ) );
260 }
261
262 ppw.endElement();
263
264 EncodingOutputStream eos = new EncodingOutputStream( fw );
265
266 addOutputStreamElement( outputStreamWriter, fw, eos, ppw, report.getStdout(), "system-out" );
267
268 addOutputStreamElement( outputStreamWriter, fw, eos, ppw, report.getStdErr(), "system-err" );
269
270 ppw.endElement();
271 }
272
273 private void addOutputStreamElement( OutputStreamWriter outputStreamWriter, OutputStream fw,
274 EncodingOutputStream eos, XMLWriter xmlWriter,
275 Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
276 String name )
277 {
278 if ( utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0 )
279 {
280
281 xmlWriter.startElement( name );
282
283 try
284 {
285 xmlWriter.writeText( "" );
286 outputStreamWriter.flush();
287 utf8RecodingDeferredFileOutputStream.close();
288 eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES );
289 utf8RecodingDeferredFileOutputStream.writeTo( eos );
290 eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
291 eos.flush();
292 }
293 catch ( IOException e )
294 {
295 throw new ReporterException( "When writing xml report stdout/stderr", e );
296 }
297 xmlWriter.endElement();
298 }
299 }
300
301
302
303
304
305
306
307 private void showProperties( XMLWriter xmlWriter )
308 {
309 xmlWriter.startElement( "properties" );
310
311 Properties systemProperties = System.getProperties();
312
313 if ( systemProperties != null )
314 {
315 Enumeration<?> propertyKeys = systemProperties.propertyNames();
316
317 while ( propertyKeys.hasMoreElements() )
318 {
319 String key = (String) propertyKeys.nextElement();
320
321 String value = systemProperties.getProperty( key );
322
323 if ( value == null )
324 {
325 value = "null";
326 }
327
328 xmlWriter.startElement( "property" );
329
330 xmlWriter.addAttribute( "name", key );
331
332 xmlWriter.addAttribute( "value", extraEscape( value, true ) );
333
334 xmlWriter.endElement();
335
336 }
337 }
338 xmlWriter.endElement();
339 }
340
341
342
343
344
345
346
347
348 private static String extraEscape( String message, boolean attribute )
349 {
350
351 if ( !containsEscapesIllegalnXml10( message ) )
352 {
353 return message;
354 }
355 return escapeXml( message, attribute );
356 }
357
358 private static class EncodingOutputStream
359 extends FilterOutputStream
360 {
361 private int c1;
362
363 private int c2;
364
365 public EncodingOutputStream( OutputStream out )
366 {
367 super( out );
368 }
369
370 public OutputStream getUnderlying()
371 {
372 return out;
373 }
374
375 private boolean isCdataEndBlock( int c )
376 {
377 return c1 == ']' && c2 == ']' && c == '>';
378 }
379
380 @Override
381 public void write( int b )
382 throws IOException
383 {
384 if ( isCdataEndBlock( b ) )
385 {
386 out.write( ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES );
387 }
388 else if ( isIllegalEscape( b ) )
389 {
390
391
392
393
394
395 out.write( ByteConstantsHolder.AMP_BYTES );
396 out.write( String.valueOf( b ).getBytes( ENCODING ) );
397 out.write( ';' );
398 }
399 else
400 {
401 out.write( b );
402 }
403 c1 = c2;
404 c2 = b;
405 }
406 }
407
408 private static boolean containsEscapesIllegalnXml10( String message )
409 {
410 int size = message.length();
411 for ( int i = 0; i < size; i++ )
412 {
413 if ( isIllegalEscape( message.charAt( i ) ) )
414 {
415 return true;
416 }
417
418 }
419 return false;
420 }
421
422 private static boolean isIllegalEscape( char c )
423 {
424 return isIllegalEscape( (int) c );
425 }
426
427 private static boolean isIllegalEscape( int c )
428 {
429 return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
430 }
431
432 private static String escapeXml( String text, boolean attribute )
433 {
434 StringBuilder sb = new StringBuilder( text.length() * 2 );
435 for ( int i = 0; i < text.length(); i++ )
436 {
437 char c = text.charAt( i );
438 if ( isIllegalEscape( c ) )
439 {
440
441
442
443
444
445 sb.append( attribute ? "&#" : "&#" ).append( (int) c ).append(
446 ';' );
447 }
448 else
449 {
450 sb.append( c );
451 }
452 }
453 return sb.toString();
454 }
455
456 private static class ByteConstantsHolder
457 {
458 private static final byte[] CDATA_START_BYTES;
459
460 private static final byte[] CDATA_END_BYTES;
461
462 private static final byte[] CDATA_ESCAPE_STRING_BYTES;
463
464 private static final byte[] AMP_BYTES;
465
466 static
467 {
468 try
469 {
470 CDATA_START_BYTES = "<![CDATA[".getBytes( ENCODING );
471 CDATA_END_BYTES = "]]>".getBytes( ENCODING );
472 CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes( ENCODING );
473 AMP_BYTES = "&#".getBytes( ENCODING );
474 }
475 catch ( UnsupportedEncodingException e )
476 {
477 throw new RuntimeException( e );
478 }
479 }
480 }
481 }