| // 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.rules.test; |
| |
| import static com.google.devtools.build.lib.packages.BuildType.LABEL; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Lists; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.Root; |
| import com.google.devtools.build.lib.analysis.AnalysisEnvironment; |
| import com.google.devtools.build.lib.analysis.FilesToRunProvider; |
| import com.google.devtools.build.lib.analysis.PrerequisiteArtifacts; |
| import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode; |
| import com.google.devtools.build.lib.analysis.RuleContext; |
| import com.google.devtools.build.lib.analysis.RunfilesSupport; |
| import com.google.devtools.build.lib.analysis.config.BuildConfiguration; |
| 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.rules.test.TestProvider.TestParams; |
| import com.google.devtools.build.lib.util.Pair; |
| import com.google.devtools.build.lib.util.Preconditions; |
| 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 final RuleContext ruleContext; |
| private RunfilesSupport runfilesSupport; |
| private Artifact executable; |
| private ExecutionInfoProvider executionRequirements; |
| private InstrumentedFilesProvider 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().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( |
| new PathFragment("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 InstrumentedFilesProvider instrumentedFiles) { |
| this.instrumentedFiles = instrumentedFiles; |
| return this; |
| } |
| |
| public TestActionBuilder setExecutionRequirements( |
| @Nullable ExecutionInfoProvider 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); |
| } |
| }, |
| |
| EXPERIMENTAL_HEURISTIC { |
| @Override public int getNumberOfShards(boolean isLocal, int shardCountFromAttr, |
| boolean testShardingCompliant, TestSize testSize) { |
| if (shardCountFromAttr >= 0) { |
| return shardCountFromAttr; |
| } |
| if (isLocal || !testShardingCompliant) { |
| return 0; |
| } |
| return testSize.getDefaultShards(); |
| } |
| }, |
| |
| 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 = new PathFragment(ruleContext.getLabel().getName()); |
| BuildConfiguration config = ruleContext.getConfiguration(); |
| AnalysisEnvironment env = ruleContext.getAnalysisEnvironment(); |
| Root root = config.getTestLogsDirectory(ruleContext.getRule().getRepository()); |
| |
| NestedSetBuilder<Artifact> inputsBuilder = NestedSetBuilder.stableOrder(); |
| inputsBuilder.addTransitive( |
| NestedSetBuilder.create(Order.STABLE_ORDER, runfilesSupport.getRunfilesMiddleman())); |
| 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; |
| |
| TreeMap<String, String> testEnv = new TreeMap<>(); |
| |
| TestTargetExecutionSettings executionSettings; |
| if (collectCodeCoverage) { |
| 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)); |
| // We don't add this attribute to non-supported test target |
| if (ruleContext.isAttrDefined("$lcov_merger", LABEL)) { |
| Artifact lcovMerger = ruleContext.getPrerequisiteArtifact("$lcov_merger", Mode.TARGET); |
| if (lcovMerger != null) { |
| inputsBuilder.addTransitive( |
| PrerequisiteArtifacts.nestedSet(ruleContext, "$lcov_merger", Mode.TARGET)); |
| // Pass this LcovMerger_deploy.jar path to collect_coverage.sh |
| testEnv.put("LCOV_MERGER", lcovMerger.getExecPathString()); |
| } |
| } |
| |
| Artifact instrumentedFileManifest = |
| InstrumentedFileManifestAction.getInstrumentedFileManifest(ruleContext, |
| instrumentedFiles.getInstrumentedFiles(), metadataFiles); |
| executionSettings = new TestTargetExecutionSettings(ruleContext, runfilesSupport, |
| executable, instrumentedFileManifest, shards); |
| inputsBuilder.add(instrumentedFileManifest); |
| for (Pair<String, String> coverageEnvEntry : instrumentedFiles.getCoverageEnvironment()) { |
| testEnv.put(coverageEnvEntry.getFirst(), coverageEnvEntry.getSecond()); |
| } |
| } else { |
| executionSettings = new TestTargetExecutionSettings(ruleContext, runfilesSupport, |
| executable, null, shards); |
| } |
| |
| testEnv.putAll(extraEnv); |
| |
| if (config.getRunUnder() != null) { |
| Artifact runUnderExecutable = executionSettings.getRunUnderExecutable(); |
| if (runUnderExecutable != null) { |
| inputsBuilder.add(runUnderExecutable); |
| } |
| } |
| |
| int runsPerTest = config.getRunsPerTestForLabel(ruleContext.getLabel()); |
| |
| Iterable<Artifact> inputs = inputsBuilder.build(); |
| int shardRuns = (shards > 0 ? shards : 1); |
| List<Artifact> results = Lists.newArrayListWithCapacity(runsPerTest * shardRuns); |
| ImmutableList.Builder<Artifact> coverageArtifacts = 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 testLog = |
| ruleContext.getPackageRelativeArtifact( |
| targetName.getRelative(shardRunDir + "test.log"), root); |
| Artifact cacheStatus = |
| ruleContext.getPackageRelativeArtifact( |
| targetName.getRelative(shardRunDir + "test.cache_status"), root); |
| |
| Artifact coverageArtifact = null; |
| if (collectCodeCoverage) { |
| coverageArtifact = ruleContext.getPackageRelativeArtifact( |
| targetName.getRelative(shardRunDir + "coverage.dat"), root); |
| coverageArtifacts.add(coverageArtifact); |
| } |
| |
| Artifact microCoverageArtifact = null; |
| if (collectCodeCoverage && config.isMicroCoverageEnabled()) { |
| microCoverageArtifact = ruleContext.getPackageRelativeArtifact( |
| targetName.getRelative(shardRunDir + "coverage.micro.dat"), root); |
| } |
| |
| env.registerAction(new TestRunnerAction( |
| ruleContext.getActionOwner(), inputs, testRuntime, |
| testLog, cacheStatus, |
| coverageArtifact, microCoverageArtifact, |
| testProperties, testEnv, executionSettings, |
| shard, run, config, ruleContext.getWorkspaceName())); |
| results.add(cacheStatus); |
| } |
| } |
| // TODO(bazel-team): Passing the reportGenerator to every TestParams is a bit strange. |
| Artifact 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); |
| } |
| |
| return new TestParams(runsPerTest, shards, TestTimeout.getTestTimeout(ruleContext.getRule()), |
| ruleContext.getRule().getRuleClass(), ImmutableList.copyOf(results), |
| coverageArtifacts.build(), reportGenerator); |
| } |
| } |