From eea8efaf39da92e4811ed7b551b2a978e34ff92e Mon Sep 17 00:00:00 2001 From: Damien Martin-Guillerez Date: Wed, 13 Jan 2016 09:08:48 +0000 Subject: Open-source the JUnit test runner. -- MOS_MIGRATED_REVID=112027454 --- .../java/com/google/testing/junit/junit4/BUILD | 27 ++ .../testing/junit/junit4/runner/AndFilter.java | 45 +++ .../junit/junit4/runner/DynamicTestException.java | 46 +++ .../testing/junit/junit4/runner/Filters.java | 62 ++++ .../junit/junit4/runner/MemoizingRequest.java | 52 ++++ .../junit/junit4/runner/RunNotifierWrapper.java | 101 ++++++ .../junit/junit4/runner/SuiteTrimmingFilter.java | 56 ++++ .../java/com/google/testing/junit/runner/BUILD | 38 +++ .../testing/junit/runner/BazelTestRunner.java | 209 +++++++++++++ .../com/google/testing/junit/runner/internal/BUILD | 18 ++ .../junit/runner/internal/SignalHandlers.java | 81 +++++ .../testing/junit/runner/internal/StackTraces.java | 152 +++++++++ .../testing/junit/runner/internal/Stderr.java | 31 ++ .../testing/junit/runner/internal/Stdout.java | 31 ++ .../com/google/testing/junit/runner/junit4/BUILD | 44 +++ .../runner/junit4/CancellableRequestFactory.java | 126 ++++++++ .../testing/junit/runner/junit4/JUnit4Config.java | 126 ++++++++ .../testing/junit/runner/junit4/JUnit4Options.java | 120 +++++++ .../testing/junit/runner/junit4/JUnit4Runner.java | 264 ++++++++++++++++ .../runner/junit4/JUnit4RunnerBaseModule.java | 105 +++++++ .../junit/runner/junit4/JUnit4RunnerModule.java | 118 +++++++ .../runner/junit4/JUnit4TestModelBuilder.java | 56 ++++ .../runner/junit4/JUnit4TestNameListener.java | 56 ++++ .../junit4/JUnit4TestStackTraceListener.java | 58 ++++ .../junit/runner/junit4/JUnit4TestXmlListener.java | 123 ++++++++ .../junit/runner/junit4/RegExTestCaseFilter.java | 78 +++++ .../runner/junit4/SettableCurrentRunningTest.java | 25 ++ .../testing/junit/runner/junit4/TopLevelSuite.java | 31 ++ .../google/testing/junit/runner/junit4/Xml.java | 32 ++ .../junit/runner/model/AntXmlResultWriter.java | 176 +++++++++++ .../com/google/testing/junit/runner/model/BUILD | 21 ++ .../testing/junit/runner/model/TestCaseNode.java | 245 +++++++++++++++ .../testing/junit/runner/model/TestNode.java | 95 ++++++ .../testing/junit/runner/model/TestResult.java | 219 +++++++++++++ .../testing/junit/runner/model/TestSuiteModel.java | 345 +++++++++++++++++++++ .../testing/junit/runner/model/TestSuiteNode.java | 129 ++++++++ .../junit/runner/model/XmlResultWriter.java | 29 ++ .../testing/junit/runner/model/XmlWriter.java | 210 +++++++++++++ .../com/google/testing/junit/runner/sharding/BUILD | 26 ++ .../runner/sharding/HashBackedShardingFilter.java | 57 ++++ .../runner/sharding/RoundRobinShardingFilter.java | 114 +++++++ .../junit/runner/sharding/ShardingEnvironment.java | 93 ++++++ .../junit/runner/sharding/ShardingFilters.java | 112 +++++++ .../google/testing/junit/runner/sharding/api/BUILD | 22 ++ .../runner/sharding/api/ShardingFilterFactory.java | 36 +++ .../junit/runner/sharding/api/WeightStrategy.java | 30 ++ .../testing/junit/runner/sharding/testing/BUILD | 26 ++ .../testing/RoundRobinShardingFilterFactory.java | 35 +++ .../sharding/testing/ShardingFilterTestCase.java | 227 ++++++++++++++ .../com/google/testing/junit/runner/util/BUILD | 23 ++ .../junit/runner/util/CurrentRunningTest.java | 38 +++ .../runner/util/GoogleTestSecurityManager.java | 98 ++++++ .../junit/runner/util/TestNameProvider.java | 28 ++ .../junit/runner/util/TestPropertyExporter.java | 158 ++++++++++ .../runner/util/TestPropertyRunnerIntegration.java | 66 ++++ 55 files changed, 4969 insertions(+) create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/junit4/BUILD create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/AndFilter.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/DynamicTestException.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/Filters.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/MemoizingRequest.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/RunNotifierWrapper.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/junit4/runner/SuiteTrimmingFilter.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/BUILD create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/BazelTestRunner.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/BUILD create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/SignalHandlers.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/StackTraces.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/Stderr.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal/Stdout.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/BUILD create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/CancellableRequestFactory.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Config.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Options.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4Runner.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerBaseModule.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4RunnerModule.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestModelBuilder.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestNameListener.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestStackTraceListener.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/JUnit4TestXmlListener.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/RegExTestCaseFilter.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/SettableCurrentRunningTest.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/TopLevelSuite.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/junit4/Xml.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/AntXmlResultWriter.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/BUILD create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestCaseNode.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestNode.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestResult.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteNode.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlResultWriter.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/XmlWriter.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/BUILD create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/HashBackedShardingFilter.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/RoundRobinShardingFilter.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/ShardingEnvironment.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/ShardingFilters.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/BUILD create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/ShardingFilterFactory.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/api/WeightStrategy.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/BUILD create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/RoundRobinShardingFilterFactory.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/sharding/testing/ShardingFilterTestCase.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/BUILD create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/CurrentRunningTest.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/GoogleTestSecurityManager.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestNameProvider.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestPropertyExporter.java create mode 100644 src/java_tools/junitrunner/java/com/google/testing/junit/runner/util/TestPropertyRunnerIntegration.java (limited to 'src/java_tools/junitrunner/java/com/google/testing') 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.

