package com.twitter.common.testing.runner; import java.io.File; import java.net.InetAddress; import java.net.UnknownHostException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; import javax.xml.bind.JAXB; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementRef; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlValue; import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.junit.runner.Description; import org.junit.runner.Result; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunListener; /** * A run listener that creates ant junit xml report compatible output describing a junit run. */ class AntJunitXmlReportListener extends RunListener { /** * A JAXB bean describing a test case exception. These may indicate either an assertion failure * or an uncaught test exception. */ @XmlRootElement static class Exception { private final String message; private final String type; private final String stacktrace; Exception() { // for JAXB message = null; type = null; stacktrace = null; } Exception(Failure failure) { message = failure.getMessage(); type = failure.getException().getClass().getName(); stacktrace = failure.getTrace(); } @XmlAttribute public String getMessage() { return message; } @XmlAttribute public String getType() { return type; } @XmlValue public String getStacktrace() { return stacktrace; } } /** * A JAXB bean describing an individual test method. */ @XmlRootElement(name = "testcase") static class TestCase { private final String classname; private final String name; private String time; private Exception failure; private Exception error; private long startNs; TestCase() { // for JAXB classname = null; name = null; } TestCase(Description test) { classname = test.getClassName(); name = test.getMethodName(); } @XmlAttribute public String getClassname() { return classname; } @XmlAttribute public String getName() { return name; } @XmlAttribute public String getTime() { return time; } @XmlElement public Exception getFailure() { return failure; } public void setFailure(Exception failure) { this.failure = failure; } @XmlElement public Exception getError() { return error; } public void setError(Exception error) { this.error = error; } public void started() { startNs = System.nanoTime(); } public void finished() { time = convertTimeSpanNs(System.nanoTime() - startNs); } } /** * A JAXB bean describing an individual system property. */ @XmlRootElement(name = "property") static class Property { private final String name; private final String value; Property() { // for JAXB name = null; value = null; } Property(String name, String value) { this.name = name; this.value = value; } @XmlAttribute public String getName() { return name; } @XmlAttribute public String getValue() { return value; } } /** * A JAXB bean describing a test class. */ @XmlRootElement(name = "testsuite") static class TestSuite { private final String name; private int errors; private int failures; private String hostname; private int tests; private String time; private String timestamp; private final List<Property> properties = ImmutableList.copyOf( Iterables.transform(System.getProperties().entrySet(), new Function<Entry<Object, Object>, Property>() { @Override public Property apply(Entry<Object, Object> entry) { return new Property(entry.getKey().toString(), entry.getValue().toString()); } })); private final List<TestCase> testCases = Lists.newArrayList(); private String out; private String err; private long startNs; TestSuite() { // for JAXB name = null; } TestSuite(Description test) { name = test.getClassName(); try { hostname = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { hostname = "localhost"; } } @XmlAttribute public int getErrors() { return errors; } @XmlAttribute public int getFailures() { return failures; } @XmlAttribute public String getHostname() { return hostname; } @XmlAttribute public String getName() { return name; } @XmlAttribute public int getTests() { return tests; } @XmlAttribute public String getTime() { return time; } @XmlAttribute public String getTimestamp() { return timestamp; } @XmlElementRef @XmlElementWrapper(name = "properties") public List<Property> getProperties() { return properties; } @XmlElementRef public List<TestCase> getTestCases() { return testCases; } @XmlElement(name = "system-out") public String getOut() { return out; } public void setOut(String out) { this.out = out; } @XmlElement(name = "system-err") public String getErr() { return err; } public void setErr(String err) { this.err = err; } public void started() { if (startNs == 0) { timestamp = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(new Date()); startNs = System.nanoTime(); } } public void finished() { if (++tests == testCases.size()) { time = convertTimeSpanNs(System.nanoTime() - startNs); } } public void incrementFailures() { failures++; } public void incrementErrors() { errors++; } public boolean wasStarted() { return startNs > 0; } } private final Map<Class<?>, TestSuite> suites = Maps.newHashMap(); private final Map<Description, TestCase> cases = Maps.newHashMap(); private final File outdir; private final StreamSource streamSource; AntJunitXmlReportListener(File outdir, StreamSource streamSource) { this.outdir = outdir; this.streamSource = streamSource; } @Override public void testRunStarted(Description description) throws java.lang.Exception { createSuites(description.getChildren()); } private void createSuites(Iterable<Description> tests) { for (Description test : tests) { createSuites(test.getChildren()); if (Util.isRunnable(test)) { Class<?> testClass = test.getTestClass(); TestSuite suite = suites.get(testClass); if (suite == null) { suite = new TestSuite(test); suites.put(testClass, suite); } TestCase testCase = new TestCase(test); suite.testCases.add(testCase); cases.put(test, testCase); } } } @Override public void testStarted(Description description) throws java.lang.Exception { suites.get(description.getTestClass()).started(); cases.get(description).started(); } @Override public void testFailure(Failure failure) throws java.lang.Exception { Exception exception = new Exception(failure); Description description = failure.getDescription(); TestSuite suite = suites.get(description.getTestClass()); TestCase testCase = cases.get(description); if (Util.isAssertionFailure(failure)) { testCase.setFailure(exception); suite.incrementFailures(); } else { testCase.setError(exception); suite.incrementErrors(); } } @Override public void testFinished(Description description) throws java.lang.Exception { cases.get(description).finished(); suites.get(description.getTestClass()).finished(); } @Override public void testRunFinished(Result result) throws java.lang.Exception { for (Entry<Class<?>, TestSuite> entry : suites.entrySet()) { Class<?> testClass = entry.getKey(); TestSuite suite = entry.getValue(); if (suite.wasStarted()) { suite.setOut(new String(streamSource.readOut(testClass), Charsets.UTF_8)); suite.setErr(new String(streamSource.readErr(testClass), Charsets.UTF_8)); JAXB.marshal(suite, new File(outdir, String.format("TEST-%s.xml", suite.name))); } } } private static String convertTimeSpanNs(long timespanNs) { return String.format("%f", timespanNs / (double) TimeUnit.SECONDS.toNanos(1)); } }