blob: e449bf974992e0d6793665d66c07347530c52dcf [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 The Bazel Authors. All rights reserved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01002//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
Ulf Adamse11063a2016-12-15 17:33:32 +000015package com.google.devtools.build.lib.exec;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010016
Googler33876782017-05-07 01:03:35 -040017import com.google.common.collect.ImmutableCollection;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010018import com.google.common.collect.ImmutableSet;
19import com.google.devtools.build.lib.view.test.TestStatus.TestCase;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010020import com.google.protobuf.UninitializedMessageException;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010021import java.io.InputStream;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010022import javax.xml.stream.XMLInputFactory;
23import javax.xml.stream.XMLStreamConstants;
24import javax.xml.stream.XMLStreamException;
25import javax.xml.stream.XMLStreamReader;
26
27/**
Ulf Adamse11063a2016-12-15 17:33:32 +000028 * Parses a test.xml generated by jUnit or any testing framework into a protocol buffer. The schema
29 * of the test.xml is a bit hazy, so there is some guesswork involved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010030 */
Ulf Adamse11063a2016-12-15 17:33:32 +000031final class TestXmlOutputParser {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010032 // jUnit can use either "testsuites" or "testsuite".
Googler33876782017-05-07 01:03:35 -040033 private static final ImmutableCollection<String> TOPLEVEL_ELEMENT_NAMES =
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010034 ImmutableSet.of("testsuites", "testsuite");
35
36 public TestCase parseXmlIntoTestResult(InputStream xmlStream)
37 throws TestXmlOutputParserException {
38 return parseXmlToTree(xmlStream);
39 }
40
41 /**
42 * Parses the a test result XML file into the corresponding protocol buffer.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010043 *
Ulf Adamse11063a2016-12-15 17:33:32 +000044 * @param xmlStream the XML data stream
45 * @return the protocol buffer with the parsed data, or null if there was an error while parsing
46 * the file.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010047 * @throws TestXmlOutputParserException when the XML file cannot be parsed
48 */
Ulf Adamse11063a2016-12-15 17:33:32 +000049 private TestCase parseXmlToTree(InputStream xmlStream) throws TestXmlOutputParserException {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010050 XMLStreamReader parser = null;
51
52 try {
53 parser = XMLInputFactory.newInstance().createXMLStreamReader(xmlStream);
54
55 while (true) {
56 int event = parser.next();
57 if (event == XMLStreamConstants.END_DOCUMENT) {
58 return null;
59 }
60
61 // First find the topmost node.
62 if (event == XMLStreamConstants.START_ELEMENT) {
63 String elementName = parser.getLocalName();
64 if (TOPLEVEL_ELEMENT_NAMES.contains(elementName)) {
65 TestCase result = parseTestSuite(parser, elementName);
66 return result;
67 }
68 }
69 }
70 } catch (XMLStreamException e) {
71 throw new TestXmlOutputParserException(e);
Ulf Adamse11063a2016-12-15 17:33:32 +000072 } catch (NumberFormatException e) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010073 // The parser is definitely != null here.
74 throw new TestXmlOutputParserException(
75 "Number could not be parsed at "
Ulf Adamse11063a2016-12-15 17:33:32 +000076 + parser.getLocation().getLineNumber()
77 + ":"
78 + parser.getLocation().getColumnNumber(),
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010079 e);
80 } catch (UninitializedMessageException e) {
81 // This happens when the XML does not contain a field that is required
82 // in the protocol buffer
83 throw new TestXmlOutputParserException(e);
84 } catch (RuntimeException e) {
85
86 // Seems like that an XNIException can leak through, even though it is not
87 // specified anywhere.
88 //
89 // It's a bad idea to refer to XNIException directly because the Xerces
90 // documentation says that it may not be available here soon (and it
91 // results in a compile-time warning anyway), so we do it the roundabout
92 // way: check if the class name has something to do with Xerces, and if
93 // so, wrap it in our own exception type, otherwise, let the stack
94 // unwinding continue.
95 String name = e.getClass().getCanonicalName();
96 if (name != null && name.contains("org.apache.xerces")) {
97 throw new TestXmlOutputParserException(e);
98 } else {
99 throw e;
100 }
101 } finally {
102 if (parser != null) {
103 try {
104 parser.close();
105 } catch (XMLStreamException e) {
106
107 // Ignore errors during closure so that we do not interfere with an
108 // already propagating exception.
109 }
110 }
111 }
112 }
113
114 /**
Ulf Adamse11063a2016-12-15 17:33:32 +0000115 * Creates an exception suitable to be thrown when and a bad end tag appears. The exception could
116 * also be thrown from here but that would result in an extra stack frame, whereas this way, the
117 * topmost frame shows the location where the error occurred.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100118 */
119 private TestXmlOutputParserException createBadElementException(
120 String expected, XMLStreamReader parser) {
Ulf Adamse11063a2016-12-15 17:33:32 +0000121 return new TestXmlOutputParserException(
122 "Expected end of XML element '"
123 + expected
124 + "' , but got '"
125 + parser.getLocalName()
126 + "' at "
127 + parser.getLocation().getLineNumber()
128 + ":"
129 + parser.getLocation().getColumnNumber());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100130 }
131
132 /**
133 * Parses a 'testsuite' element.
134 *
135 * @throws TestXmlOutputParserException if the XML document is malformed
136 * @throws XMLStreamException if there was an error processing the XML
Ulf Adamse11063a2016-12-15 17:33:32 +0000137 * @throws NumberFormatException if one of the numeric fields does not contain a valid number
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100138 */
139 private TestCase parseTestSuite(XMLStreamReader parser, String elementName)
140 throws XMLStreamException, TestXmlOutputParserException {
141 TestCase.Builder builder = TestCase.newBuilder();
jcater36745912018-05-01 13:20:00 -0700142 builder.setType(TestCase.Type.TEST_SUITE);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100143 for (int i = 0; i < parser.getAttributeCount(); i++) {
144 String name = parser.getAttributeLocalName(i).intern();
145 String value = parser.getAttributeValue(i);
146
147 if (name.equals("name")) {
148 builder.setName(value);
149 } else if (name.equals("time")) {
150 builder.setRunDurationMillis(parseTime(value));
151 }
152 }
153
154 parseContainedElements(parser, elementName, builder);
155 return builder.build();
156 }
157
158 /**
159 * Parses a time in test.xml format.
160 *
Ulf Adamse11063a2016-12-15 17:33:32 +0000161 * @throws NumberFormatException if the time is malformed (i.e. is neither an integer nor a
162 * decimal fraction with '.' as the fraction separator)
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100163 */
164 private long parseTime(String string) {
165
166 // This is ugly. For Historical Reasons, we have to check whether the number
167 // contains a decimal point or not. If it does, the number is expressed in
168 // milliseconds, otherwise, in seconds.
169 if (string.contains(".")) {
170 return Math.round(Float.parseFloat(string) * 1000);
171 } else {
172 return Long.parseLong(string);
173 }
174 }
175
176 /**
177 * Parses a 'decorator' element.
178 *
179 * @throws TestXmlOutputParserException if the XML document is malformed
180 * @throws XMLStreamException if there was an error processing the XML
Ulf Adamse11063a2016-12-15 17:33:32 +0000181 * @throws NumberFormatException if one of the numeric fields does not contain a valid number
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100182 */
183 private TestCase parseTestDecorator(XMLStreamReader parser)
184 throws XMLStreamException, TestXmlOutputParserException {
185 TestCase.Builder builder = TestCase.newBuilder();
jcater36745912018-05-01 13:20:00 -0700186 builder.setType(TestCase.Type.TEST_DECORATOR);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100187 for (int i = 0; i < parser.getAttributeCount(); i++) {
188 String name = parser.getAttributeLocalName(i);
189 String value = parser.getAttributeValue(i);
190
191 builder.setName(name);
192 if (name.equals("classname")) {
193 builder.setClassName(value);
194 } else if (name.equals("time")) {
195 builder.setRunDurationMillis(parseTime(value));
196 }
197 }
198
199 parseContainedElements(parser, "testdecorator", builder);
200 return builder.build();
201 }
202
203 /**
Ulf Adamse11063a2016-12-15 17:33:32 +0000204 * Parses child elements of the specified tag. Strictly speaking, not every element can be a child
205 * of every other, but the HierarchicalTestResult can handle that, and (in this case) it does not
206 * hurt to be a bit more flexible than necessary.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100207 *
208 * @throws TestXmlOutputParserException if the XML document is malformed
209 * @throws XMLStreamException if there was an error processing the XML
Ulf Adamse11063a2016-12-15 17:33:32 +0000210 * @throws NumberFormatException if one of the numeric fields does not contain a valid number
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100211 */
212 private void parseContainedElements(
213 XMLStreamReader parser, String elementName, TestCase.Builder builder)
214 throws XMLStreamException, TestXmlOutputParserException {
215 int failures = 0;
216 int errors = 0;
217
218 while (true) {
219 int event = parser.next();
220 switch (event) {
221 case XMLStreamConstants.START_ELEMENT:
222 String childElementName = parser.getLocalName().intern();
223
224 // We are not parsing four elements here: system-out, system-err,
225 // failure and error. They potentially contain useful information, but
226 // they can be too big to fit in the memory. We add failure and error
227 // elements to the output without a message, so that there is a
228 // difference between passed and failed test cases.
Ulf Adams07dba942015-03-05 14:47:37 +0000229 switch (childElementName) {
230 case "testsuite":
231 builder.addChild(parseTestSuite(parser, childElementName));
232 break;
233 case "testcase":
234 builder.addChild(parseTestCase(parser));
235 break;
236 case "failure":
237 failures += 1;
238 skipCompleteElement(parser);
239 break;
240 case "error":
241 errors += 1;
242 skipCompleteElement(parser);
243 break;
244 case "testdecorator":
245 builder.addChild(parseTestDecorator(parser));
246 break;
247 default:
248 // Unknown element encountered. Since the schema of the input file
249 // is a bit hazy, just skip it and go merrily on our way. Ignorance
250 // is bliss.
251 skipCompleteElement(parser);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100252 }
253 break;
254
255 case XMLStreamConstants.END_ELEMENT:
256 // Propagate errors/failures from children up to the current case
257 for (int i = 0; i < builder.getChildCount(); i += 1) {
258 if (builder.getChild(i).getStatus() == TestCase.Status.ERROR) {
259 errors += 1;
260 }
261 if (builder.getChild(i).getStatus() == TestCase.Status.FAILED) {
262 failures += 1;
263 }
264 }
265
266 if (errors > 0) {
267 builder.setStatus(TestCase.Status.ERROR);
268 } else if (failures > 0) {
269 builder.setStatus(TestCase.Status.FAILED);
270 } else {
271 builder.setStatus(TestCase.Status.PASSED);
272 }
273 // This is the end tag of the element we are supposed to parse.
274 // Hooray, tell our superiors that our mission is complete.
275 if (!parser.getLocalName().equals(elementName)) {
276 throw createBadElementException(elementName, parser);
277 }
278 return;
279 }
280 }
281 }
282
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100283 /**
284 * Parses a 'testcase' element.
285 *
286 * @throws TestXmlOutputParserException if the XML document is malformed
287 * @throws XMLStreamException if there was an error processing the XML
Ulf Adamse11063a2016-12-15 17:33:32 +0000288 * @throws NumberFormatException if the time field does not contain a valid number
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100289 */
290 private TestCase parseTestCase(XMLStreamReader parser)
291 throws XMLStreamException, TestXmlOutputParserException {
292 TestCase.Builder builder = TestCase.newBuilder();
jcater36745912018-05-01 13:20:00 -0700293 builder.setType(TestCase.Type.TEST_CASE);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100294 for (int i = 0; i < parser.getAttributeCount(); i++) {
295 String name = parser.getAttributeLocalName(i).intern();
296 String value = parser.getAttributeValue(i);
297
Ulf Adams07dba942015-03-05 14:47:37 +0000298 switch (name) {
299 case "name":
300 builder.setName(value);
301 break;
302 case "classname":
303 builder.setClassName(value);
304 break;
305 case "time":
306 builder.setRunDurationMillis(parseTime(value));
307 break;
308 case "result":
309 builder.setResult(value);
310 break;
311 case "status":
312 if (value.equals("notrun")) {
313 builder.setRun(false);
314 } else if (value.equals("run")) {
315 builder.setRun(true);
316 }
317 break;
318 default:
319 // fall through
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100320 }
321 }
322
323 parseContainedElements(parser, "testcase", builder);
324 return builder.build();
325 }
326
327 /**
Ulf Adamse11063a2016-12-15 17:33:32 +0000328 * Skips over a complete XML element on the input. Precondition: the cursor is at a START_ELEMENT.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100329 * Postcondition: the cursor is at an END_ELEMENT.
330 *
331 * @throws XMLStreamException if the XML is malformed
332 */
333 private void skipCompleteElement(XMLStreamReader parser) throws XMLStreamException {
334 int depth = 1;
335 while (true) {
336 int event = parser.next();
337
338 switch (event) {
339 case XMLStreamConstants.START_ELEMENT:
340 depth++;
341 break;
342
343 case XMLStreamConstants.END_ELEMENT:
344 if (--depth == 0) {
345 return;
346 }
347 break;
348 }
349 }
350 }
351}