blob: c5a06fea4138388d68b2d5e8729807c5f02989d7 [file] [log] [blame]
// 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.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.eventbus.AllowConcurrentEvents;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.AliasProvider;
import com.google.devtools.build.lib.analysis.AnalysisFailureEvent;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.TargetCompleteEvent;
import com.google.devtools.build.lib.analysis.test.TestResult;
import com.google.devtools.build.lib.buildtool.BuildResult;
import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
import com.google.devtools.build.lib.buildtool.buildevent.BuildInterruptedEvent;
import com.google.devtools.build.lib.buildtool.buildevent.TestFilteringCompleteEvent;
import com.google.devtools.build.lib.concurrent.ThreadSafety;
import com.google.devtools.build.lib.exec.ExecutionOptions;
import com.google.devtools.build.lib.rules.AliasConfiguredTarget;
import com.google.devtools.build.lib.runtime.TerminalTestResultNotifier.TestSummaryOptions;
import com.google.devtools.build.lib.runtime.TestResultAggregator.AggregationPolicy;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* This class aggregates and reports target-wide test statuses in real-time.
* It must be public for EventBus invocation.
*/
@ThreadSafety.ThreadSafe
public class AggregatingTestListener {
private final TestSummaryOptions summaryOptions;
private final ExecutionOptions executionOptions;
private final EventBus eventBus;
private volatile boolean blazeHalted = false;
// Store information about potential failures in the presence of --nokeep_going or
// --notest_keep_going.
private boolean skipTargetsOnFailure;
private final ConcurrentHashMap<ConfiguredTargetKey, TestResultAggregator> aggregators;
public AggregatingTestListener(
TestSummaryOptions summaryOptions, ExecutionOptions executionOptions, EventBus eventBus) {
this.summaryOptions = summaryOptions;
this.executionOptions = executionOptions;
this.eventBus = eventBus;
this.aggregators = new ConcurrentHashMap<>();
}
/** Returns an unmodifiable copy of the map of test results. */
public Map<Artifact, TestResult> getStatusMapForTesting() {
Map<Artifact, TestResult> result = new HashMap<>();
for (TestResultAggregator aggregator : aggregators.values()) {
result.putAll(aggregator.getStatusMapForTesting());
}
return result;
}
/** Returns the known aggregate results for the given target at the current moment. */
public TestSummary.Builder getCurrentSummaryForTesting(ConfiguredTarget target) {
return aggregators.get(asKey(target)).getCurrentSummaryForTesting();
}
/**
* Returns all test status artifacts associated with a given target whose runs have yet to finish.
*/
public Collection<Artifact> getIncompleteRunsForTesting(ConfiguredTarget target) {
return aggregators.get(asKey(target)).getIncompleteRunsForTesting();
}
/**
* Populates the test summary map as soon as test filtering is complete.
* This is the earliest at which the final set of targets to test is known.
*/
@Subscribe
@AllowConcurrentEvents
public void populateTests(TestFilteringCompleteEvent event) {
AggregationPolicy policy =
new AggregationPolicy(
eventBus,
executionOptions.testCheckUpToDate,
summaryOptions.testVerboseTimeoutWarnings);
// Add all target runs to the map, assuming 1:1 status artifact <-> result.
for (ConfiguredTarget target : event.getTestTargets()) {
if (isAlias(target)) {
continue;
}
TestResultAggregator aggregator =
new TestResultAggregator(target, event.getConfigurationForTarget(target), policy);
TestResultAggregator oldAggregator = aggregators.put(asKey(target), aggregator);
Preconditions.checkState(
oldAggregator == null, "target: %s, values: %s %s", target, oldAggregator, aggregator);
}
}
/**
* Records a new test run result and incrementally updates the target status.
* This event is sent upon completion of executed test runs.
*/
@Subscribe
@AllowConcurrentEvents
public void testEvent(TestResult result) {
ActionOwner testOwner = result.getTestAction().getOwner();
ConfiguredTargetKey configuredTargetKey =
ConfiguredTargetKey.of(testOwner.getLabel(), result.getTestAction().getConfiguration());
aggregators.get(configuredTargetKey).testEvent(result);
}
private void targetFailure(ConfiguredTargetKey configuredTargetKey) {
TestResultAggregator aggregator = aggregators.get(configuredTargetKey);
if (aggregator != null) {
aggregator.targetFailure(blazeHalted, skipTargetsOnFailure);
}
}
@VisibleForTesting
void buildComplete(
Collection<ConfiguredTarget> actualTargets, Collection<ConfiguredTarget> successfulTargets) {
if (actualTargets == null || successfulTargets == null) {
return;
}
for (ConfiguredTarget target :
Sets.difference(
ImmutableSet.copyOf(actualTargets), ImmutableSet.copyOf(successfulTargets))) {
if (isAlias(target)) {
continue;
}
targetFailure(asKey(target));
}
}
@Subscribe
public void buildCompleteEvent(BuildCompleteEvent event) {
BuildResult result = event.getResult();
if (result.wasCatastrophe()) {
blazeHalted = true;
}
skipTargetsOnFailure = result.getStopOnFirstFailure();
buildComplete(result.getActualTargets(), result.getSuccessfulTargets());
}
@Subscribe
public void analysisFailure(AnalysisFailureEvent event) {
targetFailure(event.getFailedTarget());
}
@Subscribe
@AllowConcurrentEvents
public void buildInterrupted(BuildInterruptedEvent event) {
blazeHalted = true;
}
/**
* Called when a build action is not executed (e.g. because a dependency failed to build). We want
* to catch such events in order to determine when a test target has failed to build.
*/
@Subscribe
@AllowConcurrentEvents
public void targetComplete(TargetCompleteEvent event) {
if (event.failed()) {
targetFailure(event.getConfiguredTargetKey());
}
}
/**
* Prints out the results of the given tests, and returns true if they all passed. Posts any
* targets which weren't already completed by the listener to the EventBus. Reports all targets on
* the console via the given notifier. Run at the end of the build, run only once.
*
* @param testTargets The list of targets being run
* @param notifier A console notifier to echo results to.
* @return true if all the tests passed, else false
*/
public boolean differentialAnalyzeAndReport(
Collection<ConfiguredTarget> testTargets,
Collection<ConfiguredTarget> skippedTargets,
TestResultNotifier notifier) {
Preconditions.checkNotNull(testTargets);
Preconditions.checkNotNull(notifier);
// The natural ordering of the summaries defines their output order.
Set<TestSummary> summaries = Sets.newTreeSet();
int totalRun = 0; // Number of targets running at least one non-cached test.
int passCount = 0;
for (ConfiguredTarget testTarget : testTargets) {
TestSummary summary;
if (isAlias(testTarget)) {
ConfiguredTargetKey actualKey =
ConfiguredTargetKey.of(
// A test is never in the host configuration.
testTarget.getLabel(),
testTarget.getConfigurationKey(),
/*isHostConfiguration=*/ false);
TestResultAggregator aggregator = aggregators.get(actualKey);
TestSummary.Builder summaryBuilder = TestSummary.newBuilder();
summaryBuilder.mergeFrom(aggregator.aggregateAndReportSummary(skipTargetsOnFailure));
summaryBuilder.setTarget(testTarget);
summary = summaryBuilder.build();
} else {
TestResultAggregator aggregator = aggregators.get(asKey(testTarget));
summary = aggregator.aggregateAndReportSummary(skipTargetsOnFailure);
}
summaries.add(summary);
// Finished aggregating; build the final console output.
if (summary.actionRan()) {
totalRun++;
}
if (TestResult.isBlazeTestStatusPassed(summary.getStatus())) {
passCount++;
}
}
int summarySize = summaries.size();
int testTargetsSize = testTargets.size();
Preconditions.checkState(
summarySize == testTargetsSize,
"Unequal sizes: %s vs %s (%s and %s)",
summarySize,
testTargetsSize,
summaries,
testTargets);
notifier.notify(summaries, totalRun);
// skipped targets are not in passCount since they have NO_STATUS
Set<ConfiguredTarget> testTargetsSet = new HashSet<>(testTargets);
Set<ConfiguredTarget> skippedTargetsSet = new HashSet<>(skippedTargets);
return passCount == Sets.difference(testTargetsSet, skippedTargetsSet).size();
}
private static boolean isAlias(ConfiguredTarget target) {
// I expect this to be consistent with target.getProvider(AliasProvider.class) != null.
return target instanceof AliasConfiguredTarget;
}
private static ConfiguredTargetKey asKey(ConfiguredTarget target) {
Preconditions.checkArgument(!isAlias(target));
return ConfiguredTargetKey.of(
// A test is never in the host configuration.
AliasProvider.getDependencyLabel(target),
target.getConfigurationKey(),
/*isHostConfiguration=*/ false);
}
}