Create ExperimentalTestRunner which is just the same as BazelTestRunner, but a testbed of upcoming changes, without breaking existing test targets.

To use the alternate test runner a java test should add the tag "experimental_testrunner" and depend on "@bazel_tools//tools/jdk:ExperimentalTestRunner_deploy.jar" (instead of @bazel_tools//tools/jdk:TestRunner_deploy.jar)

--
PiperOrigin-RevId: 149536298
MOS_MIGRATED_REVID=149536298
diff --git a/src/BUILD b/src/BUILD
index d350c6a..a418615 100644
--- a/src/BUILD
+++ b/src/BUILD
@@ -148,6 +148,7 @@
         "//src/java_tools/singlejar:SingleJar_deploy.jar",
         "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/genclass:GenClass_deploy.jar",
         "//src/java_tools/junitrunner/java/com/google/testing/junit/runner:Runner_deploy.jar",
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner:ExperimentalRunner_deploy.jar",
         "//src/java_tools/junitrunner/java/com/google/testing/coverage:embedded_tools",
         "//third_party/ijar",
         "//third_party/java/apkbuilder:embedded_tools",
diff --git a/src/create_embedded_tools.sh b/src/create_embedded_tools.sh
index 56ddf8d..d814f17 100755
--- a/src/create_embedded_tools.sh
+++ b/src/create_embedded_tools.sh
@@ -48,6 +48,7 @@
     *javac7.jar) OUTPUT_PATH=third_party/java/jdk/langtools/javac7.jar ;;
     *SingleJar_deploy.jar) OUTPUT_PATH=tools/jdk/SingleJar_deploy.jar ;;
     *GenClass_deploy.jar) OUTPUT_PATH=tools/jdk/GenClass_deploy.jar ;;
+    *ExperimentalRunner_deploy.jar) OUTPUT_PATH=tools/jdk/ExperimentalTestRunner_deploy.jar ;;
     *Runner_deploy.jar) OUTPUT_PATH=tools/jdk/TestRunner_deploy.jar ;;
     *ijar.exe) OUTPUT_PATH=tools/jdk/ijar/ijar.exe ;;
     *ijar) OUTPUT_PATH=tools/jdk/ijar/ijar ;;
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BUILD b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BUILD
index 1699cb8..c5a8006 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BUILD
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BUILD
@@ -7,9 +7,41 @@
 
 package(default_visibility = ["//src:__subpackages__"])
 
+filegroup(
+    name = "common_runner_java_files",
+    srcs = glob(
+        ["*.java"],
+        exclude =
+            [
+                "BazelTestRunner.java",
+                "ExperimentalTestRunner.java",
+            ],
+    ),
+)
+
 java_library(
     name = "test_runner",
-    srcs = glob(["*.java"]),
+    srcs = [
+        "BazelTestRunner.java",
+        ":common_runner_java_files",
+    ],
+    data = ["//tools:test_sharding_compliant"],
+    deps = [
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal",
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4",
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/model",
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding",
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/util",
+        "//third_party:junit4",
+    ],
+)
+
+java_library(
+    name = "experimental_test_runner",
+    srcs = [
+        "ExperimentalTestRunner.java",
+        ":common_runner_java_files",
+    ],
     data = ["//tools:test_sharding_compliant"],
     deps = [
         "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal",
@@ -27,6 +59,12 @@
     runtime_deps = [":test_runner"],
 )
 
+java_binary(
+    name = "ExperimentalRunner",
+    main_class = "com.google.testing.junit.runner.ExperimentalTestRunner",
+    runtime_deps = [":experimental_test_runner"],
+)
+
 filegroup(
     name = "srcs",
     testonly = 0,  # All srcs should be not test only, overwrite package default.
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BazelTestRunner.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BazelTestRunner.java
index 74a6826..66d7d91 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BazelTestRunner.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BazelTestRunner.java
@@ -18,8 +18,6 @@
 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 com.google.testing.junit.runner.model.AntXmlResultWriter;
-import com.google.testing.junit.runner.model.XmlResultWriter;
 import java.io.PrintStream;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
@@ -200,18 +198,4 @@
       }
     }
   }
