View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.surefire.report;
20  
21  import javax.xml.parsers.ParserConfigurationException;
22  import javax.xml.parsers.SAXParser;
23  import javax.xml.parsers.SAXParserFactory;
24  
25  import java.io.File;
26  import java.io.FileInputStream;
27  import java.io.IOException;
28  import java.io.InputStreamReader;
29  import java.util.ArrayList;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  
34  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
35  import org.xml.sax.Attributes;
36  import org.xml.sax.InputSource;
37  import org.xml.sax.SAXException;
38  import org.xml.sax.helpers.DefaultHandler;
39  
40  import static java.nio.charset.StandardCharsets.UTF_8;
41  import static org.apache.maven.shared.utils.StringUtils.isBlank;
42  
43  /**
44   *
45   */
46  public final class TestSuiteXmlParser extends DefaultHandler {
47      private final ConsoleLogger consoleLogger;
48  
49      private ReportTestSuite defaultSuite;
50  
51      private ReportTestSuite currentSuite;
52  
53      private Map<String, Integer> classesToSuitesIndex;
54  
55      private List<ReportTestSuite> suites;
56  
57      private StringBuilder currentElement;
58  
59      private ReportTestCase testCase;
60  
61      private ReportTestCase.FlakyFailure testCaseFlakyFailure;
62  
63      private ReportTestCase.FlakyError testCaseFlakyError;
64  
65      private boolean valid;
66  
67      private boolean parseContent;
68  
69      public TestSuiteXmlParser(ConsoleLogger consoleLogger) {
70          this.consoleLogger = consoleLogger;
71      }
72  
73      public List<ReportTestSuite> parse(String xmlPath) throws ParserConfigurationException, SAXException, IOException {
74          File f = new File(xmlPath);
75          try (InputStreamReader stream = new InputStreamReader(new FileInputStream(f), UTF_8)) {
76              return parse(stream);
77          }
78      }
79  
80      public List<ReportTestSuite> parse(InputStreamReader stream)
81              throws ParserConfigurationException, SAXException, IOException {
82          SAXParserFactory factory = SAXParserFactory.newInstance();
83  
84          SAXParser saxParser = factory.newSAXParser();
85  
86          valid = true;
87  
88          classesToSuitesIndex = new HashMap<>();
89          suites = new ArrayList<>();
90  
91          saxParser.parse(new InputSource(stream), this);
92  
93          if (currentSuite != defaultSuite) { // omit the defaultSuite if it's empty and there are alternatives
94              if (defaultSuite.getNumberOfTests() == 0) {
95                  suites.remove(classesToSuitesIndex
96                          .get(defaultSuite.getFullClassName())
97                          .intValue());
98              }
99          }
100 
101         return suites;
102     }
103 
104     /**
105      * {@inheritDoc}
106      */
107     @Override
108     public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
109         if (valid) {
110             try {
111                 switch (qName) {
112                     case "testsuite":
113                         defaultSuite = new ReportTestSuite();
114                         currentSuite = defaultSuite;
115                         String timeStr = attributes.getValue("time");
116                         if (timeStr != null) {
117                             defaultSuite.setTimeElapsed(Float.parseFloat(timeStr));
118                         } else {
119                             consoleLogger.warning("No time attribute found on testsuite element");
120                         }
121 
122                         final String name = attributes.getValue("name");
123                         final String group = attributes.getValue("group");
124                         defaultSuite.setFullClassName(
125                                 isBlank(group)
126                                         ? /*name is full class name*/ name
127                                         : /*group is package name*/ group + "." + name);
128 
129                         suites.add(defaultSuite);
130                         classesToSuitesIndex.put(defaultSuite.getFullClassName(), suites.size() - 1);
131                         break;
132                     case "testcase":
133                         // Although this element does not contain any text, this line must be retained because the
134                         // nested elements do have text content.
135                         currentElement = new StringBuilder();
136 
137                         testCase = new ReportTestCase().setName(attributes.getValue("name"));
138                         testCaseFlakyFailure = null;
139                         testCaseFlakyError = null;
140 
141                         String fullClassName = attributes.getValue("classname");
142 
143                         // if the testcase declares its own classname, it may need to belong to its own suite
144                         if (fullClassName != null) {
145                             Integer currentSuiteIndex = classesToSuitesIndex.get(fullClassName);
146                             if (currentSuiteIndex == null) {
147                                 currentSuite = new ReportTestSuite().setFullClassName(fullClassName);
148                                 suites.add(currentSuite);
149                                 classesToSuitesIndex.put(fullClassName, suites.size() - 1);
150                             } else {
151                                 currentSuite = suites.get(currentSuiteIndex);
152                             }
153                         }
154 
155                         timeStr = attributes.getValue("time");
156 
157                         testCase.setFullClassName(currentSuite.getFullClassName())
158                                 .setClassName(currentSuite.getName())
159                                 .setFullName(currentSuite.getFullClassName() + "." + testCase.getName())
160                                 .setTime(timeStr != null ? Float.parseFloat(timeStr) : 0.0f);
161 
162                         if (currentSuite != defaultSuite) {
163                             currentSuite.setTimeElapsed(testCase.getTime() + currentSuite.getTimeElapsed());
164                         }
165                         break;
166                     case "failure":
167                         currentElement = new StringBuilder();
168                         parseContent = true;
169 
170                         testCase.setFailure(attributes.getValue("message"), attributes.getValue("type"));
171                         currentSuite.incrementNumberOfFailures();
172                         break;
173                     case "error":
174                         currentElement = new StringBuilder();
175                         parseContent = true;
176 
177                         testCase.setError(attributes.getValue("message"), attributes.getValue("type"));
178                         currentSuite.incrementNumberOfErrors();
179                         break;
180                     case "skipped":
181                         String message = attributes.getValue("message");
182                         testCase.setSkipped(message != null ? message : "skipped");
183                         currentSuite.incrementNumberOfSkipped();
184                         break;
185                     case "flakyFailure":
186                         testCaseFlakyFailure = new ReportTestCase.FlakyFailure(
187                                 attributes.getValue("message"), attributes.getValue("type"));
188                         currentSuite.incrementNumberOfFlakes();
189                         break;
190                     case "flakyError":
191                         testCaseFlakyError = new ReportTestCase.FlakyError(
192                                 attributes.getValue("message"), attributes.getValue("type"));
193                         currentSuite.incrementNumberOfFlakes();
194                         break;
195                     case "stackTrace":
196                         currentElement = new StringBuilder();
197                         parseContent = true;
198                         break;
199                     case "failsafe-summary":
200                         valid = false;
201                         break;
202                     case "time":
203                         currentElement = new StringBuilder();
204                         parseContent = true;
205                         break;
206                     default:
207                         break;
208                 }
209             } catch (NumberFormatException e) {
210                 throw new SAXException("Failed to parse time value", e);
211             }
212         }
213     }
214 
215     /**
216      * {@inheritDoc}
217      */
218     @Override
219     public void endElement(String uri, String localName, String qName) throws SAXException {
220         switch (qName) {
221             case "testcase":
222                 currentSuite.getTestCases().add(testCase);
223                 break;
224             case "failure":
225             case "error":
226                 testCase.setFailureDetail(currentElement.toString())
227                         .setFailureErrorLine(parseErrorLine(currentElement, testCase.getFullClassName()));
228                 break;
229             case "time":
230                 try {
231                     defaultSuite.setTimeElapsed(Float.parseFloat(currentElement.toString()));
232                 } catch (NumberFormatException e) {
233                     throw new SAXException("Failed to parse time value", e);
234                 }
235                 break;
236             case "flakyFailure":
237                 testCase.addFlakyFailure(testCaseFlakyFailure);
238                 testCaseFlakyFailure = null;
239                 break;
240             case "flakyError":
241                 testCase.addFlakyError(testCaseFlakyError);
242                 testCaseFlakyError = null;
243                 break;
244             case "stackTrace":
245                 if (testCaseFlakyFailure != null) {
246                     testCaseFlakyFailure.setStackTrace(currentElement.toString());
247                 }
248                 if (testCaseFlakyError != null) {
249                     testCaseFlakyError.setStackTrace(currentElement.toString());
250                 }
251                 break;
252             default:
253                 break;
254         }
255         parseContent = false;
256         // TODO extract real skipped reasons
257     }
258 
259     /**
260      * {@inheritDoc}
261      */
262     @Override
263     public void characters(char[] ch, int start, int length) {
264         assert start >= 0;
265         assert length >= 0;
266         if (valid && parseContent && isNotBlank(start, length, ch)) {
267             currentElement.append(ch, start, length);
268         }
269     }
270 
271     public boolean isValid() {
272         return valid;
273     }
274 
275     static boolean isNotBlank(int from, int len, char... s) {
276         assert from >= 0;
277         assert len >= 0;
278         if (s != null) {
279             for (int i = 0; i < len; i++) {
280                 char c = s[from++];
281                 if (c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f') {
282                     return true;
283                 }
284             }
285         }
286         return false;
287     }
288 
289     static boolean isNumeric(StringBuilder s, final int from, final int to) {
290         assert from >= 0;
291         assert from <= to;
292         for (int i = from; i != to; ) {
293             if (!Character.isDigit(s.charAt(i++))) {
294                 return false;
295             }
296         }
297         return from != to;
298     }
299 
300     static String parseErrorLine(StringBuilder currentElement, String fullClassName) {
301         final String[] linePatterns = {"at " + fullClassName + '.', "at " + fullClassName + '$'};
302         int[] indexes = lastIndexOf(currentElement, linePatterns);
303         int patternStartsAt = indexes[0];
304         if (patternStartsAt != -1) {
305             int searchFrom = patternStartsAt + (linePatterns[indexes[1]]).length();
306             searchFrom = 1 + currentElement.indexOf(":", searchFrom);
307             int searchTo = currentElement.indexOf(")", searchFrom);
308             return isNumeric(currentElement, searchFrom, searchTo)
309                     ? currentElement.substring(searchFrom, searchTo)
310                     : "";
311         }
312         return "";
313     }
314 
315     static int[] lastIndexOf(StringBuilder source, String... linePatterns) {
316         int end = source.indexOf("Caused by:");
317         if (end == -1) {
318             end = source.length();
319         }
320         int startsAt = -1;
321         int pattern = -1;
322         for (int i = 0; i < linePatterns.length; i++) {
323             String linePattern = linePatterns[i];
324             int currentStartsAt = source.lastIndexOf(linePattern, end);
325             if (currentStartsAt > startsAt) {
326                 startsAt = currentStartsAt;
327                 pattern = i;
328             }
329         }
330         return new int[] {startsAt, pattern};
331     }
332 }