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 org.apache.commons.io.output.DeferredFileOutputStream;
23  import org.apache.maven.shared.utils.io.IOUtil;
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.File;
30  import java.io.FileOutputStream;
31  import java.io.FilterOutputStream;
32  import java.io.IOException;
33  import java.io.OutputStream;
34  import java.io.OutputStreamWriter;
35  import java.util.Enumeration;
36  import java.util.Properties;
37  import java.util.StringTokenizer;
38  
39  import static org.apache.maven.plugin.surefire.report.FileReporterUtils.stripIllegalFilenameChars;
40  
41  /**
42   * XML format reporter writing to <code>TEST-<i>reportName</i>[-<i>suffix</i>].xml</code> file like written and read
43   * by Ant's <a href="http://ant.apache.org/manual/Tasks/junit.html"><code>&lt;junit&gt;</code></a> and
44   * <a href="http://ant.apache.org/manual/Tasks/junitreport.html"><code>&lt;junitreport&gt;</code></a> tasks,
45   * then supported by many tools like CI servers.
46   * <p/>
47   * <pre>&lt;?xml version="1.0" encoding="UTF-8"?>
48   * &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>">
49   *  &lt;properties>
50   *    &lt;property name="<i>name</i>" value="<i>value</i>"/>
51   *    [...]
52   *  &lt;/properties>
53   *  &lt;testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"/>
54   *  &lt;testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]">
55   *    &lt;<b>error</b> message="<i>message</i>" type="<i>exception class name</i>"><i>stacktrace</i>&lt;/error>
56   *    &lt;system-out><i>system out content (present only if not empty)</i>&lt;/system-out>
57   *    &lt;system-err><i>system err content (present only if not empty)</i>&lt;/system-err>
58   *  &lt;/testcase>
59   *  &lt;testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]">
60   *    &lt;<b>failure</b> message="<i>message</i>" type="<i>exception class name</i>"><i>stacktrace</i>&lt;/failure>
61   *    &lt;system-out><i>system out content (present only if not empty)</i>&lt;/system-out>
62   *    &lt;system-err><i>system err content (present only if not empty)</i>&lt;/system-err>
63   *  &lt;/testcase>
64   *  &lt;testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]">
65   *    &lt;<b>skipped</b>/>
66   *  &lt;/testcase>
67   *  [...]</pre>
68   *
69   * @author Kristian Rosenvold
70   * @see <a href="http://wiki.apache.org/ant/Proposals/EnhancedTestReports">Ant's format enhancement proposal</a>
71   *      (not yet implemented by Ant 1.8.2)
72   */
73  public class StatelessXmlReporter
74  {
75  
76      private static final byte[] ampBytes = "&amp#".getBytes();
77  
78      private final File reportsDirectory;
79  
80      private final String reportNameSuffix;
81  
82      private final boolean trimStackTrace;
83  
84      private final String encoding = "UTF-8";
85  
86      public StatelessXmlReporter( File reportsDirectory, String reportNameSuffix, boolean trimStackTrace )
87      {
88          this.reportsDirectory = reportsDirectory;
89          this.reportNameSuffix = reportNameSuffix;
90          this.trimStackTrace = trimStackTrace;
91      }
92  
93      public void testSetCompleted( WrappedReportEntry testSetReportEntry, TestSetStats testSetStats )
94          throws ReporterException
95      {
96  
97          FileOutputStream outputStream = getOutputStream( testSetReportEntry );
98          OutputStreamWriter fw = getWriter( outputStream );
99          try
100         {
101 
102             org.apache.maven.shared.utils.xml.XMLWriter ppw =
103                 new org.apache.maven.shared.utils.xml.PrettyPrintXMLWriter( fw );
104             ppw.setEncoding( encoding );
105 
106             createTestSuiteElement( ppw, testSetReportEntry, testSetStats, reportNameSuffix );
107 
108             showProperties( ppw );
109 
110             for ( WrappedReportEntry entry : testSetStats.getReportEntries() )
111             {
112                 if ( ReportEntryType.success.equals( entry.getReportEntryType() ) )
113                 {
114                     startTestElement( ppw, entry, reportNameSuffix );
115                     ppw.endElement();
116                 }
117                 else
118                 {
119                     getTestProblems( fw, ppw, entry, trimStackTrace, reportNameSuffix, outputStream );
120                 }
121 
122             }
123             ppw.endElement(); // TestSuite
124 
125         }
126         finally
127         {
128             IOUtil.close( fw );
129         }
130     }
131 
132     private OutputStreamWriter getWriter( FileOutputStream fos )
133     {
134         try
135         {
136 
137             return new OutputStreamWriter( fos, encoding );
138         }
139         catch ( IOException e )
140         {
141             throw new ReporterException( "When writing report", e );
142         }
143     }
144 
145     private FileOutputStream getOutputStream( WrappedReportEntry testSetReportEntry )
146     {
147         File reportFile = getReportFile( testSetReportEntry, reportsDirectory, reportNameSuffix );
148 
149         File reportDir = reportFile.getParentFile();
150 
151         //noinspection ResultOfMethodCallIgnored
152         reportDir.mkdirs();
153 
154         try
155         {
156 
157             return new FileOutputStream( reportFile );
158         }
159         catch ( Exception e )
160         {
161             throw new ReporterException( "When writing report", e );
162         }
163     }
164 
165     private File getReportFile( ReportEntry report, File reportsDirectory, String reportNameSuffix )
166     {
167         File reportFile;
168 
169         if ( reportNameSuffix != null && reportNameSuffix.length() > 0 )
170         {
171             reportFile = new File( reportsDirectory, stripIllegalFilenameChars(
172                 "TEST-" + report.getName() + "-" + reportNameSuffix + ".xml" ) );
173         }
174         else
175         {
176             reportFile = new File( reportsDirectory, stripIllegalFilenameChars( "TEST-" + report.getName() + ".xml" ) );
177         }
178 
179         return reportFile;
180     }
181 
182     private static void startTestElement( XMLWriter ppw, WrappedReportEntry report, String reportNameSuffix )
183     {
184         ppw.startElement( "testcase" );
185         ppw.addAttribute( "name", report.getReportName() );
186         if ( report.getGroup() != null )
187         {
188             ppw.addAttribute( "group", report.getGroup() );
189         }
190         if ( report.getSourceName() != null )
191         {
192             if ( reportNameSuffix != null && reportNameSuffix.length() > 0 )
193             {
194                 ppw.addAttribute( "classname", report.getSourceName() + "(" + reportNameSuffix + ")" );
195             }
196             else
197             {
198                 ppw.addAttribute( "classname", report.getSourceName() );
199             }
200         }
201         ppw.addAttribute( "time", report.elapsedTimeAsString() );
202     }
203 
204     private static void createTestSuiteElement( XMLWriter ppw, WrappedReportEntry report, TestSetStats testSetStats,
205                                                 String reportNameSuffix1 )
206     {
207         ppw.startElement( "testsuite" );
208 
209         ppw.addAttribute( "name", report.getReportName( reportNameSuffix1 ) );
210 
211         if ( report.getGroup() != null )
212         {
213             ppw.addAttribute( "group", report.getGroup() );
214         }
215 
216         ppw.addAttribute( "time", testSetStats.getElapsedForTestSet() );
217 
218         ppw.addAttribute( "tests", String.valueOf( testSetStats.getCompletedCount() ) );
219 
220         ppw.addAttribute( "errors", String.valueOf( testSetStats.getErrors() ) );
221 
222         ppw.addAttribute( "skipped", String.valueOf( testSetStats.getSkipped() ) );
223 
224         ppw.addAttribute( "failures", String.valueOf( testSetStats.getFailures() ) );
225 
226     }
227 
228 
229     private void getTestProblems( OutputStreamWriter outputStreamWriter, XMLWriter ppw, WrappedReportEntry report,
230                                   boolean trimStackTrace, String reportNameSuffix, FileOutputStream fw )
231     {
232 
233         startTestElement( ppw, report, reportNameSuffix );
234 
235         ppw.startElement( report.getReportEntryType().name() );
236 
237         String stackTrace = report.getStackTrace( trimStackTrace );
238 
239         if ( report.getMessage() != null && report.getMessage().length() > 0 )
240         {
241             ppw.addAttribute( "message", extraEscape( report.getMessage(), true ) );
242         }
243 
244         if ( report.getStackTraceWriter() != null )
245         {
246             //noinspection ThrowableResultOfMethodCallIgnored
247             SafeThrowable t = report.getStackTraceWriter().getThrowable();
248             if ( t != null )
249             {
250                 if ( t.getMessage() != null )
251                 {
252                     ppw.addAttribute( "type", ( stackTrace.contains( ":" )
253                         ? stackTrace.substring( 0, stackTrace.indexOf( ":" ) )
254                         : stackTrace ) );
255                 }
256                 else
257                 {
258                     ppw.addAttribute( "type", new StringTokenizer( stackTrace ).nextToken() );
259                 }
260             }
261         }
262 
263         if ( stackTrace != null )
264         {
265             ppw.writeText( extraEscape( stackTrace, false ) );
266         }
267 
268         ppw.endElement(); // entry type
269 
270         EncodingOutputStream eos = new EncodingOutputStream( fw );
271 
272         addOutputStreamElement( outputStreamWriter, fw, eos, ppw, report.getStdout(), "system-out" );
273 
274         addOutputStreamElement( outputStreamWriter, fw, eos, ppw, report.getStdErr(), "system-err" );
275 
276         ppw.endElement(); // test element
277     }
278 
279     private void addOutputStreamElement( OutputStreamWriter outputStreamWriter, OutputStream fw,
280                                          EncodingOutputStream eos, XMLWriter xmlWriter, DeferredFileOutputStream stdOut,
281                                          String name )
282     {
283         if ( stdOut != null && stdOut.getByteCount() > 0 )
284         {
285 
286             xmlWriter.startElement( name );
287 
288             try
289             {
290                 xmlWriter.writeText( "" ); // Cheat sax to emit element
291                 outputStreamWriter.flush();
292                 stdOut.close();
293                 stdOut.writeTo( eos );
294                 eos.flush();
295             }
296             catch ( IOException e )
297             {
298                 throw new ReporterException( "When writing xml report stdout/stderr", e );
299             }
300             xmlWriter.endElement();
301         }
302     }
303 
304     /**
305      * Adds system properties to the XML report.
306      * <p/>
307      *
308      * @param xmlWriter The test suite to report to
309      */
310     private void showProperties( XMLWriter xmlWriter )
311     {
312         xmlWriter.startElement( "properties" );
313 
314         Properties systemProperties = System.getProperties();
315 
316         if ( systemProperties != null )
317         {
318             Enumeration<?> propertyKeys = systemProperties.propertyNames();
319 
320             while ( propertyKeys.hasMoreElements() )
321             {
322                 String key = (String) propertyKeys.nextElement();
323 
324                 String value = systemProperties.getProperty( key );
325 
326                 if ( value == null )
327                 {
328                     value = "null";
329                 }
330 
331                 xmlWriter.startElement( "property" );
332 
333                 xmlWriter.addAttribute( "name", key );
334 
335                 xmlWriter.addAttribute( "value", extraEscape( value, true ) );
336 
337                 xmlWriter.endElement();
338 
339             }
340         }
341         xmlWriter.endElement();
342     }
343 
344     /**
345      * Handle stuff that may pop up in java that is not legal in xml
346      *
347      * @param message   The string
348      * @param attribute true if the escaped value is inside an attribute
349      * @return The escaped string
350      */
351     private static String extraEscape( String message, boolean attribute )
352     {
353         // Someday convert to xml 1.1 which handles everything but 0 inside string
354         if ( !containsEscapesIllegalnXml10( message ) )
355         {
356             return message;
357         }
358         return escapeXml( message, attribute );
359     }
360 
361     private static class EncodingOutputStream
362         extends FilterOutputStream
363     {
364 
365         public EncodingOutputStream( OutputStream out )
366         {
367             super( out );
368         }
369 
370         @Override
371         public void write( int b )
372             throws IOException
373         {
374             if ( isIllegalEscape( b ) )
375             {
376                 // uh-oh!  This character is illegal in XML 1.0!
377                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
378                 // we're going to deliberately doubly-XML escape it...
379                 // there's nothing better we can do! :-(
380                 // SUREFIRE-456
381                 out.write( ampBytes );
382                 out.write( b );
383                 out.write( ';' ); // & Will be encoded to amp inside xml encodingSHO
384             }
385             else if ( '<' == b )
386             {
387                 out.write( '&' );
388                 out.write( 'l' );
389                 out.write( 't' );
390                 out.write( ';' ); // & Will be encoded to amp inside xml encodingSHO
391             }
392             else if ( '>' == b )
393             {
394                 out.write( '&' );
395                 out.write( 'g' );
396                 out.write( 't' );
397                 out.write( ';' ); // & Will be encoded to amp inside xml encodingSHO
398             }
399             else if ( '&' == b )
400             {
401                 out.write( '&' );
402                 out.write( 'a' );
403                 out.write( 'm' );
404                 out.write( 'p' );
405                 out.write( ';' ); // & Will be encoded to amp inside xml encodingSHO
406             }
407             else
408             {
409                 out.write( b );    //To change body of overridden methods use File | Settings | File Templates.
410             }
411         }
412     }
413 
414     private static boolean containsEscapesIllegalnXml10( String message )
415     {
416         int size = message.length();
417         for ( int i = 0; i < size; i++ )
418         {
419             if ( isIllegalEscape( message.charAt( i ) ) )
420             {
421                 return true;
422             }
423 
424         }
425         return false;
426     }
427 
428     private static boolean isIllegalEscape( char c )
429     {
430         return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
431     }
432 
433     private static boolean isIllegalEscape( int c )
434     {
435         return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
436     }
437 
438     private static String escapeXml( String text, boolean attribute )
439     {
440         StringBuilder sb = new StringBuilder( text.length() * 2 );
441         for ( int i = 0; i < text.length(); i++ )
442         {
443             char c = text.charAt( i );
444             if ( isIllegalEscape( c ) )
445             {
446                 // uh-oh!  This character is illegal in XML 1.0!
447                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
448                 // we're going to deliberately doubly-XML escape it...
449                 // there's nothing better we can do! :-(
450                 // SUREFIRE-456
451                 sb.append( attribute ? "&#" : "&amp#" ).append( (int) c ).append(
452                     ';' ); // & Will be encoded to amp inside xml encodingSHO
453             }
454             else
455             {
456                 sb.append( c );
457             }
458         }
459         return sb.toString();
460     }
461 
462 }