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