| // Copyright 2018 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.bazel.coverage; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.devtools.build.lib.actions.AbstractAction; |
| import com.google.devtools.build.lib.actions.Action; |
| import com.google.devtools.build.lib.actions.ActionExecutionContext; |
| import com.google.devtools.build.lib.actions.ActionExecutionException; |
| import com.google.devtools.build.lib.actions.ActionKeyContext; |
| import com.google.devtools.build.lib.actions.ActionOwner; |
| import com.google.devtools.build.lib.actions.ActionResult; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.ArtifactFactory; |
| import com.google.devtools.build.lib.actions.ArtifactOwner; |
| import com.google.devtools.build.lib.actions.ArtifactRoot; |
| import com.google.devtools.build.lib.actions.BaseSpawn; |
| import com.google.devtools.build.lib.actions.ExecException; |
| import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit; |
| import com.google.devtools.build.lib.actions.ResourceSet; |
| import com.google.devtools.build.lib.actions.RunfilesSupplier; |
| import com.google.devtools.build.lib.actions.Spawn; |
| import com.google.devtools.build.lib.actions.SpawnActionContext; |
| import com.google.devtools.build.lib.actions.SpawnResult; |
| import com.google.devtools.build.lib.analysis.BlazeDirectories; |
| import com.google.devtools.build.lib.analysis.ConfiguredTarget; |
| import com.google.devtools.build.lib.analysis.FilesToRunProvider; |
| import com.google.devtools.build.lib.analysis.actions.FileWriteAction; |
| import com.google.devtools.build.lib.analysis.test.CoverageReportActionFactory.CoverageReportActionsWrapper; |
| import com.google.devtools.build.lib.analysis.test.TestProvider; |
| import com.google.devtools.build.lib.analysis.test.TestProvider.TestParams; |
| import com.google.devtools.build.lib.analysis.test.TestRunnerAction; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.events.EventHandler; |
| import com.google.devtools.build.lib.util.Fingerprint; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import javax.annotation.Nullable; |
| |
| /** |
| * A class to create the coverage report generator action. |
| * |
| * <p>The coverage report action is created after every test shard action is created, at the |
| * very end of the analysis phase. There is only one coverage report action per coverage |
| * command invocation. It can also be viewed as a single sink node of the action graph. |
| * |
| * <p>Its inputs are the individual coverage.dat files from the test outputs (each shard produces |
| * one) and the baseline coverage artifacts. Note that each ConfiguredTarget among the |
| * transitive dependencies of the top level test targets may provide baseline coverage artifacts. |
| * |
| * <p>The coverage report generation can have two phases, though they both run in the same action. |
| * The source code of the coverage report tool {@code lcov_merger} is in the {@code |
| * testing/coverage/lcov_merger} directory. The deployed binaries used by Blaze are under |
| * {@code tools/coverage}. |
| * |
| * <p>The first phase is merging the individual coverage files into a single report file. The |
| * location of this file is reported by Blaze. This phase always happens if the {@code |
| * --combined_report=lcov} or {@code --combined_report=html}. |
| * |
| * <p>The second phase is generating an html report. It only happens if {@code |
| * --combined_report=html}. The action generates an html output file potentially for every |
| * tested source file into the report. Since this set of files is unknown in the analysis |
| * phase (the tool figures it out from the contents of the merged coverage report file) |
| * the action always runs locally when {@code --combined_report=html}. |
| */ |
| public final class CoverageReportActionBuilder { |
| |
| private static final ResourceSet LOCAL_RESOURCES = |
| ResourceSet.createWithRamCpu(/* memoryMb= */ 750, /* cpuUsage= */ 1); |
| |
| private static final ActionOwner ACTION_OWNER = ActionOwner.SYSTEM_ACTION_OWNER; |
| |
| // SpawnActions can't be used because they need the AnalysisEnvironment and this action is |
| // created specially at the very end of the analysis phase when we don't have it anymore. |
| @Immutable |
| private static final class CoverageReportAction extends AbstractAction |
| implements NotifyOnActionCacheHit { |
| private final ImmutableList<String> command; |
| private final boolean remotable; |
| private final String locationMessage; |
| private final RunfilesSupplier runfilesSupplier; |
| |
| protected CoverageReportAction(ActionOwner owner, Iterable<Artifact> inputs, |
| Iterable<Artifact> outputs, ImmutableList<String> command, String locationMessage, |
| boolean remotable, RunfilesSupplier runfilesSupplier) { |
| super(owner, inputs, outputs); |
| this.command = command; |
| this.remotable = remotable; |
| this.locationMessage = locationMessage; |
| this.runfilesSupplier = runfilesSupplier; |
| } |
| |
| @Override |
| public ActionResult execute(ActionExecutionContext actionExecutionContext) |
| throws ActionExecutionException, InterruptedException { |
| try { |
| ImmutableMap<String, String> executionInfo = remotable |
| ? ImmutableMap.<String, String>of() |
| : ImmutableMap.<String, String>of("local", ""); |
| Spawn spawn = new BaseSpawn( |
| command, |
| ImmutableMap.<String, String>of(), |
| executionInfo, |
| runfilesSupplier, |
| this, |
| LOCAL_RESOURCES); |
| List<SpawnResult> spawnResults = |
| actionExecutionContext.getContext(SpawnActionContext.class) |
| .exec(spawn, actionExecutionContext); |
| actionExecutionContext.getEventHandler().handle(Event.info(locationMessage)); |
| return ActionResult.create(spawnResults); |
| } catch (ExecException e) { |
| throw e.toActionExecutionException( |
| "Coverage report generation failed: ", |
| actionExecutionContext.getVerboseFailures(), |
| this); |
| } |
| } |
| |
| @Override |
| public String getMnemonic() { |
| return "CoverageReport"; |
| } |
| |
| @Override |
| protected void computeKey(ActionKeyContext actionKeyContext, Fingerprint fp) { |
| fp.addStrings(command); |
| } |
| |
| @Override |
| public void actionCacheHit(ActionCachedContext context) { |
| context.getEventHandler().handle(Event.info(locationMessage)); |
| } |
| } |
| |
| public CoverageReportActionBuilder() { |
| } |
| |
| /** Returns the coverage report action. May return null in case of an error. */ |
| @Nullable |
| public CoverageReportActionsWrapper createCoverageActionsWrapper( |
| EventHandler reporter, |
| BlazeDirectories directories, |
| Collection<ConfiguredTarget> targetsToTest, |
| Iterable<Artifact> baselineCoverageArtifacts, |
| ArtifactFactory factory, |
| ArtifactOwner artifactOwner, |
| String workspaceName, |
| ArgsFunc argsFunction, |
| LocationFunc locationFunc, |
| boolean htmlReport) { |
| |
| if (targetsToTest == null || targetsToTest.isEmpty()) { |
| return null; |
| } |
| ImmutableList.Builder<Artifact> builder = ImmutableList.<Artifact>builder(); |
| FilesToRunProvider reportGenerator = null; |
| for (ConfiguredTarget target : targetsToTest) { |
| TestParams testParams = target.getProvider(TestProvider.class).getTestParams(); |
| builder.addAll(testParams.getCoverageArtifacts()); |
| if (reportGenerator == null) { |
| reportGenerator = testParams.getCoverageReportGenerator(); |
| } |
| } |
| builder.addAll(baselineCoverageArtifacts); |
| |
| ImmutableList<Artifact> coverageArtifacts = builder.build(); |
| if (!coverageArtifacts.isEmpty()) { |
| PathFragment coverageDir = TestRunnerAction.COVERAGE_TMP_ROOT; |
| Artifact lcovArtifact = factory.getDerivedArtifact( |
| coverageDir.getRelative("lcov_files.tmp"), |
| directories.getBuildDataDirectory(workspaceName), |
| artifactOwner); |
| Action lcovFileAction = generateLcovFileWriteAction(lcovArtifact, coverageArtifacts); |
| Action coverageReportAction = generateCoverageReportAction( |
| CoverageArgs.create(directories, coverageArtifacts, lcovArtifact, factory, artifactOwner, |
| reportGenerator, workspaceName, htmlReport), |
| argsFunction, locationFunc); |
| return new CoverageReportActionsWrapper(lcovFileAction, coverageReportAction); |
| } else { |
| reporter.handle( |
| Event.error("Cannot generate coverage report - no coverage information was collected")); |
| return null; |
| } |
| } |
| |
| private FileWriteAction generateLcovFileWriteAction( |
| Artifact lcovArtifact, ImmutableList<Artifact>coverageArtifacts) { |
| List<String> filepaths = new ArrayList<>(coverageArtifacts.size()); |
| for (Artifact artifact : coverageArtifacts) { |
| filepaths.add(artifact.getExecPathString()); |
| } |
| return FileWriteAction.create( |
| ACTION_OWNER, |
| lcovArtifact, |
| Joiner.on('\n').join(filepaths), |
| /*makeExecutable=*/ false, |
| FileWriteAction.Compression.DISALLOW); |
| } |
| |
| /** |
| * Computes the arguments passed to the coverage report generator. |
| */ |
| @FunctionalInterface |
| public interface ArgsFunc { |
| ImmutableList<String> apply(CoverageArgs args); |
| } |
| |
| /** |
| * Computes the location message for the {@link CoverageReportAction}. |
| */ |
| @FunctionalInterface |
| public interface LocationFunc { |
| String apply(CoverageArgs args); |
| } |
| |
| private CoverageReportAction generateCoverageReportAction( |
| CoverageArgs args, |
| ArgsFunc argsFunc, |
| LocationFunc locationFunc) { |
| ArtifactRoot root = args.directories().getBuildDataDirectory(args.workspaceName()); |
| PathFragment coverageDir = TestRunnerAction.COVERAGE_TMP_ROOT; |
| Artifact lcovOutput = args.factory().getDerivedArtifact( |
| coverageDir.getRelative("_coverage_report.dat"), root, args.artifactOwner()); |
| Artifact reportGeneratorExec = args.reportGenerator().getExecutable(); |
| args = CoverageArgs.createCopyWithCoverageDirAndLcovOutput(args, coverageDir, lcovOutput); |
| ImmutableList<String> actionArgs = argsFunc.apply(args); |
| |
| ImmutableList<Artifact> inputs = ImmutableList.<Artifact>builder() |
| .addAll(args.coverageArtifacts()) |
| .add(reportGeneratorExec) |
| .add(args.lcovArtifact()) |
| .build(); |
| return new CoverageReportAction( |
| ACTION_OWNER, |
| inputs, |
| ImmutableList.of(lcovOutput), |
| actionArgs, |
| locationFunc.apply(args), |
| !args.htmlReport(), |
| args.reportGenerator().getRunfilesSupplier()); |
| } |
| } |