blob: 9516acc8ade66e9cc2b9f40e1bf07659690ceb06 [file] [log] [blame]
/*
* Copyright 2012-present Facebook, Inc.
*
* 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.facebook.buck.junit;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.internal.builders.AllDefaultPossibilitiesBuilder;
import org.junit.internal.builders.JUnit4Builder;
import org.junit.runner.JUnitCore;
import org.junit.runner.Request;
import org.junit.runner.Result;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runner.notification.Failure;
import org.junit.runners.model.RunnerBuilder;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import junit.framework.TestCase;
/**
* Class that runs a set of JUnit tests and writes the results to a directory.
* <p>
* IMPORTANT! This class limits itself to types that are available in both the JDK and Android Java
* API. The objective is to limit the set of files added to the ClassLoader that runs the test, as
* not to interfere with the results of the test.
*/
public final class JUnitRunner {
private final File outputDirectory;
private final List<String> testClassNames;
@SuppressWarnings("unused")
private final long defaultTestTimeoutMillis;
public JUnitRunner(
File outputDirectory,
List<String> testClassNames,
long defaultTestTimeoutMillis) {
this.outputDirectory = outputDirectory;
this.testClassNames = testClassNames;
this.defaultTestTimeoutMillis = defaultTestTimeoutMillis;
}
public void run() throws Throwable {
RunnerBuilder runnerBuilder = createRunnerBuilder();
final JUnitCore jUnit3TestRunner = new JUnitCore();
for (String className : testClassNames) {
final Class<?> testClass = Class.forName(className);
Ignore ignore = testClass.getAnnotation(Ignore.class);
boolean isTestClassIgnored = ignore != null;
List<TestResult> results;
if (isTestClassIgnored) {
// Test case has @Ignore annotation, so do nothing.
results = Collections.emptyList();
} else {
// Run each test method individually.
results = new ArrayList<TestResult>();
Method[] publicInstanceMethods = testClass.getMethods();
for (final Method method : publicInstanceMethods) {
if (!isTestMethod(method)) {
continue;
}
Runner runner = runnerBuilder.runnerForClass(testClass);
Callable<Result> runTestAndProduceJUnitResult;
if (runner instanceof BuckBlockJUnit4ClassRunner) {
final BuckBlockJUnit4ClassRunner jUnit4Runner = (BuckBlockJUnit4ClassRunner)runner;
runTestAndProduceJUnitResult = new Callable<Result>() {
@Override
public Result call() throws NoTestsRemainException {
return jUnit4Runner.runTest(method);
}
};
} else {
runTestAndProduceJUnitResult = new Callable<Result>() {
@Override
public Result call() {
Request request = Request.method(testClass, method.getName());
return jUnit3TestRunner.run(request);
}
};
}
TestResult testResult = TestResult.runTestMethod(runTestAndProduceJUnitResult, method);
results.add(testResult);
}
}
writeResult(className, results);
}
}
/**
* Creates an {@link AllDefaultPossibilitiesBuilder} that returns our custom
* {@link BuckBlockJUnit4ClassRunner} when a {@link JUnit4Builder} is requested. This ensures that
* JUnit 4 tests are executed using our runner whereas other types of tests are run with whatever
* JUnit thinks is best.
*/
private RunnerBuilder createRunnerBuilder() {
final JUnit4Builder jUnit4RunnerBuilder = new JUnit4Builder() {
@Override
public Runner runnerForClass(Class<?> testClass) throws Throwable {
return new BuckBlockJUnit4ClassRunner(testClass, defaultTestTimeoutMillis);
}
};
return new AllDefaultPossibilitiesBuilder(
/* canUseSuiteMethod */ true) {
@Override
protected JUnit4Builder junit4Builder() {
return jUnit4RunnerBuilder;
}
};
}
/**
* The test result file is written as XML to avoid introducing a dependency on JSON (see class
* overview).
*/
private void writeResult(String testClassName, List<TestResult> results)
throws IOException, ParserConfigurationException, TransformerException {
// XML writer logic taken from:
// http://www.genedavis.com/library/xml/java_dom_xml_creation.jsp
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = docBuilder.newDocument();
doc.setXmlVersion("1.1");
Element root = doc.createElement("testcase");
root.setAttribute("name", testClassName);
doc.appendChild(root);
for (TestResult result : results) {
Element test = doc.createElement("test");
// name attribute
test.setAttribute("name", result.testMethodName);
// success attribute
boolean isSuccess = result.isSuccess();
test.setAttribute("success", Boolean.toString(isSuccess));
// time attribute
long runTime = result.runTime;
test.setAttribute("time", String.valueOf(runTime));
// Include failure details, if appropriate.
if (!isSuccess) {
Failure failure = result.failure;
String message = failure.getMessage();
test.setAttribute("message", message);
String stacktrace = failure.getTrace();
test.setAttribute("stacktrace", stacktrace);
}
// stdout, if non-empty.
if (result.stdOut != null) {
Element stdOutEl = doc.createElement("stdout");
stdOutEl.appendChild(doc.createTextNode(result.stdOut));
test.appendChild(stdOutEl);
}
// stderr, if non-empty.
if (result.stdErr != null) {
Element stdErrEl = doc.createElement("stderr");
stdErrEl.appendChild(doc.createTextNode(result.stdErr));
test.appendChild(stdErrEl);
}
root.appendChild(test);
}
// Create an XML transformer that pretty-prints with a 2-space indent.
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer trans = transformerFactory.newTransformer();
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
trans.setOutputProperty(OutputKeys.INDENT, "yes");
trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
// Write the result to a file.
File outputFile = new File(outputDirectory, testClassName + ".xml");
OutputStream output = new BufferedOutputStream(new FileOutputStream(outputFile));
StreamResult streamResult = new StreamResult(output);
DOMSource source = new DOMSource(doc);
trans.transform(source, streamResult);
output.close();
}
/* @VisibleForTesting */
static boolean isTestMethod(Method method) {
// Always ignore a method if it has an @Ignore annotation.
if (method.getAnnotation(Ignore.class) != null) {
return false;
}
// JUnit 4: Methods annotated with @Test are considered tests. Also must be no-arg methods, but
// JUnit will complain about that when it tries to run the method.
if (method.getAnnotation(Test.class) != null) {
return true;
}
// JUnit 3: Declaring class is a subclass of TestCase and method is public void no-arg whose
// name starts with "test". Ideally, all tests in the codebase would use the JUnit 4 style, but
// some test cases have not been converted yet.
Class<?> declaringClass = method.getDeclaringClass();
return (TestCase.class.isAssignableFrom(declaringClass)
&& method.getName().startsWith("test")
&& method.getParameterTypes().length == 0
&& method.getReturnType().equals(Void.TYPE));
}
/**
* Expected arguments are:
* <ul>
* <li>(string) output directory
* <li>(long) default timeout in milliseconds (0 for no timeout)
* <li>(string...) fully-qualified names of test classes
* </ul>
*/
public static void main(String... args) throws Throwable {
// Verify the arguments.
if (args.length == 0) {
System.err.println("Must specify an output directory.");
System.exit(1);
} else if (args.length == 1) {
System.err.println("Must specify an output directory and a default timeout.");
System.exit(1);
} else if (args.length == 2) {
System.err.println("Must specify at least one test.");
System.exit(1);
}
// The first argument should specify the output directory.
File outputDirectory = new File(args[0]);
if (!outputDirectory.exists()) {
System.err.printf("The output directory did not exist: %s\n", outputDirectory);
System.exit(1);
}
long defaultTestTimeoutMillis = Long.parseLong(args[1]);
// Each argument other than the first one should be a class name to run.
List<String> testClassNames = Arrays.asList(args).subList(2, args.length);
// Run the tests.
new JUnitRunner(outputDirectory,
testClassNames,
defaultTestTimeoutMillis)
.run();
// Explicitly exit to force the test runner to complete even if tests have sloppily left behind
// non-daemon threads that would have otherwise forced the process to wait and eventually
// timeout.
//
// Separately, we're using a successful exit code regardless of test outcome since JUnitRunner
// is designed to execute all tests and produce a report of success or failure. We've done
// that successfully if we've gotten here.
System.exit(0);
}
}