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