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 boolean valid;
62
63 public TestSuiteXmlParser(ConsoleLogger consoleLogger) {
64 this.consoleLogger = consoleLogger;
65 }
66
67 public List<ReportTestSuite> parse(String xmlPath) throws ParserConfigurationException, SAXException, IOException {
68 File f = new File(xmlPath);
69 try (InputStreamReader stream = new InputStreamReader(new FileInputStream(f), UTF_8)) {
70 return parse(stream);
71 }
72 }
73
74 public List<ReportTestSuite> parse(InputStreamReader stream)
75 throws ParserConfigurationException, SAXException, IOException {
76 SAXParserFactory factory = SAXParserFactory.newInstance();
77
78 SAXParser saxParser = factory.newSAXParser();
79
80 valid = true;
81
82 classesToSuitesIndex = new HashMap<>();
83 suites = new ArrayList<>();
84
85 saxParser.parse(new InputSource(stream), this);
86
87 if (currentSuite != defaultSuite) {
88 if (defaultSuite.getNumberOfTests() == 0) {
89 suites.remove(classesToSuitesIndex
90 .get(defaultSuite.getFullClassName())
91 .intValue());
92 }
93 }
94
95 return suites;
96 }
97
98
99
100
101 @Override
102 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
103 if (valid) {
104 try {
105 switch (qName) {
106 case "testsuite":
107 defaultSuite = new ReportTestSuite();
108 currentSuite = defaultSuite;
109 String timeStr = attributes.getValue("time");
110 if (timeStr != null) {
111 defaultSuite.setTimeElapsed(Float.parseFloat(timeStr));
112 } else {
113 consoleLogger.warning("No time attribute found on testsuite element");
114 }
115
116 final String name = attributes.getValue("name");
117 final String group = attributes.getValue("group");
118 defaultSuite.setFullClassName(
119 isBlank(group)
120 ? name
121 : group + "." + name);
122
123 suites.add(defaultSuite);
124 classesToSuitesIndex.put(defaultSuite.getFullClassName(), suites.size() - 1);
125 break;
126 case "testcase":
127
128
129 currentElement = new StringBuilder();
130
131 testCase = new ReportTestCase().setName(attributes.getValue("name"));
132
133 String fullClassName = attributes.getValue("classname");
134
135
136 if (fullClassName != null) {
137 Integer currentSuiteIndex = classesToSuitesIndex.get(fullClassName);
138 if (currentSuiteIndex == null) {
139 currentSuite = new ReportTestSuite().setFullClassName(fullClassName);
140 suites.add(currentSuite);
141 classesToSuitesIndex.put(fullClassName, suites.size() - 1);
142 } else {
143 currentSuite = suites.get(currentSuiteIndex);
144 }
145 }
146
147 timeStr = attributes.getValue("time");
148
149 testCase.setFullClassName(currentSuite.getFullClassName())
150 .setClassName(currentSuite.getName())
151 .setFullName(currentSuite.getFullClassName() + "." + testCase.getName())
152 .setTime(timeStr != null ? Float.parseFloat(timeStr) : 0.0f);
153
154 if (currentSuite != defaultSuite) {
155 currentSuite.setTimeElapsed(testCase.getTime() + currentSuite.getTimeElapsed());
156 }
157 break;
158 case "failure":
159 currentElement = new StringBuilder();
160
161 testCase.setFailure(attributes.getValue("message"), attributes.getValue("type"));
162 currentSuite.incrementNumberOfFailures();
163 break;
164 case "error":
165 currentElement = new StringBuilder();
166
167 testCase.setError(attributes.getValue("message"), attributes.getValue("type"));
168 currentSuite.incrementNumberOfErrors();
169 break;
170 case "skipped":
171 String message = attributes.getValue("message");
172 testCase.setSkipped(message != null ? message : "skipped");
173 currentSuite.incrementNumberOfSkipped();
174 break;
175 case "flakyFailure":
176 case "flakyError":
177 currentSuite.incrementNumberOfFlakes();
178 break;
179 case "failsafe-summary":
180 valid = false;
181 break;
182 case "time":
183 currentElement = new StringBuilder();
184 break;
185 default:
186 break;
187 }
188 } catch (NumberFormatException e) {
189 throw new SAXException("Failed to parse time value", e);
190 }
191 }
192 }
193
194
195
196
197 @Override
198 public void endElement(String uri, String localName, String qName) throws SAXException {
199 switch (qName) {
200 case "testcase":
201 currentSuite.getTestCases().add(testCase);
202 break;
203 case "failure":
204 case "error":
205 testCase.setFailureDetail(currentElement.toString())
206 .setFailureErrorLine(parseErrorLine(currentElement, testCase.getFullClassName()));
207 break;
208 case "time":
209 try {
210 defaultSuite.setTimeElapsed(Float.parseFloat(currentElement.toString()));
211 } catch (NumberFormatException e) {
212 throw new SAXException("Failed to parse time value", e);
213 }
214 break;
215 default:
216 break;
217 }
218
219 }
220
221
222
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 }