| // 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.analysis.test.ExecutionInfo.DEFAULT_TEST_RUNNER_EXEC_GROUP; |
| import static com.google.devtools.build.lib.packages.BuildType.LABEL; |
| |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Lists; |
| import com.google.devtools.build.lib.actions.ActionInput; |
| import com.google.devtools.build.lib.actions.ActionOwner; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.ArtifactRoot; |
| import com.google.devtools.build.lib.actions.RunfilesSupplier; |
| import com.google.devtools.build.lib.analysis.Allowlist; |
| 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.PackageSpecificationProvider; |
| import com.google.devtools.build.lib.analysis.PrerequisiteArtifacts; |
| import com.google.devtools.build.lib.analysis.RuleContext; |
| import com.google.devtools.build.lib.analysis.RunfilesProvider; |
| import com.google.devtools.build.lib.analysis.RunfilesSupport; |
| import com.google.devtools.build.lib.analysis.ShToolchain; |
| import com.google.devtools.build.lib.analysis.SingleRunfilesSupplier; |
| 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.BuildConfigurationValue; |
| import com.google.devtools.build.lib.analysis.configuredtargets.PackageGroupConfiguredTarget; |
| 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.PackageSpecification.PackageGroupContents; |
| import com.google.devtools.build.lib.packages.TestTimeout; |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization; |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant; |
| 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.errorprone.annotations.CanIgnoreReturnValue; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| 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"; |
| |
| static class EmptyPackageProvider extends PackageGroupConfiguredTarget { |
| public EmptyPackageProvider() { |
| super(null, null, null); |
| } |
| |
| @Override |
| public NestedSet<PackageGroupContents> getPackageSpecifications() { |
| return NestedSetBuilder.emptySet(Order.STABLE_ORDER); |
| } |
| } |
| |
| @VisibleForSerialization @SerializationConstant |
| static final PackageSpecificationProvider EMPTY_PACKAGES_PROVIDER = new EmptyPackageProvider(); |
| |
| private final RuleContext ruleContext; |
| private final ImmutableList.Builder<Artifact> additionalTools; |
| private RunfilesSupport runfilesSupport; |
| private Artifact executable; |
| private ExecutionInfo executionRequirements; |
| private InstrumentedFilesInfo instrumentedFiles; |
| private int explicitShardCount; |
| private final Map<String, String> extraEnv; |
| private final Set<String> extraInheritedEnv; |
| |
| public TestActionBuilder(RuleContext ruleContext) { |
| this.ruleContext = ruleContext; |
| this.extraEnv = new TreeMap<>(); |
| this.extraInheritedEnv = new TreeSet<>(); |
| this.additionalTools = new ImmutableList.Builder<>(); |
| } |
| |
| /** |
| * Creates test actions for a test that will never be executed. |
| * |
| * <p>This is only really useful for things like creating incompatible test actions. |
| */ |
| public static TestParams createEmptyTestParams() { |
| NestedSet<Artifact> filesToBuild = NestedSetBuilder.emptySet(Order.STABLE_ORDER); |
| FilesToRunProvider filesToRunProvider = new FilesToRunProvider(filesToBuild, null, null); |
| return new TestProvider.TestParams( |
| 0, |
| 0, |
| false, |
| TestTimeout.ETERNAL, |
| "invalid", |
| ImmutableList.<Artifact.DerivedArtifact>of(), |
| ImmutableList.<Artifact>of(), |
| filesToRunProvider, |
| ImmutableList.<ActionInput>of()); |
| } |
| |
| /** |
| * Creates the test actions and artifacts using the previously set parameters. |
| * |
| * @return ordered list of test status artifacts |
| */ |
| public TestParams build() throws InterruptedException { // due to TestTargetExecutionSettings |
| Preconditions.checkNotNull(runfilesSupport); |
| TestShardingStrategy strategy = |
| ruleContext.getConfiguration().getFragment(TestConfiguration.class).testShardingStrategy(); |
| int shards = strategy.getNumberOfShards(explicitShardCount); |
| Preconditions.checkState(shards >= 0, "%s returned negative shard count %s", strategy, shards); |
| return createTestAction(shards); |
| } |
| |
| /** Set the runfiles and executable to be run as a test. */ |
| @CanIgnoreReturnValue |
| public TestActionBuilder setFilesToRunProvider(FilesToRunProvider provider) { |
| Preconditions.checkNotNull(provider.getRunfilesSupport()); |
| Preconditions.checkNotNull(provider.getExecutable()); |
| this.runfilesSupport = provider.getRunfilesSupport(); |
| this.executable = provider.getExecutable(); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestActionBuilder addTools(List<Artifact> tools) { |
| this.additionalTools.addAll(tools); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestActionBuilder setInstrumentedFiles(@Nullable InstrumentedFilesInfo instrumentedFiles) { |
| this.instrumentedFiles = instrumentedFiles; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestActionBuilder setExecutionRequirements(@Nullable ExecutionInfo executionRequirements) { |
| this.executionRequirements = executionRequirements; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestActionBuilder addExtraEnv(Map<String, String> extraEnv) { |
| this.extraEnv.putAll(extraEnv); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestActionBuilder addExtraInheritedEnv(List<String> extraInheritedEnv) { |
| this.extraInheritedEnv.addAll(extraInheritedEnv); |
| return this; |
| } |
| |
| /** Set the explicit shard count. Note that this may be overridden by the sharding strategy. */ |
| @CanIgnoreReturnValue |
| public TestActionBuilder setShardCount(int explicitShardCount) { |
| this.explicitShardCount = explicitShardCount; |
| return this; |
| } |
| |
| private ActionOwner getOwner() { |
| ActionOwner owner = |
| ruleContext.getActionOwner( |
| this.executionRequirements != null |
| ? this.executionRequirements.getExecGroup() |
| : DEFAULT_TEST_RUNNER_EXEC_GROUP); |
| return owner == null ? ruleContext.getActionOwner() : owner; |
| } |
| |
| /** |
| * 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) |
| throws InterruptedException { // due to TestTargetExecutionSettings |
| PathFragment targetName = PathFragment.create(ruleContext.getLabel().getName()); |
| BuildConfigurationValue config = ruleContext.getConfiguration(); |
| TestConfiguration testConfiguration = config.getFragment(TestConfiguration.class); |
| AnalysisEnvironment env = ruleContext.getAnalysisEnvironment(); |
| ArtifactRoot root = ruleContext.getTestLogsDirectory(); |
| |
| // 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; |
| |
| NestedSetBuilder<Artifact> inputsBuilder = NestedSetBuilder.stableOrder(); |
| inputsBuilder.addTransitive( |
| NestedSetBuilder.create(Order.STABLE_ORDER, runfilesSupport.getRunfilesMiddleman())); |
| |
| if (!isUsingTestWrapperInsteadOfTestSetupScript) { |
| NestedSet<Artifact> testRuntime = |
| PrerequisiteArtifacts.nestedSet(ruleContext, "$test_runtime"); |
| 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<>(); |
| |
| int runsPerTest = testConfiguration.getRunsPerTestForLabel(ruleContext.getLabel()); |
| |
| NestedSetBuilder<Artifact> lcovMergerFilesToRun = NestedSetBuilder.compileOrder(); |
| RunfilesSupplier lcovMergerRunfilesSupplier = null; |
| |
| 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")); |
| inputsBuilder.addTransitive( |
| ruleContext |
| .getPrerequisite(":coverage_support", RunfilesProvider.class) |
| .getDataRunfiles() |
| .getAllArtifacts()); |
| |
| 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); |
| FilesToRunProvider lcovFilesToRun = lcovMerger.getProvider(FilesToRunProvider.class); |
| if (lcovFilesToRun != null) { |
| extraTestEnv.put(LCOV_MERGER, lcovFilesToRun.getExecutable().getExecPathString()); |
| inputsBuilder.addTransitive(lcovFilesToRun.getFilesToRun()); |
| |
| lcovMergerFilesToRun.addTransitive(lcovFilesToRun.getFilesToRun()); |
| if (lcovFilesToRun.getRunfilesSupport() != null) { |
| lcovMergerFilesToRun.add(lcovFilesToRun.getRunfilesSupport().getRunfilesMiddleman()); |
| } |
| lcovMergerRunfilesSupplier = lcovFilesToRun.getRunfilesSupplier(); |
| } else { |
| NestedSet<Artifact> filesToBuild = |
| lcovMerger.getProvider(FileProvider.class).getFilesToBuild(); |
| |
| if (filesToBuild.isSingleton()) { |
| Artifact lcovMergerArtifact = filesToBuild.getSingleton(); |
| extraTestEnv.put(LCOV_MERGER, lcovMergerArtifact.getExecPathString()); |
| inputsBuilder.add(lcovMergerArtifact); |
| lcovMergerFilesToRun.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, |
| runsPerTest); |
| 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().toList()) { |
| extraTestEnv.put(coverageEnvEntry.getFirst(), coverageEnvEntry.getSecond()); |
| } |
| } else { |
| executionSettings = |
| new TestTargetExecutionSettings( |
| ruleContext, runfilesSupport, executable, null, shards, runsPerTest); |
| } |
| |
| extraTestEnv.putAll(extraEnv); |
| |
| if (config.getRunUnder() != null) { |
| Artifact runUnderExecutable = executionSettings.getRunUnderExecutable(); |
| if (runUnderExecutable != null) { |
| inputsBuilder.add(runUnderExecutable); |
| } |
| } |
| |
| NestedSet<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(); |
| |
| SingleRunfilesSupplier testRunfilesSupplier; |
| if (shardRuns > 1 || runsPerTest > 1) { |
| // When creating multiple test actions, cache the runfiles mappings across test actions. This |
| // saves a lot of garbage when shard_count and/or runs_per_test is high. |
| testRunfilesSupplier = |
| SingleRunfilesSupplier.createCaching( |
| runfilesSupport.getRunfilesDirectoryExecPath(), |
| runfilesSupport.getRunfiles(), |
| runfilesSupport.isBuildRunfileLinks(), |
| runfilesSupport.isRunfilesEnabled()); |
| } else { |
| testRunfilesSupplier = SingleRunfilesSupplier.create(runfilesSupport); |
| } |
| |
| // Use 1-based indices for user friendliness. |
| for (int shard = 0; shard < shardRuns; shard++) { |
| String shardDir = shardRuns > 1 ? String.format("shard_%d_of_%d", shard + 1, shards) : null; |
| for (int run = 0; run < runsPerTest; run++) { |
| PathFragment dir; |
| if (runsPerTest > 1) { |
| String runDir = String.format("run_%d_of_%d", run + 1, runsPerTest); |
| if (shardDir == null) { |
| dir = targetName.getRelative(runDir); |
| } else { |
| dir = targetName.getRelative(shardDir + "_" + runDir); |
| } |
| } else if (shardDir == null) { |
| dir = targetName; |
| } else { |
| dir = targetName.getRelative(shardDir); |
| } |
| |
| Artifact.DerivedArtifact testLog = |
| ruleContext.getPackageRelativeArtifact(dir.getRelative("test.log"), root); |
| Artifact.DerivedArtifact cacheStatus = |
| ruleContext.getPackageRelativeArtifact(dir.getRelative("test.cache_status"), root); |
| |
| Artifact.DerivedArtifact coverageArtifact = null; |
| Artifact coverageDirectory = null; |
| if (collectCodeCoverage) { |
| coverageArtifact = |
| ruleContext.getPackageRelativeArtifact(dir.getRelative("coverage.dat"), root); |
| coverageArtifacts.add(coverageArtifact); |
| if (testConfiguration.fetchAllCoverageOutputs()) { |
| coverageDirectory = |
| ruleContext.getPackageRelativeTreeArtifact(dir.getRelative("_coverage"), root); |
| } |
| } |
| |
| boolean cancelConcurrentTests = |
| testConfiguration.runsPerTestDetectsFlakes() |
| && testConfiguration.cancelConcurrentTests(); |
| |
| boolean splitCoveragePostProcessing = testConfiguration.splitCoveragePostProcessing(); |
| // TODO(b/234923262): Take exec_group into consideration when selecting sh tools |
| TestRunnerAction testRunnerAction = |
| new TestRunnerAction( |
| getOwner(), |
| inputs, |
| testRunfilesSupplier, |
| testActionExecutable, |
| testXmlGeneratorExecutable, |
| collectCoverageScript, |
| testLog, |
| cacheStatus, |
| coverageArtifact, |
| coverageDirectory, |
| testProperties, |
| runfilesSupport |
| .getActionEnvironment() |
| .withAdditionalVariables(extraTestEnv, extraInheritedEnv), |
| executionSettings, |
| shard, |
| run, |
| config, |
| ruleContext.getWorkspaceName(), |
| (!isUsingTestWrapperInsteadOfTestSetupScript |
| || executionSettings.needsShell(isExecutedOnWindows)) |
| ? ShToolchain.getPathOrError(ruleContext.getExecutionPlatform()) |
| : null, |
| cancelConcurrentTests, |
| splitCoveragePostProcessing, |
| lcovMergerFilesToRun, |
| lcovMergerRunfilesSupplier, |
| // Network allowlist only makes sense in workspaces which explicitly add it, use an |
| // empty one as a fallback. |
| MoreObjects.firstNonNull( |
| Allowlist.fetchPackageSpecificationProviderOrNull( |
| ruleContext, "external_network"), |
| EMPTY_PACKAGES_PROVIDER)); |
| |
| 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"); |
| reportGenerator = reportGeneratorTarget.getProvider(FilesToRunProvider.class); |
| } |
| |
| return new TestParams( |
| runsPerTest, |
| shards, |
| testConfiguration.runsPerTestDetectsFlakes(), |
| TestTimeout.getTestTimeout(ruleContext.getRule()), |
| ruleContext.getRule().getRuleClass(), |
| ImmutableList.copyOf(results), |
| coverageArtifacts.build(), |
| reportGenerator, |
| testOutputs.build()); |
| } |
| } |