| // 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 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.TestClock; |
| import com.google.testing.junit.runner.util.TestClock.TestInstant; |
| import com.google.testing.junit.runner.util.TestIntegrationsRunnerIntegration; |
| import com.google.testing.junit.runner.util.TestPropertyRunnerIntegration; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.StringWriter; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import javax.annotation.Nullable; |
| import javax.inject.Inject; |
| import org.junit.runner.Description; |
| import org.junit.runner.manipulation.Filter; |
| |
| /** |
| * Model of the tests that will be run. The model is agnostic of the particular type of test run |
| * (JUnit3 or JUnit4). The test runner uses this class to build the model, and then updates the |
| * model during the test run. |
| * |
| * <p>The leaf nodes in the model are test cases; the other nodes are test suites. |
| */ |
| public class TestSuiteModel { |
| private final TestSuiteNode rootNode; |
| private final Map<Description, TestCaseNode> testCaseMap; |
| private final Map<Description, TestNode> testsMap; |
| private final TestClock testClock; |
| private final AtomicBoolean wroteXml = new AtomicBoolean(false); |
| private final XmlResultWriter xmlResultWriter; |
| @Nullable private final Filter shardingFilter; |
| |
| private TestSuiteModel(Builder builder) { |
| rootNode = builder.rootNode; |
| testsMap = builder.testsMap; |
| testCaseMap = filterTestCases(builder.testsMap); |
| testClock = builder.testClock; |
| shardingFilter = builder.shardingFilter; |
| xmlResultWriter = builder.xmlResultWriter; |
| } |
| |
| // VisibleForTesting |
| public List<TestNode> getTopLevelTestSuites() { |
| return rootNode.getChildren(); |
| } |
| |
| // VisibleForTesting |
| public Description getTopLevelDescription() { |
| return rootNode.getDescription(); |
| } |
| |
| /** Gets the sharding filter to use; {@link Filter#ALL} if not sharding. */ |
| public Filter getShardingFilter() { |
| return shardingFilter; |
| } |
| |
| /** |
| * Returns the test case node with the given test description. |
| * |
| * <p>Note that in theory this should never return {@code null}, but if it did we would not want |
| * to throw a {@code NullPointerException} because JUnit4 would catch the exception and remove our |
| * test listener! |
| */ |
| private TestCaseNode getTestCase(Description description) { |
| // The description shouldn't be null, but in the test runner code we avoid throwing exceptions. |
| return description == null ? null : testCaseMap.get(description); |
| } |
| |
| private TestNode getTest(Description description) { |
| // The description shouldn't be null, but in the test runner code we avoid throwing exceptions. |
| return description == null ? null : testsMap.get(description); |
| } |
| |
| // VisibleForTesting |
| public int getNumTestCases() { |
| return testCaseMap.size(); |
| } |
| |
| /** |
| * Indicate that the test run has started. This should be called after all filtering has been |
| * completed. |
| * |
| * @param topLevelDescription the root {@link Description} node. |
| */ |
| public void testRunStarted(Description topLevelDescription) { |
| markChildrenAsPending(topLevelDescription); |
| } |
| |
| private void markChildrenAsPending(Description node) { |
| if (node.isTest()) { |
| testPending(node); |
| } else { |
| for (Description child : node.getChildren()) { |
| markChildrenAsPending(child); |
| } |
| } |
| } |
| |
| /** |
| * Indicate that the test case with the given key is scheduled to start. |
| * |
| * @param description key for a test case |
| */ |
| private void testPending(Description description) { |
| TestCaseNode testCase = getTestCase(description); |
| if (testCase != null) { |
| testCase.pending(); |
| } |
| } |
| |
| /** |
| * 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(now()); |
| TestPropertyRunnerIntegration.setTestCaseForThread(testCase); |
| TestIntegrationsRunnerIntegration.setTestCaseForThread(testCase); |
| } |
| } |
| |
| /** Indicate that the entire test run was interrupted. */ |
| public void testRunInterrupted() { |
| rootNode.testInterrupted(now()); |
| } |
| |
| /** |
| * Indicate that the test case with the given key has requested that a property be written in the |
| * XML. |
| * |
| * <p> |
| * |
| * @param description key for a test case |
| * @param name The property name. |
| * @param value The property value. |
| */ |
| public void testEmittedProperty(Description description, String name, String value) { |
| TestCaseNode testCase = getTestCase(description); |
| if (testCase != null) { |
| testCase.exportProperty(name, value); |
| } |
| } |
| |
| /** |
| * Adds a failure to the test with the given key. If the specified test is suite, the failure will |
| * be added to all its children. |
| * |
| * @param description key for a test case |
| */ |
| public void testFailure(Description description, Throwable throwable) { |
| TestNode test = getTest(description); |
| if (test != null) { |
| if (throwable instanceof DynamicTestException) { |
| DynamicTestException dynamicFailure = (DynamicTestException) throwable; |
| test.dynamicTestFailure(dynamicFailure.getTest(), dynamicFailure.getCause(), now()); |
| } else { |
| test.testFailure(throwable, now()); |
| } |
| } |
| } |
| |
| /** |
| * 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(now()); |
| } |
| } |
| |
| /** |
| * 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(now()); |
| } |
| } |
| |
| /** 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(now()); |
| } |
| |
| /* |
| * 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 TestInstant now() { |
| return testClock.now(); |
| } |
| |
| /** |
| * 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 |
| public 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 TestClock testClock; |
| private final Map<Description, TestNode> testsMap = new ConcurrentHashMap<>(); |
| 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( |
| TestClock testClock, |
| ShardingFilters shardingFilters, |
| ShardingEnvironment shardingEnvironment, |
| XmlResultWriter xmlResultWriter) { |
| this.testClock = testClock; |
| this.shardingFilters = shardingFilters; |
| this.shardingEnvironment = shardingEnvironment; |
| this.xmlResultWriter = xmlResultWriter; |
| } |
| |
| /** |
| * Build a model with the given name, including the given suites. This method should be called |
| * before any command line filters are applied. |
| */ |
| public TestSuiteModel build(String suiteName, Description... topLevelSuites) { |
| return build(suiteName, Collections.emptyMap(), topLevelSuites); |
| } |
| |
| /** |
| * Build a model with the given name, including the given suites. This method should be called |
| * before any command line filters are applied. |
| * |
| * <p>The given {@code properties} map will be applied to the root {@link TestSuiteNode}. |
| */ |
| public TestSuiteModel build( |
| String suiteName, Map<String, String> properties, Description... topLevelSuites) { |
| if (buildWasCalled) { |
| throw new IllegalStateException("Builder.build() was already called"); |
| } |
| buildWasCalled = true; |
| if (shardingEnvironment.isShardingEnabled()) { |
| shardingFilter = getShardingFilter(topLevelSuites); |
| } |
| rootNode = new TestSuiteNode(Description.createSuiteDescription(suiteName), properties); |
| for (Description topLevelSuite : topLevelSuites) { |
| addTestSuite(rootNode, topLevelSuite); |
| rootNode.getDescription().addChild(topLevelSuite); |
| } |
| return new TestSuiteModel(this); |
| } |
| |
| private Filter getShardingFilter(Description... topLevelSuites) { |
| Collection<Description> tests = new LinkedList<>(); |
| for (Description suite : topLevelSuites) { |
| collectTests(suite, tests); |
| } |
| shardingEnvironment.touchShardFile(); |
| return shardingFilters.createShardingFilter(tests); |
| } |
| |
| private static void collectTests(Description desc, Collection<Description> tests) { |
| if (desc.isTest()) { |
| tests.add(desc); |
| } else { |
| for (Description child : desc.getChildren()) { |
| collectTests(child, tests); |
| } |
| } |
| } |
| |
| private void addTestSuite(TestSuiteNode parentSuite, Description suiteDescription) { |
| TestSuiteNode suite = new TestSuiteNode(suiteDescription); |
| for (Description childDesc : suiteDescription.getChildren()) { |
| if (childDesc.isTest()) { |
| addTestCase(suite, childDesc); |
| } else { |
| addTestSuite(suite, childDesc); |
| } |
| } |
| // Empty suites are pruned when sharding. |
| if (shardingFilter == Filter.ALL || !suite.getChildren().isEmpty()) { |
| parentSuite.addTestSuite(suite); |
| testsMap.put(suiteDescription, suite); |
| } |
| } |
| |
| private void addTestCase(TestSuiteNode parentSuite, Description testCaseDesc) { |
| if (!testCaseDesc.isTest()) { |
| throw new IllegalArgumentException(); |
| } |
| if (!shardingFilter.shouldRun(testCaseDesc)) { |
| return; |
| } |
| TestCaseNode testCase = new TestCaseNode(testCaseDesc, parentSuite); |
| testsMap.put(testCaseDesc, testCase); |
| parentSuite.addTestCase(testCase); |
| } |
| } |
| |
| /** |
| * Converts the values of the Map from {@link TestNode} to {@link TestCaseNode} filtering out null |
| * values. |
| */ |
| private static Map<Description, TestCaseNode> filterTestCases(Map<Description, TestNode> tests) { |
| Map<Description, TestCaseNode> filteredAndConvertedTests = new HashMap<>(); |
| for (Description key : tests.keySet()) { |
| TestNode testNode = tests.get(key); |
| if (testNode instanceof TestCaseNode) { |
| filteredAndConvertedTests.put(key, (TestCaseNode) testNode); |
| } |
| } |
| return filteredAndConvertedTests; |
| } |
| } |