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