com.google.devtools.build.lib.bazel.coverage.CoverageReportActionBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.lib.bazel.coverage.CoverageReportActionBuilder.java

Source

// 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());
    }
}