-
-  static class BazelTestRunnerModule {
-    static XmlResultWriter resultWriter(AntXmlResultWriter impl) {
-      return impl;
-    }
-
-    static PrintStream stdoutStream() {
-      return System.out;
-    }
-
-    static PrintStream stderrStream() {
-      return System.err;
-    }
-  }
 }
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BazelTestRunnerModule.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BazelTestRunnerModule.java
new file mode 100644
index 0000000..a3c1c6f
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BazelTestRunnerModule.java
@@ -0,0 +1,36 @@
+// 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.testing.junit.runner.model.AntXmlResultWriter;
+import com.google.testing.junit.runner.model.XmlResultWriter;
+import java.io.PrintStream;
+
+/**
+ * Originally a Dagger module extracted out of {@link BazelTestRunner}.
+ */
+class BazelTestRunnerModule {
+  static XmlResultWriter resultWriter(AntXmlResultWriter impl) {
+    return impl;
+  }
+
+  static PrintStream stdoutStream() {
+    return System.out;
+  }
+
+  static PrintStream stderrStream() {
+    return System.err;
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/ExperimentalTestRunner.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/ExperimentalTestRunner.java
new file mode 100644
index 0000000..f13915e
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/ExperimentalTestRunner.java
@@ -0,0 +1,191 @@
+// 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.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.PrintStream;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * For now the same as {@link BazelTestRunner} but intended to be a testbed to try out new features,
+ * without breeaking existing tests.
+ */
+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 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);
+    System.out.println("WARNING: RUNNING EXPERIMENTAL TEST RUNNER");
+
+    if (args.length >= 1 && args[args.length - 1].equals("--persistent_test_runner")) {
+      System.err.println("Requested test strategy is currently unsupported.");
+      System.exit(1);
+    }
+
+    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 = getTestClass(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;
+      }
+    }
+
+    // TODO(kush): Use a new classloader for the following instantiation.
+    JUnit4Runner runner =
+        JUnit4Bazel.builder()
+            .suiteClass(new SuiteClass(suite))
+            .config(new Config(args))
+            .build()
+            .runner();
+    return runner.run().wasSuccessful() ? 0 : 1;
+  }
+
+  private static Class<?> getTestClass(String name) {
+    if (name == null) {
+      return null;
+    }
+
+    try {
+      return Class.forName(name);
+    } catch (ClassNotFoundException e) {
+      return null;
+    }
+  }
+
+  /**
+   * 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();
+      }
+    }
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/ResultWriterFactory.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/ResultWriterFactory.java
index ce8c70e..34dcdf5 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/ResultWriterFactory.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/ResultWriterFactory.java
@@ -32,8 +32,7 @@
 
   @Override
   public XmlResultWriter get() {
-    XmlResultWriter xmlResultWriter =
-        BazelTestRunner.BazelTestRunnerModule.resultWriter(implSupplier.get());
+    XmlResultWriter xmlResultWriter = BazelTestRunnerModule.resultWriter(implSupplier.get());
     assert xmlResultWriter != null;
     return xmlResultWriter;
   }
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/StderrStreamFactory.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/StderrStreamFactory.java
index 22e45ae..be2303b 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/StderrStreamFactory.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/StderrStreamFactory.java
@@ -25,7 +25,7 @@
 
   @Override
   public PrintStream get() {
-    PrintStream printStream = BazelTestRunner.BazelTestRunnerModule.stderrStream();
+    PrintStream printStream = BazelTestRunnerModule.stderrStream();
     assert printStream != null;
     return printStream;
   }
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/StdoutStreamFactory.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/StdoutStreamFactory.java
index 6e0d646..57517d5 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/StdoutStreamFactory.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/StdoutStreamFactory.java
@@ -25,7 +25,7 @@
 
   @Override
   public PrintStream get() {
-    PrintStream printStream = BazelTestRunner.BazelTestRunnerModule.stdoutStream();
+    PrintStream printStream = BazelTestRunnerModule.stdoutStream();
     assert printStream != null;
     return printStream;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaRuleClasses.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaRuleClasses.java
index 2946b29..87a02bb 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaRuleClasses.java
@@ -60,6 +60,8 @@
       PackageNameConstraint.ANY_SEGMENT, "java", "javatests");
 
   protected static final String JUNIT_TESTRUNNER = "//tools/jdk:TestRunner_deploy.jar";
+  protected static final String EXPERIMENTAL_TESTRUNNER =
+      "//tools/jdk:ExperimentalTestRunner_deploy.jar";
 
   public static final ImplicitOutputsFunction JAVA_BINARY_IMPLICIT_OUTPUTS =
       fromFunctions(
@@ -391,6 +393,17 @@
                               : null;
                         }
                       }))
+          .add(
+              attr("$experimental_testsupport", LABEL)
+                  .value(
+                      new Attribute.ComputedDefault("use_testrunner") {
+                        @Override
+                        public Object getDefault(AttributeMap rule) {
+                          return rule.get("use_testrunner", Type.BOOLEAN)
+                              ? env.getToolsLabel(EXPERIMENTAL_TESTRUNNER)
+                              : null;
+                        }
+                      }))
           /* <!-- #BLAZE_RULE($base_java_binary).ATTRIBUTE(deploy_manifest_lines) -->
           A list of lines to add to the <code>META-INF/manifest.mf</code> file generated for the
           <code>*_deploy.jar</code> target. The contents of this attribute are <em>not</em> subject
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java
index f5fa1ff..d4156ba 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java
@@ -80,6 +80,11 @@
 
   private static final String JACOCO_COVERAGE_RUNNER_MAIN_CLASS =
       "com.google.testing.coverage.JacocoCoverageRunner";
+  private static final String BAZEL_TEST_RUNNER_MAIN_CLASS =
+      "com.google.testing.junit.runner.BazelTestRunner";
+  private static final String EXPERIMENTAL_TEST_RUNNER_MAIN_CLASS =
+      "com.google.testing.junit.runner.ExperimentalTestRunner";
+  private static final String EXPERIMENTAL_TESTRUNNER_TAG = "experimental_testrunner";
 
   private BazelJavaSemantics() {
   }
@@ -120,13 +125,20 @@
     if (mainClass.isEmpty()) {
       if (ruleContext.attributes().get("use_testrunner", Type.BOOLEAN)
           && !useLegacyJavaTest(ruleContext)) {
-        return "com.google.testing.junit.runner.BazelTestRunner";
+        return useExperimentalTestRunner(ruleContext)
+            ? EXPERIMENTAL_TEST_RUNNER_MAIN_CLASS
+            : BAZEL_TEST_RUNNER_MAIN_CLASS;
       }
       mainClass = JavaCommon.determinePrimaryClass(ruleContext, sources);
     }
     return mainClass;
   }
 
+  private static boolean useExperimentalTestRunner(RuleContext ruleContext) {
+    List<String> tags = ruleContext.attributes().get("tags", Type.STRING_LIST);
+    return tags.contains(EXPERIMENTAL_TESTRUNNER_TAG);
+  }
+
   private void checkMainClass(RuleContext ruleContext, ImmutableList<Artifact> sources) {
     boolean createExecutable = ruleContext.attributes().get("create_executable", Type.BOOLEAN);
     String mainClass = getMainClassInternal(ruleContext, sources);
@@ -294,7 +306,9 @@
 
     boolean createExecutable = ruleContext.attributes().get("create_executable", Type.BOOLEAN);
     if (createExecutable && ruleContext.attributes().get("use_testrunner", Type.BOOLEAN)) {
-      return Iterables.getOnlyElement(ruleContext.getPrerequisites("$testsupport", Mode.TARGET));
+      String testSupport =
+          useExperimentalTestRunner(ruleContext) ? "$experimental_testsupport" : "$testsupport";
+      return Iterables.getOnlyElement(ruleContext.getPrerequisites(testSupport, Mode.TARGET));
     } else {
       return null;
     }
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
index 088fa53..76de5f8 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
@@ -111,7 +111,7 @@
         "filegroup(name='jacoco-blaze-agent', srcs = [])",
         "exports_files(['JavaBuilder_deploy.jar','SingleJar_deploy.jar','TestRunner_deploy.jar',",
         "               'JavaBuilderCanary_deploy.jar', 'ijar', 'GenClass_deploy.jar',",
-        "               'turbine_deploy.jar'])");
+        "               'turbine_deploy.jar','ExperimentalTestRunner_deploy.jar'])");
 
 
     ImmutableList<String> androidBuildContents = createAndroidBuildContents();