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