| // Copyright 2014 The Bazel Authors. All rights reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.devtools.build.lib.exec; | |
| import com.google.common.collect.ImmutableCollection; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.devtools.build.lib.view.test.TestStatus.TestCase; | |
| import com.google.protobuf.UninitializedMessageException; | |
| import java.io.InputStream; | |
| import javax.xml.stream.XMLInputFactory; | |
| import javax.xml.stream.XMLStreamConstants; | |
| import javax.xml.stream.XMLStreamException; | |
| import javax.xml.stream.XMLStreamReader; | |
| /** | |
| * Parses a test.xml generated by jUnit or any testing framework into a protocol buffer. The schema | |
| * of the test.xml is a bit hazy, so there is some guesswork involved. | |
| */ | |
| public final class TestXmlOutputParser { | |
| // jUnit can use either "testsuites" or "testsuite". | |
| private static final ImmutableCollection<String> TOPLEVEL_ELEMENT_NAMES = | |
| ImmutableSet.of("testsuites", "testsuite"); | |
| public TestCase parseXmlIntoTestResult(InputStream xmlStream) | |
| throws TestXmlOutputParserException { | |
| return parseXmlToTree(xmlStream); | |
| } | |
| /** | |
| * Parses the a test result XML file into the corresponding protocol buffer. | |
| * | |
| * @param xmlStream the XML data stream | |
| * @return the protocol buffer with the parsed data, or null if there was an error while parsing | |
| * the file. | |
| * @throws TestXmlOutputParserException when the XML file cannot be parsed | |
| */ | |
| private TestCase parseXmlToTree(InputStream xmlStream) throws TestXmlOutputParserException { | |
| XMLStreamReader parser = null; | |
| try { | |
| parser = XMLInputFactory.newInstance().createXMLStreamReader(xmlStream); | |
| while (true) { | |
| int event = parser.next(); | |
| if (event == XMLStreamConstants.END_DOCUMENT) { | |
| return null; | |
| } | |
| // First find the topmost node. | |
| if (event == XMLStreamConstants.START_ELEMENT) { | |
| String elementName = parser.getLocalName(); | |
| if (TOPLEVEL_ELEMENT_NAMES.contains(elementName)) { | |
| TestCase result = parseTestSuite(parser, elementName); | |
| return result; | |
| } | |
| } | |
| } | |
| } catch (XMLStreamException e) { | |
| throw new TestXmlOutputParserException(e); | |
| } catch (NumberFormatException e) { | |
| // The parser is definitely != null here. | |
| throw new TestXmlOutputParserException( | |
| "Number could not be parsed at " | |
| + parser.getLocation().getLineNumber() | |
| + ":" | |
| + parser.getLocation().getColumnNumber(), | |
| e); | |
| } catch (UninitializedMessageException e) { | |
| // This happens when the XML does not contain a field that is required | |
| // in the protocol buffer | |
| throw new TestXmlOutputParserException(e); | |
| } catch (RuntimeException e) { | |
| // Seems like that an XNIException can leak through, even though it is not | |
| // specified anywhere. | |
| // | |
| // It's a bad idea to refer to XNIException directly because the Xerces | |
| // documentation says that it may not be available here soon (and it | |
| // results in a compile-time warning anyway), so we do it the roundabout | |
| // way: check if the class name has something to do with Xerces, and if | |
| // so, wrap it in our own exception type, otherwise, let the stack | |
| // unwinding continue. | |
| String name = e.getClass().getCanonicalName(); | |
| if (name != null && name.contains("org.apache.xerces")) { | |
| throw new TestXmlOutputParserException(e); | |
| } else { | |
| throw e; | |
| } | |
| } finally { | |
| if (parser != null) { | |
| try { | |
| parser.close(); | |
| } catch (XMLStreamException e) { | |
| // Ignore errors during closure so that we do not interfere with an | |
| // already propagating exception. | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Creates an exception suitable to be thrown when and a bad end tag appears. The exception could | |
| * also be thrown from here but that would result in an extra stack frame, whereas this way, the | |
| * topmost frame shows the location where the error occurred. | |
| */ | |
| private TestXmlOutputParserException createBadElementException( | |
| String expected, XMLStreamReader parser) { | |
| return new TestXmlOutputParserException( | |
| "Expected end of XML element '" | |
| + expected | |
| + "' , but got '" | |
| + parser.getLocalName() | |
| + "' at " | |
| + parser.getLocation().getLineNumber() | |
| + ":" | |
| + parser.getLocation().getColumnNumber()); | |
| } | |
| /** | |
| * Parses a 'testsuite' element. | |
| * | |
| * @throws TestXmlOutputParserException if the XML document is malformed | |
| * @throws XMLStreamException if there was an error processing the XML | |
| * @throws NumberFormatException if one of the numeric fields does not contain a valid number | |
| */ | |
| private TestCase parseTestSuite(XMLStreamReader parser, String elementName) | |
| throws XMLStreamException, TestXmlOutputParserException { | |
| TestCase.Builder builder = TestCase.newBuilder(); | |
| builder.setType(TestCase.Type.TEST_SUITE); | |
| for (int i = 0; i < parser.getAttributeCount(); i++) { | |
| String name = parser.getAttributeLocalName(i).intern(); | |
| String value = parser.getAttributeValue(i); | |
| if (name.equals("name")) { | |
| builder.setName(value); | |
| } else if (name.equals("time")) { | |
| builder.setRunDurationMillis(parseTime(value)); | |
| } | |
| } | |
| parseContainedElements(parser, elementName, builder); | |
| return builder.build(); | |
| } | |
| /** | |
| * Parses a time in test.xml format. | |
| * | |
| * @throws NumberFormatException if the time is malformed (i.e. is neither an integer nor a | |
| * decimal fraction with '.' as the fraction separator) | |
| */ | |
| private long parseTime(String string) { | |
| // This is ugly. For Historical Reasons, we have to check whether the number | |
| // contains a decimal point or not. If it does, the number is expressed in | |
| // milliseconds, otherwise, in seconds. | |
| if (string.contains(".")) { | |
| return Math.round(Float.parseFloat(string) * 1000); | |
| } else { | |
| return Long.parseLong(string); | |
| } | |
| } | |
| /** | |
| * Parses a 'decorator' element. | |
| * | |
| * @throws TestXmlOutputParserException if the XML document is malformed | |
| * @throws XMLStreamException if there was an error processing the XML | |
| * @throws NumberFormatException if one of the numeric fields does not contain a valid number | |
| */ | |
| private TestCase parseTestDecorator(XMLStreamReader parser) | |
| throws XMLStreamException, TestXmlOutputParserException { | |
| TestCase.Builder builder = TestCase.newBuilder(); | |
| builder.setType(TestCase.Type.TEST_DECORATOR); | |
| for (int i = 0; i < parser.getAttributeCount(); i++) { | |
| String name = parser.getAttributeLocalName(i); | |
| String value = parser.getAttributeValue(i); | |
| builder.setName(name); | |
| if (name.equals("classname")) { | |
| builder.setClassName(value); | |
| } else if (name.equals("time")) { | |
| builder.setRunDurationMillis(parseTime(value)); | |
| } | |
| } | |
| parseContainedElements(parser, "testdecorator", builder); | |
| return builder.build(); | |
| } | |
| /** | |
| * Parses child elements of the specified tag. Strictly speaking, not every element can be a child | |
| * of every other, but the HierarchicalTestResult can handle that, and (in this case) it does not | |
| * hurt to be a bit more flexible than necessary. | |
| * | |
| * @throws TestXmlOutputParserException if the XML document is malformed | |
| * @throws XMLStreamException if there was an error processing the XML | |
| * @throws NumberFormatException if one of the numeric fields does not contain a valid number | |
| */ | |
| private void parseContainedElements( | |
| XMLStreamReader parser, String elementName, TestCase.Builder builder) | |
| throws XMLStreamException, TestXmlOutputParserException { | |
| int failures = 0; | |
| int errors = 0; | |
| while (true) { | |
| int event = parser.next(); | |
| switch (event) { | |
| case XMLStreamConstants.START_ELEMENT: | |
| String childElementName = parser.getLocalName().intern(); | |
| // We are not parsing four elements here: system-out, system-err, | |
| // failure and error. They potentially contain useful information, but | |
| // they can be too big to fit in the memory. We add failure and error | |
| // elements to the output without a message, so that there is a | |
| // difference between passed and failed test cases. | |
| switch (childElementName) { | |
| case "testsuite": | |
| builder.addChild(parseTestSuite(parser, childElementName)); | |
| break; | |
| case "testcase": | |
| builder.addChild(parseTestCase(parser)); | |
| break; | |
| case "failure": | |
| failures += 1; | |
| skipCompleteElement(parser); | |
| break; | |
| case "error": | |
| errors += 1; | |
| skipCompleteElement(parser); | |
| break; | |
| case "testdecorator": | |
| builder.addChild(parseTestDecorator(parser)); | |
| break; | |
| default: | |
| // Unknown element encountered. Since the schema of the input file | |
| // is a bit hazy, just skip it and go merrily on our way. Ignorance | |
| // is bliss. | |
| skipCompleteElement(parser); | |
| } | |
| break; | |
| case XMLStreamConstants.END_ELEMENT: | |
| // Propagate errors/failures from children up to the current case | |
| for (int i = 0; i < builder.getChildCount(); i += 1) { | |
| if (builder.getChild(i).getStatus() == TestCase.Status.ERROR) { | |
| errors += 1; | |
| } | |
| if (builder.getChild(i).getStatus() == TestCase.Status.FAILED) { | |
| failures += 1; | |
| } | |
| } | |
| if (errors > 0) { | |
| builder.setStatus(TestCase.Status.ERROR); | |
| } else if (failures > 0) { | |
| builder.setStatus(TestCase.Status.FAILED); | |
| } else { | |
| builder.setStatus(TestCase.Status.PASSED); | |
| } | |
| // This is the end tag of the element we are supposed to parse. | |
| // Hooray, tell our superiors that our mission is complete. | |
| if (!parser.getLocalName().equals(elementName)) { | |
| throw createBadElementException(elementName, parser); | |
| } | |
| return; | |
| } | |
| } | |
| } | |
| /** | |
| * Parses a 'testcase' element. | |
| * | |
| * @throws TestXmlOutputParserException if the XML document is malformed | |
| * @throws XMLStreamException if there was an error processing the XML | |
| * @throws NumberFormatException if the time field does not contain a valid number | |
| */ | |
| private TestCase parseTestCase(XMLStreamReader parser) | |
| throws XMLStreamException, TestXmlOutputParserException { | |
| TestCase.Builder builder = TestCase.newBuilder(); | |
| builder.setType(TestCase.Type.TEST_CASE); | |
| for (int i = 0; i < parser.getAttributeCount(); i++) { | |
| String name = parser.getAttributeLocalName(i).intern(); | |
| String value = parser.getAttributeValue(i); | |
| switch (name) { | |
| case "name": | |
| builder.setName(value); | |
| break; | |
| case "classname": | |
| builder.setClassName(value); | |
| break; | |
| case "time": | |
| builder.setRunDurationMillis(parseTime(value)); | |
| break; | |
| case "result": | |
| builder.setResult(value); | |
| break; | |
| case "status": | |
| if (value.equals("notrun")) { | |
| builder.setRun(false); | |
| } else if (value.equals("run")) { | |
| builder.setRun(true); | |
| } | |
| break; | |
| default: | |
| // fall through | |
| } | |
| } | |
| parseContainedElements(parser, "testcase", builder); | |
| return builder.build(); | |
| } | |
| /** | |
| * Skips over a complete XML element on the input. Precondition: the cursor is at a START_ELEMENT. | |
| * Postcondition: the cursor is at an END_ELEMENT. | |
| * | |
| * @throws XMLStreamException if the XML is malformed | |
| */ | |
| private void skipCompleteElement(XMLStreamReader parser) throws XMLStreamException { | |
| int depth = 1; | |
| while (true) { | |
| int event = parser.next(); | |
| switch (event) { | |
| case XMLStreamConstants.START_ELEMENT: | |
| depth++; | |
| break; | |
| case XMLStreamConstants.END_ELEMENT: | |
| if (--depth == 0) { | |
| return; | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| } |