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     public void startElement( String uri, String localName, String qName, Attributes attributes )
121         throws SAXException
122     {
123         if ( valid )
124         {
125             try
126             {
127                 // todo: use jdk7 switch-case
128                 if ( "testsuite".equals( qName ) )
129                 {
130                     defaultSuite = new ReportTestSuite();
131                     currentSuite = defaultSuite;
132 
133                     try
134                     {
135                         Number time = numberFormat.parse( attributes.getValue( "time" ) );
136 
137                         defaultSuite.setTimeElapsed( time.floatValue() );
138                     }
139                     catch ( NullPointerException e )
140                     {
141                         consoleLogger.error( "WARNING: no time attribute found on testsuite element" );
142                     }
143 
144                     final String name = attributes.getValue( "name" );
145                     final String group = attributes.getValue( "group" );
146                     defaultSuite.setFullClassName( StringUtils.isBlank( group )
147                                                        ? /*name is full class name*/ name
148                                                        : /*group is package name*/ group + "." + name );
149 
150                     suites.add( defaultSuite );
151                     classesToSuitesIndex.put( defaultSuite.getFullClassName(), suites.size() - 1 );
152                 }
153                 else if ( "testcase".equals( qName ) )
154                 {
155                     currentElement = new StringBuilder();
156 
157                     testCase = new ReportTestCase()
158                         .setName( attributes.getValue( "name" ) );
159 
160                     String fullClassName = attributes.getValue( "classname" );
161 
162                     // if the testcase declares its own classname, it may need to belong to its own suite
163                     if ( fullClassName != null )
164                     {
165                         Integer currentSuiteIndex = classesToSuitesIndex.get( fullClassName );
166                         if ( currentSuiteIndex == null )
167                         {
168                             currentSuite = new ReportTestSuite()
169                                 .setFullClassName( fullClassName );
170                             suites.add( currentSuite );
171                             classesToSuitesIndex.put( fullClassName, suites.size() - 1 );
172                         }
173                         else
174                         {
175                             currentSuite = suites.get( currentSuiteIndex );
176                         }
177                     }
178 
179                     final String timeAsString = attributes.getValue( "time" );
180                     final Number time = StringUtils.isBlank( timeAsString ) ? 0 : numberFormat.parse( timeAsString );
181 
182                     testCase.setFullClassName( currentSuite.getFullClassName() )
183                         .setClassName( currentSuite.getName() )
184                         .setFullName( currentSuite.getFullClassName() + "." + testCase.getName() )
185                         .setTime( time.floatValue() );
186 
187                     if ( currentSuite != defaultSuite )
188                     {
189                         currentSuite.setTimeElapsed( testCase.getTime() + currentSuite.getTimeElapsed() );
190                     }
191                 }
192                 else if ( "failure".equals( qName ) )
193                 {
194                     testCase.setFailure( attributes.getValue( "message" ), attributes.getValue( "type" ) );
195                     currentSuite.incrementNumberOfFailures();
196                 }
197                 else if ( "error".equals( qName ) )
198                 {
199                     testCase.setFailure( attributes.getValue( "message" ), attributes.getValue( "type" ) );
200                     currentSuite.incrementNumberOfErrors();
201                 }
202                 else if ( "skipped".equals( qName ) )
203                 {
204                     final String message = attributes.getValue( "message" );
205                     testCase.setFailure( message != null ? message : "skipped", "skipped" );
206                     currentSuite.incrementNumberOfSkipped();
207                 }
208                 else if ( "flakyFailure".equals( qName ) || "flakyError".equals( qName ) )
209                 {
210                     currentSuite.incrementNumberOfFlakes();
211                 }
212                 else if ( "failsafe-summary".equals( qName ) )
213                 {
214                     valid = false;
215                 }
216             }
217             catch ( ParseException e )
218             {
219                 throw new SAXException( e.getMessage(), e );
220             }
221         }
222     }
223 
224     /**
225      * {@inheritDoc}
226      */
227     public void endElement( String uri, String localName, String qName )
228         throws SAXException
229     {
230         // todo: use jdk7 switch-case
231         if ( "testcase".equals( qName ) )
232         {
233             currentSuite.getTestCases().add( testCase );
234         }
235         else if ( "failure".equals( qName ) || "error".equals( qName ) )
236         {
237             testCase.setFailureDetail( currentElement.toString() )
238                 .setFailureErrorLine( parseErrorLine( currentElement, testCase.getFullClassName() ) );
239         }
240         else if ( "time".equals( qName ) )
241         {
242             try
243             {
244                 defaultSuite.setTimeElapsed( numberFormat.parse( currentElement.toString() ).floatValue() );
245             }
246             catch ( ParseException e )
247             {
248                 throw new SAXException( e.getMessage(), e );
249             }
250         }
251         // TODO extract real skipped reasons
252     }
253 
254     /**
255      * {@inheritDoc}
256      */
257     public void characters( char[] ch, int start, int length )
258         throws SAXException
259     {
260         assert start >= 0;
261         assert length >= 0;
262         if ( valid && isNotBlank( start, length, ch ) )
263         {
264             currentElement.append( ch, start, length );
265         }
266     }
267 
268     public boolean isValid()
269     {
270         return valid;
271     }
272 
273     static boolean isNotBlank( int from, int len, char... s )
274     {
275         assert from >= 0;
276         assert len >= 0;
277         if ( s != null )
278         {
279             for ( int i = 0; i < len; i++ )
280             {
281                 char c = s[from++];
282                 if ( c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f' )
283                 {
284                     return true;
285                 }
286             }
287         }
288         return false;
289     }
290 
291     static boolean isNumeric( StringBuilder s, final int from, final int to )
292     {
293         assert from >= 0;
294         assert from <= to;
295         for ( int i = from; i != to; )
296         {
297             if ( !Character.isDigit( s.charAt( i++ ) ) )
298             {
299                 return false;
300             }
301         }
302         return from != to;
303     }
304 
305     static String parseErrorLine( StringBuilder currentElement, String fullClassName )
306     {
307         final String[] linePatterns = { "at " + fullClassName + '.', "at " + fullClassName + '$' };
308         int[] indexes = lastIndexOf( currentElement, linePatterns );
309         int patternStartsAt = indexes[0];
310         if ( patternStartsAt != -1 )
311         {
312             int searchFrom = patternStartsAt + ( linePatterns[ indexes[1] ] ).length();
313             searchFrom = 1 + currentElement.indexOf( ":", searchFrom );
314             int searchTo = currentElement.indexOf( ")", searchFrom );
315             return isNumeric( currentElement, searchFrom, searchTo )
316                 ? currentElement.substring( searchFrom, searchTo )
317                 : "";
318         }
319         return "";
320     }
321 
322     static int[] lastIndexOf( StringBuilder source, String... linePatterns )
323     {
324         int end = source.indexOf( "Caused by:" );
325         if ( end == -1 )
326         {
327             end = source.length();
328         }
329         int startsAt = -1;
330         int pattern = -1;
331         for ( int i = 0; i < linePatterns.length; i++ )
332         {
333             String linePattern = linePatterns[i];
334             int currentStartsAt = source.lastIndexOf( linePattern, end );
335             if ( currentStartsAt > startsAt )
336             {
337                 startsAt = currentStartsAt;
338                 pattern = i;
339             }
340         }
341         return new int[] { startsAt, pattern };
342     }
343 }