// Copyright 2015 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.testing.junit.runner.model; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; import javax.annotation.Nullable; import javax.inject.Inject; /** * Writes the JUnit test nodes and their results into Ant-JUnit XML. Ant-JUnit XML is not a * standardized format. For this implementation the * <a href="http://windyroad.com.au/dl/Open%20Source/JUnit.xsd">XML schema</a> that is generally * referred to as the best available source was used as a reference. */ public final class AntXmlResultWriter implements XmlResultWriter { private static final String JUNIT_ELEMENT_TESTSUITES = "testsuites"; private static final String JUNIT_ELEMENT_TESTSUITE = "testsuite"; private static final String JUNIT_ATTR_TESTSUITE_ERRORS = "errors"; private static final String JUNIT_ATTR_TESTSUITE_FAILURES = "failures"; private static final String JUNIT_ATTR_TESTSUITE_HOSTNAME = "hostname"; private static final String JUNIT_ATTR_TESTSUITE_NAME = "name"; private static final String JUNIT_ATTR_TESTSUITE_TESTS = "tests"; private static final String JUNIT_ATTR_TESTSUITE_TIME = "time"; private static final String JUNIT_ATTR_TESTSUITE_TIMESTAMP = "timestamp"; private static final String JUNIT_ATTR_TESTSUITE_ID = "id"; private static final String JUNIT_ATTR_TESTSUITE_PACKAGE = "package"; private static final String JUNIT_ATTR_TESTSUITE_PROPERTIES = "properties"; private static final String JUNIT_ATTR_TESTSUITE_SYSTEM_OUT = "system-out"; private static final String JUNIT_ATTR_TESTSUITE_SYSTEM_ERR = "system-err"; private static final String JUNIT_ELEMENT_PROPERTY = "property"; private static final String JUNIT_ATTR_PROPERTY_NAME = "name"; private static final String JUNIT_ATTR_PROPERTY_VALUE = "value"; private static final String JUNIT_ELEMENT_TESTCASE = "testcase"; private static final String JUNIT_ELEMENT_FAILURE = "failure"; private static final String JUNIT_ATTR_FAILURE_MESSAGE = "message"; private static final String JUNIT_ATTR_FAILURE_TYPE = "type"; private static final String JUNIT_ATTR_TESTCASE_NAME = "name"; private static final String JUNIT_ATTR_TESTCASE_CLASSNAME = "classname"; private static final String JUNIT_ATTR_TESTCASE_TIME = "time"; private int testSuiteId; @Inject public AntXmlResultWriter() {} @Override public void writeTestSuites(XmlWriter writer, TestResult result) throws IOException { testSuiteId = 0; writer.startDocument(); writer.startElement(JUNIT_ELEMENT_TESTSUITES); for (TestResult child : result.getChildResults()) { writeTestSuite(writer, child, result.getFailures()); } writer.endElement(); writer.close(); } private void writeTestSuite(XmlWriter writer, TestResult result, Iterable<Throwable> parentFailures) throws IOException { List<Throwable> allFailures = new ArrayList<>(); for (Throwable failure : parentFailures) { allFailures.add(failure); } allFailures.addAll(result.getFailures()); parentFailures = allFailures; writer.startElement(JUNIT_ELEMENT_TESTSUITE); writeTestSuiteAttributes(writer, result); writeTestSuiteProperties(writer, result); writeTestCases(writer, result, parentFailures); writeTestSuiteOutput(writer); writer.endElement(); for (TestResult child : result.getChildResults()) { if (!child.getChildResults().isEmpty()) { writeTestSuite(writer, child, parentFailures); } } } private void writeTestSuiteProperties(XmlWriter writer, TestResult result) throws IOException { writer.startElement(JUNIT_ATTR_TESTSUITE_PROPERTIES); for (Entry<String, String> entry : result.getProperties().entrySet()) { writer.startElement(JUNIT_ELEMENT_PROPERTY); writer.writeAttribute(JUNIT_ATTR_PROPERTY_NAME, entry.getKey()); writer.writeAttribute(JUNIT_ATTR_PROPERTY_VALUE, entry.getValue()); writer.endElement(); } writer.endElement(); } private void writeTestCases(XmlWriter writer, TestResult result, Iterable<Throwable> parentFailures) throws IOException { for (TestResult child : result.getChildResults()) { if (child.getChildResults().isEmpty()) { writeTestCase(writer, child, parentFailures); } } } private void writeTestSuiteOutput(XmlWriter writer) throws IOException { writer.startElement(JUNIT_ATTR_TESTSUITE_SYSTEM_OUT); // TODO(bazel-team) - where to get this from? writer.endElement(); writer.startElement(JUNIT_ATTR_TESTSUITE_SYSTEM_ERR); // TODO(bazel-team) - where to get this from? writer.endElement(); } private void writeTestSuiteAttributes(XmlWriter writer, TestResult result) throws IOException { writer.writeAttribute(JUNIT_ATTR_TESTSUITE_NAME, result.getName()); writer.writeAttribute(JUNIT_ATTR_TESTSUITE_TIMESTAMP, getFormattedTimestamp( result.getRunTimeInterval())); writer.writeAttribute(JUNIT_ATTR_TESTSUITE_HOSTNAME, "localhost"); writer.writeAttribute(JUNIT_ATTR_TESTSUITE_TESTS, result.getNumTests()); writer.writeAttribute(JUNIT_ATTR_TESTSUITE_FAILURES, result.getNumFailures()); // JUnit 4.x no longer distinguishes between errors and failures, so it should be safe to just // report errors as 0 and put everything into failures. writer.writeAttribute(JUNIT_ATTR_TESTSUITE_ERRORS, 0); writer.writeAttribute(JUNIT_ATTR_TESTSUITE_TIME, getFormattedRunTime( result.getRunTimeInterval())); // TODO(bazel-team) - do we want to report the package name here? Could we simply get it from // result.getClassName() by stripping the last element of the class name? writer.writeAttribute(JUNIT_ATTR_TESTSUITE_PACKAGE, ""); writer.writeAttribute(JUNIT_ATTR_TESTSUITE_ID, this.testSuiteId++); } private static String getFormattedRunTime(@Nullable TestInterval runTimeInterval) { return runTimeInterval == null ? "0.0" : String.valueOf(runTimeInterval.toDurationMillis() / 1000.0D); } private static String getFormattedTimestamp(@Nullable TestInterval runTimeInterval) { return runTimeInterval == null ? "" : runTimeInterval.startInstantToString(); } private void writeTestCase(XmlWriter writer, TestResult result, Iterable<Throwable> parentFailures) throws IOException { writer.startElement(JUNIT_ELEMENT_TESTCASE); writer.writeAttribute(JUNIT_ATTR_TESTCASE_NAME, result.getName()); writer.writeAttribute(JUNIT_ATTR_TESTCASE_CLASSNAME, result.getClassName()); writer.writeAttribute(JUNIT_ATTR_TESTCASE_TIME, getFormattedRunTime( result.getRunTimeInterval())); for (Throwable failure : parentFailures) { writeThrowableToXmlWriter(writer, failure); } for (Throwable failure : result.getFailures()) { writeThrowableToXmlWriter(writer, failure); } writer.endElement(); } private static void writeThrowableToXmlWriter(XmlWriter writer, Throwable failure) throws IOException { writer.startElement(JUNIT_ELEMENT_FAILURE); writer.writeAttribute( JUNIT_ATTR_FAILURE_MESSAGE, (failure.getMessage() == null) ? "" : failure.getMessage()); writer.writeAttribute(JUNIT_ATTR_FAILURE_TYPE, failure.getClass().getName()); writer.writeCharacters(formatStackTrace(failure)); writer.endElement(); } private static String formatStackTrace(Throwable throwable) { StringWriter stringWriter = new StringWriter(); PrintWriter writer = new PrintWriter(stringWriter); throwable.printStackTrace(writer); return stringWriter.getBuffer().toString(); } }