1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.IOException;
27 import java.io.InputStreamReader;
28 import java.nio.file.Files;
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 private long lastModified;
70
71 public TestSuiteXmlParser(ConsoleLogger consoleLogger) {
72 this.consoleLogger = consoleLogger;
73 }
74
75 public List<ReportTestSuite> parse(String xmlPath) throws ParserConfigurationException, SAXException, IOException {
76 File f = new File(xmlPath);
77 try (InputStreamReader stream = new InputStreamReader(Files.newInputStream(f.toPath()), UTF_8)) {
78 this.lastModified = f.lastModified();
79 return parse(stream);
80 }
81 }
82
83 public List<ReportTestSuite> parse(InputStreamReader stream)
84 throws ParserConfigurationException, SAXException, IOException {
85 SAXParserFactory factory = SAXParserFactory.newInstance();
86
87 SAXParser saxParser = factory.newSAXParser();
88
89 valid = true;
90
91 classesToSuitesIndex = new HashMap<>();
92 suites = new ArrayList<>();
93
94 saxParser.parse(new InputSource(stream), this);
95
96 if (currentSuite != defaultSuite) {
97 if (defaultSuite.getNumberOfTests() == 0) {
98 suites.remove(classesToSuitesIndex
99 .get(defaultSuite.getFullClassName())
100 .intValue());
101 }
102 }
103
104 return suites;
105 }
106
107
108
109
110 @Override
111 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
112 if (valid) {
113 try {
114 switch (qName) {
115 case "testsuite":
116 defaultSuite = new ReportTestSuite();
117 currentSuite = defaultSuite;
118 String timeStr = attributes.getValue("time");
119 if (timeStr != null) {
120 defaultSuite.setTimeElapsed(Float.parseFloat(timeStr));
121 } else {
122 consoleLogger.warning("No time attribute found on testsuite element");
123 }
124
125 final String name = attributes.getValue("name");
126 final String group = attributes.getValue("group");
127 defaultSuite.setFullClassName(
128 isBlank(group)
129 ? name
130 : group + "." + name);
131
132 suites.add(defaultSuite);
133 classesToSuitesIndex.put(defaultSuite.getFullClassName(), suites.size() - 1);
134 break;
135 case "testcase":
136
137
138 currentElement = new StringBuilder();
139
140 testCase = new ReportTestCase().setName(attributes.getValue("name"));
141 testCaseFlakyFailure = null;
142 testCaseFlakyError = null;
143
144 String fullClassName = attributes.getValue("classname");
145
146
147 if (fullClassName != null) {
148 Integer currentSuiteIndex = classesToSuitesIndex.get(fullClassName);
149 if (currentSuiteIndex == null) {
150 currentSuite = new ReportTestSuite().setFullClassName(fullClassName);
151 suites.add(currentSuite);
152 classesToSuitesIndex.put(fullClassName, suites.size() - 1);
153 } else {
154 currentSuite = suites.get(currentSuiteIndex);
155 }
156 }
157
158 timeStr = attributes.getValue("time");
159
160 testCase.setFullClassName(currentSuite.getFullClassName())
161 .setClassName(currentSuite.getName())
162 .setFullName(currentSuite.getFullClassName() + "." + testCase.getName())
163 .setTime(timeStr != null ? Float.parseFloat(timeStr) : 0.0f);
164
165 if (currentSuite != defaultSuite) {
166 currentSuite.setTimeElapsed(testCase.getTime() + currentSuite.getTimeElapsed());
167 }
168 break;
169 case "failure":
170 currentElement = new StringBuilder();
171 parseContent = true;
172
173 testCase.setFailure(attributes.getValue("message"), attributes.getValue("type"));
174 currentSuite.incrementNumberOfFailures();
175 break;
176 case "error":
177 currentElement = new StringBuilder();
178 parseContent = true;
179
180 testCase.setError(attributes.getValue("message"), attributes.getValue("type"));
181 currentSuite.incrementNumberOfErrors();
182 break;
183 case "skipped":
184 String message = attributes.getValue("message");
185 testCase.setSkipped(message != null ? message : "skipped");
186 currentSuite.incrementNumberOfSkipped();
187 break;
188 case "flakyFailure":
189 testCaseFlakyFailure = new ReportTestCase.FlakyFailure(
190 attributes.getValue("message"), attributes.getValue("type"));
191 currentSuite.incrementNumberOfFlakes();
192 break;
193 case "flakyError":
194 testCaseFlakyError = new ReportTestCase.FlakyError(
195 attributes.getValue("message"), attributes.getValue("type"));
196 currentSuite.incrementNumberOfFlakes();
197 break;
198 case "stackTrace":
199 currentElement = new StringBuilder();
200 parseContent = true;
201 break;
202 case "failsafe-summary":
203 valid = false;
204 break;
205 case "time":
206 currentElement = new StringBuilder();
207 parseContent = true;
208 break;
209 default:
210 break;
211 }
212 } catch (NumberFormatException e) {
213 throw new SAXException("Failed to parse time value", e);
214 }
215 }
216 }
217
218
219
220
221 @Override
222 public void endElement(String uri, String localName, String qName) throws SAXException {
223 switch (qName) {
224 case "testcase":
225 currentSuite.getTestCases().add(testCase);
226 currentSuite.lastModified(this.lastModified);
227 break;
228 case "failure":
229 case "error":
230 testCase.setFailureDetail(currentElement.toString())
231 .setFailureErrorLine(parseErrorLine(currentElement, testCase.getFullClassName()));
232 break;
233 case "time":
234 try {
235 defaultSuite.setTimeElapsed(Float.parseFloat(currentElement.toString()));
236 } catch (NumberFormatException e) {
237 throw new SAXException("Failed to parse time value", e);
238 }
239 break;
240 case "flakyFailure":
241 testCase.addFlakyFailure(testCaseFlakyFailure);
242 testCaseFlakyFailure = null;
243 break;
244 case "flakyError":
245 testCase.addFlakyError(testCaseFlakyError);
246 testCaseFlakyError = null;
247 break;
248 case "stackTrace":
249 if (testCaseFlakyFailure != null) {
250 testCaseFlakyFailure.setStackTrace(currentElement.toString());
251 }
252 if (testCaseFlakyError != null) {
253 testCaseFlakyError.setStackTrace(currentElement.toString());
254 }
255 break;
256 default:
257 break;
258 }
259 parseContent = false;
260
261 }
262
263
264
265
266 @Override
267 public void characters(char[] ch, int start, int length) {
268 assert start >= 0;
269 assert length >= 0;
270 if (valid && parseContent && isNotBlank(start, length, ch)) {
271 currentElement.append(ch, start, length);
272 }
273 }
274
275 public boolean isValid() {
276 return valid;
277 }
278
279 static boolean isNotBlank(int from, int len, char... s) {
280 assert from >= 0;
281 assert len >= 0;
282 if (s != null) {
283 for (int i = 0; i < len; i++) {
284 char c = s[from++];
285 if (c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f') {
286 return true;
287 }
288 }
289 }
290 return false;
291 }
292
293 static boolean isNumeric(StringBuilder s, final int from, final int to) {
294 assert from >= 0;
295 assert from <= to;
296 for (int i = from; i != to; ) {
297 if (!Character.isDigit(s.charAt(i++))) {
298 return false;
299 }
300 }
301 return from != to;
302 }
303
304 static String parseErrorLine(StringBuilder currentElement, String fullClassName) {
305 final String[] linePatterns = {"at " + fullClassName + '.', "at " + fullClassName + '$'};
306 int[] indexes = lastIndexOf(currentElement, linePatterns);
307 int patternStartsAt = indexes[0];
308 if (patternStartsAt != -1) {
309 int searchFrom = patternStartsAt + (linePatterns[indexes[1]]).length();
310 searchFrom = 1 + currentElement.indexOf(":", searchFrom);
311 int searchTo = currentElement.indexOf(")", searchFrom);
312 return isNumeric(currentElement, searchFrom, searchTo)
313 ? currentElement.substring(searchFrom, searchTo)
314 : "";
315 }
316 return "";
317 }
318
319 static int[] lastIndexOf(StringBuilder source, String... linePatterns) {
320 int end = source.indexOf("Caused by:");
321 if (end == -1) {
322 end = source.length();
323 }
324 int startsAt = -1;
325 int pattern = -1;
326 for (int i = 0; i < linePatterns.length; i++) {
327 String linePattern = linePatterns[i];
328 int currentStartsAt = source.lastIndexOf(linePattern, end);
329 if (currentStartsAt > startsAt) {
330 startsAt = currentStartsAt;
331 pattern = i;
332 }
333 }
334 return new int[] {startsAt, pattern};
335 }
336 }