+ * + * 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. + * + *

Currently sets up a security manager to catch undesirable behaviour; + * System.exit. Also has nice command line options - run with "-help" for + * details. + * + *

This class traps writes to System.err.println() and + * System.out.println() including the output of failed tests in + * the error report. + * + *

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. + * + *

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. + * + *

Return codes: + *

+ */ + 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 args; + + BazelTestRunnerModule(Class suite, List 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. + * + *

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 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 threads = Thread.getAllStackTraces().keySet(); + ImmutableMap threadMap = Maps.uniqueIndex( + threads, new Function() { + @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}

+ * + * 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}

+ * + * 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 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 outputXmlFilePath) { + this( + testIncludeFilterRegexp, + testExcludeFilterRegexp, + outputXmlFilePath, + System.getProperties()); + } + + @VisibleForTesting + JUnit4Config( + String testIncludeFilterRegexp, + String testExcludeFilterRegexp, + Optional 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 getXmlOutputPath() { + if (!xmlOutputPath.isPresent()) { + Optional envXmlOutputPath = + Optional.fromNullable(System.getenv(XML_OUTPUT_FILE_ENV_VAR)); + return envXmlOutputPath.transform(new Function() { + @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. + * + *

+ * 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 envVars, List args) { + ImmutableList.Builder unparsedArgsBuilder = ImmutableList.builder(); + Map optionsMap = Maps.newHashMap(); + + optionsMap.put(TEST_INCLUDE_FILTER_OPTION, null); + optionsMap.put(TEST_EXCLUDE_FILTER_OPTION, null); + + for (Iterator 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 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 null if + * it was not specified. + */ + String getTestIncludeFilter() { + return testIncludeFilter; + } + + /** + * Returns the value of the test_exclude_filter option, or null 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.

+ */ +public class JUnit4Runner { + private final Request request; + private final CancellableRequestFactory requestFactory; + private final Supplier modelSupplier; + private final PrintStream testRunnerOut; + private final JUnit4Config config; + private final Set runListeners; + + private GoogleTestSecurityManager googleTestSecurityManager; + private SecurityManager previousSecurityManager; + + /** + * Creates a runner. + */ + @Inject + private JUnit4Runner(Request request, CancellableRequestFactory requestFactory, + Supplier modelSupplier, @Stdout PrintStream testRunnerOut, + JUnit4Config config, Set 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.

+ * + * 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.

+ * + * 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. + * + *

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 listenerBinder = newSetBinder(binder(), RunListener.class); + listenerBinder.addBinding().to(TextListener.class); + } + + @Provides @Singleton + Supplier 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 unparsedArgs; + + public static JUnit4RunnerModule create(Class suite, List args) { + JUnit4Options options = JUnit4Options.parse(System.getenv(), ImmutableList.copyOf(args)); + JUnit4Config config = new JUnit4Config( + options.getTestIncludeFilter(), + options.getTestExcludeFilter(), + Optional.absent()); + return new JUnit4RunnerModule(suite, config, ImmutableList.copyOf(options.getUnparsedArgs())); + } + + private JUnit4RunnerModule( + Class suite, JUnit4Config config, ImmutableList 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 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 = 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 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 { + 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 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 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 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 + * XML schema 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 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 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 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 runTimeInterval) { + return !runTimeInterval.isPresent() ? "0.0" + : String.valueOf(runTimeInterval.get().toDurationMillis() / 1000.0D); + } + + private static String getFormattedTimestamp(Optional runTimeInterval) { + return !runTimeInterval.isPresent() ? "" : runTimeInterval.get().getStart().toString(); + } + + private void writeTestCase(XmlWriter writer, TestResult result, + Iterable 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 properties = Maps.newConcurrentMap(); + private final Multiset repeatedPropertyNames = ConcurrentHashMultiset.create(); + private final Queue globalFailures = new ConcurrentLinkedQueue<>(); + private final ListMultimap dynamicTestToFailures = + Multimaps.synchronizedListMultimap(LinkedListMultimap.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 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 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 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 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 dynamicFailures = dynamicTestToFailures.get(test); + boolean failed = !globalFailures.isEmpty() || !dynamicFailures.isEmpty(); + return new TestResult.Builder() + .name(test.getDisplayName()) + .className(getDescription().getDisplayName()) + .properties(ImmutableMap.of()) + .failures(dynamicFailures) + .runTimeInterval(runTime) + .status(status) + .numTests(1) + .numFailures(failed ? 1 : 0) + .childResults(ImmutableList.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 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 properties; + private final ImmutableList failures; + private final Optional runTime; + private final Status status; + private final int numTests, numFailures; + private final ImmutableList 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 getProperties() { + return properties; + } + + ImmutableList getFailures() { + return failures; + } + + Optional getRunTimeInterval() { + return runTime; + } + + Status getStatus() { + return status; + } + + boolean wasRun() { + return getStatus().wasRun(); + } + + int getNumTests() { + return numTests; + } + + int getNumFailures() { + return numFailures; + } + + ImmutableList getChildResults() { + return childResults; + } + + static final class Builder { + private String name = null; + private String className = null; + private ImmutableMap properties = null; + private ImmutableList failures = null; + private Optional runTime = null; + private Status status = null; + private Integer numTests = null; + private Integer numFailures = null; + private ImmutableList 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 properties) { + this.properties = ImmutableMap.copyOf( + checkNullToNotNull(this.properties, properties, "properties")); + return this; + } + + Builder failures(List failures) { + this.failures = ImmutableList.copyOf( + checkNullToNotNull(this.failures, failures, "failures")); + return this; + } + + Builder runTimeInterval(Optional 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 childResults) { + this.childResults = ImmutableList.copyOf( + checkNullToNotNull(this.childResults, childResults, "childResults")); + return this; + } + + TestResult build() { + return new TestResult(this); + } + + private static 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. + * + *

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 testCaseMap; + private final ImmutableMap 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 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.

+ * + * 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.

+ * + * @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 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 tests = Lists.newLinkedList(); + for (Description suite : topLevelSuites) { + collectTests(suite, tests); + } + shardingEnvironment.touchShardFile(); + return shardingFilters.createShardingFilter(tests); + } + + private static void collectTests(Description desc, Collection 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 filterTestCases(Map tests) { + return filterValues(transformValues(tests, toTestCaseNode()), notNull()); + } + + private static Function toTestCaseNode() { + return new Function() { + @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 children = Lists.newArrayList(); + + TestSuiteNode(Description description) { + super(description); + } + + @VisibleForTesting + @Override + public List 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 childResults = Lists.newLinkedList(); + + for (TestNode child : children) { + TestResult childResult = child.getResult(); + childResults.add(childResult); + numTests += childResult.getNumTests(); + numFailures += childResult.getNumFailures(); + + Optional 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.of()) + .failures(ImmutableList.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 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(""); + } + + /** + * 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() + "'); + } + } + + /** + * 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. + * + *

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. + * + *

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 testToShardMap; + @VisibleForTesting + final int shardIndex; + @VisibleForTesting + final int totalShards; + + public RoundRobinShardingFilter(Collection 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 buildTestToShardMap( + Collection testDescriptions) { + Map map = Maps.newHashMap(); + + // Sorting this list is incredibly important to correctness. Otherwise, + // "shuffled" suites would break the sharding protocol. + List 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 { + @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 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 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 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 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 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 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 descriptions = ImmutableList.builder() + .addAll(createGenericTestCaseDescriptions(6)) + .addAll(createGenericTestCaseDescriptions(6)) + .build(); + assertShardingIsCompleteAndPartitioned(createFilters(descriptions, 7), descriptions); + } + + public final void testShardingIsStable_duplicateDescriptions() { + ImmutableList descriptions = ImmutableList.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 createGenericTestCaseDescriptions(int numDescriptions) { + ImmutableList.Builder 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 createFilters(List descriptions, int numShards, + ShardingFilterFactory factory) { + ImmutableList.Builder builder = ImmutableList.builder(); + for (int shardIndex = 0; shardIndex < numShards; shardIndex++) { + builder.add(factory.createFilter(descriptions, shardIndex, numShards)); + } + return builder.build(); + } + + protected final List createFilters(List 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 simulateTestRun(List filters, + List descriptions) { + ListMultimap 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 simulateSelfRandomizingTestRun( + List filters, List descriptions) { + if (descriptions.isEmpty()) { + return ArrayListMultimap.create(); + } + Deque mutatingDescriptions = Lists.newLinkedList(descriptions); + ListMultimap 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 filters, + List descriptions) { + ListMultimap 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 filters, List descriptions) { + ListMultimap run1 = simulateTestRun(filters, descriptions); + ListMultimap run2 = simulateTestRun(filters, descriptions); + assertEquals(run1, run2); + + ListMultimap randomizedRun1 = + simulateSelfRandomizingTestRun(filters, descriptions); + ListMultimap 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}. + * + *

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(). + * + *

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.

+ * + * 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 backingMap) { + return createFake(new Callback() { + + private final Multiset 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 callbackForThread + = new ThreadLocal() { + @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; + } + } + +} -- cgit v1.2.3