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
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 public TestSuiteXmlParser(ConsoleLogger consoleLogger) {
70 this.consoleLogger = consoleLogger;
71 }
72
73 public List<ReportTestSuite> parse(String xmlPath) throws ParserConfigurationException, SAXException, IOException {
74 File f = new File(xmlPath);
75 try (InputStreamReader stream = new InputStreamReader(new FileInputStream(f), UTF_8)) {
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 break;
224 case "failure":
225 case "error":
226 testCase.setFailureDetail(currentElement.toString())
227 .setFailureErrorLine(parseErrorLine(currentElement, testCase.getFullClassName()));
228 break;
229 case "time":
230 try {
231 defaultSuite.setTimeElapsed(Float.parseFloat(currentElement.toString()));
232 } catch (NumberFormatException e) {
233 throw new SAXException("Failed to parse time value", e);
234 }
235 break;
236 case "flakyFailure":
237 testCase.addFlakyFailure(testCaseFlakyFailure);
238 testCaseFlakyFailure = null;
239 break;
240 case "flakyError":
241 testCase.addFlakyError(testCaseFlakyError);
242 testCaseFlakyError = null;
243 break;
244 case "stackTrace":
245 if (testCaseFlakyFailure != null) {
246 testCaseFlakyFailure.setStackTrace(currentElement.toString());
247 }
248 if (testCaseFlakyError != null) {
249 testCaseFlakyError.setStackTrace(currentElement.toString());
250 }
251 break;
252 default:
253 break;
254 }
255 parseContent = false;
256
257 }
258
259
260
261
262 @Override
263 public void characters(char[] ch, int start, int length) {
264 assert start >= 0;
265 assert length >= 0;
266 if (valid && parseContent && isNotBlank(start, length, ch)) {
267 currentElement.append(ch, start, length);
268 }
269 }
270
271 public boolean isValid() {
272 return valid;
273 }
274
275 static boolean isNotBlank(int from, int len, char... s) {
276 assert from >= 0;
277 assert len >= 0;
278 if (s != null) {
279 for (int i = 0; i < len; i++) {
280 char c = s[from++];
281 if (c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f') {
282 return true;
283 }
284 }
285 }
286 return false;
287 }
288
289 static boolean isNumeric(StringBuilder s, final int from, final int to) {
290 assert from >= 0;
291 assert from <= to;
292 for (int i = from; i != to; ) {
293 if (!Character.isDigit(s.charAt(i++))) {
294 return false;
295 }
296 }
297 return from != to;
298 }
299
300 static String parseErrorLine(StringBuilder currentElement, String fullClassName) {
301 final String[] linePatterns = {"at " + fullClassName + '.', "at " + fullClassName + '$'};
302 int[] indexes = lastIndexOf(currentElement, linePatterns);
303 int patternStartsAt = indexes[0];
304 if (patternStartsAt != -1) {
305 int searchFrom = patternStartsAt + (linePatterns[indexes[1]]).length();
306 searchFrom = 1 + currentElement.indexOf(":", searchFrom);
307 int searchTo = currentElement.indexOf(")", searchFrom);
308 return isNumeric(currentElement, searchFrom, searchTo)
309 ? currentElement.substring(searchFrom, searchTo)
310 : "";
311 }
312 return "";
313 }
314
315 static int[] lastIndexOf(StringBuilder source, String... linePatterns) {
316 int end = source.indexOf("Caused by:");
317 if (end == -1) {
318 end = source.length();
319 }
320 int startsAt = -1;
321 int pattern = -1;
322 for (int i = 0; i < linePatterns.length; i++) {
323 String linePattern = linePatterns[i];
324 int currentStartsAt = source.lastIndexOf(linePattern, end);
325 if (currentStartsAt > startsAt) {
326 startsAt = currentStartsAt;
327 pattern = i;
328 }
329 }
330 return new int[] {startsAt, pattern};
331 }
332 }