// 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.
*
*
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 {
/**
* 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 coverageFiles) {
checkMutation(coverageFiles);
summary.coverageFiles.addAll(coverageFiles);
return this;
}
public Builder addPassedLogs(List passedLogs) {
checkMutation(passedLogs);
summary.passedLogs.addAll(passedLogs);
return this;
}
public Builder addFailedLogs(List 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 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 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 testTimes) {
checkMutation(testTimes);
summary.testTimes.addAll(testTimes);
return this;
}
public Builder addWarnings(List 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 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 shardRunStatuses = ArrayListMultimap.create();
private int numCached;
private int numLocalActionCached;
private boolean actionRan;
private boolean ranRemotely;
private boolean wasUnreportedWrongSize;
private List failedTestCases = new ArrayList<>();
private List passedLogs = new ArrayList<>();
private List failedLogs = new ArrayList<>();
private List warnings = new ArrayList<>();
private List coverageFiles = new ArrayList<>();
private List 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 not cached.
*
* @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 getFailedTestCases() {
return failedTestCases;
}
public List getCoverageFiles() {
return coverageFiles;
}
public List getPassedLogs() {
return passedLogs;
}
public List getFailedLogs() {
return failedLogs;
}
public FailedTestCasesStatus getFailedTestCasesStatus() {
return failedTestCasesStatus;
}
/**
* Returns an immutable view of the warnings associated with this test.
*/
public List 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 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);
}
}