aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java428
1 files changed, 428 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java b/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java
new file mode 100644
index 0000000000..171f15085f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java
@@ -0,0 +1,428 @@
+// Copyright 2014 Google Inc. 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.base.Preconditions;
+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.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<TestCase>(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;
+ }
+
+ 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;
+ }
+
+ public boolean isCached() {
+ return numCached > 0;
+ }
+
+ public boolean isLocalActionCached() {
+ return numLocalActionCached > 0;
+ }
+
+ public int numLocalActionCached() {
+ return numLocalActionCached;
+ }
+
+ public int numCached() {
+ return numCached;
+ }
+
+ private int numUncached() {
+ return totalRuns() - numCached;
+ }
+
+ 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);
+ }
+}