blob: 186c9d30db6bd05a1a53b3eca441aed8545ae71e [file] [log] [blame]
// Copyright 2010 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.junit4;
import com.google.testing.junit.junit4.runner.RegExTestCaseFilter;
import com.google.testing.junit.junit4.runner.SuiteTrimmingFilter;
import com.google.testing.junit.runner.internal.Stdout;
import com.google.testing.junit.runner.internal.junit4.CancellableRequestFactory;
import com.google.testing.junit.runner.model.TestSuiteModel;
import com.google.testing.junit.runner.util.GoogleTestSecurityManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Set;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.junit.internal.runners.ErrorReportingRunner;
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.manipulation.NoTestsRemainException;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
/**
* Main entry point for running JUnit4 tests.<p>
*/
public class JUnit4Runner {
private final Request request;
private final CancellableRequestFactory requestFactory;
private final Supplier<TestSuiteModel> modelSupplier;
private final PrintStream testRunnerOut;
private final JUnit4Config config;
private final Set<RunListener> runListeners;
private final Set<Initializer> initializers;
private GoogleTestSecurityManager googleTestSecurityManager;
private SecurityManager previousSecurityManager;
/**
* Creates a runner.
*/
@Inject
JUnit4Runner(
Request request,
CancellableRequestFactory requestFactory,
Supplier<TestSuiteModel> modelSupplier,
@Stdout PrintStream testRunnerOut,
JUnit4Config config,
Set<RunListener> runListeners,
Set<Initializer> initializers) {
this.request = request;
this.requestFactory = requestFactory;
this.modelSupplier = modelSupplier;
this.config = config;
this.testRunnerOut = testRunnerOut;
this.runListeners = runListeners;
this.initializers = initializers;
}
/**
* Runs the JUnit4 test.
*
* @return Result of running the test
*/
public Result run() {
testRunnerOut.println("JUnit4 Test Runner");
checkJUnitRunnerApiVersion();
for (Initializer init : initializers) {
init.initialize();
}
// Sharding
TestSuiteModel model = modelSupplier.get();
Filter shardingFilter = model.getShardingFilter();
Request filteredRequest = applyFilters(request, shardingFilter,
config.getTestIncludeFilterRegexp(),
config.getTestExcludeFilterRegexp());
JUnitCore core = new JUnitCore();
for (RunListener runListener : runListeners) {
core.addListener(runListener);
}
File exitFile = getExitFile();
exitFileActive(exitFile);
try {
try {
if (config.shouldInstallSecurityManager()) {
installSecurityManager();
}
Request cancellableRequest = requestFactory.createRequest(filteredRequest);
return core.run(cancellableRequest);
} finally {
disableSecurityManager();
}
} finally {
exitFileInactive(exitFile);
}
}
// Support for "premature exit files": Tests may write this to communicate
// to the runner in case of premature exit.
private static File getExitFile() {
String exitFile = System.getenv("TEST_PREMATURE_EXIT_FILE");
return exitFile == null ? null : new File(exitFile);
}
private static void exitFileActive(@Nullable File file) {
if (file != null) {
try (FileOutputStream outputStream = new FileOutputStream(file, false)) {
// Overwrite file content.
outputStream.write(new byte[0]);
outputStream.close();
} catch (IOException e) {
throw new RuntimeException("Could not write exit file at " + file, e);
}
}
}
private void exitFileInactive(@Nullable File file) {
if (file != null) {
try {
file.delete();
} catch (Throwable t) {
// Just print the stack trace, to avoid masking a real test failure.
t.printStackTrace(testRunnerOut);
}
}
}
// VisibleForTesting
TestSuiteModel getModel() {
return modelSupplier.get();
}
private static Request applyFilter(Request request, Filter filter)
throws NoTestsRemainException {
Runner runner = request.getRunner();
new SuiteTrimmingFilter(filter).apply(runner);
return Request.runner(runner);
}
/**
* Apply command-line and sharding filters, if appropriate.<p>
*
* Note that this is carefully written to avoid running into potential
* problems with the way runners implement filtering. The JavaDoc for
* {@link org.junit.runner.manipulation.Filterable} states that tests that
* don't match the filter should be removed, which implies if you apply two
* filters, you will always get an intersection of the two. Unfortunately, the
* filtering implementation of {@link org.junit.runners.ParentRunner} does not
* do this, and instead uses a "last applied filter wins" strategy.<p>
*
* We work around potential problems by ensuring that if we apply a second
* filter, the filter is more restrictive than the first. We also assume that
* if filtering fails, the request will have a runner that is a
* {@link ErrorReportingRunner}. Luckily, we can cover this with tests to make
* sure we don't break if JUnit changes in the future.
*
* @param request Request to filter
* @param shardingFilter Sharding filter to use; {@link Filter#ALL} to not do sharding
* @param testIncludeFilterRegexp String denoting a regular expression with which
* to filter tests. Only test descriptions that match this regular
* expression will be run. If {@code null}, tests will not be filtered.
* @param testExcludeFilterRegexp String denoting a regular expression with which
* to filter tests. Only test descriptions that do not match this regular
* expression will be run. If {@code null}, tests will not be filtered.
* @return Filtered request (may be a request that delegates to
* {@link ErrorReportingRunner}
*/
private static Request applyFilters(Request request, Filter shardingFilter,
@Nullable String testIncludeFilterRegexp, @Nullable String testExcludeFilterRegexp) {
// Allow the user to specify a filter on the command line
boolean allowNoTests = false;
Filter filter = Filter.ALL;
if (testIncludeFilterRegexp != null) {
filter = RegExTestCaseFilter.include(testIncludeFilterRegexp);
}
if (testExcludeFilterRegexp != null) {
Filter excludeFilter = RegExTestCaseFilter.exclude(testExcludeFilterRegexp);
filter = filter.intersect(excludeFilter);
}
if (testIncludeFilterRegexp != null || testExcludeFilterRegexp != null) {
try {
request = applyFilter(request, filter);
} catch (NoTestsRemainException e) {
return createErrorReportingRequestForFilterError(filter);
}
/*
* If you filter a sharded test to run one test, we don't want all the
* shards but one to fail.
*/
allowNoTests = (shardingFilter != Filter.ALL);
}
// Sharding
if (shardingFilter != Filter.ALL) {
filter = filter.intersect(shardingFilter);
}
if (filter != Filter.ALL) {
try {
request = applyFilter(request, filter);
} catch (NoTestsRemainException e) {
if (allowNoTests) {
return Request.runner(new NoOpRunner());
} else {
return createErrorReportingRequestForFilterError(filter);
}
}
}
return request;
}
@SuppressWarnings({"ThrowableInstanceNeverThrown"})
private static Request createErrorReportingRequestForFilterError(Filter filter) {
ErrorReportingRunner runner = new ErrorReportingRunner(Filter.class, new Exception(
String.format("No tests found matching %s", filter.describe())));
return Request.runner(runner);
}
private void checkJUnitRunnerApiVersion() {
config.getJUnitRunnerApiVersion();
}
private void installSecurityManager() {
previousSecurityManager = System.getSecurityManager();
GoogleTestSecurityManager newSecurityManager = new GoogleTestSecurityManager();
System.setSecurityManager(newSecurityManager);
// set field after call to setSecurityManager() in case that call fails
googleTestSecurityManager = newSecurityManager;
}
private void disableSecurityManager() {
if (googleTestSecurityManager != null) {
GoogleTestSecurityManager.uninstallIfInstalled();
System.setSecurityManager(previousSecurityManager);
}
}
static class NoOpRunner extends Runner {
@Override
public Description getDescription() {
return Description.createTestDescription(getClass(), "nothingToDo");
}
@Override
public void run(RunNotifier notifier) {
}
}
/**
* A simple initializer which can be used to provide additional initialization logic in custom
* runners.
*
* <p>Initializers will be run in unspecified order. If an exception is thrown it will not be
* deemed recoverable and will cause the runner to error-out.
*/
public interface Initializer {
void initialize();
}
}