| // 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. |
| * |
| * <p>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. |
| * |
| * <p>Return codes: |
| * <ul> |
| * <li>Test runner failure, bad arguments, etc.: exit code of 2</li> |
| * <li>Normal test failure: exit code of 1</li> |
| * <li>All tests pass: exit code of 0</li> |
| * </ul> |
| */ |
| 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; |
| try { |
| exitCode = runTestsInSuite(suiteClassName, args); |
| } catch (Throwable e) { |
| // An exception was thrown by the runner. Print the error to the output stream so it will be |
| // logged |
| // by the executing strategy, and return a failure, so this process can gracefully shut down. |
| e.printStackTrace(); |
| exitCode = 1; |
| } |
| |
| 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(); |
| } |
| } |
| } |
| } |