blob: 0022330eb42d737c48c9a39f573cea5bd9284161 [file] [log] [blame]
// Copyright 2014 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.analysis.test;
import static com.google.devtools.build.lib.packages.BuildType.LABEL;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactRoot;
import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.PrerequisiteArtifacts;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.RunfilesSupport;
import com.google.devtools.build.lib.analysis.ShToolchain;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.actions.LazyWriteNestedSetOfPairAction;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.test.TestProvider.TestParams;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.packages.TargetUtils;
import com.google.devtools.build.lib.packages.TestSize;
import com.google.devtools.build.lib.packages.TestTimeout;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.common.options.EnumConverter;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.annotation.Nullable;
/**
* Helper class to create test actions.
*/
public final class TestActionBuilder {
private static final String CC_CODE_COVERAGE_SCRIPT = "CC_CODE_COVERAGE_SCRIPT";
private static final String LCOV_MERGER = "LCOV_MERGER";
// The coverage tool Bazel uses to generate a code coverage report for C++.
private static final String BAZEL_CC_COVERAGE_TOOL = "BAZEL_CC_COVERAGE_TOOL";
private static final String GCOV_TOOL = "GCOV";
// A file that contains a mapping between the reported source file path and the actual source
// file path, relative to the workspace directory, if the two values are different. If the
// reported source file is the same as the actual source path it will not be included in the file.
private static final String COVERAGE_REPORTED_TO_ACTUAL_SOURCES_FILE =
"COVERAGE_REPORTED_TO_ACTUAL_SOURCES_FILE";
private final RuleContext ruleContext;
private RunfilesSupport runfilesSupport;
private Artifact executable;
private ExecutionInfo executionRequirements;
private InstrumentedFilesInfo instrumentedFiles;
private int explicitShardCount;
private Map<String, String> extraEnv;
public TestActionBuilder(RuleContext ruleContext) {
this.ruleContext = ruleContext;
this.extraEnv = new TreeMap<>();
}
/**
* Creates the test actions and artifacts using the previously set parameters.
*
* @return ordered list of test status artifacts
*/
public TestParams build() {
Preconditions.checkState(runfilesSupport != null);
boolean local = TargetUtils.isTestRuleAndRunsLocally(ruleContext.getRule());
TestShardingStrategy strategy =
ruleContext.getConfiguration().getFragment(TestConfiguration.class).testShardingStrategy();
int shards = strategy.getNumberOfShards(
local, explicitShardCount, isTestShardingCompliant(),
TestSize.getTestSize(ruleContext.getRule()));
Preconditions.checkState(shards >= 0);
return createTestAction(shards);
}
private boolean isTestShardingCompliant() {
// See if it has a data dependency on the special target
// //tools:test_sharding_compliant. Test runners add this dependency
// to show they speak the sharding protocol.
// There are certain cases where this heuristic may fail, giving
// a "false positive" (where we shard the test even though the
// it isn't supported). We may want to refine this logic, but
// heuristically sharding is currently experimental. Also, we do detect
// false-positive cases and return an error.
return runfilesSupport.getRunfilesSymlinkNames().contains(
PathFragment.create("tools/test_sharding_compliant"));
}
/**
* Set the runfiles and executable to be run as a test.
*/
public TestActionBuilder setFilesToRunProvider(FilesToRunProvider provider) {
Preconditions.checkNotNull(provider.getRunfilesSupport());
Preconditions.checkNotNull(provider.getExecutable());
this.runfilesSupport = provider.getRunfilesSupport();
this.executable = provider.getExecutable();
return this;
}
public TestActionBuilder setInstrumentedFiles(@Nullable InstrumentedFilesInfo instrumentedFiles) {
this.instrumentedFiles = instrumentedFiles;
return this;
}
public TestActionBuilder setExecutionRequirements(
@Nullable ExecutionInfo executionRequirements) {
this.executionRequirements = executionRequirements;
return this;
}
public TestActionBuilder addExtraEnv(Map<String, String> extraEnv) {
this.extraEnv.putAll(extraEnv);
return this;
}
/**
* Set the explicit shard count. Note that this may be overridden by the sharding strategy.
*/
public TestActionBuilder setShardCount(int explicitShardCount) {
this.explicitShardCount = explicitShardCount;
return this;
}
/**
* Converts to {@link TestActionBuilder.TestShardingStrategy}.
*/
public static class ShardingStrategyConverter extends EnumConverter<TestShardingStrategy> {
public ShardingStrategyConverter() {
super(TestShardingStrategy.class, "test sharding strategy");
}
}
/**
* A strategy for running the same tests in many processes.
*/
public static enum TestShardingStrategy {
EXPLICIT {
@Override public int getNumberOfShards(boolean isLocal, int shardCountFromAttr,
boolean testShardingCompliant, TestSize testSize) {
return Math.max(shardCountFromAttr, 0);
}
},
DISABLED {
@Override public int getNumberOfShards(boolean isLocal, int shardCountFromAttr,
boolean testShardingCompliant, TestSize testSize) {
return 0;
}
};
public abstract int getNumberOfShards(boolean isLocal, int shardCountFromAttr,
boolean testShardingCompliant, TestSize testSize);
}
/**
* Creates a test action and artifacts for the given rule. The test action will
* use the specified executable and runfiles.
*
* @return ordered list of test artifacts, one per action. These are used to drive
* execution in Skyframe, and by AggregatingTestListener and
* TestResultAnalyzer to keep track of completed and pending test runs.
*/
private TestParams createTestAction(int shards) {
PathFragment targetName = PathFragment.create(ruleContext.getLabel().getName());
BuildConfiguration config = ruleContext.getConfiguration();
AnalysisEnvironment env = ruleContext.getAnalysisEnvironment();
ArtifactRoot root = config.getTestLogsDirectory(ruleContext.getRule().getRepository());
// TODO(laszlocsomor), TODO(ulfjack): `isExecutedOnWindows` should use the execution platform,
// not the host platform. Once Bazel can tell apart these platforms, fix the right side of this
// initialization.
final boolean isExecutedOnWindows = OS.getCurrent() == OS.WINDOWS;
final boolean isUsingTestWrapperInsteadOfTestSetupScript =
isExecutedOnWindows
&& ruleContext
.getConfiguration()
.getFragment(TestConfiguration.class)
.isUsingWindowsNativeTestWrapper();
NestedSetBuilder<Artifact> inputsBuilder = NestedSetBuilder.stableOrder();
inputsBuilder.addTransitive(
NestedSetBuilder.create(Order.STABLE_ORDER, runfilesSupport.getRunfilesMiddleman()));
if (!isUsingTestWrapperInsteadOfTestSetupScript) {
NestedSet<Artifact> testRuntime =
PrerequisiteArtifacts.nestedSet(ruleContext, "$test_runtime", Mode.HOST);
inputsBuilder.addTransitive(testRuntime);
}
TestTargetProperties testProperties = new TestTargetProperties(
ruleContext, executionRequirements);
// If the test rule does not provide InstrumentedFilesProvider, there's not much that we can do.
final boolean collectCodeCoverage = config.isCodeCoverageEnabled()
&& instrumentedFiles != null;
Artifact testActionExecutable =
isUsingTestWrapperInsteadOfTestSetupScript
? ruleContext.getHostPrerequisiteArtifact("$test_wrapper")
: ruleContext.getHostPrerequisiteArtifact("$test_setup_script");
inputsBuilder.add(testActionExecutable);
Artifact testXmlGeneratorExecutable =
isUsingTestWrapperInsteadOfTestSetupScript
? ruleContext.getHostPrerequisiteArtifact("$xml_writer")
: ruleContext.getHostPrerequisiteArtifact("$xml_generator_script");
inputsBuilder.add(testXmlGeneratorExecutable);
Artifact collectCoverageScript = null;
TreeMap<String, String> extraTestEnv = new TreeMap<>();
TestTargetExecutionSettings executionSettings;
if (collectCodeCoverage) {
collectCoverageScript = ruleContext.getHostPrerequisiteArtifact("$collect_coverage_script");
inputsBuilder.add(collectCoverageScript);
inputsBuilder.addTransitive(instrumentedFiles.getCoverageSupportFiles());
// Add instrumented file manifest artifact to the list of inputs. This file will contain
// exec paths of all source files that should be included into the code coverage output.
NestedSet<Artifact> metadataFiles = instrumentedFiles.getInstrumentationMetadataFiles();
inputsBuilder.addTransitive(metadataFiles);
inputsBuilder.addTransitive(
PrerequisiteArtifacts.nestedSet(ruleContext, ":coverage_support", Mode.DONT_CHECK));
if (ruleContext.isAttrDefined("$collect_cc_coverage", LABEL)) {
Artifact collectCcCoverage =
ruleContext.getHostPrerequisiteArtifact("$collect_cc_coverage");
inputsBuilder.add(collectCcCoverage);
extraTestEnv.put(CC_CODE_COVERAGE_SCRIPT, collectCcCoverage.getExecPathString());
}
if (!instrumentedFiles.getReportedToActualSources().isEmpty()) {
Artifact reportedToActualSourcesArtifact =
ruleContext.getUniqueDirectoryArtifact(
"_coverage_helpers", "reported_to_actual_sources.txt");
ruleContext.registerAction(
new LazyWriteNestedSetOfPairAction(
ruleContext.getActionOwner(),
reportedToActualSourcesArtifact,
instrumentedFiles.getReportedToActualSources()));
inputsBuilder.add(reportedToActualSourcesArtifact);
extraTestEnv.put(
COVERAGE_REPORTED_TO_ACTUAL_SOURCES_FILE,
reportedToActualSourcesArtifact.getExecPathString());
}
// lcov is the default CC coverage tool unless otherwise specified on the command line.
extraTestEnv.put(BAZEL_CC_COVERAGE_TOOL, GCOV_TOOL);
// We don't add this attribute to non-supported test target
String lcovMergerAttr = null;
if (ruleContext.isAttrDefined(":lcov_merger", LABEL)) {
lcovMergerAttr = ":lcov_merger";
} else if (ruleContext.isAttrDefined("$lcov_merger", LABEL)) {
lcovMergerAttr = "$lcov_merger";
}
if (lcovMergerAttr != null) {
TransitiveInfoCollection lcovMerger =
ruleContext.getPrerequisite(lcovMergerAttr, Mode.TARGET);
FilesToRunProvider lcovFilesToRun = lcovMerger.getProvider(FilesToRunProvider.class);
if (lcovFilesToRun != null) {
extraTestEnv.put(LCOV_MERGER, lcovFilesToRun.getExecutable().getExecPathString());
inputsBuilder.addTransitive(lcovFilesToRun.getFilesToRun());
} else {
NestedSet<Artifact> filesToBuild =
lcovMerger.getProvider(FileProvider.class).getFilesToBuild();
if (Iterables.size(filesToBuild) == 1) {
Artifact lcovMergerArtifact = Iterables.getOnlyElement(filesToBuild);
extraTestEnv.put(LCOV_MERGER, lcovMergerArtifact.getExecPathString());
inputsBuilder.add(lcovMergerArtifact);
} else {
ruleContext.attributeError(
lcovMergerAttr,
"the LCOV merger should be either an executable or a single artifact");
}
}
}
Artifact instrumentedFileManifest =
InstrumentedFileManifestAction.getInstrumentedFileManifest(ruleContext,
instrumentedFiles.getInstrumentedFiles(), metadataFiles);
executionSettings = new TestTargetExecutionSettings(ruleContext, runfilesSupport,
executable, instrumentedFileManifest, shards);
inputsBuilder.add(instrumentedFileManifest);
// TODO(ulfjack): Is this even ever set? If yes, does this cost us a lot of memory?
for (Pair<String, String> coverageEnvEntry : instrumentedFiles.getCoverageEnvironment()) {
extraTestEnv.put(coverageEnvEntry.getFirst(), coverageEnvEntry.getSecond());
}
} else {
executionSettings = new TestTargetExecutionSettings(ruleContext, runfilesSupport,
executable, null, shards);
}
extraTestEnv.putAll(extraEnv);
if (config.getRunUnder() != null) {
Artifact runUnderExecutable = executionSettings.getRunUnderExecutable();
if (runUnderExecutable != null) {
inputsBuilder.add(runUnderExecutable);
}
}
int runsPerTest =
config.getFragment(TestConfiguration.class).getRunsPerTestForLabel(ruleContext.getLabel());
Iterable<Artifact> inputs = inputsBuilder.build();
int shardRuns = (shards > 0 ? shards : 1);
List<Artifact.DerivedArtifact> results =
Lists.newArrayListWithCapacity(runsPerTest * shardRuns);
ImmutableList.Builder<Artifact> coverageArtifacts = ImmutableList.builder();
ImmutableList.Builder<ActionInput> testOutputs = ImmutableList.builder();
for (int run = 0; run < runsPerTest; run++) {
// Use a 1-based index for user friendliness.
String testRunDir =
runsPerTest > 1 ? String.format("run_%d_of_%d", run + 1, runsPerTest) : "";
for (int shard = 0; shard < shardRuns; shard++) {
String shardRunDir =
(shardRuns > 1 ? String.format("shard_%d_of_%d", shard + 1, shards) : "");
if (testRunDir.isEmpty()) {
shardRunDir = shardRunDir.isEmpty() ? "" : shardRunDir + PathFragment.SEPARATOR_CHAR;
} else {
testRunDir += PathFragment.SEPARATOR_CHAR;
shardRunDir = shardRunDir.isEmpty() ? testRunDir : shardRunDir + "_" + testRunDir;
}
Artifact.DerivedArtifact testLog =
ruleContext.getPackageRelativeArtifact(
targetName.getRelative(shardRunDir + "test.log"), root);
Artifact.DerivedArtifact cacheStatus =
ruleContext.getPackageRelativeArtifact(
targetName.getRelative(shardRunDir + "test.cache_status"), root);
Artifact.DerivedArtifact coverageArtifact = null;
if (collectCodeCoverage) {
coverageArtifact = ruleContext.getPackageRelativeArtifact(
targetName.getRelative(shardRunDir + "coverage.dat"), root);
coverageArtifacts.add(coverageArtifact);
}
TestRunnerAction testRunnerAction =
new TestRunnerAction(
ruleContext.getActionOwner(),
inputs,
testActionExecutable,
isUsingTestWrapperInsteadOfTestSetupScript,
testXmlGeneratorExecutable,
collectCoverageScript,
testLog,
cacheStatus,
coverageArtifact,
testProperties,
extraTestEnv,
executionSettings,
shard,
run,
config,
ruleContext.getWorkspaceName(),
(!isUsingTestWrapperInsteadOfTestSetupScript
|| executionSettings.needsShell(isExecutedOnWindows))
? ShToolchain.getPathOrError(ruleContext)
: null);
testOutputs.addAll(testRunnerAction.getSpawnOutputs());
testOutputs.addAll(testRunnerAction.getOutputs());
env.registerAction(testRunnerAction);
results.add(cacheStatus);
}
}
// TODO(bazel-team): Passing the reportGenerator to every TestParams is a bit strange.
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.
TransitiveInfoCollection reportGeneratorTarget =
ruleContext.getPrerequisite(":coverage_report_generator", Mode.HOST);
reportGenerator = reportGeneratorTarget.getProvider(FilesToRunProvider.class);
}
return new TestParams(
runsPerTest,
shards,
TestTimeout.getTestTimeout(ruleContext.getRule()),
ruleContext.getRule().getRuleClass(),
ImmutableList.copyOf(results),
coverageArtifacts.build(),
reportGenerator,
testOutputs.build());
}
}