View Javadoc

1   package org.apache.maven.plugin.surefire.report;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
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   * XML format reporter writing to <code>TEST-<i>reportName</i>[-<i>suffix</i>].xml</code> file like written and read
44   * by Ant's <a href="http://ant.apache.org/manual/Tasks/junit.html"><code>&lt;junit&gt;</code></a> and
45   * <a href="http://ant.apache.org/manual/Tasks/junitreport.html"><code>&lt;junitreport&gt;</code></a> tasks,
46   * then supported by many tools like CI servers.
47   * <p/>
48   * <pre>&lt;?xml version="1.0" encoding="UTF-8"?>
49   * &lt;testsuite name="<i>suite name</i>" [group="<i>group</i>"] tests="<i>0</i>" failures="<i>0</i>" errors="<i>0</i>" skipped="<i>0</i>" time="<i>0,###.###</i>">
50   *  &lt;properties>
51   *    &lt;property name="<i>name</i>" value="<i>value</i>"/>
52   *    [...]
53   *  &lt;/properties>
54   *  &lt;testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"/>
55   *  &lt;testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]">
56   *    &lt;<b>error</b> message="<i>message</i>" type="<i>exception class name</i>"><i>stacktrace</i>&lt;/error>
57   *    &lt;system-out><i>system out content (present only if not empty)</i>&lt;/system-out>
58   *    &lt;system-err><i>system err content (present only if not empty)</i>&lt;/system-err>
59   *  &lt;/testcase>
60   *  &lt;testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]">
61   *    &lt;<b>failure</b> message="<i>message</i>" type="<i>exception class name</i>"><i>stacktrace</i>&lt;/failure>
62   *    &lt;system-out><i>system out content (present only if not empty)</i>&lt;/system-out>
63   *    &lt;system-err><i>system err content (present only if not empty)</i>&lt;/system-err>
64   *  &lt;/testcase>
65   *  &lt;testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]">
66   *    &lt;<b>skipped</b>/>
67   *  &lt;/testcase>
68   *  [...]</pre>
69   *
70   * @author Kristian Rosenvold
71   * @see <a href="http://wiki.apache.org/ant/Proposals/EnhancedTestReports">Ant's format enhancement proposal</a>
72   *      (not yet implemented by Ant 1.8.2)
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(); // TestSuite
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         //noinspection ResultOfMethodCallIgnored
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             //noinspection ThrowableResultOfMethodCallIgnored
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(); // entry type
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(); // test element
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( "" ); // Cheat sax to emit element
286                 outputStreamWriter.flush();
287                 utf8RecodingDeferredFileOutputStream.close();
288                 eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES ); // emit cdata
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      * Adds system properties to the XML report.
303      * <p/>
304      *
305      * @param xmlWriter The test suite to report to
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      * Handle stuff that may pop up in java that is not legal in xml
343      *
344      * @param message   The string
345      * @param attribute true if the escaped value is inside an attribute
346      * @return The escaped string
347      */
348     private static String extraEscape( String message, boolean attribute )
349     {
350         // Someday convert to xml 1.1 which handles everything but 0 inside string
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                 // uh-oh!  This character is illegal in XML 1.0!
391                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
392                 // we're going to deliberately doubly-XML escape it...
393                 // there's nothing better we can do! :-(
394                 // SUREFIRE-456
395                 out.write( ByteConstantsHolder.AMP_BYTES );
396                 out.write( String.valueOf( b ).getBytes( ENCODING ) );
397                 out.write( ';' ); // & Will be encoded to amp inside xml encodingSHO
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                 // uh-oh!  This character is illegal in XML 1.0!
441                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
442                 // we're going to deliberately doubly-XML escape it...
443                 // there's nothing better we can do! :-(
444                 // SUREFIRE-456
445                 sb.append( attribute ? "&#" : "&amp#" ).append( (int) c ).append(
446                     ';' ); // & Will be encoded to amp inside xml encodingSHO
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 = "&amp#".getBytes( ENCODING );
474             }
475             catch ( UnsupportedEncodingException e )
476             {
477                 throw new RuntimeException( e );
478             }
479         }
480     }
481 }