aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/bazel
diff options
context:
space:
mode:
authorGravatar elenairina <elenairina@google.com>2018-07-06 03:06:21 -0700
committerGravatar Copybara-Service <copybara-piper@google.com>2018-07-06 03:07:25 -0700
commit9438d1042df9e70570d245f6bd259acdfbb3455e (patch)
tree106816a832efd2a1c351966de94fe52277c70f32 /src/main/java/com/google/devtools/build/lib/bazel
parentc4622ac9205d2f1b42dac8c598e83113d39e7f11 (diff)
Create Bazel coverage report action.
Fixes #5246 RELNOTES: None. PiperOrigin-RevId: 203453340
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/bazel')
-rw-r--r--src/main/java/com/google/devtools/build/lib/bazel/Bazel.java1
-rw-r--r--src/main/java/com/google/devtools/build/lib/bazel/coverage/BazelCoverageReportModule.java121
-rw-r--r--src/main/java/com/google/devtools/build/lib/bazel/coverage/CoverageArgs.java73
-rw-r--r--src/main/java/com/google/devtools/build/lib/bazel/coverage/CoverageReportActionBuilder.java260
4 files changed, 455 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/Bazel.java b/src/main/java/com/google/devtools/build/lib/bazel/Bazel.java
index 38a8ae77e1..96fa9a8d38 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/Bazel.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/Bazel.java
@@ -44,6 +44,7 @@ public final class Bazel {
com.google.devtools.build.lib.bazel.BazelDiffAwarenessModule.class,
com.google.devtools.build.lib.bazel.BazelRepositoryModule.class,
com.google.devtools.build.lib.bazel.debug.WorkspaceRuleModule.class,
+ com.google.devtools.build.lib.bazel.coverage.BazelCoverageReportModule.class,
com.google.devtools.build.lib.skylarkdebug.module.SkylarkDebuggerModule.class,
com.google.devtools.build.lib.bazel.repository.RepositoryResolvedModule.class,
com.google.devtools.build.lib.bazel.SpawnLogModule.class,
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/coverage/BazelCoverageReportModule.java b/src/main/java/com/google/devtools/build/lib/bazel/coverage/BazelCoverageReportModule.java
new file mode 100644
index 0000000000..1e7344ab3d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/coverage/BazelCoverageReportModule.java
@@ -0,0 +1,121 @@
+// 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.Preconditions;
+import com.google.common.collect.ImmutableList;
+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.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.test.CoverageReportActionFactory;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.common.options.EnumConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionDocumentationCategory;
+import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsClassProvider;
+import java.util.Collection;
+
+/** Adds support for coverage report generation. */
+public class BazelCoverageReportModule extends BlazeModule {
+
+ /** Options that affect coverage report generation. */
+ public static class Options extends OptionsBase {
+
+ @Option(
+ name = "combined_report",
+ converter = ReportTypeConverter.class,
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ defaultValue = "none",
+ help =
+ "Specifies desired cumulative coverage report type. At this point only HTML "
+ + "and LCOV reports are supported."
+ )
+ public ReportType combinedReport;
+ }
+
+ /** Possible values for the --combined_report option. */
+ public static enum ReportType {
+ NONE,
+ LCOV,
+ }
+
+ /** Converter for the --combined_report option. */
+ public static class ReportTypeConverter extends EnumConverter<ReportType> {
+ public ReportTypeConverter() {
+ super(ReportType.class, "combined coverage report type");
+ }
+ }
+
+ @Override
+ public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) {
+ return "build".equals(command.name())
+ ? ImmutableList.<Class<? extends OptionsBase>>of(Options.class)
+ : ImmutableList.<Class<? extends OptionsBase>>of();
+ }
+
+ @Override
+ public CoverageReportActionFactory getCoverageReportFactory(OptionsClassProvider commandOptions) {
+ final Options options = commandOptions.getOptions(Options.class);
+ return new CoverageReportActionFactory() {
+ @Override
+ public CoverageReportActionsWrapper createCoverageReportActionsWrapper(
+ EventHandler eventHandler,
+ BlazeDirectories directories,
+ Collection<ConfiguredTarget> targetsToTest,
+ Iterable<Artifact> baselineCoverageArtifacts,
+ ArtifactFactory artifactFactory,
+ ArtifactOwner artifactOwner,
+ String workspaceName) {
+ if (options == null || options.combinedReport == ReportType.NONE) {
+ return null;
+ }
+ Preconditions.checkArgument(options.combinedReport == ReportType.LCOV);
+ CoverageReportActionBuilder builder = new CoverageReportActionBuilder();
+ return builder.createCoverageActionsWrapper(
+ eventHandler,
+ directories,
+ targetsToTest,
+ baselineCoverageArtifacts,
+ artifactFactory,
+ artifactOwner,
+ workspaceName,
+ this::getArgs,
+ this::getLocationMessage,
+ /*htmlReport=*/ false);
+ }
+
+ private ImmutableList<String> getArgs(CoverageArgs args) {
+ ImmutableList.Builder<String> argsBuilder = ImmutableList.<String>builder().add(
+ args.reportGenerator().getExecutable().getExecPathString(),
+ // A file that contains all the exec paths to the coverage artifacts
+ "--reports_file=" + args.lcovArtifact().getExecPathString(),
+ "--output_file=" + args.lcovOutput().getExecPathString());
+ return argsBuilder.build();
+ }
+
+ private String getLocationMessage(CoverageArgs args) {
+ return "LCOV coverage report is located at " + args.lcovOutput().getPath().getPathString()
+ + "\n and execpath is " + args.lcovOutput().getExecPathString();
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/coverage/CoverageArgs.java b/src/main/java/com/google/devtools/build/lib/bazel/coverage/CoverageArgs.java
new file mode 100644
index 0000000000..1e964d08ec
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/coverage/CoverageArgs.java
@@ -0,0 +1,73 @@
+// 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.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+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.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.bazel.coverage.CoverageReportActionBuilder.ArgsFunc;
+import com.google.devtools.build.lib.bazel.coverage.CoverageReportActionBuilder.LocationFunc;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import javax.annotation.Nullable;
+
+/**
+ * A value class that holds arguments for
+ * {@link CoverageReportActionBuilder#generateCoverageReportAction}, {@link ArgsFunc} and
+ * {@link LocationFunc}.
+ */
+@AutoValue
+public abstract class CoverageArgs {
+ public abstract BlazeDirectories directories();
+ public abstract ImmutableList<Artifact> coverageArtifacts();
+ public abstract Artifact lcovArtifact();
+ public abstract ArtifactFactory factory();
+ public abstract ArtifactOwner artifactOwner();
+ public abstract FilesToRunProvider reportGenerator();
+ public abstract String workspaceName();
+ public abstract boolean htmlReport();
+ @Nullable
+ public abstract PathFragment coverageDir();
+ @Nullable
+ public abstract Artifact lcovOutput();
+
+ public static CoverageArgs create(
+ BlazeDirectories directories,
+ ImmutableList<Artifact> coverageArtifacts,
+ Artifact lcovArtifact,
+ ArtifactFactory factory,
+ ArtifactOwner artifactOwner,
+ FilesToRunProvider reportGenerator,
+ String workspaceName,
+ boolean htmlReport) {
+ return new AutoValue_CoverageArgs(directories, coverageArtifacts, lcovArtifact, factory,
+ artifactOwner, reportGenerator, workspaceName, htmlReport,
+ /*coverageDir=*/ null,
+ /*lcovOutput=*/ null);
+ }
+
+ public static CoverageArgs createCopyWithCoverageDirAndLcovOutput(
+ CoverageArgs args,
+ PathFragment coverageDir,
+ Artifact lcovOutput) {
+ return new AutoValue_CoverageArgs(
+ args.directories(), args.coverageArtifacts(), args.lcovArtifact(),
+ args.factory(), args.artifactOwner(), args.reportGenerator(), args.workspaceName(),
+ args.htmlReport(), coverageDir, lcovOutput);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/coverage/CoverageReportActionBuilder.java b/src/main/java/com/google/devtools/build/lib/bazel/coverage/CoverageReportActionBuilder.java
new file mode 100644
index 0000000000..5d65e5f06c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/coverage/CoverageReportActionBuilder.java
@@ -0,0 +1,260 @@
+// 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.
+ *
+ * <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.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<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.
+ */
+ 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());
+ }
+}