blob: 86022bcc58b3be5a0eebe9ee28e6bd551ad137c6 [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 com.facebook.buck.test.result.type.ResultType;
import com.facebook.buck.test.selectors.TestDescription;
import org.junit.Ignore;
import org.junit.internal.builders.AllDefaultPossibilitiesBuilder;
import org.junit.internal.builders.AnnotatedBuilder;
import org.junit.internal.builders.JUnit4Builder;
import org.junit.runner.Computer;
import org.junit.runner.Description;
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.Filter;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.junit.runners.model.RunnerBuilder;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 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 extends BaseRunner {
public JUnitRunner() {
}
@Override
public void run() throws Throwable {
Filter filter = new Filter() {
@Override
public boolean shouldRun(Description description) {
String methodName = description.getMethodName();
if (methodName == null) {
// JUnit will give us an org.junit.runner.Description like this for the test class
// itself. It's easier for our filtering to make decisions just at the method level,
// however, so just always return true here.
return true;
} else {
String className = description.getClassName();
TestDescription testDescription = new TestDescription(className, methodName);
if (testSelectorList.isIncluded(testDescription)) {
boolean isIgnored = description.getAnnotation(Ignore.class) != null;
if (!isIgnored) {
seenDescriptions.add(testDescription);
}
return !isDryRun;
} else {
return false;
}
}
}
@Override
public String describe() {
return FILTER_DESCRIPTION;
}
};
for (String className : testClassNames) {
final Class<?> testClass = Class.forName(className);
Ignore ignore = testClass.getAnnotation(Ignore.class);
boolean isTestClassIgnored = (ignore != null || !isTestClass(testClass));
List<TestResult> results;
if (isTestClassIgnored) {
// Test case has @Ignore annotation, so do nothing.
results = Collections.emptyList();
} else {
results = new ArrayList<>();
JUnitCore jUnitCore = new JUnitCore();
Runner suite = new Computer().getSuite(createRunnerBuilder(), new Class<?>[]{testClass});
Request request = Request.runner(suite);
request = request.filterWith(filter);
jUnitCore.addListener(new TestListener(results));
jUnitCore.run(request);
}
results = interpretResults(className, results);
if (results != null) {
writeResult(className, results);
}
}
}
/**
* This method filters a list of test results prior to writing results to a file. null is
* returned to indicate "don't write anything", which is different to writing a file containing
* 0 results.
*
* <p>
*
* JUnit handles classes-without-tests in different ways. If you are not using the
* org.junit.runner.Request.filterWith facility then JUnit ignores classes-without-tests.
* However, if you are using a filter then a class-without-tests will cause a
* NoTestsRemainException to be thrown, which is propagated back as an error.
*/
/* @Nullable */
private List<TestResult> interpretResults(String className, List<TestResult> results) {
// For dry runs, write fake results for every method seen in the given class.
if (isDryRun) {
List<TestResult> fakeResults = new ArrayList<>();
for (TestDescription seenDescription : seenDescriptions) {
if (seenDescription.getClassName().equals(className)) {
TestResult fakeResult = new TestResult(
seenDescription.getClassName(),
seenDescription.getMethodName(),
0L,
ResultType.DRY_RUN,
null,
"",
"");
fakeResults.add(fakeResult);
}
}
results = fakeResults;
}
// When not using any command line filtering options, all results should be recorded.
if (testSelectorList.isEmpty()) {
if (isSingleResultCausedByNoTestsRemainException(results)) {
// ...except for testless-classes, where we pretend nothing ran.
return new ArrayList<>();
} else {
return results;
}
}
// If the results size is 0 (which can happen at least with JUnit 4.11), results are not
// significant and shouldn't be recorded.
if (results.size() == 0) {
return null;
}
// In (at least) JUnit 4.8, we have an odd scenario where we have one result telling us we
// have no results.
if (isSingleResultCausedByNoTestsRemainException(results)) {
return null;
}
return results;
}
/**
* JUnit doesn't normally consider encountering a testless class an error. However, when
* using org.junit.runner.manipulation.Filter, testless classes *are* considered an error,
* throwing org.junit.runner.manipulation.NoTestsRemainException.
*
* If we are using test-selectors then it's possible we will run a test class but never run any
* of its test methods, because they'd all get filtered out. When this happens, the results will
* contain a single failure containing the error from the NoTestsRemainException.
*
* (NB: we can't decide at the class level whether we need to run a test class or not; we can only
* run the test class and all its test methods and handle the erroneous exception JUnit throws if
* no test-methods were actually run.)
*/
private boolean isSingleResultCausedByNoTestsRemainException(List<TestResult> results) {
if (results.size() != 1) {
return false;
}
TestResult testResult = results.get(0);
if (testResult.isSuccess()) {
return false;
}
if (testResult.failure == null) {
return false;
}
String message = testResult.failure.getMessage();
if (message == null) {
return false;
}
return message.contains("No tests found matching " + FILTER_DESCRIPTION);
}
private boolean isTestClass(Class<?> klass) {
return klass.getConstructors().length <= 1;
}
/**
* 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;
}
@Override
protected AnnotatedBuilder annotatedBuilder() {
// If there is no default timeout specified in .buckconfig, then use
// the original behavior of AllDefaultPossibilitiesBuilder.
//
// Additionally, if we are using test selectors or doing a dry-run then
// we should use the original behavior to use our
// BuckBlockJUnit4ClassRunner, which provides the Descriptions needed
// to do test selecting properly.
if (defaultTestTimeoutMillis <= 0 || isDryRun || !testSelectorList.isEmpty()) {
return super.annotatedBuilder();
}
return new AnnotatedBuilder(this) {
@Override
public Runner buildRunner(Class<? extends Runner> runnerClass,
Class<?> testClass) throws Exception {
Runner originalRunner = super.buildRunner(runnerClass, testClass);
return new DelegateRunnerWithTimeout(originalRunner, defaultTestTimeoutMillis);
}
};
}
};
}
/**
* Creates RunListener that will prepare individual result for each test
* and store it to results list afterwards.
*/
private static class TestListener extends RunListener {
private final List<TestResult> results;
private PrintStream originalOut, originalErr, stdOutStream, stdErrStream;
private ByteArrayOutputStream rawStdOutBytes, rawStdErrBytes;
private Result result;
private RunListener resultListener;
private Failure assumptionFailure;
// To help give a reasonable (though imprecise) guess at the runtime for unpaired failures
private long startTime = System.currentTimeMillis();
public TestListener(List<TestResult> results) {
this.results = results;
}
@Override
public void testStarted(Description description) throws Exception {
// Create an intermediate stdout/stderr to capture any debugging statements (usually in the
// form of System.out.println) the developer is using to debug the test.
originalOut = System.out;
originalErr = System.err;
rawStdOutBytes = new ByteArrayOutputStream();
rawStdErrBytes = new ByteArrayOutputStream();
stdOutStream = new PrintStream(
rawStdOutBytes, true /* autoFlush */, ENCODING);
stdErrStream = new PrintStream(
rawStdErrBytes, true /* autoFlush */, ENCODING);
System.setOut(stdOutStream);
System.setErr(stdErrStream);
// Prepare single-test result.
result = new Result();
resultListener = result.createListener();
resultListener.testRunStarted(description);
resultListener.testStarted(description);
}
@Override
public void testFinished(Description description) throws Exception {
// Shutdown single-test result.
resultListener.testFinished(description);
resultListener.testRunFinished(result);
resultListener = null;
// Restore the original stdout/stderr.
System.setOut(originalOut);
System.setErr(originalErr);
// Get the stdout/stderr written during the test as strings.
stdOutStream.flush();
stdErrStream.flush();
int numFailures = result.getFailureCount();
String className = description.getClassName();
String methodName = description.getMethodName();
// In practice, I have seen one case of a test having more than one failure:
// com.xtremelabs.robolectric.shadows.H2DatabaseTest#shouldUseH2DatabaseMap() had 2
// failures. However, I am not sure what to make of it, so we let it through.
if (numFailures < 0) {
throw new IllegalStateException(String.format(
"Unexpected number of failures while testing %s#%s(): %d (%s)",
className,
methodName,
numFailures,
result.getFailures()));
}
Failure failure;
ResultType type;
if (assumptionFailure != null) {
failure = assumptionFailure;
type = ResultType.ASSUMPTION_VIOLATION;
// Clear the assumption-failure field before the next test result appears.
assumptionFailure = null;
} else if (numFailures == 0) {
failure = null;
type = ResultType.SUCCESS;
} else {
failure = result.getFailures().get(0);
type = ResultType.FAILURE;
}
String stdOut = rawStdOutBytes.size() == 0 ? null : rawStdOutBytes.toString(ENCODING);
String stdErr = rawStdErrBytes.size() == 0 ? null : rawStdErrBytes.toString(ENCODING);
results.add(new TestResult(className,
methodName,
result.getRunTime(),
type,
failure == null ? null : failure.getException(),
stdOut,
stdErr));
}
/**
* The regular listener we created from the singular result, in this class, will not by
* default treat assumption failures as regular failures, and will not store them. As a
* consequence, we store them ourselves!
*
* We store the assumption-failure in a temporary field, which we'll make sure we clear each
* time we write results.
*/
@Override
public void testAssumptionFailure(Failure failure) {
assumptionFailure = failure;
if (resultListener != null) {
// Left in only to help catch future bugs -- right now this does nothing.
resultListener.testAssumptionFailure(failure);
}
}
@Override
public void testFailure(Failure failure) throws Exception {
if (resultListener == null) {
recordUnpairedFailure(failure);
} else {
resultListener.testFailure(failure);
}
}
@Override
public void testIgnored(Description description) throws Exception {
if (resultListener != null) {
resultListener.testIgnored(description);
}
}
/**
* It's possible to encounter a Failure before we've started any tests (and therefore before
* testStarted() has been called). The known example is a @BeforeClass that throws an
* exception, but there may be others.
* <p>
* Recording these unexpected failures helps us propagate failures back up to the "buck test"
* process.
*/
private void recordUnpairedFailure(Failure failure) {
long runtime = System.currentTimeMillis() - startTime;
Description description = failure.getDescription();
results.add(new TestResult(
description.getClassName(),
description.getMethodName(),
runtime,
ResultType.FAILURE,
failure.getException(),
null,
null));
}
}
}