// Copyright 2017 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;
import com.google.devtools.build.lib.worker.WorkerProtocol.WorkRequest;
import com.google.devtools.build.lib.worker.WorkerProtocol.WorkResponse;
import com.google.testing.junit.runner.internal.StackTraces;
import com.google.testing.junit.runner.junit4.JUnit4InstanceModules.Config;
import com.google.testing.junit.runner.junit4.JUnit4InstanceModules.SuiteClass;
import com.google.testing.junit.runner.junit4.JUnit4Runner;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* Like {@link BazelTestRunner} but runs the tests in their own classloader.
*/
public class ExperimentalTestRunner {
/**
* If no arguments are passed on the command line, use this System property to
* determine which test suite to run.
*/
static final String TEST_SUITE_PROPERTY_NAME = "bazel.test_suite";
private static URL[] classpaths = null;
private static URLClassLoader targetClassLoader;
private ExperimentalTestRunner() {
// utility class; should not be instantiated
}
/**
* Takes as arguments the classes or packages to test.
*
*
To help just run one test or method in a suite, the test suite
* may be passed in via system properties (-Dbazel.test_suite).
* An empty args parameter means to run all tests in the suite.
* A non-empty args parameter means to run only the specified tests/methods.
*
*
Return codes:
*
* - Test runner failure, bad arguments, etc.: exit code of 2
* - Normal test failure: exit code of 1
* - All tests pass: exit code of 0
*
*/
public static void main(String[] args) {
PrintStream stderr = System.err;
String suiteClassName = System.getProperty(TEST_SUITE_PROPERTY_NAME);
if ("true".equals(System.getenv("PERSISTENT_TEST_RUNNER"))) {
System.exit(runPersistentTestRunner(suiteClassName));
}
System.out.println("WARNING: RUNNING EXPERIMENTAL TEST RUNNER");
if (!checkTestSuiteProperty(suiteClassName)) {
System.exit(2);
}
int exitCode = runTestsInSuite(suiteClassName, args);
System.err.printf("%nExperimentalTestRunner exiting with a return value of %d%n", exitCode);
System.err.println("JVM shutdown hooks (if any) will run now.");
System.err.println("The JVM will exit once they complete.");
System.err.println();
printStackTracesIfJvmExitHangs(stderr);
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date shutdownTime = new Date();
String formattedShutdownTime = format.format(shutdownTime);
System.err.printf("-- JVM shutdown starting at %s --%n%n", formattedShutdownTime);
System.exit(exitCode);
}
/**
* Ensures that the bazel.test_suite in argument is not {@code null} or print error and
* explanation.
*
* @param testSuiteProperty system property to check
*/
private static boolean checkTestSuiteProperty(String testSuiteProperty) {
if (testSuiteProperty == null) {
System.err.printf(
"Error: The test suite Java system property %s is required but missing.%n",
TEST_SUITE_PROPERTY_NAME);
System.err.println();
System.err.println("This property is set automatically when running with Bazel like such:");
System.err.printf(" java -D%s=[test-suite-class] %s%n",
TEST_SUITE_PROPERTY_NAME, ExperimentalTestRunner.class.getName());
System.err.printf(" java -D%s=[test-suite-class] -jar [deploy-jar]%n",
TEST_SUITE_PROPERTY_NAME);
System.err.println("E.g.:");
System.err.printf(" java -D%s=org.example.testing.junit.runner.SmallTests %s%n",
TEST_SUITE_PROPERTY_NAME, ExperimentalTestRunner.class.getName());
System.err.printf(" java -D%s=org.example.testing.junit.runner.SmallTests "
+ "-jar SmallTests_deploy.jar%n",
TEST_SUITE_PROPERTY_NAME);
return false;
}
return true;
}
private static int runTestsInSuite(String suiteClassName, String[] args) {
Class> suite = getTargetSuiteClass(suiteClassName);
if (suite == null) {
// No class found corresponding to the system property passed in from Bazel
if (args.length == 0 && suiteClassName != null) {
System.err.printf("Class not found: [%s]%n", suiteClassName);
return 2;
}
}
JUnit4Runner runner =
JUnit4Bazel.builder()
.suiteClass(new SuiteClass(suite))
.config(new Config(args))
.build()
.runner();
// Some frameworks such as Mockito use the Thread's context classloader.
Thread.currentThread().setContextClassLoader(targetClassLoader);
int result = 1;
try {
result = runner.run().wasSuccessful() ? 0 : 1;
} catch (RuntimeException e) {
System.err.println("Test run failed with exception");
e.printStackTrace();
}
return result;
}
/**
* Run in a loop awaiting instructions for the next test run.
*
* @param suiteClassName name of the class which is passed on to JUnit to determine the test suite
* @return 0 when we encounter an EOF from input, or non-zero values if we encounter an
* unrecoverable error.
*/
private static int runPersistentTestRunner(String suiteClassName) {
PrintStream originalStdOut = System.out;
PrintStream originalStdErr = System.err;
while (true) {
try {
WorkRequest request = WorkRequest.parseDelimitedFrom(System.in);
if (request == null) {
break;
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PrintStream printStream = new PrintStream(outputStream, true);
System.setOut(printStream);
System.setErr(printStream);
String[] arguments = request.getArgumentsList().toArray(new String[0]);
int exitCode = -1;
try {
exitCode = runTestsInSuite(suiteClassName, arguments);
} finally {
System.setOut(originalStdOut);
System.setErr(originalStdErr);
}
WorkResponse response =
WorkResponse
.newBuilder()
.setOutput(outputStream.toString())
.setExitCode(exitCode)
.build();
response.writeDelimitedTo(System.out);
System.out.flush();
} catch (IOException e) {
e.printStackTrace();
return 1;
}
}
return 0;
}
/**
* Get the actual Test Suite class corresponding to the given name.
*/
private static Class> getTargetSuiteClass(String suiteClassName) {
if (suiteClassName == null) {
return null;
}
try {
targetClassLoader = new URLClassLoader(getClasspaths());
Class> targetSuiteClass = targetClassLoader.loadClass(suiteClassName);
System.out.printf(
"Running test suites for class: %s, created by classLoader: %s%n",
targetSuiteClass, targetSuiteClass.getClassLoader());
return targetSuiteClass;
} catch (ClassNotFoundException | IOException e) {
System.err.println("Exception in loading class:" + e.getMessage());
return null;
}
}
/**
* Used to get the classpaths which should be used to load the classes of the test target.
*
* @throws MalformedURLException when we are unable to create a given classpath.
* @return array of URLs containing the classpaths or null if classpaths could not be located.
*/
private static URL[] getClasspaths() throws IOException {
if (classpaths != null) {
return classpaths;
}
String selfLocation = System.getenv("SELF_LOCATION");
// TODO(kush): Get this to work for windows style paths.
String classpathFileLocation = selfLocation + "_classpaths_file";
Path file = Paths.get(classpathFileLocation);
byte[] classPathFileBytes = null;
try {
classPathFileBytes = Files.readAllBytes(file);
} catch (IOException e) {
System.err.println("exception in reading file:" + e.getMessage());
throw e;
}
String classloaderPrefixPath = System.getenv("CLASSLOADER_PREFIX_PATH");
String workingDir = System.getProperty("user.dir");
String[] targetClassPaths = new String(classPathFileBytes, StandardCharsets.UTF_8).split(":");
classpaths = new URL[targetClassPaths.length];
String locationPrefix = "file://" + workingDir + "/" + classloaderPrefixPath;
for (int index = 0; index < targetClassPaths.length; index++) {
try {
classpaths[index] = new URL(locationPrefix + targetClassPaths[index]);
} catch (MalformedURLException e) {
System.err.println("Unable to create URL for:" + targetClassPaths[index]);
throw e;
}
}
return classpaths;
}
/**
* Prints out stack traces if the JVM does not exit quickly. This can help detect shutdown hooks
* that are preventing the JVM from exiting quickly.
*
* @param out Print stream to use
*/
private static void printStackTracesIfJvmExitHangs(final PrintStream out) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
sleepUninterruptibly(5);
out.println("JVM still up after five seconds. Dumping stack traces for all threads.");
StackTraces.printAll(out);
}
}, "ExperimentalTestRunner: Print stack traces if JVM exit hangs");
thread.setDaemon(true);
thread.start();
}
/**
* Invokes SECONDS.{@link TimeUnit#sleep(long) sleep(sleepForSeconds)} uninterruptibly.
*/
private static void sleepUninterruptibly(long sleepForSeconds) {
boolean interrupted = false;
try {
long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(sleepForSeconds);
while (true) {
try {
// TimeUnit.sleep() treats negative timeouts just like zero.
TimeUnit.NANOSECONDS.sleep(end - System.nanoTime());
return;
} catch (InterruptedException e) {
interrupted = true;
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
}