| // Copyright 2014 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.devtools.build.lib.runtime; | |
| import com.google.common.annotations.VisibleForTesting; | |
| import com.google.common.collect.ArrayListMultimap; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.Multimap; | |
| import com.google.devtools.build.lib.actions.Artifact; | |
| import com.google.devtools.build.lib.analysis.ConfiguredTarget; | |
| import com.google.devtools.build.lib.analysis.FilesToRunProvider; | |
| import com.google.devtools.build.lib.util.Preconditions; | |
| import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter.Mode; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus; | |
| import com.google.devtools.build.lib.view.test.TestStatus.FailedTestCasesStatus; | |
| import com.google.devtools.build.lib.view.test.TestStatus.TestCase; | |
| import java.util.ArrayList; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.TreeMap; | |
| /** | |
| * Test summary entry. Stores summary information for a single test rule. | |
| * Also used to sort summary output by status. | |
| * | |
| * <p>Invariant: | |
| * All TestSummary mutations should be performed through the Builder. | |
| * No direct TestSummary methods (except the constructor) may mutate the object. | |
| */ | |
| @VisibleForTesting // Ideally package-scoped. | |
| public class TestSummary implements Comparable<TestSummary> { | |
| /** | |
| * Builder class responsible for creating and altering TestSummary objects. | |
| */ | |
| public static class Builder { | |
| private TestSummary summary; | |
| private boolean built; | |
| private Builder() { | |
| summary = new TestSummary(); | |
| built = false; | |
| } | |
| private void mergeFrom(TestSummary existingSummary) { | |
| // Yuck, manually fill in fields. | |
| summary.shardRunStatuses = ArrayListMultimap.create(existingSummary.shardRunStatuses); | |
| setTarget(existingSummary.target); | |
| setStatus(existingSummary.status); | |
| addCoverageFiles(existingSummary.coverageFiles); | |
| addPassedLogs(existingSummary.passedLogs); | |
| addFailedLogs(existingSummary.failedLogs); | |
| if (existingSummary.failedTestCasesStatus != null) { | |
| addFailedTestCases(existingSummary.getFailedTestCases(), | |
| existingSummary.getFailedTestCasesStatus()); | |
| } | |
| addTestTimes(existingSummary.testTimes); | |
| addWarnings(existingSummary.warnings); | |
| setActionRan(existingSummary.actionRan); | |
| setNumCached(existingSummary.numCached); | |
| setRanRemotely(existingSummary.ranRemotely); | |
| setWasUnreportedWrongSize(existingSummary.wasUnreportedWrongSize); | |
| } | |
| // Implements copy on write logic, allowing reuse of the same builder. | |
| private void checkMutation() { | |
| // If mutating the builder after an object was built, create another copy. | |
| if (built) { | |
| built = false; | |
| TestSummary lastSummary = summary; | |
| summary = new TestSummary(); | |
| mergeFrom(lastSummary); | |
| } | |
| } | |
| // This used to return a reference to the value on success. | |
| // However, since it can alter the summary member, inlining it in an | |
| // assignment to a property of summary was unsafe. | |
| private void checkMutation(Object value) { | |
| Preconditions.checkNotNull(value); | |
| checkMutation(); | |
| } | |
| public Builder setTarget(ConfiguredTarget target) { | |
| checkMutation(target); | |
| summary.target = target; | |
| return this; | |
| } | |
| public Builder setStatus(BlazeTestStatus status) { | |
| checkMutation(status); | |
| summary.status = status; | |
| return this; | |
| } | |
| public Builder addCoverageFiles(List<Path> coverageFiles) { | |
| checkMutation(coverageFiles); | |
| summary.coverageFiles.addAll(coverageFiles); | |
| return this; | |
| } | |
| public Builder addPassedLogs(List<Path> passedLogs) { | |
| checkMutation(passedLogs); | |
| summary.passedLogs.addAll(passedLogs); | |
| return this; | |
| } | |
| public Builder addFailedLogs(List<Path> failedLogs) { | |
| checkMutation(failedLogs); | |
| summary.failedLogs.addAll(failedLogs); | |
| return this; | |
| } | |
| public Builder collectFailedTests(TestCase testCase) { | |
| if (testCase == null) { | |
| summary.failedTestCasesStatus = FailedTestCasesStatus.NOT_AVAILABLE; | |
| return this; | |
| } | |
| summary.failedTestCasesStatus = FailedTestCasesStatus.FULL; | |
| return collectFailedTestCases(testCase); | |
| } | |
| private Builder collectFailedTestCases(TestCase testCase) { | |
| if (testCase.getChildCount() > 0) { | |
| // This is a non-leaf result. Traverse its children, but do not add its | |
| // name to the output list. It should not contain any 'failure' or | |
| // 'error' tags, but we want to be lax here, because the syntax of the | |
| // test.xml file is also lax. | |
| for (TestCase child : testCase.getChildList()) { | |
| collectFailedTestCases(child); | |
| } | |
| } else { | |
| // This is a leaf result. If it passed, don't add it. | |
| if (testCase.getStatus() == TestCase.Status.PASSED) { | |
| return this; | |
| } | |
| String name = testCase.getName(); | |
| String className = testCase.getClassName(); | |
| if (name == null || className == null) { | |
| // A test case detail is not really interesting if we cannot tell which | |
| // one it is. | |
| this.summary.failedTestCasesStatus = FailedTestCasesStatus.PARTIAL; | |
| return this; | |
| } | |
| this.summary.failedTestCases.add(testCase); | |
| } | |
| return this; | |
| } | |
| public Builder addFailedTestCases(List<TestCase> testCases, FailedTestCasesStatus status) { | |
| checkMutation(status); | |
| checkMutation(testCases); | |
| if (summary.failedTestCasesStatus == null) { | |
| summary.failedTestCasesStatus = status; | |
| } else if (summary.failedTestCasesStatus != status) { | |
| summary.failedTestCasesStatus = FailedTestCasesStatus.PARTIAL; | |
| } | |
| if (testCases.isEmpty()) { | |
| return this; | |
| } | |
| // union of summary.failedTestCases, testCases | |
| Map<String, TestCase> allCases = new TreeMap<>(); | |
| if (summary.failedTestCases != null) { | |
| for (TestCase detail : summary.failedTestCases) { | |
| allCases.put(detail.getClassName() + "." + detail.getName(), detail); | |
| } | |
| } | |
| for (TestCase detail : testCases) { | |
| allCases.put(detail.getClassName() + "." + detail.getName(), detail); | |
| } | |
| summary.failedTestCases = new ArrayList<>(allCases.values()); | |
| return this; | |
| } | |
| public Builder addTestTimes(List<Long> testTimes) { | |
| checkMutation(testTimes); | |
| summary.testTimes.addAll(testTimes); | |
| return this; | |
| } | |
| public Builder addWarnings(List<String> warnings) { | |
| checkMutation(warnings); | |
| summary.warnings.addAll(warnings); | |
| return this; | |
| } | |
| public Builder setActionRan(boolean actionRan) { | |
| checkMutation(); | |
| summary.actionRan = actionRan; | |
| return this; | |
| } | |
| /** | |
| * Set the number of results cached, locally or remotely. | |
| * | |
| * @param numCached number of results cached locally or remotely | |
| * @return this Builder | |
| */ | |
| public Builder setNumCached(int numCached) { | |
| checkMutation(); | |
| summary.numCached = numCached; | |
| return this; | |
| } | |
| public Builder setNumLocalActionCached(int numLocalActionCached) { | |
| checkMutation(); | |
| summary.numLocalActionCached = numLocalActionCached; | |
| return this; | |
| } | |
| public Builder setRanRemotely(boolean ranRemotely) { | |
| checkMutation(); | |
| summary.ranRemotely = ranRemotely; | |
| return this; | |
| } | |
| public Builder setWasUnreportedWrongSize(boolean wasUnreportedWrongSize) { | |
| checkMutation(); | |
| summary.wasUnreportedWrongSize = wasUnreportedWrongSize; | |
| return this; | |
| } | |
| /** | |
| * Records a new result for the given shard of the test. | |
| * | |
| * @return an immutable view of the statuses associated with the shard, with the new element. | |
| */ | |
| public List<BlazeTestStatus> addShardStatus(int shardNumber, BlazeTestStatus status) { | |
| Preconditions.checkState(summary.shardRunStatuses.put(shardNumber, status), | |
| "shardRunStatuses must allow duplicate statuses"); | |
| return ImmutableList.copyOf(summary.shardRunStatuses.get(shardNumber)); | |
| } | |
| /** | |
| * Returns the created TestSummary object. | |
| * Any actions following a build() will create another copy of the same values. | |
| * Since no mutators are provided directly by TestSummary, a copy will not | |
| * be produced if two builds are invoked in a row without calling a setter. | |
| */ | |
| public TestSummary build() { | |
| peek(); | |
| if (!built) { | |
| makeSummaryImmutable(); | |
| // else: it is already immutable. | |
| } | |
| Preconditions.checkState(built, "Built flag was not set"); | |
| return summary; | |
| } | |
| /** | |
| * Within-package, it is possible to read directly from an | |
| * incompletely-built TestSummary. Used to pass Builders around directly. | |
| */ | |
| TestSummary peek() { | |
| Preconditions.checkNotNull(summary.target, "Target cannot be null"); | |
| Preconditions.checkNotNull(summary.status, "Status cannot be null"); | |
| return summary; | |
| } | |
| private void makeSummaryImmutable() { | |
| // Once finalized, the list types are immutable. | |
| summary.passedLogs = Collections.unmodifiableList(summary.passedLogs); | |
| summary.failedLogs = Collections.unmodifiableList(summary.failedLogs); | |
| summary.warnings = Collections.unmodifiableList(summary.warnings); | |
| summary.coverageFiles = Collections.unmodifiableList(summary.coverageFiles); | |
| summary.testTimes = Collections.unmodifiableList(summary.testTimes); | |
| built = true; | |
| } | |
| } | |
| private ConfiguredTarget target; | |
| private BlazeTestStatus status; | |
| // Currently only populated if --runs_per_test_detects_flakes is enabled. | |
| private Multimap<Integer, BlazeTestStatus> shardRunStatuses = ArrayListMultimap.create(); | |
| private int numCached; | |
| private int numLocalActionCached; | |
| private boolean actionRan; | |
| private boolean ranRemotely; | |
| private boolean wasUnreportedWrongSize; | |
| private List<TestCase> failedTestCases = new ArrayList<>(); | |
| private List<Path> passedLogs = new ArrayList<>(); | |
| private List<Path> failedLogs = new ArrayList<>(); | |
| private List<String> warnings = new ArrayList<>(); | |
| private List<Path> coverageFiles = new ArrayList<>(); | |
| private List<Long> testTimes = new ArrayList<>(); | |
| private FailedTestCasesStatus failedTestCasesStatus = null; | |
| // Don't allow public instantiation; go through the Builder. | |
| private TestSummary() { | |
| } | |
| /** | |
| * Creates a new Builder allowing construction of a new TestSummary object. | |
| */ | |
| public static Builder newBuilder() { | |
| return new Builder(); | |
| } | |
| /** | |
| * Creates a new Builder initialized with a copy of the existing object's values. | |
| */ | |
| public static Builder newBuilderFromExisting(TestSummary existing) { | |
| Builder builder = new Builder(); | |
| builder.mergeFrom(existing); | |
| return builder; | |
| } | |
| public ConfiguredTarget getTarget() { | |
| return target; | |
| } | |
| public BlazeTestStatus getStatus() { | |
| return status; | |
| } | |
| /** | |
| * Whether or not any results associated with this test were cached locally | |
| * or remotely. | |
| * | |
| * @return true if any results were cached, false if not | |
| */ | |
| public boolean isCached() { | |
| return numCached > 0; | |
| } | |
| public boolean isLocalActionCached() { | |
| return numLocalActionCached > 0; | |
| } | |
| public int numLocalActionCached() { | |
| return numLocalActionCached; | |
| } | |
| /** | |
| * @return number of results that were cached locally or remotely | |
| */ | |
| public int numCached() { | |
| return numCached; | |
| } | |
| private int numUncached() { | |
| return totalRuns() - numCached; | |
| } | |
| /** | |
| * Whether or not any action was taken for this test, that is there was some | |
| * result that was <em>not cached</em>. | |
| * | |
| * @return true if some action was taken for this test, false if not | |
| */ | |
| public boolean actionRan() { | |
| return actionRan; | |
| } | |
| public boolean ranRemotely() { | |
| return ranRemotely; | |
| } | |
| public boolean wasUnreportedWrongSize() { | |
| return wasUnreportedWrongSize; | |
| } | |
| public List<TestCase> getFailedTestCases() { | |
| return failedTestCases; | |
| } | |
| public List<Path> getCoverageFiles() { | |
| return coverageFiles; | |
| } | |
| public List<Path> getPassedLogs() { | |
| return passedLogs; | |
| } | |
| public List<Path> getFailedLogs() { | |
| return failedLogs; | |
| } | |
| public FailedTestCasesStatus getFailedTestCasesStatus() { | |
| return failedTestCasesStatus; | |
| } | |
| /** | |
| * Returns an immutable view of the warnings associated with this test. | |
| */ | |
| public List<String> getWarnings() { | |
| return Collections.unmodifiableList(warnings); | |
| } | |
| private static int getSortKey(BlazeTestStatus status) { | |
| return status == BlazeTestStatus.PASSED ? -1 : status.ordinal(); | |
| } | |
| @Override | |
| public int compareTo(TestSummary that) { | |
| if (this.isCached() != that.isCached()) { | |
| return this.isCached() ? -1 : 1; | |
| } else if ((this.isCached() && that.isCached()) && (this.numUncached() != that.numUncached())) { | |
| return this.numUncached() - that.numUncached(); | |
| } else if (this.status != that.status) { | |
| return getSortKey(this.status) - getSortKey(that.status); | |
| } else { | |
| Artifact thisExecutable = this.target.getProvider(FilesToRunProvider.class).getExecutable(); | |
| Artifact thatExecutable = that.target.getProvider(FilesToRunProvider.class).getExecutable(); | |
| return thisExecutable.getPath().compareTo(thatExecutable.getPath()); | |
| } | |
| } | |
| public List<Long> getTestTimes() { | |
| // The return result is unmodifiable (UnmodifiableList instance) | |
| return testTimes; | |
| } | |
| public int getNumCached() { | |
| return numCached; | |
| } | |
| public int totalRuns() { | |
| return testTimes.size(); | |
| } | |
| static Mode getStatusMode(BlazeTestStatus status) { | |
| return status == BlazeTestStatus.PASSED | |
| ? Mode.INFO | |
| : (status == BlazeTestStatus.FLAKY ? Mode.WARNING : Mode.ERROR); | |
| } | |
| } |