View Javadoc
1   package org.apache.maven.plugins.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.FileInputStream;
24  import java.io.IOException;
25  import java.io.InputStreamReader;
26  import java.text.NumberFormat;
27  import java.text.ParseException;
28  import java.util.ArrayList;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Map;
33  
34  import javax.xml.parsers.ParserConfigurationException;
35  import javax.xml.parsers.SAXParser;
36  import javax.xml.parsers.SAXParserFactory;
37  
38  import org.apache.maven.shared.utils.StringUtils;
39  import org.xml.sax.Attributes;
40  import org.xml.sax.InputSource;
41  import org.xml.sax.SAXException;
42  import org.xml.sax.helpers.DefaultHandler;
43  
44  /**
45   *
46   */
47  public final class TestSuiteXmlParser
48      extends DefaultHandler
49  {
50      private final NumberFormat numberFormat = NumberFormat.getInstance( Locale.ENGLISH );
51  
52      private ReportTestSuite defaultSuite;
53  
54      private ReportTestSuite currentSuite;
55  
56      private Map<String, Integer> classesToSuitesIndex;
57  
58      private List<ReportTestSuite> suites;
59  
60      private StringBuilder currentElement;
61  
62      private ReportTestCase testCase;
63  
64      private boolean valid;
65  
66      public List<ReportTestSuite> parse( String xmlPath )
67          throws ParserConfigurationException, SAXException, IOException
68      {
69          FileInputStream fileInputStream = new FileInputStream( new File( xmlPath ) );
70          InputStreamReader  inputStreamReader = new InputStreamReader( fileInputStream, "UTF-8" );
71  
72          try
73          {
74              return parse( inputStreamReader );
75          }
76          finally
77          {
78              inputStreamReader.close();
79              fileInputStream.close();
80          }
81      }
82  
83      public List<ReportTestSuite> parse( InputStreamReader stream )
84          throws ParserConfigurationException, SAXException, IOException
85      {
86          SAXParserFactory factory = SAXParserFactory.newInstance();
87  
88          SAXParser saxParser = factory.newSAXParser();
89  
90          valid = true;
91  
92          classesToSuitesIndex = new HashMap<String, Integer>();
93          suites = new ArrayList<ReportTestSuite>();
94  
95          saxParser.parse( new InputSource( stream ), this );
96  
97          if ( currentSuite != defaultSuite )
98          { // omit the defaultSuite if it's empty and there are alternatives
99              if ( defaultSuite.getNumberOfTests() == 0 )
100             {
101                 suites.remove( classesToSuitesIndex.get( defaultSuite.getFullClassName() ).intValue() );
102             }
103         }
104 
105         return suites;
106     }
107 
108     /**
109      * {@inheritDoc}
110      */
111     public void startElement( String uri, String localName, String qName, Attributes attributes )
112         throws SAXException
113     {
114         if ( valid )
115         {
116             try
117             {
118                 if ( "testsuite".equals( qName ) )
119                 {
120                     defaultSuite = new ReportTestSuite();
121                     currentSuite = defaultSuite;
122 
123                     try
124                     {
125                         Number time = numberFormat.parse( attributes.getValue( "time" ) );
126 
127                         defaultSuite.setTimeElapsed( time.floatValue() );
128                     }
129                     catch ( NullPointerException e )
130                     {
131                         System.err.println( "WARNING: no time attribute found on testsuite element" );
132                     }
133 
134                     final String name = attributes.getValue( "name" );
135                     final String group = attributes.getValue( "group" );
136                     defaultSuite.setFullClassName( StringUtils.isBlank( group )
137                                                        ? /*name is full class name*/ name
138                                                        : /*group is package name*/ group + "." + name );
139 
140                     suites.add( defaultSuite );
141                     classesToSuitesIndex.put( defaultSuite.getFullClassName(), suites.size() - 1 );
142                 }
143                 else if ( "testcase".equals( qName ) )
144                 {
145                     currentElement = new StringBuilder();
146 
147                     testCase = new ReportTestCase()
148                         .setName( attributes.getValue( "name" ) );
149 
150                     String fullClassName = attributes.getValue( "classname" );
151 
152                     // if the testcase declares its own classname, it may need to belong to its own suite
153                     if ( fullClassName != null )
154                     {
155                         Integer currentSuiteIndex = classesToSuitesIndex.get( fullClassName );
156                         if ( currentSuiteIndex == null )
157                         {
158                             currentSuite = new ReportTestSuite()
159                                 .setFullClassName( fullClassName );
160                             suites.add( currentSuite );
161                             classesToSuitesIndex.put( fullClassName, suites.size() - 1 );
162                         }
163                         else
164                         {
165                             currentSuite = suites.get( currentSuiteIndex );
166                         }
167                     }
168 
169                     final String timeAsString = attributes.getValue( "time" );
170                     final Number time = StringUtils.isBlank( timeAsString ) ? 0 : numberFormat.parse( timeAsString );
171 
172                     testCase.setFullClassName( currentSuite.getFullClassName() )
173                         .setClassName( currentSuite.getName() )
174                         .setFullName( currentSuite.getFullClassName() + "." + testCase.getName() )
175                         .setTime( time.floatValue() );
176 
177                     if ( currentSuite != defaultSuite )
178                     {
179                         currentSuite.setTimeElapsed( testCase.getTime() + currentSuite.getTimeElapsed() );
180                     }
181                 }
182                 else if ( "failure".equals( qName ) )
183                 {
184                     testCase.setFailure( attributes.getValue( "message" ), attributes.getValue( "type" ) );
185                     currentSuite.incrementNumberOfFailures();
186                 }
187                 else if ( "error".equals( qName ) )
188                 {
189                     testCase.setFailure( attributes.getValue( "message" ), attributes.getValue( "type" ) );
190                     currentSuite.incrementNumberOfErrors();
191                 }
192                 else if ( "skipped".equals( qName ) )
193                 {
194                     final String message = attributes.getValue( "message" );
195                     testCase.setFailure( message != null ? message : "skipped", "skipped" );
196                     currentSuite.incrementNumberOfSkipped();
197                 }
198                 else if ( "flakyFailure".equals( qName ) || "flakyError".equals( qName ) )
199                 {
200                     currentSuite.incrementNumberOfFlakes();
201                 }
202                 else if ( "failsafe-summary".equals( qName ) )
203                 {
204                     valid = false;
205                 }
206             }
207             catch ( ParseException e )
208             {
209                 throw new SAXException( e.getMessage(), e );
210             }
211         }
212     }
213 
214     /**
215      * {@inheritDoc}
216      */
217     public void endElement( String uri, String localName, String qName )
218         throws SAXException
219     {
220         if ( "testcase".equals( qName ) )
221         {
222             currentSuite.getTestCases().add( testCase );
223         }
224         else if ( "failure".equals( qName ) || "error".equals( qName ) )
225         {
226             testCase.setFailureDetail( currentElement.toString() )
227                 .setFailureErrorLine( parseErrorLine( currentElement, testCase.getFullClassName() ) );
228         }
229         else if ( "time".equals( qName ) )
230         {
231             try
232             {
233                 defaultSuite.setTimeElapsed( numberFormat.parse( currentElement.toString() ).floatValue() );
234             }
235             catch ( ParseException e )
236             {
237                 throw new SAXException( e.getMessage(), e );
238             }
239         }
240         // TODO extract real skipped reasons
241     }
242 
243     /**
244      * {@inheritDoc}
245      */
246     public void characters( char[] ch, int start, int length )
247         throws SAXException
248     {
249         assert start >= 0;
250         assert length >= 0;
251         if ( valid && isNotBlank( start, length, ch ) )
252         {
253             currentElement.append( ch, start, length );
254         }
255     }
256 
257     public boolean isValid()
258     {
259         return valid;
260     }
261 
262     static boolean isNotBlank( int from, int len, char... s )
263     {
264         assert from >= 0;
265         assert len >= 0;
266         if ( s != null )
267         {
268             for ( int i = 0; i < len; i++ )
269             {
270                 char c = s[from++];
271                 if ( c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f' )
272                 {
273                     return true;
274                 }
275             }
276         }
277         return false;
278     }
279 
280     static boolean isNumeric( StringBuilder s, final int from, final int to )
281     {
282         assert from >= 0;
283         assert from <= to;
284         for ( int i = from; i != to; )
285         {
286             if ( !Character.isDigit( s.charAt( i++ ) ) )
287             {
288                 return false;
289             }
290         }
291         return from != to;
292     }
293 
294     static String parseErrorLine( StringBuilder currentElement, String fullClassName )
295     {
296         final String[] linePatterns = { "at " + fullClassName + '.', "at " + fullClassName + '$' };
297         int[] indexes = lastIndexOf( currentElement, linePatterns );
298         int patternStartsAt = indexes[0];
299         if ( patternStartsAt != -1 )
300         {
301             int searchFrom = patternStartsAt + ( linePatterns[ indexes[1] ] ).length();
302             searchFrom = 1 + currentElement.indexOf( ":", searchFrom );
303             int searchTo = currentElement.indexOf( ")", searchFrom );
304             return isNumeric( currentElement, searchFrom, searchTo )
305                 ? currentElement.substring( searchFrom, searchTo )
306                 : "";
307         }
308         return "";
309     }
310 
311     static int[] lastIndexOf( StringBuilder source, String... linePatterns )
312     {
313         int end = source.indexOf( "Caused by:" );
314         if ( end == -1 )
315         {
316             end = source.length();
317         }
318         int startsAt = -1;
319         int pattern = -1;
320         for ( int i = 0; i < linePatterns.length; i++ )
321         {
322             String linePattern = linePatterns[i];
323             int currentStartsAt = source.lastIndexOf( linePattern, end );
324             if ( currentStartsAt > startsAt )
325             {
326                 startsAt = currentStartsAt;
327                 pattern = i;
328             }
329         }
330         return new int[] { startsAt, pattern };
331     }
332 }