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.text.NumberFormat;
30  import java.text.ParseException;
31  import java.util.ArrayList;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  
36  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
37  import org.xml.sax.Attributes;
38  import org.xml.sax.InputSource;
39  import org.xml.sax.SAXException;
40  import org.xml.sax.helpers.DefaultHandler;
41  
42  import static java.nio.charset.StandardCharsets.UTF_8;
43  import static java.util.Locale.ENGLISH;
44  import static org.apache.maven.shared.utils.StringUtils.isBlank;
45  
46  /**
47   *
48   */
49  public final class TestSuiteXmlParser extends DefaultHandler {
50      private final NumberFormat numberFormat = NumberFormat.getInstance(ENGLISH);
51  
52      private final ConsoleLogger consoleLogger;
53  
54      private ReportTestSuite defaultSuite;
55  
56      private ReportTestSuite currentSuite;
57  
58      private Map<String, Integer> classesToSuitesIndex;
59  
60      private List<ReportTestSuite> suites;
61  
62      private StringBuilder currentElement;
63  
64      private ReportTestCase testCase;
65  
66      private boolean valid;
67  
68      public TestSuiteXmlParser(ConsoleLogger consoleLogger) {
69          this.consoleLogger = consoleLogger;
70      }
71  
72      public List<ReportTestSuite> parse(String xmlPath) throws ParserConfigurationException, SAXException, IOException {
73          File f = new File(xmlPath);
74          try (InputStreamReader stream = new InputStreamReader(new FileInputStream(f), UTF_8)) {
75              return parse(stream);
76          }
77      }
78  
79      public List<ReportTestSuite> parse(InputStreamReader stream)
80              throws ParserConfigurationException, SAXException, IOException {
81          SAXParserFactory factory = SAXParserFactory.newInstance();
82  
83          SAXParser saxParser = factory.newSAXParser();
84  
85          valid = true;
86  
87          classesToSuitesIndex = new HashMap<>();
88          suites = new ArrayList<>();
89  
90          saxParser.parse(new InputSource(stream), this);
91  
92          if (currentSuite != defaultSuite) { // omit the defaultSuite if it's empty and there are alternatives
93              if (defaultSuite.getNumberOfTests() == 0) {
94                  suites.remove(classesToSuitesIndex
95                          .get(defaultSuite.getFullClassName())
96                          .intValue());
97              }
98          }
99  
100         return suites;
101     }
102 
103     /**
104      * {@inheritDoc}
105      */
106     @Override
107     public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
108         if (valid) {
109             try {
110                 switch (qName) {
111                     case "testsuite":
112                         defaultSuite = new ReportTestSuite();
113                         currentSuite = defaultSuite;
114 
115                         try {
116                             Number time = numberFormat.parse(attributes.getValue("time"));
117 
118                             defaultSuite.setTimeElapsed(time.floatValue());
119                         } catch (NullPointerException e) {
120                             consoleLogger.error("WARNING: no time attribute found on testsuite element");
121                         }
122 
123                         final String name = attributes.getValue("name");
124                         final String group = attributes.getValue("group");
125                         defaultSuite.setFullClassName(
126                                 isBlank(group)
127                                         ? /*name is full class name*/ name
128                                         : /*group is package name*/ group + "." + name);
129 
130                         suites.add(defaultSuite);
131                         classesToSuitesIndex.put(defaultSuite.getFullClassName(), suites.size() - 1);
132                         break;
133                     case "testcase":
134                         currentElement = new StringBuilder();
135 
136                         testCase = new ReportTestCase().setName(attributes.getValue("name"));
137 
138                         String fullClassName = attributes.getValue("classname");
139 
140                         // if the testcase declares its own classname, it may need to belong to its own suite
141                         if (fullClassName != null) {
142                             Integer currentSuiteIndex = classesToSuitesIndex.get(fullClassName);
143                             if (currentSuiteIndex == null) {
144                                 currentSuite = new ReportTestSuite().setFullClassName(fullClassName);
145                                 suites.add(currentSuite);
146                                 classesToSuitesIndex.put(fullClassName, suites.size() - 1);
147                             } else {
148                                 currentSuite = suites.get(currentSuiteIndex);
149                             }
150                         }
151 
152                         String timeAsString = attributes.getValue("time");
153                         Number time = isBlank(timeAsString) ? 0 : numberFormat.parse(timeAsString);
154 
155                         testCase.setFullClassName(currentSuite.getFullClassName())
156                                 .setClassName(currentSuite.getName())
157                                 .setFullName(currentSuite.getFullClassName() + "." + testCase.getName())
158                                 .setTime(time.floatValue());
159 
160                         if (currentSuite != defaultSuite) {
161                             currentSuite.setTimeElapsed(testCase.getTime() + currentSuite.getTimeElapsed());
162                         }
163                         break;
164                     case "failure":
165                         testCase.setFailure(attributes.getValue("message"), attributes.getValue("type"));
166                         currentSuite.incrementNumberOfFailures();
167                         break;
168                     case "error":
169                         testCase.setError(attributes.getValue("message"), attributes.getValue("type"));
170                         currentSuite.incrementNumberOfErrors();
171                         break;
172                     case "skipped":
173                         String message = attributes.getValue("message");
174                         testCase.setSkipped(message != null ? message : "skipped");
175                         currentSuite.incrementNumberOfSkipped();
176                         break;
177                     case "flakyFailure":
178                     case "flakyError":
179                         currentSuite.incrementNumberOfFlakes();
180                         break;
181                     case "failsafe-summary":
182                         valid = false;
183                         break;
184                     default:
185                         break;
186                 }
187             } catch (ParseException e) {
188                 throw new SAXException(e.getMessage(), e);
189             }
190         }
191     }
192 
193     /**
194      * {@inheritDoc}
195      */
196     @Override
197     public void endElement(String uri, String localName, String qName) throws SAXException {
198         switch (qName) {
199             case "testcase":
200                 currentSuite.getTestCases().add(testCase);
201                 break;
202             case "failure":
203             case "error":
204                 testCase.setFailureDetail(currentElement.toString())
205                         .setFailureErrorLine(parseErrorLine(currentElement, testCase.getFullClassName()));
206                 break;
207             case "time":
208                 try {
209                     defaultSuite.setTimeElapsed(
210                             numberFormat.parse(currentElement.toString()).floatValue());
211                 } catch (ParseException e) {
212                     throw new SAXException(e.getMessage(), 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 }