diff options
16 files changed, 636 insertions, 22 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD index fedf4990a2..6b27d1d86b 100644 --- a/src/main/java/com/google/devtools/build/lib/BUILD +++ b/src/main/java/com/google/devtools/build/lib/BUILD @@ -708,6 +708,7 @@ java_library( "bazel/rules/sh/sh_stub_template_windows.txt", ], deps = [ + ":bazel-coverage", ":bazel/BazelRepositoryModule", ":exitcode-external", "//src/main/java/com/google/devtools/build/lib:bazel", @@ -796,6 +797,25 @@ java_library( ) java_library( + name = "bazel-coverage", + srcs = glob(["bazel/coverage/*.java"]), + deps = [ + "//src/main/java/com/google/devtools/build/lib:build-base", + "//src/main/java/com/google/devtools/build/lib:events", + "//src/main/java/com/google/devtools/build/lib:runtime", + "//src/main/java/com/google/devtools/build/lib:util", + "//src/main/java/com/google/devtools/build/lib/actions", + "//src/main/java/com/google/devtools/build/lib/concurrent", + "//src/main/java/com/google/devtools/build/lib/vfs", + "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment", + "//src/main/java/com/google/devtools/common/options", + "//third_party:auto_value", + "//third_party:guava", + "//third_party:jsr305", + ], +) + +java_library( name = "bazel-repository", srcs = glob( [ diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java b/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java index 11b1ec3eab..4c4c501d07 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java @@ -190,8 +190,7 @@ public class BaseRuleClasses { .cfg(HostTransition.INSTANCE) .value( coverageReportGeneratorAttribute( - env.getToolsLabel(DEFAULT_COVERAGE_REPORT_GENERATOR_VALUE))) - .singleArtifact()) + env.getToolsLabel(DEFAULT_COVERAGE_REPORT_GENERATOR_VALUE)))) // The target itself and run_under both run on the same machine. .add(attr(":run_under", LABEL).value(RUN_UNDER).skipPrereqValidatorCheck()) .executionPlatformConstraintsAllowed(ExecutionPlatformConstraintsAllowed.PER_TARGET) diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java b/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java index 204d94931e..2143efaa19 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java @@ -404,7 +404,8 @@ public class BuildView { allTargetsToTest, baselineCoverageArtifacts, getArtifactFactory(), - CoverageReportValue.COVERAGE_REPORT_KEY); + CoverageReportValue.COVERAGE_REPORT_KEY, + loadingResult.getWorkspaceName()); if (actionsWrapper != null) { ImmutableList<ActionAnalysisMetadata> actions = actionsWrapper.getActions(); skyframeExecutor.injectCoverageReportData(actions); diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java index ad4ecc4086..34cd54fd8c 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java @@ -197,8 +197,7 @@ public class SkylarkRuleClassFunctions implements SkylarkRuleFunctionsApi<Artifa BaseRuleClasses.coverageReportGeneratorAttribute( labelCache.getUnchecked( toolsRepository - + BaseRuleClasses.DEFAULT_COVERAGE_REPORT_GENERATOR_VALUE))) - .singleArtifact()) + + BaseRuleClasses.DEFAULT_COVERAGE_REPORT_GENERATOR_VALUE)))) .add(attr(":run_under", LABEL).value(RUN_UNDER)) .executionPlatformConstraintsAllowed(ExecutionPlatformConstraintsAllowed.PER_TARGET) .build(); diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/CoverageReportActionFactory.java b/src/main/java/com/google/devtools/build/lib/analysis/test/CoverageReportActionFactory.java index e5678f389a..24750a4505 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/test/CoverageReportActionFactory.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/CoverageReportActionFactory.java @@ -66,5 +66,7 @@ public interface CoverageReportActionFactory { BlazeDirectories directories, Collection<ConfiguredTarget> targetsToTest, Iterable<Artifact> baselineCoverageArtifacts, - ArtifactFactory artifactFactory, ArtifactOwner artifactOwner); + ArtifactFactory artifactFactory, + ArtifactOwner artifactOwner, + String workspaceName); } diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java index e421c15596..7fe18a2300 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java @@ -323,13 +323,14 @@ public final class TestActionBuilder { } } // TODO(bazel-team): Passing the reportGenerator to every TestParams is a bit strange. - Artifact reportGenerator = null; + FilesToRunProvider reportGenerator = null; if (config.isCodeCoverageEnabled()) { // It's not enough to add this if the rule has coverage enabled because the command line may // contain rules with baseline coverage but no test rules that have coverage enabled, and in // that case, we still need the report generator. - reportGenerator = ruleContext.getPrerequisiteArtifact( - ":coverage_report_generator", Mode.HOST); + TransitiveInfoCollection reportGeneratorTarget = + ruleContext.getPrerequisite(":coverage_report_generator", Mode.HOST); + reportGenerator = reportGeneratorTarget.getProvider(FilesToRunProvider.class); } return new TestParams(runsPerTest, shards, TestTimeout.getTestTimeout(ruleContext.getRule()), diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestProvider.java index ce99d72e69..f356deb4bc 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestProvider.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestProvider.java @@ -16,6 +16,7 @@ package com.google.devtools.build.lib.analysis.test; import com.google.common.collect.ImmutableList; import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.analysis.FilesToRunProvider; import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; import com.google.devtools.build.lib.analysis.TransitiveInfoProvider; import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; @@ -71,7 +72,7 @@ public final class TestProvider implements TransitiveInfoProvider { private final String testRuleClass; private final ImmutableList<Artifact> testStatusArtifacts; private final ImmutableList<Artifact> coverageArtifacts; - private final Artifact coverageReportGenerator; + private final FilesToRunProvider coverageReportGenerator; /** * Don't call this directly. Instead use @@ -80,7 +81,7 @@ public final class TestProvider implements TransitiveInfoProvider { TestParams(int runs, int shards, TestTimeout timeout, String testRuleClass, ImmutableList<Artifact> testStatusArtifacts, ImmutableList<Artifact> coverageArtifacts, - Artifact coverageReportGenerator) { + FilesToRunProvider coverageReportGenerator) { this.runs = runs; this.shards = shards; this.timeout = timeout; @@ -136,7 +137,7 @@ public final class TestProvider implements TransitiveInfoProvider { /** * Returns the coverage report generator tool. */ - public Artifact getCoverageReportGenerator() { + public FilesToRunProvider getCoverageReportGenerator() { return coverageReportGenerator; } } 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()); + } +} diff --git a/src/test/shell/bazel/bazel_coverage_test.sh b/src/test/shell/bazel/bazel_coverage_test.sh index 41a51a65cf..8235f7cf79 100755 --- a/src/test/shell/bazel/bazel_coverage_test.sh +++ b/src/test/shell/bazel/bazel_coverage_test.sh @@ -232,6 +232,94 @@ EOF fi } +function test_java_test_coverage_combined_report() { + + cat <<EOF > BUILD +java_test( + name = "test", + srcs = glob(["src/test/**/*.java"]), + test_class = "com.example.TestCollatz", + deps = [":collatz-lib"], +) + +java_library( + name = "collatz-lib", + srcs = glob(["src/main/**/*.java"]), +) +EOF + + mkdir -p src/main/com/example + cat <<EOF > src/main/com/example/Collatz.java +package com.example; + +public class Collatz { + + public static int getCollatzFinal(int n) { + if (n == 1) { + return 1; + } + if (n % 2 == 0) { + return getCollatzFinal(n / 2); + } else { + return getCollatzFinal(n * 3 + 1); + } + } + +} +EOF + + mkdir -p src/test/com/example + cat <<EOF > src/test/com/example/TestCollatz.java +package com.example; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +public class TestCollatz { + + @Test + public void testGetCollatzFinal() { + assertEquals(Collatz.getCollatzFinal(1), 1); + assertEquals(Collatz.getCollatzFinal(5), 1); + assertEquals(Collatz.getCollatzFinal(10), 1); + assertEquals(Collatz.getCollatzFinal(21), 1); + } + +} +EOF + + bazel coverage //:test --coverage_report_generator=@bazel_tools//tools/test/LcovMerger/java/com/google/devtools/lcovmerger:Main --combined_report=lcov &>$TEST_log \ + || echo "Coverage for //:test failed" + + cat <<EOF > result.dat +SF:com/example/Collatz.java +FN:3,com/example/Collatz::<init> ()V +FN:6,com/example/Collatz::getCollatzFinal (I)I +FNDA:0,com/example/Collatz::<init> ()V +FNDA:1,com/example/Collatz::getCollatzFinal (I)I +FNF:2 +FNH:1 +BA:6,2 +BA:9,2 +BRF:2 +BRH:2 +DA:3,0 +DA:6,3 +DA:7,2 +DA:9,4 +DA:10,5 +DA:12,7 +LH:5 +LF:6 +end_of_record +EOF + + if ! cmp result.dat ./bazel-out/_coverage/_coverage_report.dat; then + diff result.dat bazel-out/_coverage/_coverage_report.dat >> $TEST_log + fail "Coverage output file is different with expected" + fi +} + function test_java_test_java_import_coverage() { cat <<EOF > BUILD diff --git a/tools/test/BUILD b/tools/test/BUILD index 3f7d28b72a..4a4e56b284 100644 --- a/tools/test/BUILD +++ b/tools/test/BUILD @@ -24,7 +24,7 @@ filegroup( filegroup( name = "coverage_report_generator", - srcs = ["//tools/coverage:coverage_report_generator"], + srcs = ["@bazel_tools//tools/test/LcovMerger/java/com/google/devtools/lcovmerger:Main"], ) filegroup( diff --git a/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/BUILD.tools b/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/BUILD.tools index 37cf8e4657..29a60f0fef 100644 --- a/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/BUILD.tools +++ b/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/BUILD.tools @@ -7,6 +7,7 @@ java_import( java_binary( name = "Main", + create_executable = 1, main_class = "com.google.devtools.lcovmerger.Main", runtime_deps = [":all_lcov_merger_lib"], ) diff --git a/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/Main.java b/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/Main.java index 5bac98e80d..2984d7b274 100644 --- a/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/Main.java +++ b/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/Main.java @@ -15,12 +15,15 @@ package com.google.devtools.lcovmerger; import static com.google.devtools.lcovmerger.LcovConstants.TRACEFILE_EXTENSION; +import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.annotations.VisibleForTesting; +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -48,14 +51,23 @@ public class Main { System.exit(1); } - List<File> lcovTracefiles = getLcovTracefiles(flags.get("coverage_dir")); + List<File> lcovTracefiles = new ArrayList<>(); + if (flags.containsKey("coverage_dir")) { + logger.log(Level.SEVERE, "Retrieving tracefiles from coverage_dir."); + lcovTracefiles = getLcovTracefilesFromDir(flags.get("coverage_dir")); + } else if (flags.containsKey("reports_file")) { + logger.log(Level.SEVERE, "Retrieving tracefiles from reports_file."); + lcovTracefiles = getLcovTracefilesFromFile(flags.get("reports_file")); + } if (lcovTracefiles.isEmpty()) { logger.log(Level.SEVERE, "No lcov file found."); System.exit(1); } + logger.log(Level.SEVERE, "Found " + lcovTracefiles.size() + " tracefiles."); Coverage coverage = new Coverage(); for (File tracefile : lcovTracefiles) { try { + logger.log(Level.SEVERE, "Parsing tracefile " + tracefile.toString()); List<SourceFileCoverage> sourceFilesCoverage = LcovParser.parse(new FileInputStream(tracefile)); for (SourceFileCoverage sourceFileCoverage : sourceFilesCoverage) { @@ -72,7 +84,8 @@ public class Main { File coverageFile = new File(outputFile); LcovPrinter.print(new FileOutputStream(coverageFile), coverage); } catch (IOException e) { - logger.log(Level.SEVERE, "Could not write to output file " + outputFile); + logger.log(Level.SEVERE, + "Could not write to output file " + outputFile + " due to " + e.getMessage()); exitStatus = 1; } System.exit(exitStatus); @@ -83,19 +96,37 @@ public class Main { * directory. */ @VisibleForTesting - static List<File> getLcovTracefiles(String coverageDir) { + static List<File> getLcovTracefilesFromDir(String coverageDir) { List<File> datFiles = new ArrayList<>(); try (Stream<Path> stream = Files.walk(Paths.get(coverageDir))) { datFiles = stream.filter(p -> p.toString().endsWith(TRACEFILE_EXTENSION)) .map(path -> path.toFile()) .collect(Collectors.toList()); } catch (IOException ex) { - logger.log(Level.SEVERE, "error reading folder " + coverageDir + ": " + ex.getMessage()); + logger.log(Level.SEVERE, "Error reading folder " + coverageDir + ": " + ex.getMessage()); } return datFiles; } + static List<File> getLcovTracefilesFromFile(String reportsFile) { + List<File> datFiles = new ArrayList<>(); + try (FileInputStream inputStream = new FileInputStream(reportsFile)) { + InputStreamReader inputStreamReader = new InputStreamReader(inputStream, UTF_8); + BufferedReader reader = new BufferedReader(inputStreamReader); + for (String tracefile = reader.readLine(); tracefile != null; tracefile = reader.readLine()) { + // TODO(elenairina): baseline coverage contains some file names that need to be modified + if (!tracefile.endsWith("baseline_coverage.dat")) { + datFiles.add(new File(tracefile)); + } + } + + } catch (IOException e) { + logger.log(Level.SEVERE, "Error reading file " + reportsFile + ": " + e.getMessage()); + } + return datFiles; + } + /** * Parse flags in the form of "--coverage_dir=... -output_file=..." */ @@ -114,12 +145,28 @@ public class Main { } // Validate flags - if (!flags.containsKey("coverage_dir")) { - throw new IllegalArgumentException("coverage_dir was not specified"); + for (String flag : flags.keySet()) { + switch (flag) { + case "coverage_dir": + case "reports_file": + case "output_file": + continue; + default: + throw new IllegalArgumentException("Unknown flag --" + flag); + } + } + + if (!flags.containsKey("coverage_dir") && !flags.containsKey("reports_file")) { + throw new IllegalArgumentException( + "At least one of --coverage_dir or --reports_file should be specified."); + } + if (flags.containsKey("coverage_dir") && flags.containsKey("reports_file")) { + throw new IllegalArgumentException( + "Only one of --coverage_dir or --reports_file must be specified."); } if (!flags.containsKey("output_file")) { // Different from blaze, this should be mandatory - throw new IllegalArgumentException("output_file was not specified"); + throw new IllegalArgumentException("--output_file was not specified"); } return flags; diff --git a/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/MainTest.java b/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/MainTest.java index 07be4215e5..a6b99fb12e 100644 --- a/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/MainTest.java +++ b/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/MainTest.java @@ -41,7 +41,7 @@ public class MainTest { @Test public void testMainEmptyCoverageDir() { - assertThat(Main.getLcovTracefiles(coverageDir.toAbsolutePath().toString())).isEmpty(); + assertThat(Main.getLcovTracefilesFromDir(coverageDir.toAbsolutePath().toString())).isEmpty(); } @Test @@ -52,7 +52,7 @@ public class MainTest { Files.createTempFile(ccCoverageDir, "tracefile1", ".dat"); Files.createTempFile(javaCoverageDir, "tracefile2", ".dat"); - List<File> tracefiles = Main.getLcovTracefiles(coverageDir.toAbsolutePath().toString()); + List<File> tracefiles = Main.getLcovTracefilesFromDir(coverageDir.toAbsolutePath().toString()); assertThat(tracefiles).hasSize(2); } } |