| // 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.collect.Sets; | |
| import com.google.common.eventbus.EventBus; | |
| import com.google.common.eventbus.Subscribe; | |
| import com.google.devtools.build.lib.actions.Artifact; | |
| import com.google.devtools.build.lib.analysis.ConfiguredTarget; | |
| import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; | |
| import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent; | |
| import com.google.devtools.build.lib.cmdline.Label; | |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible; | |
| import com.google.devtools.build.lib.exec.ExecutionOptions; | |
| import com.google.devtools.build.lib.packages.TestSize; | |
| import com.google.devtools.build.lib.packages.TestTimeout; | |
| import com.google.devtools.build.lib.rules.test.TestProvider; | |
| import com.google.devtools.build.lib.rules.test.TestResult; | |
| import com.google.devtools.build.lib.runtime.TerminalTestResultNotifier.TestSummaryOptions; | |
| import com.google.devtools.build.lib.util.Preconditions; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus; | |
| import java.util.ArrayList; | |
| import java.util.Collection; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| /** | |
| * Prints results to the terminal, showing the results of each test target. | |
| */ | |
| @ThreadCompatible | |
| public class TestResultAnalyzer { | |
| private final Path execRoot; | |
| private final TestSummaryOptions summaryOptions; | |
| private final ExecutionOptions executionOptions; | |
| private final EventBus eventBus; | |
| // Store information about potential failures in the presence of --nokeep_going or | |
| // --notest_keep_going. | |
| private boolean skipTargetsOnFailure; | |
| /** | |
| * @param summaryOptions Parsed test summarization options. | |
| * @param executionOptions Parsed build/test execution options. | |
| * @param eventBus For reporting failed to build and cached tests. | |
| */ | |
| public TestResultAnalyzer(Path execRoot, | |
| TestSummaryOptions summaryOptions, | |
| ExecutionOptions executionOptions, | |
| EventBus eventBus) { | |
| this.execRoot = execRoot; | |
| this.summaryOptions = summaryOptions; | |
| this.executionOptions = executionOptions; | |
| this.eventBus = eventBus; | |
| eventBus.register(this); | |
| } | |
| @Subscribe | |
| public void doneBuild(BuildCompleteEvent event) { | |
| skipTargetsOnFailure = event.getResult().getStopOnFirstFailure(); | |
| } | |
| /** | |
| * 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 listener An aggregating listener with intermediate results | |
| * @param notifier A console notifier to echo results to. | |
| * @return true if all the tests passed, else false | |
| */ | |
| public boolean differentialAnalyzeAndReport( | |
| Collection<ConfiguredTarget> testTargets, | |
| AggregatingTestListener listener, | |
| TestResultNotifier notifier) { | |
| Preconditions.checkNotNull(testTargets); | |
| Preconditions.checkNotNull(listener); | |
| 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 = aggregateAndReportSummary(testTarget, listener).build(); | |
| summaries.add(summary); | |
| // Finished aggregating; build the final console output. | |
| if (summary.actionRan()) { | |
| totalRun++; | |
| } | |
| if (TestResult.isBlazeTestStatusPassed(summary.getStatus())) { | |
| passCount++; | |
| } | |
| } | |
| Preconditions.checkState(summaries.size() == testTargets.size()); | |
| notifier.notify(summaries, totalRun); | |
| return passCount == testTargets.size(); | |
| } | |
| private static BlazeTestStatus aggregateStatus(BlazeTestStatus status, BlazeTestStatus other) { | |
| return status.ordinal() > other.ordinal() ? status : other; | |
| } | |
| /** | |
| * Helper for differential analysis which aggregates the TestSummary | |
| * for an individual target, reporting runs on the EventBus if necessary. | |
| */ | |
| private TestSummary.Builder aggregateAndReportSummary( | |
| ConfiguredTarget testTarget, | |
| AggregatingTestListener listener) { | |
| // If already reported by the listener, no work remains for this target. | |
| TestSummary.Builder summary = listener.getCurrentSummary(testTarget); | |
| Label testLabel = testTarget.getLabel(); | |
| Preconditions.checkNotNull(summary, | |
| "%s did not complete test filtering, but has a test result", testLabel); | |
| if (listener.targetReported(testTarget)) { | |
| return summary; | |
| } | |
| Collection<Artifact> incompleteRuns = listener.getIncompleteRuns(testTarget); | |
| Map<Artifact, TestResult> statusMap = listener.getStatusMap(); | |
| // We will get back multiple TestResult instances if test had to be retried several | |
| // times before passing. Sharding and multiple runs of the same test without retries | |
| // will be represented by separate artifacts and will produce exactly one TestResult. | |
| for (Artifact testStatus : TestProvider.getTestStatusArtifacts(testTarget)) { | |
| // When a build is interrupted ( eg. a broken target with --nokeep_going ) runResult could | |
| // be null for an unrelated test because we were not able to even try to execute the test. | |
| // In that case, for tests that were previously passing we return null ( == NO STATUS), | |
| // because checking if the cached test target is up-to-date would require running the | |
| // dependency checker transitively. | |
| TestResult runResult = statusMap.get(testStatus); | |
| boolean isIncompleteRun = incompleteRuns.contains(testStatus); | |
| if (runResult == null) { | |
| summary = markIncomplete(summary); | |
| } else if (isIncompleteRun) { | |
| // Only process results which were not recorded by the listener. | |
| boolean newlyFetched = !statusMap.containsKey(testStatus); | |
| summary = incrementalAnalyze(summary, runResult); | |
| if (newlyFetched) { | |
| eventBus.post(runResult); | |
| } | |
| Preconditions.checkState( | |
| listener.getIncompleteRuns(testTarget).contains(testStatus) == isIncompleteRun, | |
| "TestListener changed in differential analysis. Ensure it isn't still registered."); | |
| } | |
| } | |
| // The target was not posted by the listener and must be posted now. | |
| eventBus.post(summary.build()); | |
| return summary; | |
| } | |
| /** | |
| * Incrementally updates a TestSummary given an existing summary | |
| * and a new TestResult. Only call on built targets. | |
| * | |
| * @param summaryBuilder Existing unbuilt test summary associated with a target. | |
| * @param result New test result to aggregate into the summary. | |
| * @return The updated TestSummary. | |
| */ | |
| public TestSummary.Builder incrementalAnalyze(TestSummary.Builder summaryBuilder, | |
| TestResult result) { | |
| // Cache retrieval should have been performed already. | |
| Preconditions.checkNotNull(result); | |
| Preconditions.checkNotNull(summaryBuilder); | |
| TestSummary existingSummary = Preconditions.checkNotNull(summaryBuilder.peek()); | |
| TransitiveInfoCollection target = existingSummary.getTarget(); | |
| Preconditions.checkNotNull( | |
| target, "The existing TestSummary must be associated with a target"); | |
| BlazeTestStatus status = existingSummary.getStatus(); | |
| int numCached = existingSummary.numCached(); | |
| int numLocalActionCached = existingSummary.numLocalActionCached(); | |
| // If a test was neither cached locally nor remotely we say action was taken. | |
| if (!(result.isCached() || result.getData().getRemotelyCached())) { | |
| summaryBuilder.setActionRan(true); | |
| } else { | |
| numCached++; | |
| } | |
| if (result.isCached()) { | |
| numLocalActionCached++; | |
| } | |
| PathFragment coverageData = result.getCoverageData(); | |
| if (coverageData != null) { | |
| summaryBuilder.addCoverageFiles( | |
| Collections.singletonList(execRoot.getRelative(coverageData))); | |
| } | |
| if (!executionOptions.runsPerTestDetectsFlakes) { | |
| status = aggregateStatus(status, result.getData().getStatus()); | |
| } else { | |
| int shardNumber = result.getShardNum(); | |
| int runsPerTestForLabel = target.getProvider(TestProvider.class).getTestParams().getRuns(); | |
| List<BlazeTestStatus> singleShardStatuses = summaryBuilder.addShardStatus( | |
| shardNumber, result.getData().getStatus()); | |
| if (singleShardStatuses.size() == runsPerTestForLabel) { | |
| BlazeTestStatus shardStatus = BlazeTestStatus.NO_STATUS; | |
| int passes = 0; | |
| for (BlazeTestStatus runStatusForShard : singleShardStatuses) { | |
| shardStatus = aggregateStatus(shardStatus, runStatusForShard); | |
| if (TestResult.isBlazeTestStatusPassed(runStatusForShard)) { | |
| passes++; | |
| } | |
| } | |
| // Under the RunsPerTestDetectsFlakes option, return flaky if 1 <= p < n shards pass. | |
| // If all results pass or fail, aggregate the passing/failing shardStatus. | |
| if (passes == 0 || passes == runsPerTestForLabel) { | |
| status = aggregateStatus(status, shardStatus); | |
| } else { | |
| status = aggregateStatus(status, BlazeTestStatus.FLAKY); | |
| } | |
| } | |
| } | |
| List<Path> passed = new ArrayList<>(); | |
| if (result.getData().hasPassedLog()) { | |
| passed.add(result.getTestAction().getTestLog().getPath().getRelative( | |
| result.getData().getPassedLog())); | |
| } | |
| List<Path> failed = new ArrayList<>(); | |
| for (String path : result.getData().getFailedLogsList()) { | |
| failed.add(result.getTestAction().getTestLog().getPath().getRelative(path)); | |
| } | |
| summaryBuilder | |
| .addTestTimes(result.getData().getTestTimesList()) | |
| .addPassedLogs(passed) | |
| .addFailedLogs(failed) | |
| .addWarnings(result.getData().getWarningList()) | |
| .collectFailedTests(result.getData().getTestCase()) | |
| .setRanRemotely(result.getData().getIsRemoteStrategy()); | |
| List<String> warnings = new ArrayList<>(); | |
| if (status == BlazeTestStatus.PASSED | |
| && shouldEmitTestSizeWarningInSummary( | |
| summaryOptions.testVerboseTimeoutWarnings, | |
| warnings, | |
| result.getData().getTestProcessTimesList(), | |
| target)) { | |
| summaryBuilder.setWasUnreportedWrongSize(true); | |
| } | |
| return summaryBuilder | |
| .setStatus(status) | |
| .setNumCached(numCached) | |
| .setNumLocalActionCached(numLocalActionCached) | |
| .addWarnings(warnings); | |
| } | |
| private TestSummary.Builder markIncomplete(TestSummary.Builder summaryBuilder) { | |
| // TODO(bazel-team): (2010) Make NotRunTestResult support both tests failed to built and | |
| // tests with no status and post it here. | |
| TestSummary summary = summaryBuilder.peek(); | |
| BlazeTestStatus status = summary.getStatus(); | |
| if (skipTargetsOnFailure) { | |
| status = BlazeTestStatus.NO_STATUS; | |
| } else if (status != BlazeTestStatus.NO_STATUS) { | |
| status = aggregateStatus(status, BlazeTestStatus.INCOMPLETE); | |
| } | |
| return summaryBuilder.setStatus(status); | |
| } | |
| TestSummary.Builder markUnbuilt(TestSummary.Builder summary, boolean blazeHalted) { | |
| BlazeTestStatus runStatus = | |
| blazeHalted | |
| ? BlazeTestStatus.BLAZE_HALTED_BEFORE_TESTING | |
| : (executionOptions.testCheckUpToDate || skipTargetsOnFailure | |
| ? BlazeTestStatus.NO_STATUS | |
| : BlazeTestStatus.FAILED_TO_BUILD); | |
| return summary.setStatus(runStatus); | |
| } | |
| /** | |
| * Checks whether the specified test timeout could have been smaller and adds | |
| * a warning message if verbose is true. | |
| * | |
| * <p>Returns true if there was a test with the wrong timeout, but if was not | |
| * reported. | |
| */ | |
| private static boolean shouldEmitTestSizeWarningInSummary(boolean verbose, | |
| List<String> warnings, List<Long> testTimes, TransitiveInfoCollection target) { | |
| TestTimeout specifiedTimeout = | |
| target.getProvider(TestProvider.class).getTestParams().getTimeout(); | |
| long maxTimeOfShard = 0; | |
| for (Long shardTime : testTimes) { | |
| if (shardTime != null) { | |
| maxTimeOfShard = Math.max(maxTimeOfShard, shardTime); | |
| } | |
| } | |
| int maxTimeInSeconds = (int) (maxTimeOfShard / 1000); | |
| if (!specifiedTimeout.isInRangeFuzzy(maxTimeInSeconds)) { | |
| TestTimeout expectedTimeout = TestTimeout.getSuggestedTestTimeout(maxTimeInSeconds); | |
| TestSize expectedSize = TestSize.getTestSize(expectedTimeout); | |
| if (verbose) { | |
| StringBuilder builder = new StringBuilder(String.format( | |
| "%s: Test execution time (%.1fs excluding execution overhead) outside of " | |
| + "range for %s tests. Consider setting timeout=\"%s\"", | |
| target.getLabel(), | |
| maxTimeOfShard / 1000.0, | |
| specifiedTimeout.prettyPrint(), | |
| expectedTimeout)); | |
| if (expectedSize != null) { | |
| builder.append(" or size=\"").append(expectedSize).append("\""); | |
| } | |
| builder.append("."); | |
| warnings.add(builder.toString()); | |
| return false; | |
| } | |
| return true; | |
| } else { | |
| return false; | |
| } | |
| } | |
| } |