// 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 = 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(); } } } }