Open-source the JUnit test runner.

--
MOS_MIGRATED_REVID=112027454
diff --git a/src/BUILD b/src/BUILD
index a1dac3d..48afe1a 100644
--- a/src/BUILD
+++ b/src/BUILD
@@ -102,6 +102,7 @@
         "//src/java_tools/buildjar:JavaBuilder_deploy.jar",
         "//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",
         "//third_party/ijar",
         "//third_party/java/apkbuilder:embedded_tools",
     ] + select({
diff --git a/src/create_embedded_tools.sh b/src/create_embedded_tools.sh
index e0faa3a..dfd3419 100755
--- a/src/create_embedded_tools.sh
+++ b/src/create_embedded_tools.sh
@@ -31,6 +31,7 @@
     *JavaBuilder_deploy.jar) OUTPUT_PATH=tools/jdk/JavaBuilder_deploy.jar ;;
     *SingleJar_deploy.jar) OUTPUT_PATH=tools/jdk/SingleJar_deploy.jar ;;
     *GenClass_deploy.jar) OUTPUT_PATH=tools/jdk/GenClass_deploy.jar ;;
+    *Runner_deploy.jar) OUTPUT_PATH=tools/jdk/TestRunner_deploy.jar ;;
     *ijar) OUTPUT_PATH=tools/jdk/ijar ;;
     *src/objc_tools/*) OUTPUT_PATH=tools/objc/precomp_${i##*/} ;;
     *xcode*StdRedirect.dylib) OUTPUT_PATH=tools/objc/StdRedirect.dylib ;;
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/BUILD b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/BUILD
new file mode 100644
index 0000000..9430e54
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/BUILD
@@ -0,0 +1,27 @@
+# Description:
+#
+#   JUnit 4.x extensions
+
+package(
+    default_testonly = 1,
+    default_visibility = ["//src:__subpackages__"],
+)
+
+# Libraries
+# =========================================================
+
+# Extensions for writing custom JUnit4 runners
+java_library(
+    name = "runner",
+    testonly = 0,  # TODO(bazel-team): make it testonly
+    srcs = glob(["runner/*.java"]),
+    deps = [
+        "//third_party:guava",
+        "//third_party:junit4",
+    ],
+)
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+)
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/AndFilter.java b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/AndFilter.java
new file mode 100644
index 0000000..5a1edda
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/AndFilter.java
@@ -0,0 +1,45 @@
+// Copyright 2015 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.junit4.runner;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+
+/**
+ * A filter that returns {@code true} if both of its components return {@code
+ * true}.
+ */
+@Deprecated
+class AndFilter extends Filter {
+  private final Filter filter1;
+  private final Filter filter2;
+
+  public AndFilter(Filter filter1, Filter filter2) {
+    this.filter1 = checkNotNull(filter1);
+    this.filter2 = checkNotNull(filter2);
+  }
+
+  @Override
+  public boolean shouldRun(Description description) {
+    return filter1.shouldRun(description) && filter2.shouldRun(description);
+  }
+  
+  @Override
+  public String describe() {
+    return String.format("%s && %s", filter1.describe(), filter2.describe());
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/DynamicTestException.java b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/DynamicTestException.java
new file mode 100644
index 0000000..37a2454
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/DynamicTestException.java
@@ -0,0 +1,46 @@
+// Copyright 2015 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.junit4.runner;
+
+import com.google.common.base.Preconditions;
+
+import org.junit.runner.Description;
+
+/**
+ * The test runner may throw a {@code DynamicTestFailureException} to indicate a
+ * test case failed due to a failure in a dynamically-discovered test within
+ * a JUnit test case.
+ */
+public class DynamicTestException extends Exception {
+  private final Description test;
+
+  /**
+   * Constructs a {@code DynamicTestFailureException} that indicates a
+   * dynamically-discovered test, specified as a (@link Description}, failed
+   * due to the specified {@code cause}.
+   */
+  public DynamicTestException(Description test, Throwable cause) {
+    super(cause);
+    Preconditions.checkArgument(test.isTest());
+    this.test = test;
+  }
+
+  /**
+   * Returns the description of the dynamically-added test case.
+   */
+  public final Description getTest() {
+    return test;
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/Filters.java b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/Filters.java
new file mode 100644
index 0000000..27081da
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/Filters.java
@@ -0,0 +1,62 @@
+// Copyright 2015 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.junit4.runner;
+
+import org.junit.runner.Request;
+import org.junit.runner.Runner;
+import org.junit.runner.manipulation.Filter;
+import org.junit.runner.manipulation.NoTestsRemainException;
+
+/**
+ * Common filters.
+ */
+@Deprecated
+public final class Filters {
+
+  private Filters() {}
+
+  /**
+   * Returns a filter that evaluates to {@code true} if both of its
+   * components evaluates to {@code true}. The filters are evaluated in
+   * order, and evaluation will be "short-circuited" if the first filter
+   * returns {@code false}.
+   */
+  public static Filter and(Filter delegate1, Filter delegate2) {
+    return delegate1 == Filter.ALL ? delegate2
+        : (delegate2 == Filter.ALL ? delegate1
+            : new AndFilter(delegate1, delegate2));
+  }
+
+  /**
+   * Returns a Request that only contains those tests that should run when
+   * a filter is applied, filtering out all empty suites.<p>
+   *
+   * Note that if the request passed into this method caches its runner,
+   * that runner will be modified to use the given filter. To be safe,
+   * do not use the passed-in request after calling this method.
+   *
+   * @param request Request to filter
+   * @param filter Filter to apply
+   * @return request
+   * @throws NoTestsRemainException if the applying the filter removes all tests
+   */
+  public static Request apply(Request request, Filter filter) throws NoTestsRemainException {
+    filter = new SuiteTrimmingFilter(filter);
+    Runner runner = request.getRunner();
+    filter.apply(runner);
+
+    return Request.runner(runner);
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/MemoizingRequest.java b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/MemoizingRequest.java
new file mode 100644
index 0000000..d4bb807
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/MemoizingRequest.java
@@ -0,0 +1,52 @@
+// Copyright 2015 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.junit4.runner;
+
+import org.junit.runner.Request;
+import org.junit.runner.Runner;
+
+/**
+ * A {@link Request} that memoizies another {@code Request}.
+ * This class is meant to be overridden to modify some behaviors.
+ */
+@Deprecated
+public class MemoizingRequest extends Request {
+  private final Request requestDelegate;
+  private Runner runnerDelegate;
+
+  public MemoizingRequest(Request delegate) {
+    this.requestDelegate = delegate;
+  }
+
+  @Override
+  public final synchronized Runner getRunner() {
+    if (runnerDelegate == null) {
+      runnerDelegate = createRunner(requestDelegate);
+    }
+    return runnerDelegate;
+  }
+
+  /**
+   * Creates the runner. This method is called at most once.
+   * Subclasses can override this method for different behavior.
+   * The default implementation returns the runner created by the delegate.
+   *
+   * @param delegate request to delegate to
+   * @return runner
+   */
+  protected Runner createRunner(Request delegate) {
+    return delegate.getRunner();
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/RunNotifierWrapper.java b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/RunNotifierWrapper.java
new file mode 100644
index 0000000..8d2b203
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/RunNotifierWrapper.java
@@ -0,0 +1,101 @@
+// Copyright 2015 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.junit4.runner;
+
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runner.notification.StoppedByUserException;
+
+/**
+ * A {@link RunNotifier} that delegates all its operations to another {@code RunNotifier}.
+ * This class is meant to be overridden to modify some behaviors.
+ */
+public abstract class RunNotifierWrapper extends RunNotifier {
+  private final RunNotifier delegate;
+
+  /**
+   * Creates a new instance.
+   *
+   * @param delegate notifier to delegate to
+   */
+  public RunNotifierWrapper(RunNotifier delegate) {
+    this.delegate = delegate;
+  }
+
+  /**
+   * @return the delegate
+   */
+  protected final RunNotifier getDelegate() {
+    return delegate;
+  }
+
+  @Override
+  public void addFirstListener(RunListener listener) {
+    delegate.addFirstListener(listener);
+  }
+
+  @Override
+  public void addListener(RunListener listener) {
+    delegate.addListener(listener);
+  }
+
+  @Override
+  public void removeListener(RunListener listener) {
+    delegate.removeListener(listener);
+  }
+
+  @Override
+  public void fireTestRunStarted(Description description) {
+    delegate.fireTestRunStarted(description);
+  }
+  
+  @Override
+  public void fireTestStarted(Description description) throws StoppedByUserException {
+    delegate.fireTestStarted(description);
+  }
+  
+  @Override
+  public void fireTestIgnored(Description description) {
+    delegate.fireTestIgnored(description);
+  }
+
+  @Override
+  public void fireTestAssumptionFailed(Failure failure) {
+    delegate.fireTestAssumptionFailed(failure);
+  }
+
+  @Override
+  public void fireTestFailure(Failure failure) {
+    delegate.fireTestFailure(failure);
+  }
+
+  @Override
+  public void fireTestFinished(Description description) {
+    delegate.fireTestFinished(description);
+  }
+
+  @Override
+  public void fireTestRunFinished(Result result) {
+    delegate.fireTestRunFinished(result);
+  }
+
+  @Override
+  public void pleaseStop() {
+    delegate.pleaseStop();
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/SuiteTrimmingFilter.java b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/SuiteTrimmingFilter.java
new file mode 100644
index 0000000..f15e631
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/SuiteTrimmingFilter.java
@@ -0,0 +1,56 @@
+// Copyright 2015 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.junit4.runner;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+
+/**
+ * A filter that decorates another filter, filtering out any suites
+ * that contain no tests.
+ */
+public final class SuiteTrimmingFilter extends Filter {
+  private final Filter delegate;
+
+  public SuiteTrimmingFilter(Filter delegate) {
+    this.delegate = checkNotNull(delegate);
+  }
+
+  @Override
+  public String describe() {
+    return delegate.describe();
+  }
+
+  @Override
+  public final boolean shouldRun(Description description) {
+    if (!delegate.shouldRun(description)) {
+      return false;
+    }
+
+    if (description.isTest()) {
+      return true;
+    }
+
+    // explicitly check if any children want to run
+    for (Description each : description.getChildren()) {
+      if (shouldRun(each)) {
+        return true;
+      }
+    }
+    return false;           
+  }
+}
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
new file mode 100644
index 0000000..78ed2d1
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BUILD
@@ -0,0 +1,38 @@
+DEFAULT_VISIBILITY = [
+    "//java/com/google/testing/junit/runner:__subpackages__",
+    "//javatests/com/google/testing/junit/runner:__subpackages__",
+    "//third_party/bazel/src/java_tools/junitrunner/java/com/google/testing/junit/runner:__subpackages__",
+    "//third_party/bazel/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner:__subpackages__",
+]
+
+package(default_visibility = ["//src:__subpackages__"])
+
+java_library(
+    name = "runner",
+    srcs = ["BazelTestRunner.java"],
+    data = ["//tools:test_sharding_compliant"],
+    # Disable sunapi warnings about sun.misc.Signal.
+    # There are no non-Sun APIs for doing this.
+    javacopts = ["-Xlint:-sunapi"],
+    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",
+        "//third_party:guava",
+        "//third_party:guice",
+        "//third_party:joda_time",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+    ],
+)
+
+java_binary(
+    name = "Runner",
+    main_class = "com.google.testing.junit.runner.BazelTestRunner",
+    runtime_deps = [":runner"],
+)
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+)
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
new file mode 100644
index 0000000..efde522
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/BazelTestRunner.java
@@ -0,0 +1,209 @@
+// Copyright 2015 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.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Uninterruptibles;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.testing.junit.runner.internal.StackTraces;
+import com.google.testing.junit.runner.internal.Stderr;
+import com.google.testing.junit.runner.internal.Stdout;
+import com.google.testing.junit.runner.junit4.JUnit4Runner;
+import com.google.testing.junit.runner.junit4.JUnit4RunnerModule;
+import com.google.testing.junit.runner.model.AntXmlResultWriter;
+import com.google.testing.junit.runner.model.XmlResultWriter;
+
+import org.joda.time.DateTime;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+
+import java.io.PrintStream;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A class to run JUnit tests in a controlled environment.
+ *
+ * <p>Currently sets up a security manager to catch undesirable behaviour;
+ * System.exit. Also has nice command line options - run with "-help" for
+ * details.
+ *
+ * <p>This class traps writes to <code>System.err.println()</code> and
+ * <code>System.out.println()</code> including the output of failed tests in
+ * the error report.
+ *
+ * <p>It also traps SIGTERM signals to make sure that the test report is
+ * written when the signal is closed by the unit test framework for running
+ * over time.
+ */
+public class BazelTestRunner {
+  /**
+   * 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 BazelTestRunner() {
+    // 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 (!checkTestSuiteProperty(suiteClassName)) {
+      System.exit(2);
+    }
+
+    int exitCode = runTestsInSuite(suiteClassName, args);
+
+    System.err.printf("%nBazelTestRunner 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);
+
+    DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
+    DateTime shutdownTime = new DateTime();
+    String formattedShutdownTime = formatter.print(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, BazelTestRunner.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, BazelTestRunner.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;
+      }
+    }
+
+    Injector injector = Guice.createInjector(
+        new BazelTestRunnerModule(suite, ImmutableList.copyOf(args)));
+
+    JUnit4Runner runner = injector.getInstance(JUnit4Runner.class);
+    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() {
+        Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS);
+        out.println("JVM still up after five seconds. Dumping stack traces for all threads.");
+        StackTraces.printAll(out);
+      }
+    }, "BazelTestRunner: Print stack traces if JVM exit hangs");
+
+    thread.setDaemon(true);
+    thread.start();
+  }
+
+  static class BazelTestRunnerModule extends AbstractModule {
+    final Class<?> suite;
+    final List<String> args;
+
+    BazelTestRunnerModule(Class<?> suite, List<String> args) {
+      this.suite = suite;
+      this.args = args;
+    }
+
+    @Override
+    protected void configure() {
+      install(JUnit4RunnerModule.create(suite, args));
+      bind(XmlResultWriter.class).to(AntXmlResultWriter.class);
+    }
+
+    @Provides @Singleton @Stdout
+    PrintStream provideStdoutStream() {
+      return System.out;
+    }
+
+    @Provides @Singleton @Stderr
+    PrintStream provideStderrStream() {
+      return System.err;
+    }
+  };
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/BUILD b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/BUILD
new file mode 100644
index 0000000..696fd36
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/BUILD
@@ -0,0 +1,18 @@
+package(default_visibility = ["//src:__subpackages__"])
+
+java_library(
+    name = "internal",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//third_party:guava",
+        "//third_party:guice",
+        "//third_party:joda_time",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+    ],
+)
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+)
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/SignalHandlers.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/SignalHandlers.java
new file mode 100644
index 0000000..c1573d5
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/SignalHandlers.java
@@ -0,0 +1,81 @@
+// 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.internal;
+
+import com.google.inject.Inject;
+
+import sun.misc.Signal;
+import sun.misc.SignalHandler;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Helper class to install signal handlers.
+ */
+public class SignalHandlers {
+  private final HandlerInstaller handlerInstaller;
+
+  /**
+   * Creates a handler installer that installs signal handlers.
+   */
+  public static HandlerInstaller createRealHandlerInstaller() {
+    return new HandlerInstaller() {
+      @Override
+      public SignalHandler install(Signal signal, SignalHandler handler) {
+        return Signal.handle(signal, handler);
+      }
+    };
+  }
+
+  @Inject
+  public SignalHandlers(HandlerInstaller installer) {
+    this.handlerInstaller = installer;
+  }
+
+  /**
+   * Adds the given signal handler to the existing ones.
+   *
+   * <p>Signal handlers are responsible to catch any exception if the following
+   * handlers need to be executed when a handler throws an exception.
+   *
+   * @param signal The signal to handle.
+   * @param signalHandler The handler to install.
+   */
+  public void installHandler(Signal signal, final SignalHandler signalHandler) {
+    final AtomicReference<SignalHandler> previousHandlerReference =
+        new AtomicReference<>();
+    previousHandlerReference.set(handlerInstaller.install(signal, new SignalHandler() {
+      @Override
+      public void handle(Signal signal) {
+        signalHandler.handle(signal);
+        SignalHandler previousHandler = previousHandlerReference.get();
+        if (previousHandler != null) {
+          previousHandler.handle(signal);
+        }
+      }
+    }));
+  }
+
+  /**
+   * Wraps sun.misc.Signal#handle(sun.misc.Signal, sun.misc.SignalHandler)
+   * to help with testing.
+   */
+  public interface HandlerInstaller {
+    /**
+     * @see sun.misc.Signal#handle(sun.misc.Signal, sun.misc.SignalHandler)     
+     */
+    SignalHandler install(Signal signal, SignalHandler handler);
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/StackTraces.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/StackTraces.java
new file mode 100644
index 0000000..3769506
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/StackTraces.java
@@ -0,0 +1,152 @@
+// Copyright 2011 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.internal;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import java.io.PrintStream;
+import java.lang.management.LockInfo;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MonitorInfo;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+import java.util.Arrays;
+import java.util.Set;
+
+/**
+ * Utilities for stack traces.
+ */
+public class StackTraces {
+
+  /**
+   * Prints all stack traces to the given stream.
+   *
+   * @param out Stream to print to
+   */
+  public static void printAll(PrintStream out) {
+    out.println("Starting full thread dump ...\n");
+    ThreadMXBean mb = ManagementFactory.getThreadMXBean();
+
+    // ThreadInfo has comprehensive information such as locks.
+    ThreadInfo[] threadInfos = mb.dumpAllThreads(true, true);
+
+    // But we can know whether a thread is daemon only from Thread
+    Set<Thread> threads = Thread.getAllStackTraces().keySet();
+    ImmutableMap<Long, Thread> threadMap = Maps.uniqueIndex(
+        threads, new Function<Thread, Long>() {
+          @Override public Long apply(Thread thread) {
+            return thread.getId();
+          }
+        });
+
+    // Dump non-daemon threads first
+    for (ThreadInfo threadInfo : threadInfos) {
+      Thread thread = threadMap.get(threadInfo.getThreadId());
+      if (thread != null && !thread.isDaemon()) {
+        dumpThreadInfo(threadInfo, thread, out);
+      }
+    }
+
+    // Dump daemon threads
+    for (ThreadInfo threadInfo : threadInfos) {
+      Thread thread = threadMap.get(threadInfo.getThreadId());
+      if (thread != null && thread.isDaemon()) {
+        dumpThreadInfo(threadInfo, thread, out);
+      }
+    }
+
+    long[] deadlockedThreads = mb.findDeadlockedThreads();
+    if (deadlockedThreads != null) {
+      out.println("Detected deadlocked threads: " +
+                Arrays.toString(deadlockedThreads));
+    }
+    long[] monitorDeadlockedThreads = mb.findMonitorDeadlockedThreads();
+    if (monitorDeadlockedThreads != null) {
+      out.println("Detected monitor deadlocked threads: " +
+                  Arrays.toString(monitorDeadlockedThreads));
+    }
+    out.println("\nDone full thread dump.");
+    out.flush();
+  }
+
+  // Adopted from ThreadInfo.toString(), without MAX_FRAMES limit
+  private static void dumpThreadInfo(ThreadInfo t, Thread thread, PrintStream out) {
+    out.print("\"" + t.getThreadName() + "\"" +
+              " Id=" + t.getThreadId() + " " +
+              t.getThreadState());
+    if (t.getLockName() != null) {
+      out.print(" on " + t.getLockName());
+    }
+    if (t.getLockOwnerName() != null) {
+      out.print(" owned by \"" + t.getLockOwnerName() +
+                "\" Id=" + t.getLockOwnerId());
+    }
+    if (t.isSuspended()) {
+      out.print(" (suspended)");
+    }
+    if (t.isInNative()) {
+      out.print(" (in native)");
+    }
+    if (thread.isDaemon()) {
+      out.print(" (daemon)");
+    }
+    out.print('\n');
+    StackTraceElement[] stackTrace = t.getStackTrace();
+    MonitorInfo[] lockedMonitors = t.getLockedMonitors();
+    for (int i = 0; i < stackTrace.length; i++) {
+      StackTraceElement ste = stackTrace[i];
+      out.print("\tat " + ste.toString());
+      out.print('\n');
+      if (i == 0 && t.getLockInfo() != null) {
+        Thread.State ts = t.getThreadState();
+        switch (ts) {
+          case BLOCKED:
+            out.print("\t-  blocked on " + t.getLockInfo());
+            out.print('\n');
+            break;
+          case WAITING:
+            out.print("\t-  waiting on " + t.getLockInfo());
+            out.print('\n');
+            break;
+          case TIMED_WAITING:
+            out.print("\t-  waiting on " + t.getLockInfo());
+            out.print('\n');
+            break;
+          default:
+        }
+      }
+
+      for (MonitorInfo mi : lockedMonitors) {
+        if (mi.getLockedStackDepth() == i) {
+          out.print("\t-  locked " + mi);
+          out.print('\n');
+        }
+      }
+    }
+
+    LockInfo[] locks = t.getLockedSynchronizers();
+    if (locks.length > 0) {
+      out.print("\n\tNumber of locked synchronizers = " + locks.length);
+      out.print('\n');
+      for (LockInfo li : locks) {
+        out.print("\t- " + li);
+        out.print('\n');
+      }
+    }
+    out.print('\n');
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/Stderr.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/Stderr.java
new file mode 100644
index 0000000..80b37eb
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/Stderr.java
@@ -0,0 +1,31 @@
+// Copyright 2012 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.internal;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Binding annotation for a {@link java.io.PrintStream} printing to stderr.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER, ElementType.METHOD})
+@BindingAnnotation
+public @interface Stderr {
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/Stdout.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/Stdout.java
new file mode 100644
index 0000000..051968b
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/Stdout.java
@@ -0,0 +1,31 @@
+// Copyright 2015 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.internal;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Binding annotation for a {@link java.io.PrintStream} printing to stdout.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER, ElementType.METHOD})
+@BindingAnnotation
+public @interface Stdout {
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/BUILD b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/BUILD
new file mode 100644
index 0000000..a9a1f86
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/BUILD
@@ -0,0 +1,44 @@
+DEFAULT_VISIBILITY = [
+    "//java/com/google/testing/junit/runner:__subpackages__",
+    "//javatests/com/google/testing/junit/runner:__subpackages__",
+    "//third_party/bazel/src/java_tools/junitrunner/java/com/google/testing/junit/runner:__subpackages__",
+    "//third_party/bazel/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner:__subpackages__",
+]
+
+package(default_visibility = ["//src:__subpackages__"])
+
+java_library(
+    name = "junit4",
+    srcs = glob(
+        ["*.java"],
+        exclude = ["RegExTestCaseFilter.java"],
+    ),
+    deps = [
+        ":filter",
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/junit4:runner",
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal",
+        "//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/sharding/api",
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/util",
+        "//third_party:guava",
+        "//third_party:guice",
+        "//third_party:joda_time",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+    ],
+)
+
+java_library(
+    name = "filter",
+    srcs = ["RegExTestCaseFilter.java"],
+    deps = [
+        "//third_party:guava",
+        "//third_party:junit4",
+    ],
+)
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+)
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/CancellableRequestFactory.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/CancellableRequestFactory.java
new file mode 100644
index 0000000..e899166
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/CancellableRequestFactory.java
@@ -0,0 +1,126 @@
+// Copyright 2012 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.common.base.Preconditions;
+import com.google.inject.Singleton;
+import com.google.testing.junit.junit4.runner.MemoizingRequest;
+import com.google.testing.junit.junit4.runner.RunNotifierWrapper;
+
+import org.junit.runner.Description;
+import org.junit.runner.Request;
+import org.junit.runner.Runner;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runner.notification.StoppedByUserException;
+
+/**
+ * Creates requests that can be cancelled.
+ */
+@Singleton
+class CancellableRequestFactory {
+  private boolean requestCreated;
+  private volatile ThreadSafeRunNotifier currentNotifier;
+  private volatile boolean cancelRequested = false;
+
+  /**
+   * Creates a request that can be cancelled. Can only be called once.
+   *
+   * @param delegate request to wrap
+   */
+  public Request createRequest(Request delegate) {
+    Preconditions.checkState(!requestCreated, "a request was already created");
+    return new MemoizingRequest(delegate) {
+      @Override
+      protected Runner createRunner(Request delegate) {
+        return new CancellableRunner(delegate.getRunner());
+      }
+    };
+  }
+
+  /**
+   * Cancels the request created by this request factory.
+   */
+  public void cancelRun() {
+    cancelRequested = true;
+    RunNotifier notifier = currentNotifier;
+    if (notifier != null) {
+      notifier.pleaseStop();
+    }
+  }
+
+
+  private class CancellableRunner extends Runner {
+    private final Runner delegate;
+
+    public CancellableRunner(Runner delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public Description getDescription() {
+      return delegate.getDescription();
+    }
+
+    @Override
+    public void run(RunNotifier notifier) {
+      currentNotifier = new ThreadSafeRunNotifier(notifier);
+      if (cancelRequested) {
+        currentNotifier.pleaseStop();
+      }
+
+      try {
+        delegate.run(currentNotifier);
+      } catch (StoppedByUserException e) {
+        if (cancelRequested) {
+          throw new RuntimeException("Test run interrupted", e);
+        }
+        throw e;
+      }
+    }
+  }
+
+
+  private static class ThreadSafeRunNotifier extends RunNotifierWrapper {
+    private volatile boolean stopRequested;
+
+    public ThreadSafeRunNotifier(RunNotifier delegate) {
+      super(delegate);
+    }
+
+    /**
+     * {@inheritDoc}<p>
+     *
+     * The implementation is almost an exact copy of the version in
+     * {@code RunNotifier} but is thread-safe.
+     */
+    @Override
+    public void fireTestStarted(Description description) throws StoppedByUserException {
+      if (stopRequested) {
+        throw new StoppedByUserException();
+      }
+      getDelegate().fireTestStarted(description);
+    }
+
+    /**
+     * {@inheritDoc}<p>
+     *
+     * This method is thread-safe.
+     */
+    @Override
+    public void pleaseStop() {
+      stopRequested = true;
+    }
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Config.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Config.java
new file mode 100644
index 0000000..a25d3a3
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Config.java
@@ -0,0 +1,126 @@
+// 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.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.Properties;
+
+/**
+ * Configuration for the JUnit4 test runner.
+ */
+class JUnit4Config {
+  @VisibleForTesting
+  static final String JUNIT_API_VERSION_PROPERTY = "com.google.testing.junit.runner.apiVersion";
+
+  private final String testIncludeFilterRegexp;
+  private final String testExcludeFilterRegexp;
+  private final Optional<Path> xmlOutputPath;
+  private final String junitApiVersion;
+  private final boolean shouldInstallSecurityManager;
+
+  private static final String XML_OUTPUT_FILE_ENV_VAR = "XML_OUTPUT_FILE";
+
+  public JUnit4Config(
+      String testIncludeFilterRegexp,
+      String testExcludeFilterRegexp,
+      Optional<Path> outputXmlFilePath) {
+    this(
+        testIncludeFilterRegexp,
+        testExcludeFilterRegexp,
+        outputXmlFilePath,
+        System.getProperties());
+  }
+
+  @VisibleForTesting
+  JUnit4Config(
+      String testIncludeFilterRegexp,
+      String testExcludeFilterRegexp,
+      Optional<Path> xmlOutputPath,
+      Properties systemProperties) {
+    this.testIncludeFilterRegexp = testIncludeFilterRegexp;
+    this.testExcludeFilterRegexp = testExcludeFilterRegexp;
+    this.xmlOutputPath = xmlOutputPath;
+    junitApiVersion = systemProperties.getProperty(JUNIT_API_VERSION_PROPERTY, "1").trim();
+    shouldInstallSecurityManager = systemProperties.getProperty("java.security.manager") == null;
+  }
+
+  /**
+   * @return Whether the test security manager should be installed
+   */
+  public boolean shouldInstallSecurityManager() {
+    return shouldInstallSecurityManager;
+  }
+
+  /**
+   * Returns the XML output path, or null if not specified.
+   */
+  public Optional<Path> getXmlOutputPath() {
+    if (!xmlOutputPath.isPresent()) {
+      Optional<String> envXmlOutputPath =
+          Optional.fromNullable(System.getenv(XML_OUTPUT_FILE_ENV_VAR));
+      return envXmlOutputPath.transform(new Function<String, Path>() {
+          @Override
+          public Path apply(String path) {
+            return FileSystems.getDefault().getPath(path);
+          }
+        });
+    }
+
+    return xmlOutputPath;
+  }
+
+  /**
+   * Gets the version of the JUnit Runner that the test is expecting.
+   * Some features may be enabled or disabled based on this value.
+   *
+   * @return api version
+   * @throws IllegalStateException if the API version is unsupported.
+   */
+  public int getJUnitRunnerApiVersion() {
+    int apiVersion = 0;
+    try {
+      apiVersion = Integer.parseInt(junitApiVersion);
+    } catch (NumberFormatException e) {
+      // ignore; handled below
+    }
+
+    Preconditions.checkState(apiVersion == 1,
+        "Unsupported JUnit Runner API version %s=%s (must be \"1\")", JUNIT_API_VERSION_PROPERTY,
+        junitApiVersion);
+    return apiVersion;
+  }
+
+  /**
+   * Returns a regular expression representing an inclusive filter.
+   * Only test descriptions that match this regular expression should be run.
+   */
+  public String getTestIncludeFilterRegexp() {
+    return testIncludeFilterRegexp;
+  }
+
+  /**
+   * Returns a regular expression representing an exclusive filter.
+   * Test descriptions that match this regular expression should not be run.
+   */
+  public String getTestExcludeFilterRegexp() {
+    return testExcludeFilterRegexp;
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Options.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Options.java
new file mode 100644
index 0000000..d421774
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Options.java
@@ -0,0 +1,120 @@
+// Copyright 2011 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.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Simple options parser for JUnit 4.
+ *
+ * <p>
+ * For the options "test_filter" and "test_exclude_filter", this class properly handles arguments in
+ * either the form "--test_filter=foo" or "--test_filter foo".
+ */
+class JUnit4Options {
+
+  public static final String TEST_INCLUDE_FILTER_OPTION = "--test_filter";
+  public static final String TEST_EXCLUDE_FILTER_OPTION = "--test_exclude_filter";
+
+  // This gets passed in by the build system.
+  private static final String TESTBRIDGE_TEST_ONLY = "TESTBRIDGE_TEST_ONLY";
+
+  /**
+   * Parses the given array of arguments and returns a JUnit4Options
+   * object representing the parsed arguments.
+   */
+  static JUnit4Options parse(Map<String, String> envVars, List<String> args) {
+    ImmutableList.Builder<String> unparsedArgsBuilder = ImmutableList.builder();
+    Map<String, String> optionsMap = Maps.newHashMap();
+
+    optionsMap.put(TEST_INCLUDE_FILTER_OPTION, null);
+    optionsMap.put(TEST_EXCLUDE_FILTER_OPTION, null);
+
+    for (Iterator<String> it = args.iterator(); it.hasNext();) {
+      String arg = it.next();
+      int indexOfEquals = arg.indexOf("=");
+
+      if (indexOfEquals > 0) {
+        String optionName = arg.substring(0, indexOfEquals);
+        if (optionsMap.containsKey(optionName)) {
+          optionsMap.put(optionName, arg.substring(indexOfEquals + 1));
+          continue;
+        }
+      } else if (optionsMap.containsKey(arg)) {
+        // next argument is the regexp
+        if (!it.hasNext()) {
+          throw new RuntimeException("No filter expression specified after " + arg);
+        }
+        optionsMap.put(arg, it.next());
+        continue;
+      }
+      unparsedArgsBuilder.add(arg);
+    }
+    // If TESTBRIDGE_TEST_ONLY is set in the environment, forward it to the
+    // --test_filter flag.
+    String testFilter = envVars.get(TESTBRIDGE_TEST_ONLY);
+    if (testFilter != null && optionsMap.get(TEST_INCLUDE_FILTER_OPTION) == null) {
+      optionsMap.put(TEST_INCLUDE_FILTER_OPTION, testFilter);
+    }
+
+    ImmutableList<String> unparsedArgs = unparsedArgsBuilder.build();
+    return new JUnit4Options(optionsMap.get(TEST_INCLUDE_FILTER_OPTION),
+                             optionsMap.get(TEST_EXCLUDE_FILTER_OPTION),
+                             unparsedArgs.toArray(new String[unparsedArgs.size()]));
+  }
+
+  private final String testIncludeFilter;
+  private final String testExcludeFilter;
+  private final String[] unparsedArgs;
+
+  @VisibleForTesting
+  JUnit4Options(@Nullable String testIncludeFilter, @Nullable String testExcludeFilter,
+                String[] unparsedArgs) {
+    this.testIncludeFilter = testIncludeFilter;
+    this.testExcludeFilter = testExcludeFilter;
+    this.unparsedArgs = unparsedArgs;
+  }
+
+  /**
+   * Returns the value of the test_filter option, or <code>null</code> if
+   * it was not specified.
+   */
+  String getTestIncludeFilter() {
+    return testIncludeFilter;
+  }
+
+  /**
+   * Returns the value of the test_exclude_filter option, or <code>null</code> if
+   * it was not specified.
+   */
+  String getTestExcludeFilter() {
+    return testExcludeFilter;
+  }
+
+  /**
+   * Returns an array of the arguments that did not match any known option.
+   */
+  String[] getUnparsedArgs() {
+    return unparsedArgs;
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Runner.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Runner.java
new file mode 100644
index 0000000..4134131
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Runner.java
@@ -0,0 +1,264 @@
+// 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.common.annotations.VisibleForTesting;
+import com.google.common.base.Supplier;
+import com.google.common.io.Files;
+import com.google.testing.junit.junit4.runner.SuiteTrimmingFilter;
+import com.google.testing.junit.runner.internal.Stdout;
+import com.google.testing.junit.runner.model.TestSuiteModel;
+import com.google.testing.junit.runner.util.GoogleTestSecurityManager;
+
+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;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+/**
+ * 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 GoogleTestSecurityManager googleTestSecurityManager;
+  private SecurityManager previousSecurityManager;
+
+  /**
+   * Creates a runner.
+   */
+  @Inject
+  private JUnit4Runner(Request request, CancellableRequestFactory requestFactory,
+      Supplier<TestSuiteModel> modelSupplier, @Stdout PrintStream testRunnerOut,
+      JUnit4Config config, Set<RunListener> runListeners) {
+    this.request = request;
+    this.requestFactory = requestFactory;
+    this.modelSupplier = modelSupplier;
+    this.config = config;
+    this.testRunnerOut = testRunnerOut;
+    this.runListeners = runListeners;
+  }
+
+  /**
+   * Runs the JUnit4 test.
+   *
+   * @return Result of running the test
+   */
+  public Result run() {
+    testRunnerOut.println("JUnit4 Test Runner");
+    checkJUnitRunnerApiVersion();
+
+    // 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 {
+        Files.write(new byte[0], file);
+      } 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) {
+    }
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerBaseModule.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerBaseModule.java
new file mode 100644
index 0000000..28e6466
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerBaseModule.java
@@ -0,0 +1,105 @@
+// Copyright 2012 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 static com.google.inject.multibindings.Multibinder.newSetBinder;
+import static com.google.testing.junit.runner.sharding.ShardingFilters.DEFAULT_SHARDING_STRATEGY;
+
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.inject.AbstractModule;
+import com.google.inject.Key;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.multibindings.Multibinder;
+import com.google.testing.junit.junit4.runner.MemoizingRequest;
+import com.google.testing.junit.runner.internal.Stdout;
+import com.google.testing.junit.runner.model.TestSuiteModel;
+import com.google.testing.junit.runner.sharding.api.ShardingFilterFactory;
+
+import org.junit.internal.TextListener;
+import org.junit.runner.Request;
+import org.junit.runner.notification.RunListener;
+
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Guice module for creating {@link JUnit4Runner}. This contains the common
+ * bindings used when either the runner runs actual tests or when we do
+ * integration tests of the runner itself.
+ *
+ * <p>Note: we do not use {@code Modules.override()} to test the runner because
+ * there are bindings that we use when the runner runs actual tests that set
+ * global state, and we don't want to do that when we test the runner itself.
+ */
+class JUnit4RunnerBaseModule extends AbstractModule {
+  private final Class<?> suiteClass;
+
+  public JUnit4RunnerBaseModule(Class<?> suiteClass) {
+    this.suiteClass = suiteClass;
+  }
+
+  @Override
+  protected void configure() {
+    requireBinding(Key.get(PrintStream.class, Stdout.class));
+    requireBinding(JUnit4Config.class);
+    requireBinding(TestSuiteModel.Builder.class);
+
+    // We require explicit bindings so we don't use an unexpected just-in-time binding
+    bind(JUnit4Runner.class);
+    bind(JUnit4TestModelBuilder.class);
+    bind(CancellableRequestFactory.class);
+
+    // Normal bindings
+    bind(ShardingFilterFactory.class).toInstance(DEFAULT_SHARDING_STRATEGY);
+    bindConstant().annotatedWith(TopLevelSuite.class).to(suiteClass.getCanonicalName());
+
+    // Bind listeners
+    Multibinder<RunListener> listenerBinder = newSetBinder(binder(), RunListener.class);
+    listenerBinder.addBinding().to(TextListener.class);
+  }
+
+  @Provides @Singleton
+  Supplier<TestSuiteModel> provideTestSuiteModelSupplier(JUnit4TestModelBuilder builder) {
+    return Suppliers.memoize(builder);
+  }
+
+  @Provides @Singleton
+  TextListener provideTextListener(@Stdout PrintStream testRunnerOut) {
+    return new TextListener(asUtf8PrintStream(testRunnerOut));
+  }
+
+  private static PrintStream asUtf8PrintStream(OutputStream stream) {
+    try {
+      return new PrintStream(stream, false /* autoFlush */, StandardCharsets.UTF_8.toString());
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalStateException("UTF-8 must be supported as per the java language spec", e);
+    }
+  }
+
+  @Provides @Singleton
+  Request provideRequest() {
+    /*
+     * JUnit4Runner requests the Runner twice, once to build the model (before
+     * filtering) and once to run the tests (after filtering). Constructing the
+     * Runner can be expensive, so Memoize the Runner.
+     */
+    Request request = Request.aClass(suiteClass);
+    return new MemoizingRequest(request);
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerModule.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerModule.java
new file mode 100644
index 0000000..5f15524
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerModule.java
@@ -0,0 +1,118 @@
+// Copyright 2015 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 static com.google.inject.multibindings.Multibinder.newSetBinder;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Ticker;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.multibindings.Multibinder;
+import com.google.testing.junit.runner.internal.SignalHandlers;
+import com.google.testing.junit.runner.util.TestNameProvider;
+
+import org.junit.runner.notification.RunListener;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Guice module for real test runs.
+ */
+public class JUnit4RunnerModule extends AbstractModule {
+  private final Class<?> suite;
+  private final JUnit4Config config;
+  private final ImmutableList<String> unparsedArgs;
+
+  public static JUnit4RunnerModule create(Class<?> suite, List<String> args) {
+    JUnit4Options options = JUnit4Options.parse(System.getenv(), ImmutableList.copyOf(args));
+    JUnit4Config config = new JUnit4Config(
+        options.getTestIncludeFilter(),
+        options.getTestExcludeFilter(),
+        Optional.<Path>absent());
+    return new JUnit4RunnerModule(suite, config, ImmutableList.copyOf(options.getUnparsedArgs()));
+  }
+
+  private JUnit4RunnerModule(
+      Class<?> suite, JUnit4Config config, ImmutableList<String> unparsedArgs) {
+    this.suite = suite;
+    this.config = config;
+    this.unparsedArgs = unparsedArgs;
+  }
+
+  @Override
+  protected void configure() {
+    install(new JUnit4RunnerBaseModule(suite));
+
+    // We require explicit bindings so we don't use an unexpected just-in-time binding
+    bind(SignalHandlers.class);
+
+    // Normal bindings
+    bind(JUnit4Config.class).toInstance(config);
+    bind(Ticker.class).toInstance(Ticker.systemTicker());
+    bind(SignalHandlers.HandlerInstaller.class).toInstance(
+        SignalHandlers.createRealHandlerInstaller());
+
+    // Bind listeners
+    Multibinder<RunListener> listenerBinder = newSetBinder(binder(), RunListener.class);
+    listenerBinder.addBinding().to(JUnit4TestNameListener.class);
+    listenerBinder.addBinding().to(JUnit4TestXmlListener.class);
+    listenerBinder.addBinding().to(JUnit4TestStackTraceListener.class);
+  }
+
+  @Provides @Singleton @Xml
+  OutputStream provideXmlStream() {
+    Optional<Path> path = config.getXmlOutputPath();
+
+    if (path.isPresent()) {
+      try {
+        // TODO(bazel-team): Change the provider method to return ByteSink or CharSink
+        return new FileOutputStream(path.get().toFile());
+      } catch (FileNotFoundException e) {
+        /*
+         * We try to avoid throwing exceptions in the runner code. There is no
+         * way to induce a test failure here, so the only thing we can do is
+         * print a message and move on.
+         */
+        e.printStackTrace();
+      }
+    }
+
+    return ByteStreams.nullOutputStream();
+  }
+
+  @Provides @Singleton
+  SettableCurrentRunningTest provideCurrentRunningTest() {
+    return new SettableCurrentRunningTest() {
+      void setGlobalTestNameProvider(TestNameProvider provider) {
+        testNameProvider = provider;
+      }
+    };
+  }
+
+  /**
+   * Gets the list of unparsed command line arguments.
+   */
+  public ImmutableList<String> getUnparsedArgs() {
+    return unparsedArgs;
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestModelBuilder.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestModelBuilder.java
new file mode 100644
index 0000000..62df88d
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestModelBuilder.java
@@ -0,0 +1,56 @@
+// 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.common.base.Preconditions;
+import com.google.common.base.Supplier;
+import com.google.inject.Singleton;
+import com.google.testing.junit.runner.model.TestSuiteModel;
+import com.google.testing.junit.runner.model.TestSuiteModel.Builder;
+
+import org.junit.runner.Description;
+import org.junit.runner.Request;
+
+import javax.inject.Inject;
+
+/**
+ * Builds a {@link TestSuiteModel} for JUnit4 tests.
+ */
+@Singleton
+class JUnit4TestModelBuilder implements Supplier<TestSuiteModel> {
+  private final Request request;
+  private final String suiteName;
+  private final Builder builder;
+
+  @Inject
+  public JUnit4TestModelBuilder(Request request, @TopLevelSuite String suiteName, Builder builder) {
+    this.request = request;
+    this.suiteName = suiteName;
+    this.builder = builder;
+  }
+
+  /**
+   * Creates a model for a JUnit4 suite. This can be expensive; callers should
+   * consider memoizing the result.
+   *
+   * @return model.
+   */
+  @Override
+  public TestSuiteModel get() {
+    Description root = request.getRunner().getDescription();
+    Preconditions.checkArgument(root.isSuite(), "Top test must be a suite");
+    return builder.build(suiteName, root);
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestNameListener.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestNameListener.java
new file mode 100644
index 0000000..b7da73f
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestNameListener.java
@@ -0,0 +1,56 @@
+// Copyright 2011 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.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.testing.junit.runner.util.TestNameProvider;
+
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunListener;
+
+/**
+ * A listener to get the name of a JUnit4 test. 
+ */
+@Singleton
+class JUnit4TestNameListener extends RunListener {
+  private final ThreadLocal<Description> runningTest = new ThreadLocal<>();
+  private final SettableCurrentRunningTest currentRunningTest;
+
+  @Inject
+  public JUnit4TestNameListener(SettableCurrentRunningTest currentRunningTest) {
+    this.currentRunningTest = currentRunningTest;
+  }
+
+  @Override
+  public void testRunStarted(Description description) throws Exception {
+    currentRunningTest.setGlobalTestNameProvider(new TestNameProvider() {
+      @Override
+      public Description get() {
+        return runningTest.get();
+      }
+    });
+  }
+
+  @Override
+  public void testStarted(Description description) throws Exception {
+    runningTest.set(description);
+  }
+
+  @Override
+  public void testFinished(Description description) throws Exception {
+    runningTest.set(null);
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestStackTraceListener.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestStackTraceListener.java
new file mode 100644
index 0000000..abb0614
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestStackTraceListener.java
@@ -0,0 +1,58 @@
+// Copyright 2015 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.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.testing.junit.runner.internal.SignalHandlers;
+import com.google.testing.junit.runner.internal.StackTraces;
+import com.google.testing.junit.runner.internal.Stderr;
+
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunListener;
+
+import sun.misc.Signal;
+import sun.misc.SignalHandler;
+
+import java.io.PrintStream;
+
+/**
+ * A listener than dumps all stack traces when the test receives a SIGTERM.
+ */
+@Singleton
+class JUnit4TestStackTraceListener extends RunListener  {
+  private final SignalHandlers signalHandlers;
+  private final PrintStream errPrintStream;
+
+  @Inject
+  public JUnit4TestStackTraceListener(
+      SignalHandlers signalHandlers, @Stderr PrintStream errPrintStream) {
+    this.signalHandlers = signalHandlers;
+    this.errPrintStream = errPrintStream;
+  }
+
+  @Override
+  public void testRunStarted(Description description) throws Exception {
+    signalHandlers.installHandler(new Signal("TERM"), new WriteStackTraceSignalHandler());
+  }
+
+  private class WriteStackTraceSignalHandler implements SignalHandler {
+    @Override
+    public void handle(Signal signal) {
+      errPrintStream.println("Dumping stack traces for all threads\n");
+      StackTraces.printAll(errPrintStream);
+    }
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestXmlListener.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestXmlListener.java
new file mode 100644
index 0000000..5e572e1
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestXmlListener.java
@@ -0,0 +1,123 @@
+// Copyright 2012 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.common.base.Supplier;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.testing.junit.runner.internal.SignalHandlers;
+import com.google.testing.junit.runner.internal.Stderr;
+import com.google.testing.junit.runner.model.TestSuiteModel;
+
+import org.junit.Ignore;
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+
+import sun.misc.Signal;
+import sun.misc.SignalHandler;
+
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+/**
+ * A listener that writes the test output as XML.
+ */
+@Singleton
+class JUnit4TestXmlListener extends RunListener  {
+  
+  private final Supplier<TestSuiteModel> modelSupplier;
+  private final CancellableRequestFactory requestFactory;
+  private final SignalHandlers signalHandlers;
+  private final OutputStream xmlStream;
+  private final PrintStream errPrintStream;
+  private volatile TestSuiteModel model;
+
+  @Inject
+  public JUnit4TestXmlListener(Supplier<TestSuiteModel> modelSupplier,
+      CancellableRequestFactory requestFactory, SignalHandlers signalHandlers,
+      @Xml OutputStream xmlStream, @Stderr PrintStream errPrintStream) {
+    this.modelSupplier = modelSupplier;
+    this.requestFactory = requestFactory;
+    this.signalHandlers = signalHandlers;
+    this.xmlStream = xmlStream;
+    this.errPrintStream = errPrintStream;
+  }
+
+  @Override
+  public void testRunStarted(Description description) throws Exception {
+    model = modelSupplier.get();
+    signalHandlers.installHandler(new Signal("TERM"), new WriteXmlSignalHandler());
+  }
+
+  @Override
+  public void testStarted(Description description) throws Exception {
+    model.testStarted(description);
+  }
+
+  @Override
+  public void testAssumptionFailure(Failure failure) {
+    model.testSkipped(failure.getDescription());
+  }
+
+  @Override
+  public void testFailure(Failure failure) throws Exception {
+    model.testFailure(failure.getDescription(), failure.getException());
+  }
+
+  @Override
+  public void testIgnored(Description description) throws Exception {
+    // TODO(bazel-team) There's a known issue in the JUnit4 ParentRunner that
+    // fires testIgnored on test suites that are being skipped due to an
+    // assumption failure.
+    if (isSuiteAssumptionFailure(description)) {
+      model.testSkipped(description);
+    } else {
+      model.testSuppressed(description);
+    }
+  }
+
+  private boolean isSuiteAssumptionFailure(Description description) {
+    return description.isSuite() && description.getAnnotation(Ignore.class) == null;
+  }
+
+  @Override
+  public void testFinished(Description description) throws Exception {
+    model.testFinished(description);
+  }
+
+  @Override
+  public void testRunFinished(Result result) throws Exception {
+    model.writeAsXml(xmlStream);
+  }
+
+  private class WriteXmlSignalHandler implements SignalHandler {
+
+    @Override
+    public void handle(Signal signal) {
+      try {
+        errPrintStream.printf("%nReceived %s; writing test XML%n", signal.toString());
+        requestFactory.cancelRun();
+        model.testRunInterrupted();
+        model.writeAsXml(xmlStream);
+        errPrintStream.println("Done writing test XML");
+      } catch (Exception e) {
+        errPrintStream.println("Could not write test XML");
+        e.printStackTrace(errPrintStream);
+      }
+    }
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/RegExTestCaseFilter.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/RegExTestCaseFilter.java
new file mode 100644
index 0000000..303f0b7
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/RegExTestCaseFilter.java
@@ -0,0 +1,78 @@
+// 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.common.base.Strings;
+
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+
+import java.util.regex.Pattern;
+
+/**
+ * Filter that filters out test cases that either matches or does not match a specified regular
+ * expression.
+ */
+public final class RegExTestCaseFilter extends Filter {
+  private static final String TEST_NAME_FORMAT = "%s#%s";
+
+  private final Pattern pattern;
+  private final boolean isNegated;
+
+  /**
+   * Returns a filter that evaluates to {@code true} if the test case description matches
+   * specified regular expression. Otherwise, returns {@code false}.
+   */
+  public static RegExTestCaseFilter include(String regularExpression) {
+    return new RegExTestCaseFilter(regularExpression, false);
+  }
+
+ /**
+   * Returns a filter that evaluates to {@code false} if the test case description matches
+   * specified regular expression. Otherwise, returns {@code true}.
+   */
+  public static RegExTestCaseFilter exclude(String regularExpression) {
+    return new RegExTestCaseFilter(regularExpression, true);
+  }
+
+  private RegExTestCaseFilter(String regularExpression, boolean isNegated) {
+    this.isNegated = isNegated;
+    this.pattern = Pattern.compile(regularExpression);
+  }
+
+  @Override
+  public boolean shouldRun(Description description) {
+    if (description.isSuite()) {
+      return true;
+    }
+
+    boolean match = pattern.matcher(formatDescriptionName(description)).find();
+    return isNegated ? !match : match;
+  }
+
+  @Override
+  public String describe() {
+    return String.format("%sRegEx[%s]", isNegated ? "NOT " : "", pattern.toString());
+  }
+
+  private static String formatDescriptionName(Description description) {
+    String methodName = Strings.nullToEmpty(description.getMethodName());
+    String className = Strings.nullToEmpty(description.getClassName());
+    if (methodName.trim().isEmpty() || className.trim().isEmpty()) {
+      return description.getDisplayName();
+    }
+    return String.format(TEST_NAME_FORMAT, className, methodName);
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/SettableCurrentRunningTest.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/SettableCurrentRunningTest.java
new file mode 100644
index 0000000..785d414
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/SettableCurrentRunningTest.java
@@ -0,0 +1,25 @@
+// Copyright 2015 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.runner.util.CurrentRunningTest;
+import com.google.testing.junit.runner.util.TestNameProvider;
+
+/**
+ * A {@link CurrentRunningTest} variant that allows to set the testNameProvider via a method call.
+ */
+abstract class SettableCurrentRunningTest extends CurrentRunningTest {
+  abstract void setGlobalTestNameProvider(TestNameProvider provider);
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/TopLevelSuite.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/TopLevelSuite.java
new file mode 100644
index 0000000..7f9d720
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/TopLevelSuite.java
@@ -0,0 +1,31 @@
+// Copyright 2012 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.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Binding annotation for an object that represents the top-level suite.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER, ElementType.METHOD})
+@BindingAnnotation
+@interface TopLevelSuite {
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/Xml.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/Xml.java
new file mode 100644
index 0000000..d531b2b
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/Xml.java
@@ -0,0 +1,32 @@
+// Copyright 2012 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.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Binding annotation that indicates that the given {@code String} or stream
+ * represents XML.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER, ElementType.METHOD})
+@BindingAnnotation
+@interface Xml {
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/AntXmlResultWriter.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/AntXmlResultWriter.java
new file mode 100644
index 0000000..1ccabb2
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/AntXmlResultWriter.java
@@ -0,0 +1,176 @@
+// Copyright 2015 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.model;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+
+import org.joda.time.Interval;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Map.Entry;
+
+/**
+ * Writes the JUnit test nodes and their results into Ant-JUnit XML. Ant-JUnit XML is not a
+ * standardized format. For this implementation the
+ * <a href="http://windyroad.com.au/dl/Open%20Source/JUnit.xsd">XML schema</a> that is generally
+ * referred to as the best available source was used as a reference.
+ */
+public final class AntXmlResultWriter implements XmlResultWriter {
+  private static final String JUNIT_ELEMENT_TESTSUITES = "testsuites";
+  private static final String JUNIT_ELEMENT_TESTSUITE = "testsuite";
+  private static final String JUNIT_ATTR_TESTSUITE_ERRORS = "errors";
+  private static final String JUNIT_ATTR_TESTSUITE_FAILURES = "failures";
+  private static final String JUNIT_ATTR_TESTSUITE_HOSTNAME = "hostname";
+  private static final String JUNIT_ATTR_TESTSUITE_NAME = "name";
+  private static final String JUNIT_ATTR_TESTSUITE_TESTS = "tests";
+  private static final String JUNIT_ATTR_TESTSUITE_TIME = "time";
+  private static final String JUNIT_ATTR_TESTSUITE_TIMESTAMP = "timestamp";
+  private static final String JUNIT_ATTR_TESTSUITE_ID = "id";
+  private static final String JUNIT_ATTR_TESTSUITE_PACKAGE = "package";
+  private static final String JUNIT_ATTR_TESTSUITE_PROPERTIES = "properties";
+  private static final String JUNIT_ATTR_TESTSUITE_SYSTEM_OUT = "system-out";
+  private static final String JUNIT_ATTR_TESTSUITE_SYSTEM_ERR = "system-err";
+  private static final String JUNIT_ELEMENT_PROPERTY = "property";
+  private static final String JUNIT_ATTR_PROPERTY_NAME = "name";
+  private static final String JUNIT_ATTR_PROPERTY_VALUE = "value";
+  private static final String JUNIT_ELEMENT_TESTCASE = "testcase";
+  private static final String JUNIT_ELEMENT_FAILURE = "failure";
+  private static final String JUNIT_ATTR_FAILURE_MESSAGE = "message";
+  private static final String JUNIT_ATTR_FAILURE_TYPE = "type";
+  private static final String JUNIT_ATTR_TESTCASE_NAME = "name";
+  private static final String JUNIT_ATTR_TESTCASE_CLASSNAME = "classname";
+  private static final String JUNIT_ATTR_TESTCASE_TIME = "time";
+
+  private int testSuiteId;
+
+  public void writeTestSuites(XmlWriter writer, TestResult result) throws IOException {
+    testSuiteId = 0;
+    writer.startDocument();
+    writer.startElement(JUNIT_ELEMENT_TESTSUITES);
+    for (TestResult child : result.getChildResults()) {
+      writeTestSuite(writer, child, result.getFailures());
+    }
+    writer.endElement();
+    writer.close();
+  }
+
+  private void writeTestSuite(XmlWriter writer, TestResult result,
+      Iterable<Throwable> parentFailures)
+      throws IOException {
+    parentFailures = Iterables.concat(parentFailures, result.getFailures());
+
+    writer.startElement(JUNIT_ELEMENT_TESTSUITE);
+
+    writeTestSuiteAttributes(writer, result);
+    writeTestSuiteProperties(writer, result);
+    writeTestCases(writer, result, parentFailures);
+    writeTestSuiteOutput(writer);
+
+    writer.endElement();
+
+    for (TestResult child : result.getChildResults()) {
+      if (!child.getChildResults().isEmpty()) {
+        writeTestSuite(writer, child, parentFailures);
+      }
+    }
+  }
+
+  private void writeTestSuiteProperties(XmlWriter writer, TestResult result) throws IOException {
+    writer.startElement(JUNIT_ATTR_TESTSUITE_PROPERTIES);
+    for (Entry<String, String> entry : result.getProperties().entrySet()) {
+      writer.startElement(JUNIT_ELEMENT_PROPERTY);
+      writer.writeAttribute(JUNIT_ATTR_PROPERTY_NAME, entry.getKey());
+      writer.writeAttribute(JUNIT_ATTR_PROPERTY_VALUE, entry.getValue());
+      writer.endElement();
+    }
+    writer.endElement();
+  }
+
+  private void writeTestCases(XmlWriter writer, TestResult result,
+      Iterable<Throwable> parentFailures) throws IOException {
+    for (TestResult child : result.getChildResults()) {
+      if (child.getChildResults().isEmpty()) {
+        writeTestCase(writer, child, parentFailures);
+      }
+    }
+  }
+
+  private void writeTestSuiteOutput(XmlWriter writer) throws IOException {
+    writer.startElement(JUNIT_ATTR_TESTSUITE_SYSTEM_OUT);
+    // TODO(bazel-team) - where to get this from?
+    writer.endElement();
+    writer.startElement(JUNIT_ATTR_TESTSUITE_SYSTEM_ERR);
+    // TODO(bazel-team) - where to get this from?
+    writer.endElement();
+  }
+
+  private void writeTestSuiteAttributes(XmlWriter writer, TestResult result) throws IOException {
+    writer.writeAttribute(JUNIT_ATTR_TESTSUITE_NAME, result.getName());
+    writer.writeAttribute(JUNIT_ATTR_TESTSUITE_TIMESTAMP, getFormattedTimestamp(
+        result.getRunTimeInterval()));
+    writer.writeAttribute(JUNIT_ATTR_TESTSUITE_HOSTNAME, "localhost");
+    writer.writeAttribute(JUNIT_ATTR_TESTSUITE_TESTS, result.getNumTests());
+    writer.writeAttribute(JUNIT_ATTR_TESTSUITE_FAILURES, result.getNumFailures());
+    // JUnit 4.x no longer distinguishes between errors and failures, so it should be safe to just
+    // report errors as 0 and put everything into failures.
+    writer.writeAttribute(JUNIT_ATTR_TESTSUITE_ERRORS, 0);
+    writer.writeAttribute(JUNIT_ATTR_TESTSUITE_TIME, getFormattedRunTime(
+        result.getRunTimeInterval()));
+    // TODO(bazel-team) - do we want to report the package name here? Could we simply get it from
+    // result.getClassName() by stripping the last element of the class name?
+    writer.writeAttribute(JUNIT_ATTR_TESTSUITE_PACKAGE, "");
+    writer.writeAttribute(JUNIT_ATTR_TESTSUITE_ID, this.testSuiteId++);
+  }
+
+  private static String getFormattedRunTime(Optional<Interval> runTimeInterval) {
+    return !runTimeInterval.isPresent() ? "0.0"
+        : String.valueOf(runTimeInterval.get().toDurationMillis() / 1000.0D);
+  }
+
+  private static String getFormattedTimestamp(Optional<Interval> runTimeInterval) {
+    return !runTimeInterval.isPresent() ? "" : runTimeInterval.get().getStart().toString();
+  }
+
+  private void writeTestCase(XmlWriter writer, TestResult result,
+      Iterable<Throwable> parentFailures)
+      throws IOException {
+    writer.startElement(JUNIT_ELEMENT_TESTCASE);
+
+    for (Throwable failure : Iterables.concat(parentFailures, result.getFailures())) {
+      writer.startElement(JUNIT_ELEMENT_FAILURE);
+      writer.writeAttribute(JUNIT_ATTR_FAILURE_MESSAGE, Strings.nullToEmpty(failure.getMessage()));
+      writer.writeAttribute(JUNIT_ATTR_FAILURE_TYPE, failure.getClass().getName());
+      writer.writeCharacters(formatStackTrace(failure));
+      writer.endElement();
+    }
+
+    writer.writeAttribute(JUNIT_ATTR_TESTCASE_NAME, result.getName());
+    writer.writeAttribute(JUNIT_ATTR_TESTCASE_CLASSNAME, result.getClassName());
+    writer.writeAttribute(JUNIT_ATTR_TESTCASE_TIME, getFormattedRunTime(
+        result.getRunTimeInterval()));
+    writer.endElement();
+  }
+
+  private static String formatStackTrace(Throwable throwable) {
+    StringWriter stringWriter = new StringWriter();
+    PrintWriter writer = new PrintWriter(stringWriter);
+    throwable.printStackTrace(writer);
+    return stringWriter.getBuffer().toString();
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/BUILD b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/BUILD
new file mode 100644
index 0000000..59840c2
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/BUILD
@@ -0,0 +1,21 @@
+package(default_visibility = ["//src:__subpackages__"])
+
+java_library(
+    name = "model",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/junit4:runner",
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding",
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/util",
+        "//third_party:guava",
+        "//third_party:guice",
+        "//third_party:joda_time",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+    ],
+)
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+)
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestCaseNode.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestCaseNode.java
new file mode 100644
index 0000000..98a6cb1
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestCaseNode.java
@@ -0,0 +1,245 @@
+// Copyright 2015 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.model;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.testing.junit.runner.util.TestPropertyExporter.INITIAL_INDEX_FOR_REPEATED_PROPERTY;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.collect.ConcurrentHashMultiset;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.Multiset;
+import com.google.testing.junit.runner.model.TestResult.Status;
+import com.google.testing.junit.runner.util.TestPropertyExporter;
+
+import org.joda.time.Interval;
+import org.junit.runner.Description;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import javax.annotation.Nullable;
+
+/**
+ * A leaf in the test suite model.
+ */
+class TestCaseNode extends TestNode implements TestPropertyExporter.Callback {
+  private final TestSuiteNode parent;
+  private final Map<String, String> properties = Maps.newConcurrentMap();
+  private final Multiset<String> repeatedPropertyNames = ConcurrentHashMultiset.create();
+  private final Queue<Throwable> globalFailures = new ConcurrentLinkedQueue<>();
+  private final ListMultimap<Description, Throwable> dynamicTestToFailures =
+      Multimaps.synchronizedListMultimap(LinkedListMultimap.<Description, Throwable>create());
+
+  @Nullable private volatile Interval runTimeInterval = null;
+  private volatile State state = State.INITIAL;
+
+  TestCaseNode(Description description, TestSuiteNode parent) {
+    super(description);
+    this.parent = parent;
+  }
+
+  @VisibleForTesting
+  @Override
+  public List<TestNode> getChildren() {
+    return Collections.emptyList();
+  }
+
+
+  /**
+   * Indicates that the test represented by this node has started.
+   *
+   * @param now Time that the test started
+   */
+  public void started(long now) {
+    compareAndSetState(State.INITIAL, State.STARTED, now);
+  }
+  
+  @Override
+  public void testInterrupted(long now) {
+    if (compareAndSetState(State.STARTED, State.INTERRUPTED, now)) {
+      return;
+    }
+    compareAndSetState(State.INITIAL, State.CANCELLED, now);
+  }
+
+  @Override
+  public void exportProperty(String name, String value) {
+    properties.put(name, value);
+  }
+
+  @Override
+  public String exportRepeatedProperty(String name, String value) {
+    String propertyName = getRepeatedPropertyName(name);
+    properties.put(propertyName, value);
+    return propertyName;
+  }
+
+  @Override
+  public void testSkipped(long now) {
+    compareAndSetState(State.STARTED, State.SKIPPED, now);
+  }
+  
+
+  @Override
+  public void testSuppressed(long now) {
+    compareAndSetState(State.INITIAL, State.SUPPRESSED, now);
+  }
+
+  /**
+   * Indicates that the test represented by this node has finished.
+   *
+   * @param now Time that the test finished
+   */
+  public void finished(long now) {
+    compareAndSetState(State.STARTED, State.FINISHED, now);
+  }
+  
+  @Override
+  public void testFailure(Throwable throwable, long now) {
+    compareAndSetState(State.INITIAL, State.FINISHED, now);
+    globalFailures.add(throwable);
+  }
+  
+  @Override
+  public void dynamicTestFailure(Description test, Throwable throwable, long now) {
+    compareAndSetState(State.INITIAL, State.FINISHED, now);
+    dynamicTestToFailures.put(test, throwable);
+  }
+
+  private String getRepeatedPropertyName(String name) {
+    int index = repeatedPropertyNames.add(name, 1) + INITIAL_INDEX_FOR_REPEATED_PROPERTY;
+    return name + index;
+  }
+
+  @Override
+  public boolean isTestCase() {
+    return true;
+  }
+
+  private synchronized boolean compareAndSetState(State fromState, State toState, long now) {
+    if (fromState == state && toState != checkNotNull(state)) {
+      state = toState;
+      runTimeInterval = runTimeInterval == null 
+          ? new Interval(now, now) : runTimeInterval.withEndMillis(now);
+      return true;
+    }
+    return false;
+  }
+
+  public Optional<Interval> getRuntime() {
+    return Optional.fromNullable(runTimeInterval);
+  }
+
+  /**
+   * @return The equivalent {@link TestResult.Status} if the test execution ends with the FSM
+   *      at this state.
+   */
+  public TestResult.Status getTestResultStatus() {
+    return state.getTestResultStatus();
+  }
+
+  @Override
+  protected TestResult buildResult() {
+    // Some test descriptions, like those provided by JavaScript tests, are
+    // constructed by Description.createSuiteDescription, not
+    // createTestDescription, because they don't have a "class" per se.
+    // In this case, getMethodName returns null and we fill in the className
+    // attribute with the name of the parent test suite.
+    String name = getDescription().getMethodName();
+    String className = getDescription().getClassName();
+    if (name == null) {
+      name = className;
+      className = parent.getDescription().getDisplayName();
+    }
+
+    // For now, we give each dynamic test an empty properties map and the same
+    // run time and status as its parent test case, but this may change.
+    List<TestResult> childResults = Lists.newLinkedList();
+    for (Description dynamicTest : getDescription().getChildren()) {
+      childResults.add(buildDynamicResult(dynamicTest, getRuntime(), getTestResultStatus()));
+    }
+
+    int numTests = getDescription().isTest() ? 1 : getDescription().getChildren().size();
+    int numFailures = globalFailures.isEmpty() ? dynamicTestToFailures.keySet().size() : numTests;
+    return new TestResult.Builder()
+        .name(name)
+        .className(className)
+        .properties(properties)
+        .failures(ImmutableList.copyOf(globalFailures))
+        .runTimeInterval(getRuntime())
+        .status(getTestResultStatus())
+        .numTests(numTests)
+        .numFailures(numFailures)
+        .childResults(childResults)
+        .build();
+  }
+
+  private TestResult buildDynamicResult(Description test, Optional<Interval> runTime,
+      TestResult.Status status) {
+    // The dynamic test fails if the testcase itself fails or there is
+    // a dynamic failure specifically for the dynamic test.
+    List<Throwable> dynamicFailures = dynamicTestToFailures.get(test);
+    boolean failed = !globalFailures.isEmpty() || !dynamicFailures.isEmpty();
+    return new TestResult.Builder()
+        .name(test.getDisplayName())
+        .className(getDescription().getDisplayName())
+        .properties(ImmutableMap.<String, String>of())
+        .failures(dynamicFailures)
+        .runTimeInterval(runTime)
+        .status(status)
+        .numTests(1)
+        .numFailures(failed ? 1 : 0)
+        .childResults(ImmutableList.<TestResult>of())
+        .build();
+  }
+
+  /**
+   * States of a TestCaseNode (see (link) for all the transitions and states descriptions).
+   */
+  private static enum State {
+    INITIAL(TestResult.Status.SKIPPED), 
+    STARTED(TestResult.Status.INTERRUPTED),
+    SKIPPED(TestResult.Status.SKIPPED),
+    SUPPRESSED(TestResult.Status.SUPPRESSED),
+    CANCELLED(TestResult.Status.CANCELLED),
+    INTERRUPTED(TestResult.Status.INTERRUPTED),
+    FINISHED(TestResult.Status.COMPLETED);
+
+    private final Status status;
+
+    State(TestResult.Status status) {
+      this.status = status;
+    }
+
+    /**
+     * @return The equivalent {@link TestResult.Status} if the test execution ends with the FSM
+     *      at this state. 
+     */
+    public TestResult.Status getTestResultStatus() {
+      return status;
+    }
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestNode.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestNode.java
new file mode 100644
index 0000000..ccda6f2
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestNode.java
@@ -0,0 +1,95 @@
+// Copyright 2015 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.model;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import org.junit.runner.Description;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * A node in a test suite.
+ */
+public abstract class TestNode {
+  private final Description description;
+  @Nullable private TestResult result = null;
+
+  TestNode(Description description) {
+    this.description = Preconditions.checkNotNull(description);
+  }
+
+  /**
+   * {@link Description} of this test node.
+   */
+  public final Description getDescription() {
+    return description;
+  }
+
+  /**
+   * Returns this node's children (test suites or tests cases).
+   */
+  @VisibleForTesting
+  public abstract List<TestNode> getChildren();
+
+  /**
+   * Returns true if this node is a test case (e.g. junit4 test), false otherwise (e.g. junit4 test
+   * suite). The {@link TestSuiteModel} distinguishes between test cases and suites based on the
+   * value returned by {@link Description#isTest()}.
+   */
+  public abstract boolean isTestCase();
+
+  /**
+   * Indicates that the test represented by this node was skipped.
+   */
+  public abstract void testSkipped(long now);
+
+  /**
+   * Indicates that the test represented by this node was ignored or suppressed due to being 
+   * annotated with {@code @Ignore} or {@code @Suppress}.
+   */
+  public abstract void testSuppressed(long now);
+
+  /**
+   * Indicates that the test represented by this node was interrupted.
+   */
+  public abstract void testInterrupted(long now);
+
+  /**
+   * Adds a failure to the test represented by this node.
+   */
+  public abstract void testFailure(Throwable throwable, long now);
+
+  /**
+   * Indicates that a dynamically generated test case or suite failed.
+   */
+  public abstract void dynamicTestFailure(Description test, Throwable throwable, long now);
+
+  /**
+   * Template-method that creates a {@link TestResult} object that represents the test outcome of 
+   * this node.
+   */
+  protected abstract TestResult buildResult();
+
+  final TestResult getResult() {
+    if (result == null) {
+      result = buildResult();
+    }
+    return result;
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestResult.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestResult.java
new file mode 100644
index 0000000..b9900a8
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestResult.java
@@ -0,0 +1,219 @@
+// Copyright 2015 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.model;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.joda.time.Interval;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Result of executing a test suite or test case.
+ */
+final class TestResult {
+
+  /**
+   * Possible result values to a test.
+   */
+  enum Status {
+    /**
+     * Test case was not run because the test decided that it should not be run 
+     * (e.g.: due to a failed assumption in a JUnit4-style tests).
+     */
+    SKIPPED(false),
+
+    /**
+     * Test case was not run because the user specified that it should be filtered out of the
+     * test run.
+     */
+    FILTERED(false),
+
+    /**
+     * Test case was not run because the test was labeled in the code as suppressed 
+     * (e.g.: the test was annotated with {@code @Suppress} or {@code @Ignore}).
+     */
+    SUPPRESSED(false),
+
+    /**
+     * Test case was not started because the test harness run was interrupted by a 
+     * signal or timed out.
+     */
+    CANCELLED(false),
+
+    /**
+     * Test case was started but not finished because the test harness run was interrupted by a 
+     * signal or timed out.
+     */
+    INTERRUPTED(true),
+
+    /**
+     * Test case was run and completed (possibly failing or throwing an exception, but not 
+     * interrupted).
+     */
+    COMPLETED(true);
+
+    private final boolean wasRun;
+
+    Status(boolean wasRun) {
+      this.wasRun = wasRun;
+    }
+
+    /**
+     * Equivalent semantic value to wasRun {@code status="run|notrun"} on 
+     * the XML schema.
+     */
+    public boolean wasRun() {
+      return wasRun;
+    }
+  }
+
+  private final String name, className;
+  private final ImmutableMap<String, String> properties;
+  private final ImmutableList<Throwable> failures;
+  private final Optional<Interval> runTime;
+  private final Status status;
+  private final int numTests, numFailures;
+  private final ImmutableList<TestResult> childResults;
+
+  private TestResult(Builder builder) {
+    name = checkNotNull(builder.name, "name not set");
+    className = checkNotNull(builder.className, "className not set");
+    properties = checkNotNull(builder.properties, "properties not set");
+    failures = checkNotNull(builder.failures, "failures not set");
+    runTime = checkNotNull(builder.runTime, "runTime not set");
+    status = checkNotNull(builder.status, "status not set");
+    numTests = checkNotNull(builder.numTests, "numTests not set");
+    numFailures = checkNotNull(builder.numFailures, "numFailures not set");
+    childResults = checkNotNull(builder.childResults, "childResults not set");
+  }
+
+  String getName() {
+    return name;
+  }
+
+  String getClassName() {
+    return className;
+  }
+
+  ImmutableMap<String, String> getProperties() {
+    return properties;
+  }
+
+  ImmutableList<Throwable> getFailures() {
+    return failures;
+  }
+
+  Optional<Interval> getRunTimeInterval() {
+    return runTime;
+  }
+
+  Status getStatus() {
+    return status;
+  }
+
+  boolean wasRun() {
+    return getStatus().wasRun();
+  }
+
+  int getNumTests() {
+    return numTests;
+  }
+
+  int getNumFailures() {
+    return numFailures;
+  }
+
+  ImmutableList<TestResult> getChildResults() {
+    return childResults;
+  }
+
+  static final class Builder {
+    private String name = null;
+    private String className = null;
+    private ImmutableMap<String, String> properties = null;
+    private ImmutableList<Throwable> failures = null;
+    private Optional<Interval> runTime = null;
+    private Status status = null;
+    private Integer numTests = null;
+    private Integer numFailures = null;
+    private ImmutableList<TestResult> childResults = null;
+
+    Builder() {}
+
+    Builder name(String name) {
+      this.name = checkNullToNotNull(this.name, name, "name");
+      return this;
+    }
+
+    Builder className(String className) {
+      this.className = checkNullToNotNull(this.className, className, "className");
+      return this;
+    }
+
+    Builder properties(Map<String, String> properties) {
+      this.properties = ImmutableMap.copyOf(
+          checkNullToNotNull(this.properties, properties, "properties"));
+      return this;
+    }
+
+    Builder failures(List<Throwable> failures) {
+      this.failures = ImmutableList.copyOf(
+          checkNullToNotNull(this.failures, failures, "failures"));
+      return this;
+    }
+
+    Builder runTimeInterval(Optional<Interval> runTime) {
+      this.runTime = checkNullToNotNull(this.runTime, runTime, "runTime");
+      return this;
+    }
+
+    Builder status(Status status) {
+      this.status = checkNullToNotNull(this.status, status, "status");
+      return this;
+    }
+
+    Builder numTests(int numTests) {
+      this.numTests = checkNullToNotNull(this.numTests, numTests, "numTests");
+      return this;
+    }
+
+    Builder numFailures(int numFailures) {
+      this.numFailures = checkNullToNotNull(this.numFailures, numFailures, "numFailures");
+      return this;
+    }
+
+    Builder childResults(List<TestResult> childResults) {
+      this.childResults = ImmutableList.copyOf(
+          checkNullToNotNull(this.childResults, childResults, "childResults"));
+      return this;
+    }
+
+    TestResult build() {
+      return new TestResult(this);
+    }
+
+    private static <T> T checkNullToNotNull(T currValue, T newValue, String desc) {
+      checkState(currValue == null, desc + " already set");
+      return checkNotNull(newValue, desc + " is null");
+    }
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java
new file mode 100644
index 0000000..a8b1231
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java
@@ -0,0 +1,345 @@
+// 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.model;
+
+import static com.google.common.base.Predicates.notNull;
+import static com.google.common.collect.Maps.filterValues;
+import static com.google.common.collect.Maps.transformValues;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Ticker;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.MapMaker;
+import com.google.testing.junit.junit4.runner.DynamicTestException;
+import com.google.testing.junit.runner.sharding.ShardingEnvironment;
+import com.google.testing.junit.runner.sharding.ShardingFilters;
+import com.google.testing.junit.runner.util.TestPropertyRunnerIntegration;
+
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+/**
+ * Model of the tests that will be run. The model is agnostic of the particular
+ * type of test run (JUnit3 or JUnit4). The test runner uses this class to build
+ * the model, and then updates the model during the test run.
+ *
+ * <p>The leaf nodes in the model are test cases; the other nodes are test suites.
+ */
+public class TestSuiteModel {
+  private final TestSuiteNode rootNode;
+  private final ImmutableMap<Description, TestCaseNode> testCaseMap;
+  private final ImmutableMap<Description, TestNode> testsMap;
+  private final Ticker ticker;
+  private final AtomicBoolean wroteXml = new AtomicBoolean(false);
+  private final XmlResultWriter xmlResultWriter;
+  @Nullable private final Filter shardingFilter;
+
+  private TestSuiteModel(Builder builder) {
+    rootNode = builder.rootNode;
+    testsMap = ImmutableMap.copyOf(builder.testsMap);
+    testCaseMap = ImmutableMap.copyOf(filterTestCases(builder.testsMap));
+    ticker = builder.ticker;
+    shardingFilter = builder.shardingFilter;
+    xmlResultWriter = builder.xmlResultWriter;
+  }
+
+  @VisibleForTesting
+  public List<TestNode> getTopLevelTestSuites() {
+    return rootNode.getChildren();
+  }
+
+  /**
+   * Gets the sharding filter to use; {@link Filter#ALL} if not sharding.
+   */
+  public Filter getShardingFilter() {
+    return shardingFilter;
+  }
+
+  /**
+   * Returns the test case node with the given test description.<p>
+   *
+   * Note that in theory this should never return {@code null}, but
+   * if it did we would not want to throw a {@code NullPointerException}
+   * because JUnit4 would catch the exception and remove our test
+   * listener!
+   */
+  private TestCaseNode getTestCase(Description description) {
+    return testCaseMap.get(description);
+  }
+
+  private TestNode getTest(Description description) {
+    return testsMap.get(description);
+  }
+
+  @VisibleForTesting
+  public int getNumTestCases() {
+    return testCaseMap.size();
+  }
+
+  /**
+   * Indicate that the test case with the given key has started.
+   *
+   * @param description key for a test case
+   */
+  public void testStarted(Description description) {
+    TestCaseNode testCase = getTestCase(description);
+    if (testCase != null) {
+      testCase.started(currentMillis());
+      TestPropertyRunnerIntegration.setTestCaseForThread(testCase);
+    }
+  }
+
+  /**
+   * Indicate that the entire test run was interrupted.
+   */
+  public void testRunInterrupted() {
+    rootNode.testInterrupted(currentMillis());
+  }
+
+  /**
+   * Indicate that the test case with the given key has requested that
+   * a property be written in the XML.<p>
+   *
+   * @param description key for a test case
+   * @param name The property name.
+   * @param value The property value.
+   */
+  public void testEmittedProperty(Description description, String name, String value) {
+    TestCaseNode testCase = getTestCase(description);
+    if (testCase != null) {
+      testCase.exportProperty(name, value);
+    }
+  }
+
+  /**
+   * Adds a failure to the test with the given key. If the specified test is suite, the failure
+   * will be added to all its children.
+   *
+   * @param description key for a test case
+   */
+  public void testFailure(Description description, Throwable throwable) {
+    TestNode test = getTest(description);
+    if (test != null) {
+      if (throwable instanceof DynamicTestException) {
+        DynamicTestException dynamicFailure = (DynamicTestException) throwable;
+        test.dynamicTestFailure(
+            dynamicFailure.getTest(), dynamicFailure.getCause(), currentMillis());
+      } else {
+        test.testFailure(throwable, currentMillis());
+      }
+    }
+  }
+
+  /**
+   * Indicates that the test case with the given key was skipped
+   *
+   * @param description key for a test case
+   */
+  public void testSkipped(Description description) {
+    TestNode test = getTest(description);
+    if (test != null) {
+      test.testSkipped(currentMillis());
+    }
+  }
+  
+
+  /**
+   * Indicates that the test case with the given key was ignored or suppressed
+   *
+   * @param description key for a test case
+   */
+  public void testSuppressed(Description description) {
+    TestNode test = getTest(description);
+    if (test != null) {
+      test.testSuppressed(currentMillis());
+    }
+  }
+
+  /**
+   * Indicate that the test case with the given description has finished.
+   */
+  public void testFinished(Description description) {
+    TestCaseNode testCase = getTestCase(description);
+    if (testCase != null) {
+      testCase.finished(currentMillis());
+    }
+
+    /*
+     * Note: we don't call TestPropertyExporter, so if any properties are
+     * exported before the next test runs, they will be associated with the
+     * current test.
+     */
+  }
+
+  private long currentMillis() {
+    return NANOSECONDS.toMillis(ticker.read());
+  }
+
+  /**
+   * Writes the model to XML
+   *
+   * @param outputStream stream to output to
+   * @throws IOException if the underlying writer throws an exception
+   */
+  public void writeAsXml(OutputStream outputStream) throws IOException {
+    write(new XmlWriter(outputStream));
+  }
+
+  @VisibleForTesting
+  void write(XmlWriter writer) throws IOException {
+    if (wroteXml.compareAndSet(false, true)) {
+      xmlResultWriter.writeTestSuites(writer, rootNode.getResult());
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    return toString().hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof TestSuiteModel)) {
+      return false;
+    }
+    TestSuiteModel that = (TestSuiteModel) obj;
+
+    // We only use this for testing, so using toString() is good enough
+    return this.toString().equals(that.toString());
+  }
+
+  @Override
+  public String toString() {
+    try {
+      StringWriter stringWriter = new StringWriter();
+      write(XmlWriter.createForTesting(stringWriter));
+      return stringWriter.toString();
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * A builder for creating a model of a test suite.
+   */
+  public static class Builder {
+    private final Ticker ticker;
+    private final Map<Description, TestNode> testsMap = new MapMaker().makeMap();
+    private final ShardingEnvironment shardingEnvironment;
+    private final ShardingFilters shardingFilters;
+    private final XmlResultWriter xmlResultWriter;
+    private TestSuiteNode rootNode;
+    private Filter shardingFilter = Filter.ALL;
+    private boolean buildWasCalled = false;
+
+    @Inject
+    public Builder(Ticker ticker, ShardingFilters shardingFilters,
+        ShardingEnvironment shardingEnvironment, XmlResultWriter xmlResultWriter) {
+      this.ticker = ticker;
+      this.shardingFilters = shardingFilters;
+      this.shardingEnvironment = shardingEnvironment;
+      this.xmlResultWriter = xmlResultWriter;
+    }
+
+    public TestSuiteModel build(String suiteName, Description... topLevelSuites) {
+      Preconditions.checkState(!buildWasCalled, "Builder.build() was already called");
+      buildWasCalled = true;
+      if (shardingEnvironment.isShardingEnabled()) {
+        shardingFilter = getShardingFilter(topLevelSuites);
+      }
+      rootNode = new TestSuiteNode(Description.createSuiteDescription(suiteName));
+      for (Description topLevelSuite : topLevelSuites) {
+        addTestSuite(rootNode, topLevelSuite);
+      }
+      return new TestSuiteModel(this);
+    }
+
+    private Filter getShardingFilter(Description... topLevelSuites) {
+      Collection<Description> tests = Lists.newLinkedList();
+      for (Description suite : topLevelSuites) {
+        collectTests(suite, tests);
+      }
+      shardingEnvironment.touchShardFile();
+      return shardingFilters.createShardingFilter(tests);
+    }
+
+    private static void collectTests(Description desc, Collection<Description> tests) {
+      if (desc.isTest()) {
+        tests.add(desc);
+      } else {
+        for (Description child : desc.getChildren()) {
+          collectTests(child, tests);
+        }
+      }
+    }
+
+    private void addTestSuite(TestSuiteNode parentSuite, Description suiteDescription) {
+      TestSuiteNode suite = new TestSuiteNode(suiteDescription);
+      for (Description childDesc : suiteDescription.getChildren()) {
+        if (childDesc.isTest()) {
+          addTestCase(suite, childDesc);
+        } else {
+          addTestSuite(suite, childDesc);
+        }
+      }
+      // Empty suites are pruned when sharding.
+      if (shardingFilter == Filter.ALL || !suite.getChildren().isEmpty()) {
+        parentSuite.addTestSuite(suite);
+        testsMap.put(suiteDescription, suite);
+      }
+    }
+
+    private void addTestCase(TestSuiteNode parentSuite, Description testCaseDesc) {
+      Preconditions.checkArgument(testCaseDesc.isTest());
+      if (!shardingFilter.shouldRun(testCaseDesc)) {
+        return;
+      }
+      TestCaseNode testCase = new TestCaseNode(testCaseDesc, parentSuite);
+      testsMap.put(testCaseDesc, testCase);
+      parentSuite.addTestCase(testCase);
+    }
+  }
+
+  private static Map<Description, TestCaseNode> filterTestCases(Map<Description, TestNode> tests) {
+    return filterValues(transformValues(tests, toTestCaseNode()), notNull());
+  }
+
+  private static Function<TestNode, TestCaseNode> toTestCaseNode() {
+    return new Function<TestNode, TestCaseNode>() {
+      @Override
+      public TestCaseNode apply(TestNode test) {
+        return test instanceof TestCaseNode ? (TestCaseNode) test : null;
+      }
+    };
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteNode.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteNode.java
new file mode 100644
index 0000000..f6656e4
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteNode.java
@@ -0,0 +1,129 @@
+// Copyright 2015 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.model;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.testing.junit.runner.model.TestResult.Status;
+
+import org.joda.time.Interval;
+import org.junit.runner.Description;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A parent node in the test suite model.
+ */
+class TestSuiteNode extends TestNode {
+
+  private final List<TestNode> children = Lists.newArrayList();
+
+  TestSuiteNode(Description description) {
+    super(description);
+  }
+
+  @VisibleForTesting
+  @Override
+  public List<TestNode> getChildren() {
+    return Collections.unmodifiableList(children);
+  }
+
+  @Override
+  public boolean isTestCase() {
+    return false;
+  }
+
+  @Override
+  public void testFailure(Throwable throwable, long now) {
+    for (TestNode child : getChildren()) {
+      child.testFailure(throwable, now);
+    }
+  }
+
+  @Override
+  public void dynamicTestFailure(Description test, Throwable throwable, long now) {
+    for (TestNode child : getChildren()) {
+      child.dynamicTestFailure(test, throwable, now);
+    }
+  }
+
+  @Override
+  public void testInterrupted(long now) {
+    for (TestNode child : getChildren()) {
+      child.testInterrupted(now);
+    }
+  }
+
+  @Override
+  public void testSkipped(long now) {
+    for (TestNode child : getChildren()) {
+      child.testSkipped(now);
+    }
+  }
+
+  @Override
+  public void testSuppressed(long now) {
+    for (TestNode child : getChildren()) {
+      child.testSuppressed(now);
+    }
+  }
+
+  void addTestSuite(TestSuiteNode suite) {
+    children.add(suite);
+  }
+
+  void addTestCase(TestCaseNode testCase) {
+    children.add(testCase);
+  }
+
+  @Override
+  protected TestResult buildResult() {
+    Interval runTime = null;
+    int numTests = 0, numFailures = 0;
+    LinkedList<TestResult> childResults = Lists.newLinkedList();
+
+    for (TestNode child : children) {
+      TestResult childResult = child.getResult();
+      childResults.add(childResult);
+      numTests += childResult.getNumTests();
+      numFailures += childResult.getNumFailures();
+
+      Optional<Interval> optionalChildRunTime = childResult.getRunTimeInterval();
+      if (optionalChildRunTime.isPresent()) {
+        Interval childRunTime = optionalChildRunTime.get();
+        runTime = runTime == null ? childRunTime
+            : new Interval(Math.min(runTime.getStartMillis(), childRunTime.getStartMillis()),
+                         Math.max(runTime.getEndMillis(), childRunTime.getEndMillis()));
+      }
+    }
+
+    return new TestResult.Builder()
+        .name(getDescription().getDisplayName())
+        .className("")
+        .properties(ImmutableMap.<String, String>of())
+        .failures(ImmutableList.<Throwable>of())
+        .runTimeInterval(Optional.fromNullable(runTime))
+        .status(Status.SKIPPED)
+        .numTests(numTests)
+        .numFailures(numFailures)
+        .childResults(childResults)
+        .build();
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlResultWriter.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlResultWriter.java
new file mode 100644
index 0000000..cb2264c
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlResultWriter.java
@@ -0,0 +1,29 @@
+// Copyright 2015 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.model;
+
+import java.io.IOException;
+
+public interface XmlResultWriter {
+  /**
+   * Converts the {@link TestResult} to an XML representation and writes it into the
+   * {@link XmlWriter} passed to the constructor.
+   *
+   * @param writer where to write the generated XML.
+   * @param result the {@link TestResult} to process.
+   * @throws IOException
+   */
+  void writeTestSuites(XmlWriter writer, TestResult result) throws IOException;
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlWriter.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlWriter.java
new file mode 100644
index 0000000..97636ed
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlWriter.java
@@ -0,0 +1,210 @@
+// 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.model;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.xml.XmlEscapers;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.List;
+
+/**
+ * Writer for XML documents. We do not use third-party code, because all
+ * java_test rules have the test runner in their run-time classpath.
+ */
+class XmlWriter {
+  @VisibleForTesting
+  static final String EOL = System.getProperty("line.separator", "\n");
+  
+  private final Writer writer;
+  private boolean started;
+  private boolean inElement;
+  private final List<String> elementStack = Lists.newArrayList();
+
+  /**
+   * Creates an XML writer that writes to the given {@code OutputStream}.
+   *
+   * @param outputStream stream to write to
+   */
+  public XmlWriter(OutputStream outputStream) {
+    this(new OutputStreamWriter(outputStream, UTF_8));
+  }
+
+  /**
+   * Creates an XML writer for testing purposes. Note that if you decide to
+   * serialize the {@code StringWriter} (to disk or network) encode it in {@code
+   * UTF-8}.
+   *
+   * @param writer
+   */
+  @VisibleForTesting
+  static XmlWriter createForTesting(StringWriter writer) {
+    return new XmlWriter(writer);
+  }
+  
+  private XmlWriter(Writer writer) {
+    this.writer = writer;
+  }
+
+  /**
+   * Starts the XML document.
+   *
+   * @throws IOException if the underlying writer throws an exception
+   */
+  public void startDocument() throws IOException {
+    Preconditions.checkState(!started, "already started");
+
+    started = true;
+    Writer out = writer;
+    out.write("<?xml version='1.0' encoding='UTF-8'?>");
+  }
+
+  /**
+   * Completes the XML document and closes the underlying writer.
+   *
+   * @throws IOException if the underlying writer throws an exception
+   */
+  public void close() throws IOException {
+    while (!elementStack.isEmpty()) {
+      endElement();
+    }
+    writer.append(EOL);
+    writer.close();
+  }
+
+  private void closeElement() throws IOException {
+    if (inElement) {
+      writer.append('>');
+      inElement = false;
+    }
+  }
+  
+  private String indentation() {
+    return Strings.repeat("  ", elementStack.size());
+  }
+
+  /**
+   * Starts an XML element. The element is left open until either
+   * {@link #endElement()} or {@link #close()} are called. This method may be
+   * called multiple times before calling {@link #endElement()}; the writer
+   * keeps a stack of currently open elements.
+   * 
+   * @param elementName name of the element (must be XML safe or escaped)
+   * @throws IOException if the underlying writer throws an exception
+   */
+  public void startElement(String elementName) throws IOException {
+    Preconditions.checkState(started);
+    closeElement();
+    inElement = true;
+    writer.append(EOL + indentation() + "<" + elementName);
+    elementStack.add(elementName);
+  }
+
+  /**
+   * Ends the current XML element.
+   *
+   * @throws IOException if the underlying writer throws an exception
+   */
+  public void endElement() throws IOException {
+    String elementName = elementStack.remove(elementStack.size() - 1);
+    if (inElement) {
+      writer.write(" />");
+      inElement = false;
+    } else {
+      writer.write(EOL + indentation() + "</");
+      writer.write(elementName);
+      writer.write('>');
+    }
+  }
+
+  /**
+   * Writes an attribute with the given integer value to the currently open XML
+   * element.
+   * 
+   * @param name attribute name
+   * @param value attribute value
+   * @throws IOException
+   */
+  public void writeAttribute(String name, int value) throws IOException {
+    writeAttributeWithoutEscaping(name, String.valueOf(value));
+  }
+
+  /**
+   * Writes an attribute with the given double value to the currently open XML
+   * element.
+   * 
+   * @param name attribute name
+   * @param value attribute value (must be XML safe or escaped)
+   * @throws IOException
+   */
+  public void writeAttribute(String name, double value) throws IOException {
+    writeAttributeWithoutEscaping(name, String.valueOf(value));
+  }
+
+  /**
+   * Writes an attribute to the currently open XML element.
+   *
+   * @param name attribute name (must be XML safe or escaped)
+   * @param value attribute value (will be escaped by this method)
+   * @throws IOException
+   */
+  public void writeAttribute(String name, String value) throws IOException {
+    if (value != null) {
+      value = XmlEscapers.xmlAttributeEscaper().escape(value);
+    }
+    writeAttributeWithoutEscaping(name, value);
+  }
+
+  private void writeAttributeWithoutEscaping(String name, String value) throws IOException {
+    writer.write(" " + name + "='");
+    if (value != null) {
+      writer.write(value);
+    }
+    writer.write("'");
+  }
+
+  /**
+   * Writes the given characters as the content of the element. Closes the
+   * element if it is currently open.
+   *
+   * @param text String to append to the current content of the element
+   * @throws IOException
+   */
+  public void writeCharacters(String text) throws IOException {
+    if (Strings.isNullOrEmpty(text)) {
+      return;
+    }
+
+    closeElement();
+    writer.write(XmlEscapers.xmlContentEscaper().escape(text));
+  }
+
+  /**
+   * Gets the writer that this object uses for writing.
+   */
+  @VisibleForTesting
+  Writer getUnderlyingWriter() {
+    return writer;
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/BUILD b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/BUILD
new file mode 100644
index 0000000..43a7d05
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/BUILD
@@ -0,0 +1,26 @@
+DEFAULT_VISIBILITY = [
+    "//java/com/google/testing/junit/runner:__subpackages__",
+    "//javatests/com/google/testing/junit/runner:__subpackages__",
+    "//third_party/bazel/src/java_tools/junitrunner/java/com/google/testing/junit/runner:__subpackages__",
+    "//third_party/bazel/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner:__subpackages__",
+]
+
+package(default_visibility = ["//src:__subpackages__"])
+
+# TODO(bazel-team): This should be testonly = 1.
+java_library(
+    name = "sharding",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api",
+        "//third_party:guava",
+        "//third_party:guice",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+    ],
+)
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+)
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/HashBackedShardingFilter.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/HashBackedShardingFilter.java
new file mode 100644
index 0000000..9225ca2
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/HashBackedShardingFilter.java
@@ -0,0 +1,57 @@
+// 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.sharding;
+
+import com.google.common.base.Preconditions;
+
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+
+/**
+ * Sharding filter that uses the hashcode of the test description to
+ * assign it to a shard.
+ */
+class HashBackedShardingFilter extends Filter {
+
+  private final int shardIndex;
+  private final int totalShards;
+
+  public HashBackedShardingFilter(int shardIndex, int totalShards) {
+    Preconditions.checkArgument(shardIndex >= 0);
+    Preconditions.checkArgument(totalShards > shardIndex);
+    this.shardIndex = shardIndex;
+    this.totalShards = totalShards;
+  }
+
+  @Override
+  public boolean shouldRun(Description description) {
+    if (description.isSuite()) {
+      return true;
+    }
+    int mod = description.getDisplayName().hashCode() % totalShards;
+    if (mod < 0) {
+      mod += totalShards;
+    }
+    Preconditions.checkState(mod >= 0 && mod < totalShards);
+
+    return mod == shardIndex;
+  }
+
+  @Override
+  public String describe() {
+    return "hash-backed sharding filter";
+  }
+
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/RoundRobinShardingFilter.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/RoundRobinShardingFilter.java
new file mode 100644
index 0000000..38d9f11
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/RoundRobinShardingFilter.java
@@ -0,0 +1,114 @@
+// 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.sharding;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implements the round-robin sharding strategy.
+ *
+ * <p>This is done by equally dividing up the tests across all the shards
+ * Each test is numbered and the test number is modded with the number of
+ * shards and checked against the shard number to see whether it should run
+ * on a particular shard.
+ *
+ * <p>Equals and hashCode implementations are not necessary for correct
+ * sharding, but are done so that this filter can be compared in tests.
+ */
+public final class RoundRobinShardingFilter extends Filter {
+
+  @VisibleForTesting
+  final Map<Description, Integer> testToShardMap;
+  @VisibleForTesting
+  final int shardIndex;
+  @VisibleForTesting
+  final int totalShards;
+
+  public RoundRobinShardingFilter(Collection<Description> testDescriptions,
+      int shardIndex, int totalShards) {
+    Preconditions.checkArgument(shardIndex >= 0);
+    Preconditions.checkArgument(totalShards > shardIndex);
+    this.testToShardMap = buildTestToShardMap(testDescriptions);
+    this.shardIndex = shardIndex;
+    this.totalShards = totalShards;
+  }
+
+  /**
+   * Given a list of test case descriptions, returns a mapping from each
+   * to its index in the list.
+   */
+  private static Map<Description, Integer> buildTestToShardMap(
+      Collection<Description> testDescriptions) {
+    Map<Description, Integer> map = Maps.newHashMap();
+
+    // Sorting this list is incredibly important to correctness. Otherwise,
+    // "shuffled" suites would break the sharding protocol.
+    List<Description> sortedDescriptions = Lists.newArrayList(testDescriptions);
+    Collections.sort(sortedDescriptions, new DescriptionComparator());
+
+    // If we get two descriptions that are equal, the shard number for the second
+    // one will overwrite the shard number for the first.  Thus they'll run on the
+    // same shard.
+    int index = 0;
+    for (Description description : sortedDescriptions) {
+      Preconditions.checkArgument(description.isTest(),
+          "Test suite should not be included in the set of tests to shard: %s",
+          description.getDisplayName());
+      map.put(description, index);
+      index++;
+    }
+    return Collections.unmodifiableMap(map);
+  }
+
+  @Override
+  public boolean shouldRun(Description description) {
+    if (description.isSuite()) {
+      return true;
+    }
+    Integer testNumber = testToShardMap.get(description);
+    if (testNumber == null) {
+      throw new IllegalArgumentException("This filter keeps a mapping from each test "
+          + "description to a shard, and the given description was not passed in when "
+          + "filter was constructed: " + description);
+    }
+    return (testNumber % totalShards) == shardIndex;
+  }
+
+  @Override
+  public String describe() {
+    return "round robin sharding filter";
+  }
+
+  @VisibleForTesting
+  static class DescriptionComparator implements Comparator<Description> {
+    @Override
+    public int compare(Description d1, Description d2) {
+      return d1.getDisplayName().compareTo(d2.getDisplayName());
+    }
+  }
+
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/ShardingEnvironment.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/ShardingEnvironment.java
new file mode 100644
index 0000000..6d3836f
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/ShardingEnvironment.java
@@ -0,0 +1,93 @@
+// 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.sharding;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.Files;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Utility class that encapsulates dependencies from sharding implementations
+ * on the test environment.  See http://bazel.io/docs/test-sharding.html for a
+ * list of all environment variables related to test sharding.
+ */
+public class ShardingEnvironment {
+
+  /**
+   * A singleton instance of ShardingEnvironment declared for convenience.
+   */
+  public static final ShardingEnvironment DEFAULT = new ShardingEnvironment();
+
+  /** Usage: -Dtest.sharding.strategy=round_robin */
+  private static final String TEST_SHARDING_STRATEGY = "test.sharding.strategy";
+
+  /**
+   * Return true iff the current test should be sharded.
+   */
+  public boolean isShardingEnabled() {
+    return System.getenv("TEST_TOTAL_SHARDS") != null;
+  }
+
+  /**
+   * Returns the 0-indexed test shard number, where
+   * 0 <= shard index < total shards.
+   * If the environment does not specify a test shard number, returns 0.
+   */
+  public int getShardIndex() {
+    String shardIndex = System.getenv("TEST_SHARD_INDEX");
+    return shardIndex == null ? 0 : Integer.parseInt(shardIndex);
+  }
+
+  /**
+   * Returns the total number of test shards, or 1 if not specified by the
+   * test environment.
+   */
+  public int getTotalShards() {
+    String totalShards = System.getenv("TEST_TOTAL_SHARDS");
+    return totalShards == null ? 1 : Integer.parseInt(totalShards);
+  }
+
+  /**
+   * Creates the shard file that is used to indicate that tests are
+   * being sharded.
+   */
+  public void touchShardFile() {
+    String shardStatusPath = System.getenv("TEST_SHARD_STATUS_FILE");
+    File shardFile = (shardStatusPath == null ? null : new File(shardStatusPath));
+    touchShardFile(shardFile);
+  }
+
+  @VisibleForTesting
+  static void touchShardFile(File shardFile) {
+    if (shardFile != null) {
+      try {
+        Files.touch(shardFile);
+      } catch (IOException e) {
+        throw new RuntimeException("Error writing shard file " + shardFile, e);
+      }
+    }
+  }
+
+  /**
+   * Returns the test sharding strategy optionally specified by the JVM flag
+   * {@link #TEST_SHARDING_STRATEGY}, which maps to the enums in
+   * {@link com.google.testing.junit.runner.sharding.ShardingFilters.ShardingStrategy}.
+   */
+  public String getTestShardingStrategy() {
+    return System.getProperty(TEST_SHARDING_STRATEGY);
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/ShardingFilters.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/ShardingFilters.java
new file mode 100644
index 0000000..206a8e9
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/ShardingFilters.java
@@ -0,0 +1,112 @@
+// 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.sharding;
+
+import com.google.inject.Inject;
+import com.google.testing.junit.runner.sharding.api.ShardingFilterFactory;
+
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+
+import java.util.Collection;
+
+/**
+ * A factory for test sharding filters.
+ */
+public class ShardingFilters {
+
+  /**
+   * An enum of strategies for generating test sharding filters.
+   */
+  public static enum ShardingStrategy implements ShardingFilterFactory {
+
+    /**
+     * {@link com.google.testing.junit.runner.sharding.HashBackedShardingFilter}
+     */
+    HASH {
+      @Override
+      public Filter createFilter(Collection<Description> testDescriptions,
+          int shardIndex, int totalShards) {
+        return new HashBackedShardingFilter(shardIndex, totalShards);
+      }
+    },
+
+    /**
+     * {@link com.google.testing.junit.runner.sharding.RoundRobinShardingFilter}
+     */
+    ROUND_ROBIN {
+      @Override
+      public Filter createFilter(Collection<Description> testDescriptions,
+          int shardIndex, int totalShards) {
+        return new RoundRobinShardingFilter(testDescriptions, shardIndex, totalShards);
+      }
+    }
+  }
+  
+  public static final ShardingFilterFactory DEFAULT_SHARDING_STRATEGY =
+      ShardingStrategy.ROUND_ROBIN;
+  private final ShardingEnvironment shardingEnvironment;
+  private final ShardingFilterFactory defaultShardingStrategy;
+
+  /**
+   * Creates a factory with the given sharding environment and the
+   * default sharding strategy.
+   */
+  public ShardingFilters(ShardingEnvironment shardingEnvironment) {
+    this(shardingEnvironment, DEFAULT_SHARDING_STRATEGY);
+  }
+
+  /**
+   * Creates a factory with the given sharding environment and sharding
+   * strategy.
+   */
+  @Inject
+  public ShardingFilters(ShardingEnvironment shardingEnvironment,
+      ShardingFilterFactory defaultShardingStrategy) {
+    this.shardingEnvironment = shardingEnvironment;
+    this.defaultShardingStrategy = defaultShardingStrategy;
+  }
+
+  /**
+   * Creates a sharding filter according to strategy specified by the
+   * sharding environment.
+   */
+  public Filter createShardingFilter(Collection<Description> descriptions) {
+    ShardingFilterFactory factory = getShardingFilterFactory();
+    return factory.createFilter(descriptions, shardingEnvironment.getShardIndex(),
+        shardingEnvironment.getTotalShards());
+  }
+
+  private ShardingFilterFactory getShardingFilterFactory() {
+    String strategy = shardingEnvironment.getTestShardingStrategy();
+    if (strategy == null) {
+      return defaultShardingStrategy;
+    }
+    ShardingFilterFactory shardingFilterFactory;
+    try {
+      shardingFilterFactory = ShardingStrategy.valueOf(strategy.toUpperCase());
+    } catch (IllegalArgumentException e) {
+      try {
+        Class<?> strategyClass = Thread.currentThread().getContextClassLoader().loadClass(strategy);
+        shardingFilterFactory = (ShardingFilterFactory) strategyClass.newInstance();
+      } catch (ClassNotFoundException | InstantiationException |
+          IllegalAccessException | IllegalArgumentException e2) {
+        throw new RuntimeException(
+            "Could not create custom sharding strategy class " + strategy, e2);  
+      }
+    }
+    return shardingFilterFactory; 
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/BUILD b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/BUILD
new file mode 100644
index 0000000..6c1ed76
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/BUILD
@@ -0,0 +1,22 @@
+DEFAULT_VISIBILITY = [
+    "//java/com/google/testing/junit/runner:__subpackages__",
+    "//javatests/com/google/testing/junit/runner:__subpackages__",
+    "//third_party/bazel/src/java_tools/junitrunner/java/com/google/testing/junit/runner:__subpackages__",
+    "//third_party/bazel/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner:__subpackages__",
+]
+
+package(default_visibility = ["//src:__subpackages__"])
+
+# TODO(bazel-team): This should be testonly = 1.
+java_library(
+    name = "api",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//third_party:junit4",
+    ],
+)
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+)
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/ShardingFilterFactory.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/ShardingFilterFactory.java
new file mode 100644
index 0000000..71cb9ea
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/ShardingFilterFactory.java
@@ -0,0 +1,36 @@
+// Copyright 2015 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.sharding.api;
+
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+
+import java.util.Collection;
+
+/**
+ * Creates custom test sharding filters. Classes that implement this interface must have a public
+ * no-argument constructor.
+ */
+public interface ShardingFilterFactory {
+
+  /**
+   * Creates a test sharding filter.
+   *  
+   * @param testDescriptions collection of descriptions of the tests to be run 
+   * @param shardIndex 0-indexed test shard number, where 0 <= shard index < totalShards
+   * @param totalShards the total number of test shards
+   */
+  Filter createFilter(Collection<Description> testDescriptions, int shardIndex, int totalShards);
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/WeightStrategy.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/WeightStrategy.java
new file mode 100644
index 0000000..3ee6458
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/WeightStrategy.java
@@ -0,0 +1,30 @@
+// Copyright 2015 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.sharding.api;
+
+import org.junit.runner.Description;
+
+/**
+ * Extracts the weight associated with a test for use by sharding filters.
+ */
+public interface WeightStrategy {
+  
+  /**
+   * Returns the weight of a test extracted from its description.
+   * 
+   * @param description the description that contains the associated weight for a test
+   */
+  int getDescriptionWeight(Description description);
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/BUILD b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/BUILD
new file mode 100644
index 0000000..de546e1
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/BUILD
@@ -0,0 +1,26 @@
+DEFAULT_VISIBILITY = [
+    "//java/com/google/testing/junit/runner:__subpackages__",
+    "//javatests/com/google/testing/junit/runner:__subpackages__",
+    "//third_party/bazel/src/java_tools/junitrunner/java/com/google/testing/junit/runner:__subpackages__",
+    "//third_party/bazel/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner:__subpackages__",
+]
+
+package(default_visibility = ["//src:__subpackages__"])
+
+java_library(
+    name = "testing",
+    testonly = 1,
+    srcs = glob(["*.java"]),
+    deps = [
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding",
+        "//src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api",
+        "//third_party:guava",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+)
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/RoundRobinShardingFilterFactory.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/RoundRobinShardingFilterFactory.java
new file mode 100644
index 0000000..45af5de
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/RoundRobinShardingFilterFactory.java
@@ -0,0 +1,35 @@
+// Copyright 2015 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.sharding.testing;
+
+import com.google.testing.junit.runner.sharding.RoundRobinShardingFilter;
+import com.google.testing.junit.runner.sharding.api.ShardingFilterFactory;
+
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+
+import java.util.Collection;
+
+/**
+ * Creates a {@link RoundRobinShardingFilter} for use in tests.
+ */
+public class RoundRobinShardingFilterFactory implements ShardingFilterFactory {
+
+    @Override
+    public Filter createFilter(
+        Collection<Description> testDescriptions, int shardIndex, int totalShards) {
+      return new RoundRobinShardingFilter(testDescriptions, shardIndex, totalShards);
+    }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/ShardingFilterTestCase.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/ShardingFilterTestCase.java
new file mode 100644
index 0000000..1cffdb7
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/ShardingFilterTestCase.java
@@ -0,0 +1,227 @@
+// 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.sharding.testing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.testing.junit.runner.sharding.api.ShardingFilterFactory;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Common base class for all sharding filter tests.
+ */
+public abstract class ShardingFilterTestCase extends TestCase {
+
+  static final List<Description> TEST_DESCRIPTIONS = createGenericTestCaseDescriptions(6);
+
+  /**
+   * Returns a filter of the subclass type using the given descriptions,
+   * shard index, and total number of shards.
+   */
+  protected abstract ShardingFilterFactory createShardingFilterFactory();
+  
+  public final void testShardingIsCompleteAndPartitioned_oneShard() {
+    assertShardingIsCompleteAndPartitioned(createFilters(TEST_DESCRIPTIONS, 1), TEST_DESCRIPTIONS);
+  }
+
+  public final void testShardingIsStable_oneShard() {
+    assertShardingIsStable(createFilters(TEST_DESCRIPTIONS, 1), TEST_DESCRIPTIONS);
+  }
+
+  public final void testShardingIsCompleteAndPartitioned_moreTestsThanShards() {
+    assertShardingIsCompleteAndPartitioned(createFilters(TEST_DESCRIPTIONS, 5), TEST_DESCRIPTIONS);
+  }
+
+  public final void testShardingIsStable_moreTestsThanShards() {
+    assertShardingIsStable(createFilters(TEST_DESCRIPTIONS, 5), TEST_DESCRIPTIONS);
+  }
+
+  public final void testShardingIsCompleteAndPartitioned_sameNumberOfTestsAndShards() {
+    assertShardingIsCompleteAndPartitioned(createFilters(TEST_DESCRIPTIONS, 6), TEST_DESCRIPTIONS);
+  }
+
+  public final void testShardingIsStable_sameNumberOfTestsAndShards() {
+    assertShardingIsStable(createFilters(TEST_DESCRIPTIONS, 6), TEST_DESCRIPTIONS);
+  }
+
+  public final void testShardingIsCompleteAndPartitioned_moreShardsThanTests() {
+    assertShardingIsCompleteAndPartitioned(createFilters(TEST_DESCRIPTIONS, 7), TEST_DESCRIPTIONS);
+  }
+
+  public final void testShardingIsStable_moreShardsThanTests() {
+    assertShardingIsStable(createFilters(TEST_DESCRIPTIONS, 7), TEST_DESCRIPTIONS);
+  }
+
+  public final void testShardingIsCompleteAndPartitioned_duplicateDescriptions() {
+    ImmutableList<Description> descriptions = ImmutableList.<Description>builder()
+        .addAll(createGenericTestCaseDescriptions(6))
+        .addAll(createGenericTestCaseDescriptions(6))
+        .build();
+    assertShardingIsCompleteAndPartitioned(createFilters(descriptions, 7), descriptions);
+  }
+
+  public final void testShardingIsStable_duplicateDescriptions() {
+    ImmutableList<Description> descriptions = ImmutableList.<Description>builder()
+        .addAll(createGenericTestCaseDescriptions(6))
+        .addAll(createGenericTestCaseDescriptions(6))
+        .build();
+    assertShardingIsStable(createFilters(descriptions, 7), descriptions);
+  }
+  
+  public final void testShouldRunTestSuite() {    
+    Description testSuiteDescription = createTestSuiteDescription();   
+    Filter filter = createShardingFilterFactory().createFilter(TEST_DESCRIPTIONS, 0, 1); 
+    assertTrue(filter.shouldRun(testSuiteDescription));    
+  }
+
+  /**
+   * Creates a list of generic test case descriptions.
+   *
+   * @param numDescriptions the number of generic test descriptions to add to the list.
+   */
+  public static List<Description> createGenericTestCaseDescriptions(int numDescriptions) {
+    ImmutableList.Builder<Description> builder = ImmutableList.builder();
+    for (int i = 0; i < numDescriptions; i++) {
+      builder.add(Description.createTestDescription(Test.class, "test" + i));
+    }
+    return builder.build();
+  }
+  
+  protected static final List<Filter> createFilters(List<Description> descriptions, int numShards,
+      ShardingFilterFactory factory) {
+    ImmutableList.Builder<Filter> builder = ImmutableList.builder();
+    for (int shardIndex = 0; shardIndex < numShards; shardIndex++) {
+      builder.add(factory.createFilter(descriptions, shardIndex, numShards));
+    }
+    return builder.build();
+  }
+  
+  protected final List<Filter> createFilters(List<Description> descriptions, int numShards) {
+    return createFilters(descriptions, numShards, createShardingFilterFactory());
+  }
+
+  protected static void assertThrowsExceptionForUnknownDescription(Filter filter) {
+    try {
+      filter.shouldRun(Description.createTestDescription(Object.class, "unknown"));
+      fail("expected thrown exception");
+    } catch (IllegalArgumentException expected) { }
+  }
+
+  /**
+   * Simulates test sharding with the given filters and test descriptions.
+   *
+   * @param filters a list of filters, one per test shard
+   * @param descriptions a list of test descriptions
+   * @return a mapping from each filter to the descriptions of the tests that would be run
+   *   by the shard associated with that filter.
+   */
+  protected static ListMultimap<Filter, Description> simulateTestRun(List<Filter> filters,
+      List<Description> descriptions) {
+    ListMultimap<Filter, Description> descriptionsRun = ArrayListMultimap.create();
+    for (Filter filter : filters) {
+      for (Description description : descriptions) {
+        if (filter.shouldRun(description)) {
+          descriptionsRun.put(filter, description);
+        }
+      }
+    }
+    return descriptionsRun;
+  }
+
+  /**
+   * Simulates test sharding with the given filters and test descriptions, for a
+   * set of test descriptions that is in a different order in every test shard.
+   *
+   * @param filters a list of filters, one per test shard
+   * @param descriptions a list of test descriptions
+   * @return a mapping from each filter to the descriptions of the tests that would be run
+   *   by the shard associated with that filter.
+   */
+  protected static ListMultimap<Filter, Description> simulateSelfRandomizingTestRun(
+      List<Filter> filters, List<Description> descriptions) {
+    if (descriptions.isEmpty()) {
+      return ArrayListMultimap.create();
+    }
+    Deque<Description> mutatingDescriptions = Lists.newLinkedList(descriptions);
+    ListMultimap<Filter, Description> descriptionsRun = ArrayListMultimap.create();
+
+    for (Filter filter : filters) {
+      // rotate the queue so that each filter gets the descriptions in a different order
+      mutatingDescriptions.addLast(mutatingDescriptions.pollFirst());
+      for (Description description : descriptions) {
+        if (filter.shouldRun(description)) {
+          descriptionsRun.put(filter, description);
+        }
+      }
+    }
+    return descriptionsRun;
+  }
+
+  /**
+   * Creates a test suite description (a Description that returns true
+   * when {@link org.junit.runner.Description#isSuite()} is called.)
+   */
+  protected static Description createTestSuiteDescription() {
+    Description testSuiteDescription = Description.createSuiteDescription("testSuite");
+    testSuiteDescription.addChild(Description.createSuiteDescription("testCase"));
+    return testSuiteDescription;
+  }
+
+  /**
+   * Tests that the sharding is complete (each test is run at least once) and
+   * partitioned (each test is run at most once) -- in other words, that
+   * each test is run exactly once.  This is a requirement of all test
+   * sharding functions.
+   */
+  protected static void assertShardingIsCompleteAndPartitioned(List<Filter> filters,
+      List<Description> descriptions) {
+    ListMultimap<Filter, Description> run = simulateTestRun(filters, descriptions);
+    assertThat(run.values()).containsExactlyElementsIn(descriptions);
+
+    simulateSelfRandomizingTestRun(filters, descriptions);
+    assertThat(run.values()).containsExactlyElementsIn(descriptions);
+  }
+
+  /**
+   * Tests that sharding is stable for the given filters, regardless of the
+   * ordering of the descriptions.  This is useful for verifying that sharding
+   * works with self-randomizing test suites, and a requirement of all test
+   * sharding functions.
+   */
+  protected static void assertShardingIsStable(
+      List<Filter> filters, List<Description> descriptions) {
+    ListMultimap<Filter, Description> run1 = simulateTestRun(filters, descriptions);
+    ListMultimap<Filter, Description> run2 = simulateTestRun(filters, descriptions);
+    assertEquals(run1, run2);
+
+    ListMultimap<Filter, Description> randomizedRun1 =
+        simulateSelfRandomizingTestRun(filters, descriptions);
+    ListMultimap<Filter, Description> randomizedRun2 =
+        simulateSelfRandomizingTestRun(filters, descriptions);
+    assertEquals(randomizedRun1, randomizedRun2);
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/BUILD b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/BUILD
new file mode 100644
index 0000000..c176a80
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/BUILD
@@ -0,0 +1,23 @@
+DEFAULT_VISIBILITY = [
+    "//java/com/google/testing/junit/runner:__subpackages__",
+    "//javatests/com/google/testing/junit/runner:__subpackages__",
+    "//third_party/bazel/src/java_tools/junitrunner/java/com/google/testing/junit/runner:__subpackages__",
+    "//third_party/bazel/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner:__subpackages__",
+]
+
+package(default_visibility = ["//src:__subpackages__"])
+
+java_library(
+    name = "util",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//third_party:guava",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+    ],
+)
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+)
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/CurrentRunningTest.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/CurrentRunningTest.java
new file mode 100644
index 0000000..bc02f7d
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/CurrentRunningTest.java
@@ -0,0 +1,38 @@
+// Copyright 2009 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.util;
+
+import org.junit.runner.Description;
+
+/**
+ * Utility class for recording and retrieving information about what test
+ * is running in the current thread. This class is currently compatible
+ * with JUnit 3 and JUnit4.
+ */
+public class CurrentRunningTest {
+  protected static TestNameProvider testNameProvider;
+
+  /**
+   * If called during a JUnit test run with our test runner, returns the test running in the current
+   * thread. Otherwise (for example, when the test is run directly in an IDE), returns {@code null}.
+   *
+   * <p>Our test runner is special only in that it installs {@link #testNameProvider} to listen for
+   * test start/stop events using
+   * {@link org.junit.runner.JUnitCore#addListener(org.junit.runner.notification.RunListener)}.
+   */
+  public static Description get() {
+    return testNameProvider != null ? testNameProvider.get() : null;
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/GoogleTestSecurityManager.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/GoogleTestSecurityManager.java
new file mode 100644
index 0000000..623b1e6
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/GoogleTestSecurityManager.java
@@ -0,0 +1,98 @@
+// Copyright 2004 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.util;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.security.Permission;
+
+/**
+ * A security manager that prevents things that are dangerous or
+ * bad in a testing environment. Currently prevents System.exit() and
+ * System.setSecurityManager().
+ *
+ * <p>For simplicity this is a Java 1.1 style security manager, ignoring
+ * the whole Permissions framework. This should be fine unless you
+ * are testing code that itself manipulates SecurityManagers.
+ */
+public final class GoogleTestSecurityManager extends SecurityManager {
+  private volatile boolean enabled = true;
+
+  /** Prevent System.exit() from ever being called. */
+  @Override public void checkExit(int code) {
+    if (enabled) {
+      throw new SecurityException("Test code should never call System.exit()");
+    }
+  }
+
+  //
+  // The code below overrides the Java2 security mechanism to allow
+  // (almost) all requests. This is OK vis-a-vis the Java default
+  // (which is to run with no security policy at all).
+  //
+  // The default Java security policy is to pass through to the
+  // Permissions check mechanism, which in turn by default denies
+  // things. We override all of that (in essence, disabling Java2
+  // Permissions) and just allow everything.
+  //
+
+  /**
+   * Cache a copy of the permission that System.setSecurityManager() checks.
+   */
+  private final RuntimePermission securityManagerPermission =
+      new RuntimePermission("setSecurityManager");
+
+  /** Allow everything but creation of security managers. */
+  @Override public void checkPermission(Permission p) {
+    if (enabled && securityManagerPermission.equals(p)) {
+      throw new SecurityException("GoogleTestSecurityManager is not designed to handle other " +
+          "security managers.");
+    }
+  }
+
+  /** Allow everything. */
+  @Override public void checkPermission(Permission p, java.lang.Object o) {
+    return;
+  }
+
+  public boolean isEnabled() { return enabled; }
+  
+  /**
+   * If {@code GoogleTestSecurityManager} is the current security manager,
+   * uninstall it.
+   */
+  public static void uninstallIfInstalled() {
+    SecurityManager securityManager = System.getSecurityManager();
+    if (securityManager instanceof GoogleTestSecurityManager) {
+      GoogleTestSecurityManager testSecurityManager = (GoogleTestSecurityManager) securityManager;
+      boolean wasEnabled = testSecurityManager.isEnabled();
+      
+      try {
+        testSecurityManager.setEnabled(false);
+        System.setSecurityManager(null);
+      } finally {
+        testSecurityManager.setEnabled(wasEnabled);
+      }
+    }
+  }
+
+  /**
+   * The security manager can be disabled by the test runner (or any other
+   * class in the same package) to allow it to exit with a meaningful result
+   * code.
+   */
+  @VisibleForTesting
+  synchronized void setEnabled(boolean enabled) { this.enabled = enabled; }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestNameProvider.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestNameProvider.java
new file mode 100644
index 0000000..3767e64
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestNameProvider.java
@@ -0,0 +1,28 @@
+// Copyright 2011 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.util;
+
+import org.junit.runner.Description;
+
+/**
+ * Provides the description of the current running test.
+ */
+public interface TestNameProvider {
+
+  /**
+   * Gets the description of the current test.
+   */
+  public Description get();
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestPropertyExporter.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestPropertyExporter.java
new file mode 100644
index 0000000..36fecd4
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestPropertyExporter.java
@@ -0,0 +1,158 @@
+// Copyright 2011 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.util;
+
+import static com.google.testing.junit.runner.util.TestPropertyRunnerIntegration.getCallbackForThread;
+
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+
+import java.util.Map;
+
+/**
+ * Exports test properties to the test XML.
+ */
+public class TestPropertyExporter {
+  /*
+   * The global {@code TestPropertyExporter}, which writes the properties into
+   * the test XML if the test is running from the command line.<p>
+   *
+   * If you have test infrastructure that needs to export properties, consider
+   * injecting an instance of {@code TestPropertyExporter}. Your tests can
+   * use one of the static methods in this class to create a fake instance.
+   */
+  public static final TestPropertyExporter INSTANCE = new TestPropertyExporter(
+      new DefaultCallback());
+
+  // Set to 1000 so that it will play nice with code that doesn't use exportRepeatedProperty
+  // yet.
+  public static final int INITIAL_INDEX_FOR_REPEATED_PROPERTY = 1000;
+
+  private final Callback callback;
+
+  /**
+   * Creates a fake {@code TestPropertyExporter} instance, storing values
+   * in the passed-in map.
+   *
+   * @param backingMap Map to use to store values
+   * @return exporter instance
+   */
+  public static TestPropertyExporter createFake(final Map<String, String> backingMap) {
+    return createFake(new Callback() {
+
+      private final Multiset<String> repeatedPropertyNames = HashMultiset.create();
+
+      @Override public void exportProperty(String name, String value) {
+        backingMap.put(name, value);
+      }
+
+      @Override
+      public String exportRepeatedProperty(String name, String value) {
+        String propertyName = getRepeatedPropertyName(name);
+        backingMap.put(propertyName, value);
+        return propertyName;
+      }
+
+      private String getRepeatedPropertyName(String name) {
+        int index = repeatedPropertyNames.add(name, 1) + INITIAL_INDEX_FOR_REPEATED_PROPERTY;
+        return name + index;
+      }
+    });
+  }
+
+  /**
+   * Creates a fake {@code TestPropertyExporter} instance, passing values
+   * to the passed-in callback.
+   *
+   * @param callback Callback to use when values are exported
+   * @return exporter instance
+   */
+  public static TestPropertyExporter createFake(final Callback callback) {
+    return new TestPropertyExporter(callback);
+  }
+
+  protected TestPropertyExporter(Callback callback) {
+    this.callback = callback;
+  }
+
+  /**
+   * Exports a property to the test runner. This method is a no-op unless called
+   * by the thread running the current test.
+   *
+   * @param name The property name.
+   * @param value The property value.
+   * @throws IllegalArgumentException if the name is not a valid name
+   */
+  public void exportProperty(String name, String value) {
+    callback.exportProperty(name, value);
+  }
+
+  /**
+   * Exports a property to the test runner by adding the value to the list of values for the
+   * given property name.
+   * When the properties get written to the XML, each name will have a numeric value appended to it
+   * that is guaranteed to be unique for the given test case.
+   * This method is a no-op unless called by the thread running the current test.
+   *
+   * @param name The property name.
+   * @param value The property value.
+   * @return the name of the property that was exported
+   * @throws IllegalArgumentException if the name is not a valid name
+   */
+  public String exportRepeatedProperty(String name, String value) {
+    return callback.exportRepeatedProperty(name, value);
+  }
+
+  /**
+   * Callback that is used to store test properties.
+   */
+  public interface Callback {
+
+    /**
+     * Export the property.
+     *
+     * @param name The property name.
+     * @param value The property value.
+     */
+    void exportProperty(String name, String value);
+
+    /**
+     * Export the property with an incrementing numeric suffix.
+     *
+     * @param name The property name.
+     * @param value The property value.
+     * @return the name of the property that was exported
+     */
+    String exportRepeatedProperty(String name, String value);
+  }
+
+
+  /**
+   * Default callback implementation.
+   * Calls the test runner to write the property to the XML.
+   */
+  private static class DefaultCallback implements Callback {
+
+    @Override
+    public void exportProperty(String name, String value) {
+      getCallbackForThread().exportProperty(name, value);
+    }
+
+    @Override
+    public String exportRepeatedProperty(String name, String value) {
+      return getCallbackForThread().exportRepeatedProperty(name, value);
+    }
+  }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestPropertyRunnerIntegration.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestPropertyRunnerIntegration.java
new file mode 100644
index 0000000..b640def
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestPropertyRunnerIntegration.java
@@ -0,0 +1,66 @@
+// Copyright 2011 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.util;
+
+import com.google.testing.junit.runner.util.TestPropertyExporter.Callback;
+
+import javax.annotation.Nullable;
+
+/**
+ * JUnit runner integration code for test properties. Most code should not
+ * use this, and should instead use {@link TestPropertyExporter}.
+ */
+public class TestPropertyRunnerIntegration {
+  private static ThreadLocal<Callback> callbackForThread
+      = new ThreadLocal<Callback>() {
+        @Override
+        protected TestPropertyExporter.Callback initialValue() {
+          return NoOpCallback.INSTANCE;
+        }
+  };
+
+  /**
+   * Sets the per-thread callback.
+   *
+   * @param callback Callback
+   */
+  public static TestPropertyExporter.Callback setTestCaseForThread(@Nullable Callback callback) {
+    Callback previousCallback = callbackForThread.get();
+    if (callback == null) {
+      callbackForThread.remove();
+    } else {
+      callbackForThread.set(callback);
+    }
+    return previousCallback;
+  }
+
+  static Callback getCallbackForThread() {
+    return callbackForThread.get();
+  }
+
+  private static class NoOpCallback implements Callback {
+    private static final Callback INSTANCE = new NoOpCallback();
+
+    @Override
+    public void exportProperty(String name, String value) {
+    }
+
+    @Override
+    public String exportRepeatedProperty(String name, String value) {
+      return name;
+    }
+  }
+
+}