diff options
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/runtime/AggregatingTestListener.java')
-rw-r--r-- | src/main/java/com/google/devtools/build/lib/runtime/AggregatingTestListener.java | 255 |
1 files changed, 255 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/AggregatingTestListener.java b/src/main/java/com/google/devtools/build/lib/runtime/AggregatingTestListener.java new file mode 100644 index 0000000000..cc240c41ba --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/runtime/AggregatingTestListener.java @@ -0,0 +1,255 @@ +// 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.HashMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.MapMaker; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +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.AnalysisFailureEvent; +import com.google.devtools.build.lib.analysis.ConfiguredTarget; +import com.google.devtools.build.lib.analysis.LabelAndConfiguration; +import com.google.devtools.build.lib.analysis.TargetCompleteEvent; +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.events.ExceptionListener; +import com.google.devtools.build.lib.rules.test.TestProvider; +import com.google.devtools.build.lib.rules.test.TestResult; +import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; + +/** + * 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 ConcurrentMap<Artifact, TestResult> statusMap = new MapMaker().makeMap(); + + private final TestResultAnalyzer analyzer; + private final EventBus eventBus; + private final EventHandlerPreconditions preconditionHelper; + private volatile boolean blazeHalted = false; + + + // summaryLock guards concurrent access to these two collections, which should be kept + // synchronized with each other. + private final Map<LabelAndConfiguration, TestSummary.Builder> summaries; + private final Multimap<LabelAndConfiguration, Artifact> remainingRuns; + private final Object summaryLock = new Object(); + + public AggregatingTestListener(TestResultAnalyzer analyzer, + EventBus eventBus, + ExceptionListener listener) { + this.analyzer = analyzer; + this.eventBus = eventBus; + this.preconditionHelper = new EventHandlerPreconditions(listener); + + this.summaries = Maps.newHashMap(); + this.remainingRuns = HashMultimap.create(); + } + + /** + * @return An unmodifiable copy of the map of test results. + */ + public Map<Artifact, TestResult> getStatusMap() { + return ImmutableMap.copyOf(statusMap); + } + + /** + * 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) { + // Add all target runs to the map, assuming 1:1 status artifact <-> result. + synchronized (summaryLock) { + for (ConfiguredTarget target : event.getTestTargets()) { + Iterable<Artifact> statusArtifacts = + target.getProvider(TestProvider.class).getTestParams().getTestStatusArtifacts(); + preconditionHelper.checkState(remainingRuns.putAll(asKey(target), statusArtifacts)); + + // And create an empty summary suitable for incremental analysis. + // Also has the nice side effect of mapping labels to RuleConfiguredTargets. + TestSummary.Builder summary = TestSummary.newBuilder() + .setTarget(target) + .setStatus(BlazeTestStatus.NO_STATUS); + preconditionHelper.checkState(summaries.put(asKey(target), summary) == null); + } + } + } + + /** + * 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) { + Preconditions.checkState( + statusMap.put(result.getTestStatusArtifact(), result) == null, + "Duplicate result reported for an individual test shard"); + + ActionOwner testOwner = result.getTestAction().getOwner(); + LabelAndConfiguration targetLabel = LabelAndConfiguration.of( + testOwner.getLabel(), result.getTestAction().getConfiguration()); + + TestSummary finalTestSummary = null; + synchronized (summaryLock) { + TestSummary.Builder summary = summaries.get(targetLabel); + preconditionHelper.checkNotNull(summary); + if (!remainingRuns.remove(targetLabel, result.getTestStatusArtifact())) { + // This can happen if a buildCompleteEvent() was processed before this event reached us. + // This situation is likely to happen if --notest_keep_going is set with multiple targets. + return; + } + + summary = analyzer.incrementalAnalyze(summary, result); + + // If all runs are processed, the target is finished and ready to report. + if (!remainingRuns.containsKey(targetLabel)) { + finalTestSummary = summary.build(); + } + } + + // Report finished targets. + if (finalTestSummary != null) { + eventBus.post(finalTestSummary); + } + } + + private void targetFailure(LabelAndConfiguration label) { + TestSummary finalSummary; + synchronized (summaryLock) { + if (!remainingRuns.containsKey(label)) { + // Blaze does not guarantee that BuildResult.getSuccessfulTargets() and posted TestResult + // events are in sync. Thus, it is possible that a test event was posted, but the target is + // not present in the set of successful targets. + return; + } + + TestSummary.Builder summary = summaries.get(label); + if (summary == null) { + // Not a test target; nothing to do. + return; + } + finalSummary = analyzer.markUnbuilt(summary, blazeHalted).build(); + + // These are never going to run; removing them marks the target complete. + remainingRuns.removeAll(label); + } + eventBus.post(finalSummary); + } + + @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))) { + targetFailure(asKey(target)); + } + } + + @Subscribe + public void buildCompleteEvent(BuildCompleteEvent event) { + if (event.getResult().wasCatastrophe()) { + blazeHalted = true; + } + buildComplete(event.getResult().getActualTargets(), event.getResult().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(new LabelAndConfiguration(event.getTarget())); + } + } + + /** + * Returns the known aggregate results for the given target at the current moment. + */ + public TestSummary.Builder getCurrentSummary(ConfiguredTarget target) { + synchronized (summaryLock) { + return summaries.get(asKey(target)); + } + } + + /** + * Returns all test status artifacts associated with a given target + * whose runs have yet to finish. + */ + public Collection<Artifact> getIncompleteRuns(ConfiguredTarget target) { + synchronized (summaryLock) { + return Collections.unmodifiableCollection(remainingRuns.get(asKey(target))); + } + } + + /** + * Returns true iff all runs of the target are accounted for. + */ + public boolean targetReported(ConfiguredTarget target) { + synchronized (summaryLock) { + return summaries.containsKey(asKey(target)) && !remainingRuns.containsKey(asKey(target)); + } + } + + /** + * Returns the {@link TestResultAnalyzer} associated with this listener. + */ + public TestResultAnalyzer getAnalyzer() { + return analyzer; + } + + private LabelAndConfiguration asKey(ConfiguredTarget target) { + return new LabelAndConfiguration(target); + } +} |