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 public final class TestSuiteXmlParser extends DefaultHandler {
44 private final ConsoleLogger consoleLogger;
45
46 private ReportTestSuite defaultSuite;
47
48 private ReportTestSuite currentSuite;
49
50 private Map<String, Integer> classesToSuitesIndex;
51
52 private List<ReportTestSuite> suites;
53
54 private StringBuilder currentElement;
55
56 private ReportTestCase testCase;
57
58 private ReportTestCase.FlakyFailure testCaseFlakyFailure;
59
60 private ReportTestCase.FlakyError testCaseFlakyError;
61
62 private boolean valid;
63
64 private boolean parseContent;
65
66 private long lastModified;
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(Files.newInputStream(f.toPath()), UTF_8)) {
75 this.lastModified = f.lastModified();
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) {
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
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
127 : group + "." + name);
128
129 suites.add(defaultSuite);
130 classesToSuitesIndex.put(defaultSuite.getFullClassName(), suites.size() - 1);
131 break;
132 case "testcase":
133
134
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
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
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 currentSuite.lastModified(this.lastModified);
224 break;
225 case "failure":
226 case "error":
227 testCase.setFailureDetail(currentElement.toString())
228 .setFailureErrorLine(parseErrorLine(currentElement, testCase.getFullClassName()));
229 break;
230 case "time":
231 try {
232 defaultSuite.setTimeElapsed(Float.parseFloat(currentElement.toString()));
233 } catch (NumberFormatException e) {
234 throw new SAXException("Failed to parse time value", e);
235 }
236 break;
237 case "flakyFailure":
238 testCase.addFlakyFailure(testCaseFlakyFailure);
239 testCaseFlakyFailure = null;
240 break;
241 case "flakyError":
242 testCase.addFlakyError(testCaseFlakyError);
243 testCaseFlakyError = null;
244 break;
245 case "stackTrace":
246 if (testCaseFlakyFailure != null) {
247 testCaseFlakyFailure.setStackTrace(currentElement.toString());
248 }
249 if (testCaseFlakyError != null) {
250 testCaseFlakyError.setStackTrace(currentElement.toString());
251 }
252 break;
253 default:
254 break;
255 }
256 parseContent = false;
257
258 }
259
260
261
262
263 @Override
264 public void characters(char[] ch, int start, int length) {
265 assert start >= 0;
266 assert length >= 0;
267 if (valid && parseContent && isNotBlank(start, length, ch)) {
268 currentElement.append(ch, start, length);
269 }
270 }
271
272 public boolean isValid() {
273 return valid;
274 }
275
276 static boolean isNotBlank(int from, int len, char... s) {
277 assert from >= 0;
278 assert len >= 0;
279 if (s != null) {
280 for (int i = 0; i < len; i++) {
281 char c = s[from++];
282 if (c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f') {
283 return true;
284 }
285 }
286 }
287 return false;
288 }
289
290 static boolean isNumeric(StringBuilder s, final int from, final int to) {
291 assert from >= 0;
292 assert from <= to;
293 for (int i = from; i != to; ) {
294 if (!Character.isDigit(s.charAt(i++))) {
295 return false;
296 }
297 }
298 return from != to;
299 }
300
301 static String parseErrorLine(StringBuilder currentElement, String fullClassName) {
302 final String[] linePatterns = {"at " + fullClassName + '.', "at " + fullClassName + '$'};
303 int[] indexes = lastIndexOf(currentElement, linePatterns);
304 int patternStartsAt = indexes[0];
305 if (patternStartsAt != -1) {
306 int searchFrom = patternStartsAt + (linePatterns[indexes[1]]).length();
307 searchFrom = 1 + currentElement.indexOf(":", searchFrom);
308 int searchTo = currentElement.indexOf(")", searchFrom);
309 return isNumeric(currentElement, searchFrom, searchTo)
310 ? currentElement.substring(searchFrom, searchTo)
311 : "";
312 }
313 return "";
314 }
315
316 static int[] lastIndexOf(StringBuilder source, String... linePatterns) {
317 int end = source.indexOf("Caused by:");
318 if (end == -1) {
319 end = source.length();
320 }
321 int startsAt = -1;
322 int pattern = -1;
323 for (int i = 0; i < linePatterns.length; i++) {
324 String linePattern = linePatterns[i];
325 int currentStartsAt = source.lastIndexOf(linePattern, end);
326 if (currentStartsAt > startsAt) {
327 startsAt = currentStartsAt;
328 pattern = i;
329 }
330 }
331 return new int[] {startsAt, pattern};
332 }
333 }