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