// 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; /** * A class to create the coverage report generator action. * *

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. * *

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. * *

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}. * *

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}. * *

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.createWithRamCpuIo(750 /*MB*/, 0.5 /*CPU*/, 0.0 /*IO*/); 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 command; private final boolean remotable; private final String locationMessage; private final RunfilesSupplier runfilesSupplier; protected CoverageReportAction(ActionOwner owner, Iterable inputs, Iterable outputs, ImmutableList 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 executionInfo = remotable ? ImmutableMap.of() : ImmutableMap.of("local", ""); Spawn spawn = new BaseSpawn( command, ImmutableMap.of(), executionInfo, runfilesSupplier, this, LOCAL_RESOURCES); List 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. */ public CoverageReportActionsWrapper createCoverageActionsWrapper( EventHandler reporter, BlazeDirectories directories, Collection targetsToTest, Iterable baselineCoverageArtifacts, ArtifactFactory factory, ArtifactOwner artifactOwner, String workspaceName, ArgsFunc argsFunction, LocationFunc locationFunc, boolean htmlReport) { if (targetsToTest == null || targetsToTest.isEmpty()) { return null; } ImmutableList.Builder builder = ImmutableList.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 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, ImmutableListcoverageArtifacts) { List 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 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 actionArgs = argsFunc.apply(args); ImmutableList inputs = ImmutableList.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()); } }