aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar Damien Martin-Guillerez <dmarting@google.com>2016-01-13 09:08:48 +0000
committerGravatar Damien Martin-Guillerez <dmarting@google.com>2016-01-13 13:16:32 +0000
commiteea8efaf39da92e4811ed7b551b2a978e34ff92e (patch)
tree14cc3b3baf5730a3b804b626be9f6102d31c2d21 /src
parent2665d68341e1e82daec9143ae2822a6e00980890 (diff)
Open-source the JUnit test runner.
-- MOS_MIGRATED_REVID=112027454
Diffstat (limited to 'src')
-rw-r--r--src/BUILD1
-rwxr-xr-xsrc/create_embedded_tools.sh1
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/junit4/BUILD27
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/AndFilter.java45
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/DynamicTestException.java46
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/Filters.java62
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/MemoizingRequest.java52
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/RunNotifierWrapper.java101
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/SuiteTrimmingFilter.java56
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/BUILD38
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/BazelTestRunner.java209
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/BUILD18
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/SignalHandlers.java81
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/StackTraces.java152
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/Stderr.java31
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/Stdout.java31
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/BUILD44
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/CancellableRequestFactory.java126
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Config.java126
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Options.java120
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Runner.java264
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerBaseModule.java105
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerModule.java118
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestModelBuilder.java56
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestNameListener.java56
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestStackTraceListener.java58
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestXmlListener.java123
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/RegExTestCaseFilter.java78
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/SettableCurrentRunningTest.java25
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/TopLevelSuite.java31
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/Xml.java32
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/AntXmlResultWriter.java176
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/BUILD21
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestCaseNode.java245
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestNode.java95
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestResult.java219
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java345
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteNode.java129
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlResultWriter.java29
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlWriter.java210
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/BUILD26
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/HashBackedShardingFilter.java57
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/RoundRobinShardingFilter.java114
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/ShardingEnvironment.java93
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/ShardingFilters.java112
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/BUILD22
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/ShardingFilterFactory.java36
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/WeightStrategy.java30
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/BUILD26
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/RoundRobinShardingFilterFactory.java35
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/ShardingFilterTestCase.java227
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/BUILD23
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/CurrentRunningTest.java38
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/GoogleTestSecurityManager.java98
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestNameProvider.java28
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestPropertyExporter.java158
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestPropertyRunnerIntegration.java66
57 files changed, 4971 insertions, 0 deletions
diff --git a/src/BUILD b/src/BUILD
index a1dac3d059..48afe1a0f1 100644
--- a/src/BUILD
+++ b/src/BUILD
@@ -102,6 +102,7 @@ genrule(
"//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 e0faa3a1e1..dfd34193e1 100755
--- a/src/create_embedded_tools.sh
+++ b/src/create_embedded_tools.sh
@@ -31,6 +31,7 @@ for i in $*; do
*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 0000000000..9430e5487b
--- /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 0000000000..5a1edda879
--- /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 0000000000..37a2454284
--- /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 0000000000..27081da669
--- /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 0000000000..d4bb807333
--- /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 0000000000..8d2b203d5f
--- /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 0000000000..f15e631d5d
--- /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 0000000000..78ed2d18c6
--- /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 0000000000..efde522c57
--- /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 0000000000..696fd36cea
--- /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 0000000000..c1573d5a78
--- /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 0000000000..37695061eb
--- /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 0000000000..80b37eb0d0
--- /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 0000000000..051968bc98
--- /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 0000000000..a9a1f8632d
--- /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 0000000000..e8991662e0
--- /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 0000000000..a25d3a3746
--- /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 0000000000..d421774a3c
--- /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 0000000000..4134131216
--- /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 0000000000..28e6466067
--- /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 0000000000..5f15524808
--- /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 0000000000..62df88db45
--- /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 0000000000..b7da73fa53
--- /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 0000000000..abb0614afd
--- /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 0000000000..5e572e1e10
--- /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 0000000000..303f0b7d2d
--- /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 0000000000..785d414148
--- /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 0000000000..7f9d720daa
--- /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 0000000000..d531b2b288
--- /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 0000000000..1ccabb2da4
--- /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 0000000000..59840c2ff4
--- /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 0000000000..98a6cb1761
--- /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 0000000000..ccda6f2ffd
--- /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 0000000000..b9900a8de5
--- /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 0000000000..a8b123178b
--- /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 0000000000..f6656e4fb6
--- /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 0000000000..cb2264cf3b
--- /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 0000000000..97636edb05
--- /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 0000000000..43a7d05201
--- /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 0000000000..9225ca2d86
--- /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 0000000000..38d9f110b7
--- /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 0000000000..6d3836fadd
--- /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 0000000000..206a8e9a1d
--- /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 0000000000..6c1ed767e0
--- /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 0000000000..71cb9ea5f3
--- /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 0000000000..3ee6458b61
--- /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 0000000000..de546e1b73
--- /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 0000000000..45af5deb1e
--- /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 0000000000..1cffdb7676
--- /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 0000000000..c176a8032e
--- /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 0000000000..bc02f7d45e
--- /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 0000000000..623b1e6de7
--- /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 0000000000..3767e6466a
--- /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 0000000000..36fecd4bb5
--- /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 0000000000..b640def516
--- /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;
+ }
+ }
+
+}