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.text.NumberFormat;
30 import java.text.ParseException;
31 import java.util.ArrayList;
32 import java.util.HashMap;
33 import java.util.List;
34 import java.util.Map;
35
36 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
37 import org.xml.sax.Attributes;
38 import org.xml.sax.InputSource;
39 import org.xml.sax.SAXException;
40 import org.xml.sax.helpers.DefaultHandler;
41
42 import static java.nio.charset.StandardCharsets.UTF_8;
43 import static java.util.Locale.ENGLISH;
44 import static org.apache.maven.shared.utils.StringUtils.isBlank;
45
46
47
48
49 public final class TestSuiteXmlParser extends DefaultHandler {
50 private final NumberFormat numberFormat = NumberFormat.getInstance(ENGLISH);
51
52 private final ConsoleLogger consoleLogger;
53
54 private ReportTestSuite defaultSuite;
55
56 private ReportTestSuite currentSuite;
57
58 private Map<String, Integer> classesToSuitesIndex;
59
60 private List<ReportTestSuite> suites;
61
62 private StringBuilder currentElement;
63
64 private ReportTestCase testCase;
65
66 private boolean valid;
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(new FileInputStream(f), UTF_8)) {
75 return parse(stream);
76 }
77 }
78
79 public List<ReportTestSuite> parse(InputStreamReader stream)
80 throws ParserConfigurationException, SAXException, IOException {
81 SAXParserFactory factory = SAXParserFactory.newInstance();
82
83 SAXParser saxParser = factory.newSAXParser();
84
85 valid = true;
86
87 classesToSuitesIndex = new HashMap<>();
88 suites = new ArrayList<>();
89
90 saxParser.parse(new InputSource(stream), this);
91
92 if (currentSuite != defaultSuite) {
93 if (defaultSuite.getNumberOfTests() == 0) {
94 suites.remove(classesToSuitesIndex
95 .get(defaultSuite.getFullClassName())
96 .intValue());
97 }
98 }
99
100 return suites;
101 }
102
103
104
105
106 @Override
107 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
108 if (valid) {
109 try {
110 switch (qName) {
111 case "testsuite":
112 defaultSuite = new ReportTestSuite();
113 currentSuite = defaultSuite;
114
115 try {
116 Number time = numberFormat.parse(attributes.getValue("time"));
117
118 defaultSuite.setTimeElapsed(time.floatValue());
119 } catch (NullPointerException e) {
120 consoleLogger.error("WARNING: no time attribute found on testsuite element");
121 }
122
123 final String name = attributes.getValue("name");
124 final String group = attributes.getValue("group");
125 defaultSuite.setFullClassName(
126 isBlank(group)
127 ? name
128 : group + "." + name);
129
130 suites.add(defaultSuite);
131 classesToSuitesIndex.put(defaultSuite.getFullClassName(), suites.size() - 1);
132 break;
133 case "testcase":
134 currentElement = new StringBuilder();
135
136 testCase = new ReportTestCase().setName(attributes.getValue("name"));
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 String timeAsString = attributes.getValue("time");
153 Number time = isBlank(timeAsString) ? 0 : numberFormat.parse(timeAsString);
154
155 testCase.setFullClassName(currentSuite.getFullClassName())
156 .setClassName(currentSuite.getName())
157 .setFullName(currentSuite.getFullClassName() + "." + testCase.getName())
158 .setTime(time.floatValue());
159
160 if (currentSuite != defaultSuite) {
161 currentSuite.setTimeElapsed(testCase.getTime() + currentSuite.getTimeElapsed());
162 }
163 break;
164 case "failure":
165 testCase.setFailure(attributes.getValue("message"), attributes.getValue("type"));
166 currentSuite.incrementNumberOfFailures();
167 break;
168 case "error":
169 testCase.setError(attributes.getValue("message"), attributes.getValue("type"));
170 currentSuite.incrementNumberOfErrors();
171 break;
172 case "skipped":
173 String message = attributes.getValue("message");
174 testCase.setSkipped(message != null ? message : "skipped");
175 currentSuite.incrementNumberOfSkipped();
176 break;
177 case "flakyFailure":
178 case "flakyError":
179 currentSuite.incrementNumberOfFlakes();
180 break;
181 case "failsafe-summary":
182 valid = false;
183 break;
184 default:
185 break;
186 }
187 } catch (ParseException e) {
188 throw new SAXException(e.getMessage(), e);
189 }
190 }
191 }
192
193
194
195
196 @Override
197 public void endElement(String uri, String localName, String qName) throws SAXException {
198 switch (qName) {
199 case "testcase":
200 currentSuite.getTestCases().add(testCase);
201 break;
202 case "failure":
203 case "error":
204 testCase.setFailureDetail(currentElement.toString())
205 .setFailureErrorLine(parseErrorLine(currentElement, testCase.getFullClassName()));
206 break;
207 case "time":
208 try {
209 defaultSuite.setTimeElapsed(
210 numberFormat.parse(currentElement.toString()).floatValue());
211 } catch (ParseException e) {
212 throw new SAXException(e.getMessage(), 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 }