diff options
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/analysis')
27 files changed, 2956 insertions, 21 deletions
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 539b4e3b03..df7d1d5e13 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 @@ -37,6 +37,7 @@ import com.google.devtools.build.lib.analysis.config.HostTransition; import com.google.devtools.build.lib.analysis.config.PatchTransition; import com.google.devtools.build.lib.analysis.config.RunUnder; import com.google.devtools.build.lib.analysis.constraints.EnvironmentRule; +import com.google.devtools.build.lib.analysis.test.TestConfiguration; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.packages.Attribute; import com.google.devtools.build.lib.packages.Attribute.LateBoundLabel; @@ -48,7 +49,6 @@ import com.google.devtools.build.lib.packages.RuleClass; import com.google.devtools.build.lib.packages.RuleClass.Builder; import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType; import com.google.devtools.build.lib.packages.TestSize; -import com.google.devtools.build.lib.rules.test.TestConfiguration; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.util.FileTypeSet; import java.util.List; 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 5396334615..87cb74ad9c 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 @@ -46,6 +46,9 @@ import com.google.devtools.build.lib.analysis.config.DynamicTransitionMapper; import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException; import com.google.devtools.build.lib.analysis.config.PatchTransition; import com.google.devtools.build.lib.analysis.constraints.TopLevelConstraintSemantics; +import com.google.devtools.build.lib.analysis.test.CoverageReportActionFactory; +import com.google.devtools.build.lib.analysis.test.CoverageReportActionFactory.CoverageReportActionsWrapper; +import com.google.devtools.build.lib.analysis.test.InstrumentedFilesProvider; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.cmdline.PackageIdentifier; import com.google.devtools.build.lib.collect.nestedset.NestedSet; @@ -74,9 +77,6 @@ import com.google.devtools.build.lib.packages.Target; import com.google.devtools.build.lib.packages.TargetUtils; import com.google.devtools.build.lib.pkgcache.LoadingResult; import com.google.devtools.build.lib.pkgcache.PackageManager.PackageManagerStatistics; -import com.google.devtools.build.lib.rules.test.CoverageReportActionFactory; -import com.google.devtools.build.lib.rules.test.CoverageReportActionFactory.CoverageReportActionsWrapper; -import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider; import com.google.devtools.build.lib.skyframe.AspectValue; import com.google.devtools.build.lib.skyframe.AspectValue.AspectKey; import com.google.devtools.build.lib.skyframe.AspectValue.AspectValueKey; diff --git a/src/main/java/com/google/devtools/build/lib/analysis/FileConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/FileConfiguredTarget.java index 2768ceca1f..dc65db2c64 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/FileConfiguredTarget.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/FileConfiguredTarget.java @@ -16,13 +16,13 @@ package com.google.devtools.build.lib.analysis; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.analysis.fileset.FilesetProvider; +import com.google.devtools.build.lib.analysis.test.InstrumentedFilesProvider; 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.FileTarget; import com.google.devtools.build.lib.packages.Info; import com.google.devtools.build.lib.packages.Provider; -import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider; import com.google.devtools.build.lib.util.FileType; /** diff --git a/src/main/java/com/google/devtools/build/lib/analysis/OutputFileConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/OutputFileConfiguredTarget.java index b9959e1dcb..ba449aec84 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/OutputFileConfiguredTarget.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/OutputFileConfiguredTarget.java @@ -15,10 +15,10 @@ package com.google.devtools.build.lib.analysis; import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.analysis.test.InstrumentedFilesProvider; +import com.google.devtools.build.lib.analysis.test.InstrumentedFilesProviderImpl; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.packages.OutputFile; -import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider; -import com.google.devtools.build.lib.rules.test.InstrumentedFilesProviderImpl; import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter; import com.google.devtools.build.lib.util.Pair; import com.google.devtools.build.lib.util.Preconditions; diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java index a11536a035..67b4a6ca65 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java @@ -24,6 +24,12 @@ import com.google.devtools.build.lib.analysis.constraints.ConstraintSemantics; import com.google.devtools.build.lib.analysis.constraints.EnvironmentCollection; import com.google.devtools.build.lib.analysis.constraints.SupportedEnvironments; import com.google.devtools.build.lib.analysis.constraints.SupportedEnvironmentsProvider; +import com.google.devtools.build.lib.analysis.test.ExecutionInfoProvider; +import com.google.devtools.build.lib.analysis.test.InstrumentedFilesProvider; +import com.google.devtools.build.lib.analysis.test.TestActionBuilder; +import com.google.devtools.build.lib.analysis.test.TestEnvironmentProvider; +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.cmdline.Label; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; @@ -34,12 +40,6 @@ import com.google.devtools.build.lib.packages.NativeProvider; import com.google.devtools.build.lib.packages.Provider; import com.google.devtools.build.lib.packages.Target; import com.google.devtools.build.lib.packages.TargetUtils; -import com.google.devtools.build.lib.rules.test.ExecutionInfoProvider; -import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider; -import com.google.devtools.build.lib.rules.test.TestActionBuilder; -import com.google.devtools.build.lib.rules.test.TestEnvironmentProvider; -import com.google.devtools.build.lib.rules.test.TestProvider; -import com.google.devtools.build.lib.rules.test.TestProvider.TestParams; import com.google.devtools.build.lib.syntax.EvalException; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.util.Preconditions; diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java index 7e50b36ba0..48ac8fb8c1 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java @@ -21,6 +21,7 @@ import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.EventReportingArtifacts; import com.google.devtools.build.lib.analysis.TopLevelArtifactHelper.ArtifactsInOutputGroup; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; +import com.google.devtools.build.lib.analysis.test.TestProvider; import com.google.devtools.build.lib.buildeventstream.ArtifactGroupNamer; import com.google.devtools.build.lib.buildeventstream.BuildEvent; import com.google.devtools.build.lib.buildeventstream.BuildEventConverters; @@ -39,7 +40,6 @@ import com.google.devtools.build.lib.collect.nestedset.NestedSetView; import com.google.devtools.build.lib.collect.nestedset.Order; import com.google.devtools.build.lib.packages.AttributeMap; import com.google.devtools.build.lib.packages.TestSize; -import com.google.devtools.build.lib.rules.test.TestProvider; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.skyframe.SkyValue; diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelper.java b/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelper.java index a9b77b4074..9e7d534597 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelper.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelper.java @@ -19,10 +19,10 @@ import static com.google.devtools.build.lib.util.Preconditions.checkNotNull; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.analysis.test.TestProvider; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; -import com.google.devtools.build.lib.rules.test.TestProvider; import com.google.devtools.build.lib.skyframe.AspectValue; import javax.annotation.Nullable; 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 e716d00b11..f4f5084c16 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 @@ -42,6 +42,7 @@ import com.google.devtools.build.lib.analysis.OutputGroupProvider; import com.google.devtools.build.lib.analysis.PlatformSemantics; import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; import com.google.devtools.build.lib.analysis.skylark.SkylarkAttr.Descriptor; +import com.google.devtools.build.lib.analysis.test.TestConfiguration; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.cmdline.LabelSyntaxException; import com.google.devtools.build.lib.cmdline.LabelValidator; @@ -72,7 +73,6 @@ import com.google.devtools.build.lib.packages.SkylarkExportable; import com.google.devtools.build.lib.packages.SkylarkProvider; import com.google.devtools.build.lib.packages.TargetUtils; import com.google.devtools.build.lib.packages.TestSize; -import com.google.devtools.build.lib.rules.test.TestConfiguration; import com.google.devtools.build.lib.skylarkinterface.Param; import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter; import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature; diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java index 599f6f5d4b..74a69ca17e 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java @@ -26,6 +26,9 @@ import com.google.devtools.build.lib.analysis.Runfiles; import com.google.devtools.build.lib.analysis.RunfilesProvider; import com.google.devtools.build.lib.analysis.RunfilesSupport; import com.google.devtools.build.lib.analysis.SkylarkProviderValidationUtil; +import com.google.devtools.build.lib.analysis.test.InstrumentedFilesCollector; +import com.google.devtools.build.lib.analysis.test.InstrumentedFilesCollector.InstrumentationSpec; +import com.google.devtools.build.lib.analysis.test.InstrumentedFilesProvider; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.events.Location; @@ -33,9 +36,6 @@ import com.google.devtools.build.lib.packages.Info; import com.google.devtools.build.lib.packages.NativeProvider; import com.google.devtools.build.lib.packages.Rule; import com.google.devtools.build.lib.packages.TargetUtils; -import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector; -import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector.InstrumentationSpec; -import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider; import com.google.devtools.build.lib.skylarkinterface.SkylarkValue; import com.google.devtools.build.lib.syntax.BaseFunction; import com.google.devtools.build.lib.syntax.ClassObject; diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java index 916acabb2f..5f383d6763 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java @@ -37,6 +37,8 @@ import com.google.devtools.build.lib.analysis.RuleContext; import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; import com.google.devtools.build.lib.analysis.config.FragmentCollection; +import com.google.devtools.build.lib.analysis.test.InstrumentedFilesCollector; +import com.google.devtools.build.lib.analysis.test.InstrumentedFilesProvider; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.events.Location; @@ -52,8 +54,6 @@ import com.google.devtools.build.lib.packages.OutputFile; import com.google.devtools.build.lib.packages.Package; import com.google.devtools.build.lib.packages.Provider; import com.google.devtools.build.lib.packages.RawAttributeMapper; -import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector; -import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider; import com.google.devtools.build.lib.shell.ShellUtils; import com.google.devtools.build.lib.shell.ShellUtils.TokenizationException; import com.google.devtools.build.lib.skylarkinterface.Param; diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/BaselineCoverageAction.java b/src/main/java/com/google/devtools/build/lib/analysis/test/BaselineCoverageAction.java new file mode 100644 index 0000000000..2953ed68de --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/BaselineCoverageAction.java @@ -0,0 +1,124 @@ +// 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 com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.eventbus.EventBus; +import com.google.devtools.build.lib.actions.ActionExecutionContext; +import com.google.devtools.build.lib.actions.ActionOwner; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit; +import com.google.devtools.build.lib.analysis.RuleContext; +import com.google.devtools.build.lib.analysis.actions.AbstractFileWriteAction; +import com.google.devtools.build.lib.cmdline.Label; +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.concurrent.ThreadSafety.Immutable; +import com.google.devtools.build.lib.util.Fingerprint; +import com.google.devtools.build.lib.vfs.PathFragment; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Generates baseline (empty) coverage for the given non-test target. + */ +@VisibleForTesting +@Immutable +public final class BaselineCoverageAction extends AbstractFileWriteAction + implements NotifyOnActionCacheHit { + private final NestedSet<Artifact> instrumentedFiles; + + private BaselineCoverageAction( + ActionOwner owner, NestedSet<Artifact> instrumentedFiles, Artifact output) { + super(owner, ImmutableList.<Artifact>of(), output, false); + this.instrumentedFiles = instrumentedFiles; + } + + @Override + public String getMnemonic() { + return "BaselineCoverage"; + } + + @Override + public String computeKey() { + return new Fingerprint() + .addStrings(getInstrumentedFilePathStrings()) + .hexDigestAndReset(); + } + + private Iterable<String> getInstrumentedFilePathStrings() { + List<String> result = new ArrayList<>(); + for (Artifact instrumentedFile : instrumentedFiles) { + result.add(instrumentedFile.getExecPathString()); + } + return result; + } + + @Override + public DeterministicWriter newDeterministicWriter(ActionExecutionContext ctx) { + return new DeterministicWriter() { + @Override + public void writeOutputFile(OutputStream out) throws IOException { + PrintWriter writer = new PrintWriter(out); + for (String execPath : getInstrumentedFilePathStrings()) { + writer.write("SF:" + execPath + "\n"); + writer.write("end_of_record\n"); + } + writer.flush(); + } + }; + } + + @Override + protected void afterWrite(ActionExecutionContext actionExecutionContext) { + notifyAboutBaselineCoverage(actionExecutionContext.getEventBus()); + } + + @Override + public void actionCacheHit(ActionCachedContext context) { + notifyAboutBaselineCoverage(context.getEventBus()); + } + + /** + * Notify interested parties about new baseline coverage data. + */ + private void notifyAboutBaselineCoverage(EventBus eventBus) { + Artifact output = Iterables.getOnlyElement(getOutputs()); + String ownerString = Label.print(getOwner().getLabel()); + eventBus.post(new BaselineCoverageResult(output, ownerString)); + } + + /** + * Returns collection of baseline coverage artifacts associated with the given target. + * Will always return 0 or 1 elements. + */ + static NestedSet<Artifact> create( + RuleContext ruleContext, NestedSet<Artifact> instrumentedFiles) { + // Baseline coverage artifacts will still go into "testlogs" directory. + Artifact coverageData = ruleContext.getPackageRelativeArtifact( + PathFragment.create(ruleContext.getTarget().getName()).getChild("baseline_coverage.dat"), + ruleContext.getConfiguration().getTestLogsDirectory( + ruleContext.getRule().getRepository())); + ruleContext.registerAction(new BaselineCoverageAction( + ruleContext.getActionOwner(), instrumentedFiles, coverageData)); + return NestedSetBuilder.create(Order.STABLE_ORDER, coverageData); + } +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/BaselineCoverageResult.java b/src/main/java/com/google/devtools/build/lib/analysis/test/BaselineCoverageResult.java new file mode 100644 index 0000000000..ec31264284 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/BaselineCoverageResult.java @@ -0,0 +1,40 @@ +// 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 com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.util.Preconditions; + +/** + * This event is used to notify about a successfully built baseline coverage artifact. + */ +public class BaselineCoverageResult { + + private final Artifact baselineCoverageData; + private final String ownerString; + + public BaselineCoverageResult(Artifact baselineCoverageData, String ownerString) { + this.baselineCoverageData = Preconditions.checkNotNull(baselineCoverageData); + this.ownerString = Preconditions.checkNotNull(ownerString); + } + + public Artifact getArtifact() { + return baselineCoverageData; + } + + public String getOwnerString() { + return ownerString; + } +} 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 new file mode 100644 index 0000000000..e5678f389a --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/CoverageReportActionFactory.java @@ -0,0 +1,70 @@ +// 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 com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.lib.actions.ActionAnalysisMetadata; +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.events.EventHandler; +import java.util.Collection; +import javax.annotation.Nullable; + +/** + * A factory class to create coverage report actions. + */ +public interface CoverageReportActionFactory { + /** + * Wraps the necessary actions to get a coverage report as well as the final output artifacts. + * The lcovWriteAction creates a file containing a set of lcov files. This file is used as an + * input artifact for coverageReportAction. We are only interested about the output artifacts from + * coverageReportAction. + */ + public static final class CoverageReportActionsWrapper { + private final ActionAnalysisMetadata lcovWriteAction; + private final ActionAnalysisMetadata coverageReportAction; + + public CoverageReportActionsWrapper ( + ActionAnalysisMetadata lcovWriteAction, ActionAnalysisMetadata coverageReportAction) { + this.lcovWriteAction = lcovWriteAction; + this.coverageReportAction = coverageReportAction; + } + + public ImmutableList<ActionAnalysisMetadata> getActions() { + return ImmutableList.of(lcovWriteAction, coverageReportAction); + } + + public ImmutableSet<Artifact> getCoverageOutputs() { + return coverageReportAction.getOutputs(); + } + } + + /** + * Returns a CoverageReportActionsWrapper. May return null if it's not necessary to create + * such Actions based on the input parameters and some other data available to the factory + * implementation, such as command line options. + */ + @Nullable + CoverageReportActionsWrapper createCoverageReportActionsWrapper( + EventHandler eventHandler, + BlazeDirectories directories, + Collection<ConfiguredTarget> targetsToTest, + Iterable<Artifact> baselineCoverageArtifacts, + ArtifactFactory artifactFactory, ArtifactOwner artifactOwner); +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/ExecutionInfoProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/test/ExecutionInfoProvider.java new file mode 100644 index 0000000000..616db25bb4 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/ExecutionInfoProvider.java @@ -0,0 +1,48 @@ +// 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 com.google.common.collect.ImmutableMap; +import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; +import com.google.devtools.build.lib.packages.Info; +import com.google.devtools.build.lib.packages.NativeProvider; +import java.util.Map; + +/** + * This provider can be implemented by rules which need special environments to run in (especially + * tests). + */ +@Immutable +public final class ExecutionInfoProvider extends Info { + + /** Skylark constructor and identifier for ExecutionInfoProvider. */ + public static final NativeProvider<ExecutionInfoProvider> SKYLARK_CONSTRUCTOR = + new NativeProvider<ExecutionInfoProvider>(ExecutionInfoProvider.class, "ExecutionInfo") {}; + + private final ImmutableMap<String, String> executionInfo; + + public ExecutionInfoProvider(Map<String, String> requirements) { + super(SKYLARK_CONSTRUCTOR, ImmutableMap.<String, Object>of("requirements", requirements)); + this.executionInfo = ImmutableMap.copyOf(requirements); + } + + /** + * Returns a map to indicate special execution requirements, such as hardware + * platforms, etc. Rule tags, such as "requires-XXX", may also be added + * as keys to the map. + */ + public ImmutableMap<String, String> getExecutionInfo() { + return executionInfo; + } +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/InstrumentedFileManifestAction.java b/src/main/java/com/google/devtools/build/lib/analysis/test/InstrumentedFileManifestAction.java new file mode 100644 index 0000000000..5e1c60f200 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/InstrumentedFileManifestAction.java @@ -0,0 +1,113 @@ +// 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 java.nio.charset.StandardCharsets.ISO_8859_1; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.collect.Iterables; +import com.google.devtools.build.lib.actions.ActionExecutionContext; +import com.google.devtools.build.lib.actions.ActionOwner; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.analysis.RuleContext; +import com.google.devtools.build.lib.analysis.actions.AbstractFileWriteAction; +import com.google.devtools.build.lib.collect.nestedset.NestedSet; +import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; +import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; +import com.google.devtools.build.lib.util.Fingerprint; +import com.google.devtools.build.lib.util.Preconditions; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.Arrays; + +/** + * Writes a manifest of instrumented source and metadata files. + */ +@Immutable +final class InstrumentedFileManifestAction extends AbstractFileWriteAction { + private static final Function<Artifact, String> TO_EXEC_PATH = new Function<Artifact, String>() { + @Override + public String apply(Artifact artifact) { + return artifact.getExecPathString(); + } + }; + + private static final String GUID = "3833f0a3-7ea1-4d9f-b96f-66eff4c922b0"; + + private final NestedSet<Artifact> files; + + @VisibleForTesting + InstrumentedFileManifestAction(ActionOwner owner, NestedSet<Artifact> files, Artifact output) { + super(owner, /*inputs=*/Artifact.NO_ARTIFACTS, output, /*makeExecutable=*/false); + this.files = files; + } + + @Override + public DeterministicWriter newDeterministicWriter(ActionExecutionContext ctx) { + return new DeterministicWriter() { + @Override + public void writeOutputFile(OutputStream out) throws IOException { + // Sort the exec paths before writing them out. + String[] fileNames = + Iterables.toArray(Iterables.transform(files, TO_EXEC_PATH), String.class); + Arrays.sort(fileNames); + try (Writer writer = new OutputStreamWriter(out, ISO_8859_1)) { + for (String name : fileNames) { + writer.write(name); + writer.write('\n'); + } + } + } + }; + } + + @Override + protected String computeKey() { + Fingerprint f = new Fingerprint(); + f.addString(GUID); + // Not sorting here is probably cheaper, though it might lead to unnecessary re-execution. + f.addStrings(Iterables.transform(files, TO_EXEC_PATH)); + return f.hexDigestAndReset(); + } + + /** + * Instantiates instrumented file manifest for the given target. + * + * @param ruleContext context of the executable configured target + * @param additionalSourceFiles additional instrumented source files, as + * collected by the {@link InstrumentedFilesCollector} + * @param metadataFiles *.gcno/*.em files collected by the {@link InstrumentedFilesCollector} + * @return instrumented file manifest artifact + */ + public static Artifact getInstrumentedFileManifest(RuleContext ruleContext, + NestedSet<Artifact> additionalSourceFiles, NestedSet<Artifact> metadataFiles) { + // Instrumented manifest makes sense only for rules with binary output. + Preconditions.checkState(ruleContext.getRule().hasBinaryOutput()); + Artifact instrumentedFileManifest = ruleContext.getBinArtifact( + ruleContext.getTarget().getName() + ".instrumented_files"); + + NestedSet<Artifact> inputs = NestedSetBuilder.<Artifact>stableOrder() + .addTransitive(additionalSourceFiles) + .addTransitive(metadataFiles) + .build(); + ruleContext.registerAction(new InstrumentedFileManifestAction( + ruleContext.getActionOwner(), inputs, instrumentedFileManifest)); + + return instrumentedFileManifest; + } +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/InstrumentedFilesCollector.java b/src/main/java/com/google/devtools/build/lib/analysis/test/InstrumentedFilesCollector.java new file mode 100644 index 0000000000..30d7d8f7c4 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/InstrumentedFilesCollector.java @@ -0,0 +1,308 @@ +// 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 com.google.common.collect.ImmutableList; +import com.google.devtools.build.lib.actions.ActionAnalysisMetadata; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.analysis.AnalysisEnvironment; +import com.google.devtools.build.lib.analysis.FileProvider; +import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode; +import com.google.devtools.build.lib.analysis.RuleContext; +import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; +import com.google.devtools.build.lib.analysis.config.BuildConfiguration; +import com.google.devtools.build.lib.cmdline.Label; +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.concurrent.ThreadSafety.Immutable; +import com.google.devtools.build.lib.packages.BuildType; +import com.google.devtools.build.lib.util.FileType; +import com.google.devtools.build.lib.util.FileTypeSet; +import com.google.devtools.build.lib.util.Pair; +import com.google.devtools.build.lib.util.Preconditions; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * A helper class for collecting instrumented files and metadata for a target. + */ +public final class InstrumentedFilesCollector { + + /** + * Forwards any instrumented files from the given target's dependencies (as defined in + * {@code dependencyAttributes}) for further export. No files from this target are considered + * instrumented. + * + * @return instrumented file provider of all dependencies in {@code dependencyAttributes} + */ + public static InstrumentedFilesProvider forward( + RuleContext ruleContext, String... dependencyAttributes) { + return collect( + ruleContext, + new InstrumentationSpec(FileTypeSet.NO_FILE).withDependencyAttributes(dependencyAttributes), + null, + null); + } + + public static InstrumentedFilesProvider collect( + RuleContext ruleContext, + InstrumentationSpec spec, + LocalMetadataCollector localMetadataCollector, + Iterable<Artifact> rootFiles) { + return collect(ruleContext, spec, localMetadataCollector, rootFiles, + NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER), + NestedSetBuilder.<Pair<String, String>>emptySet(Order.STABLE_ORDER), + false); + } + + /** + * Collects transitive instrumentation data from dependencies, collects local source files from + * dependencies, collects local metadata files by traversing the action graph of the current + * configured target, collect rule-specific instrumentation support file sand creates baseline + * coverage actions for the transitive closure of source files (if + * <code>withBaselineCoverage</code> is true). + */ + public static InstrumentedFilesProvider collect( + RuleContext ruleContext, + InstrumentationSpec spec, + LocalMetadataCollector localMetadataCollector, + Iterable<Artifact> rootFiles, + NestedSet<Artifact> coverageSupportFiles, + NestedSet<Pair<String, String>> coverageEnvironment, + boolean withBaselineCoverage) { + Preconditions.checkNotNull(ruleContext); + Preconditions.checkNotNull(spec); + + if (!ruleContext.getConfiguration().isCodeCoverageEnabled()) { + return InstrumentedFilesProviderImpl.EMPTY; + } + + NestedSetBuilder<Artifact> instrumentedFilesBuilder = NestedSetBuilder.stableOrder(); + NestedSetBuilder<Artifact> metadataFilesBuilder = NestedSetBuilder.stableOrder(); + NestedSetBuilder<Artifact> baselineCoverageInstrumentedFilesBuilder = + NestedSetBuilder.stableOrder(); + NestedSetBuilder<Artifact> coverageSupportFilesBuilder = + NestedSetBuilder.<Artifact>stableOrder() + .addTransitive(coverageSupportFiles); + NestedSetBuilder<Pair<String, String>> coverageEnvironmentBuilder = + NestedSetBuilder.<Pair<String, String>>compileOrder() + .addTransitive(coverageEnvironment); + + + // Transitive instrumentation data. + for (TransitiveInfoCollection dep : + getAllPrerequisites(ruleContext, spec.dependencyAttributes)) { + InstrumentedFilesProvider provider = dep.getProvider(InstrumentedFilesProvider.class); + if (provider != null) { + instrumentedFilesBuilder.addTransitive(provider.getInstrumentedFiles()); + metadataFilesBuilder.addTransitive(provider.getInstrumentationMetadataFiles()); + baselineCoverageInstrumentedFilesBuilder.addTransitive( + provider.getBaselineCoverageInstrumentedFiles()); + coverageSupportFilesBuilder.addTransitive(provider.getCoverageSupportFiles()); + coverageEnvironmentBuilder.addTransitive(provider.getCoverageEnvironment()); + } + } + + // Local sources. + NestedSet<Artifact> localSources = NestedSetBuilder.emptySet(Order.STABLE_ORDER); + if (shouldIncludeLocalSources(ruleContext)) { + NestedSetBuilder<Artifact> localSourcesBuilder = NestedSetBuilder.stableOrder(); + for (TransitiveInfoCollection dep : + getAllPrerequisites(ruleContext, spec.sourceAttributes)) { + if (!spec.splitLists && dep.getProvider(InstrumentedFilesProvider.class) != null) { + continue; + } + for (Artifact artifact : dep.getProvider(FileProvider.class).getFilesToBuild()) { + if (artifact.isSourceArtifact() && + spec.instrumentedFileTypes.matches(artifact.getFilename())) { + localSourcesBuilder.add(artifact); + } + } + } + localSources = localSourcesBuilder.build(); + } + instrumentedFilesBuilder.addTransitive(localSources); + if (withBaselineCoverage) { + // Also add the local sources to the baseline coverage instrumented sources, if the current + // rule supports baseline coverage. + // TODO(ulfjack): Generate a local baseline coverage action, and then merge at the leaves. + baselineCoverageInstrumentedFilesBuilder.addTransitive(localSources); + } + + // Local metadata files. + if (localMetadataCollector != null) { + localMetadataCollector.collectMetadataArtifacts(rootFiles, + ruleContext.getAnalysisEnvironment(), metadataFilesBuilder); + } + + // Baseline coverage actions. + NestedSet<Artifact> baselineCoverageFiles = baselineCoverageInstrumentedFilesBuilder.build(); + + // Create one baseline coverage action per target, but for the transitive closure of files. + NestedSet<Artifact> baselineCoverageArtifacts = + BaselineCoverageAction.create(ruleContext, baselineCoverageFiles); + return new InstrumentedFilesProviderImpl( + instrumentedFilesBuilder.build(), + metadataFilesBuilder.build(), + baselineCoverageFiles, + baselineCoverageArtifacts, + coverageSupportFilesBuilder.build(), + coverageEnvironmentBuilder.build()); + } + + /** + * Return whether the sources of the rule in {@code ruleContext} should be instrumented based on + * the --instrumentation_filter and --instrument_test_targets config settings. + */ + public static boolean shouldIncludeLocalSources(RuleContext ruleContext) { + return shouldIncludeLocalSources(ruleContext.getConfiguration(), ruleContext.getLabel(), + ruleContext.isTestTarget()); + } + + /** + * Return whether the sources included by {@code target} (a {@link TransitiveInfoCollection} + * representing a rule) should be instrumented according the --instrumentation_filter and + * --instrument_test_targets settings in {@code config}. + */ + public static boolean shouldIncludeLocalSources(BuildConfiguration config, + TransitiveInfoCollection target) { + return shouldIncludeLocalSources(config, target.getLabel(), + target.getProvider(TestProvider.class) != null); + } + + private static boolean shouldIncludeLocalSources(BuildConfiguration config, Label label, + boolean isTest) { + return ((config.shouldInstrumentTestTargets() || !isTest) + && config.getInstrumentationFilter().isIncluded(label.toString())); + } + + /** + * The set of file types and attributes to visit to collect instrumented files for a certain rule + * type. The class is intentionally immutable, so that a single instance is sufficient for all + * rules of the same type (and in some cases all rules of related types, such as all {@code foo_*} + * rules). + */ + @Immutable + public static final class InstrumentationSpec { + private final FileTypeSet instrumentedFileTypes; + + /** The list of attributes which should be checked for sources. */ + private final Collection<String> sourceAttributes; + + /** The list of attributes from which to collect transitive coverage information. */ + private final Collection<String> dependencyAttributes; + + /** Whether the source and dependency lists are separate. */ + private final boolean splitLists; + + public InstrumentationSpec(FileTypeSet instrumentedFileTypes, + String... instrumentedAttributes) { + this(instrumentedFileTypes, ImmutableList.copyOf(instrumentedAttributes)); + } + + public InstrumentationSpec(FileTypeSet instrumentedFileTypes, + Collection<String> instrumentedAttributes) { + this(instrumentedFileTypes, instrumentedAttributes, instrumentedAttributes, false); + } + + private InstrumentationSpec(FileTypeSet instrumentedFileTypes, + Collection<String> instrumentedSourceAttributes, + Collection<String> instrumentedDependencyAttributes, + boolean splitLists) { + this.instrumentedFileTypes = instrumentedFileTypes; + this.sourceAttributes = ImmutableList.copyOf(instrumentedSourceAttributes); + this.dependencyAttributes = + ImmutableList.copyOf(instrumentedDependencyAttributes); + this.splitLists = splitLists; + } + + /** + * Returns a new instrumentation spec with the given attribute names replacing the ones + * stored in this object. + */ + public InstrumentationSpec withAttributes(String... instrumentedAttributes) { + return new InstrumentationSpec(instrumentedFileTypes, instrumentedAttributes); + } + + /** + * Returns a new instrumentation spec with the given attribute names replacing the ones + * stored in this object. + */ + public InstrumentationSpec withSourceAttributes(String... instrumentedAttributes) { + return new InstrumentationSpec(instrumentedFileTypes, + ImmutableList.copyOf(instrumentedAttributes), dependencyAttributes, true); + } + + /** + * Returns a new instrumentation spec with the given attribute names replacing the ones + * stored in this object. + */ + public InstrumentationSpec withDependencyAttributes(String... instrumentedAttributes) { + return new InstrumentationSpec(instrumentedFileTypes, + sourceAttributes, ImmutableList.copyOf(instrumentedAttributes), true); + } + } + + /** + * The implementation for the local metadata collection. The intention is that implementations + * recurse over the locally (i.e., for that configured target) created actions and collect + * metadata files. + */ + public abstract static class LocalMetadataCollector { + /** + * Recursively runs over the local actions and add metadata files to the metadataFilesBuilder. + */ + public abstract void collectMetadataArtifacts( + Iterable<Artifact> artifacts, AnalysisEnvironment analysisEnvironment, + NestedSetBuilder<Artifact> metadataFilesBuilder); + + /** + * Adds action output of a particular type to metadata files. + * + * <p>Only adds the first output that matches the given file type. + * + * @param metadataFilesBuilder builder to collect metadata files + * @param action the action whose outputs to scan + * @param fileType the filetype of outputs which should be collected + */ + protected void addOutputs(NestedSetBuilder<Artifact> metadataFilesBuilder, + ActionAnalysisMetadata action, FileType fileType) { + for (Artifact output : action.getOutputs()) { + if (fileType.matches(output.getFilename())) { + metadataFilesBuilder.add(output); + break; + } + } + } + } + + /** + * An explicit constant for a {@link LocalMetadataCollector} that doesn't collect anything. + */ + public static final LocalMetadataCollector NO_METADATA_COLLECTOR = null; + + private static Iterable<TransitiveInfoCollection> getAllPrerequisites( + RuleContext ruleContext, Collection<String> attributeNames) { + List<TransitiveInfoCollection> prerequisites = new ArrayList<>(); + for (String attr : attributeNames) { + if (ruleContext.getRule().isAttrDefined(attr, BuildType.LABEL_LIST) || + ruleContext.getRule().isAttrDefined(attr, BuildType.LABEL)) { + prerequisites.addAll(ruleContext.getPrerequisites(attr, Mode.DONT_CHECK)); + } + } + return prerequisites; + } +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/InstrumentedFilesProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/test/InstrumentedFilesProvider.java new file mode 100644 index 0000000000..f4773d79f2 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/InstrumentedFilesProvider.java @@ -0,0 +1,66 @@ +// 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 com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.analysis.TransitiveInfoProvider; +import com.google.devtools.build.lib.collect.nestedset.NestedSet; +import com.google.devtools.build.lib.util.Pair; + +/** + * A provider of instrumented file sources and instrumentation metadata. + */ +public interface InstrumentedFilesProvider extends TransitiveInfoProvider { + + /** + * The transitive closure of instrumented source files. + */ + NestedSet<Artifact> getInstrumentedFiles(); + + /** + * Returns a collection of instrumentation metadata files. + */ + NestedSet<Artifact> getInstrumentationMetadataFiles(); + + /** + * The transitive closure of instrumented source files for which baseline coverage should be + * generated. In general, this is a subset of the instrumented source files: it only contains + * instrumented source files from rules that support baseline coverage. + */ + NestedSet<Artifact> getBaselineCoverageInstrumentedFiles(); + + /** + * The output artifact of the baseline coverage action; this is only ever a single artifact, which + * contains baseline coverage for the entire transitive closure of source files. + */ + // TODO(ulfjack): Change this to a single Artifact. Also change how it's generated. It's better to + // generate actions such that each action only covers the source files of a single rule, in + // particular because baseline coverage is language-specific (it requires a parser for the + // specific language), and we don't want to depend on all language parsers from any single rule. + NestedSet<Artifact> getBaselineCoverageArtifacts(); + + /** + * Extra files that are needed on the inputs of test actions for coverage collection to happen, + * for example, {@code gcov}. + * + * <p>They aren't mentioned in the instrumented files manifest. + */ + NestedSet<Artifact> getCoverageSupportFiles(); + + /** + * Environment variables that need to be set for tests collecting code coverage. + */ + NestedSet<Pair<String, String>> getCoverageEnvironment(); +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/InstrumentedFilesProviderImpl.java b/src/main/java/com/google/devtools/build/lib/analysis/test/InstrumentedFilesProviderImpl.java new file mode 100644 index 0000000000..d0ca5fdf69 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/InstrumentedFilesProviderImpl.java @@ -0,0 +1,86 @@ +// 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 com.google.devtools.build.lib.actions.Artifact; +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.util.Pair; + +/** + * An implementation class for the InstrumentedFilesProvider interface. + */ +public final class InstrumentedFilesProviderImpl implements InstrumentedFilesProvider { + public static final InstrumentedFilesProvider EMPTY = + new InstrumentedFilesProviderImpl( + NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER), + NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER), + NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER), + NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER), + NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER), + NestedSetBuilder.<Pair<String, String>>emptySet(Order.COMPILE_ORDER)); + + private final NestedSet<Artifact> instrumentedFiles; + private final NestedSet<Artifact> instrumentationMetadataFiles; + private final NestedSet<Artifact> baselineCoverageFiles; + private final NestedSet<Artifact> baselineCoverageArtifacts; + private final NestedSet<Artifact> coverageSupportFiles; + private final NestedSet<Pair<String, String>> coverageEnvironment; + + public InstrumentedFilesProviderImpl( + NestedSet<Artifact> instrumentedFiles, + NestedSet<Artifact> instrumentationMetadataFiles, + NestedSet<Artifact> baselineCoverageFiles, + NestedSet<Artifact> baselineCoverageArtifacts, + NestedSet<Artifact> coverageSupportFiles, + NestedSet<Pair<String, String>> coverageEnvironment) { + this.instrumentedFiles = instrumentedFiles; + this.instrumentationMetadataFiles = instrumentationMetadataFiles; + this.baselineCoverageFiles = baselineCoverageFiles; + this.baselineCoverageArtifacts = baselineCoverageArtifacts; + this.coverageSupportFiles = coverageSupportFiles; + this.coverageEnvironment = coverageEnvironment; + } + + @Override + public NestedSet<Artifact> getInstrumentedFiles() { + return instrumentedFiles; + } + + @Override + public NestedSet<Artifact> getInstrumentationMetadataFiles() { + return instrumentationMetadataFiles; + } + + @Override + public NestedSet<Artifact> getBaselineCoverageInstrumentedFiles() { + return baselineCoverageFiles; + } + + @Override + public NestedSet<Artifact> getBaselineCoverageArtifacts() { + return baselineCoverageArtifacts; + } + + @Override + public NestedSet<Artifact> getCoverageSupportFiles() { + return coverageSupportFiles; + } + + @Override + public NestedSet<Pair<String, String>> getCoverageEnvironment() { + return coverageEnvironment; + } +} 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 new file mode 100644 index 0000000000..3194f3f529 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java @@ -0,0 +1,327 @@ +// 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.collect.ImmutableList; +import com.google.common.collect.Iterables; +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.FileProvider; +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.TransitiveInfoCollection; +import com.google.devtools.build.lib.analysis.config.BuildConfiguration; +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.syntax.Type; +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().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 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 = PathFragment.create(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> extraTestEnv = 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)) { + TransitiveInfoCollection lcovMerger = + ruleContext.getPrerequisite("$lcov_merger", 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("$lcov_merger", + "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> results = Lists.newArrayListWithCapacity(runsPerTest * shardRuns); + ImmutableList.Builder<Artifact> coverageArtifacts = ImmutableList.builder(); + + boolean useTestRunner = false; + if (ruleContext.attributes().has("use_testrunner", Type.BOOLEAN)) { + useTestRunner = ruleContext.attributes().get("use_testrunner", Type.BOOLEAN); + } + + 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); + } + + env.registerAction(new TestRunnerAction( + ruleContext.getActionOwner(), inputs, testRuntime, + testLog, cacheStatus, + coverageArtifact, + testProperties, extraTestEnv, executionSettings, + shard, run, config, ruleContext.getWorkspaceName(), + useTestRunner)); + 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); + } +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionContext.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionContext.java new file mode 100644 index 0000000000..cc483f6ab6 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionContext.java @@ -0,0 +1,40 @@ +// 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 com.google.devtools.build.lib.actions.ActionContext; +import com.google.devtools.build.lib.actions.ActionExecutionContext; +import com.google.devtools.build.lib.actions.ExecException; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.view.test.TestStatus.TestResultData; +import java.io.IOException; + +/** + * A context for the execution of test actions ({@link TestRunnerAction}). + */ +public interface TestActionContext extends ActionContext { + + /** + * Executes the test command, directing standard out / err to {@code outErr}. The status of + * the test should be communicated by posting a {@link TestResult} object to the eventbus. + */ + void exec(TestRunnerAction action, + ActionExecutionContext actionExecutionContext) throws ExecException, InterruptedException; + + /** + * Creates a cached test result. + */ + TestResult newCachedTestResult(Path execRoot, TestRunnerAction action, TestResultData cached) + throws IOException; +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestConfiguration.java new file mode 100644 index 0000000000..5eebe89a4c --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestConfiguration.java @@ -0,0 +1,204 @@ +// Copyright 2017 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 com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment; +import com.google.devtools.build.lib.analysis.config.BuildConfiguration.RunsPerTestConverter; +import com.google.devtools.build.lib.analysis.config.BuildOptions; +import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment; +import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory; +import com.google.devtools.build.lib.analysis.config.FragmentOptions; +import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException; +import com.google.devtools.build.lib.analysis.config.PerLabelOptions; +import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.events.Event; +import com.google.devtools.build.lib.events.EventHandler; +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.TriState; +import java.util.List; + +/** Test-related options. */ +public class TestConfiguration extends Fragment { + + /** Command-line options. */ + public static class TestOptions extends FragmentOptions { + @Option( + name = "test_filter", + allowMultiple = false, + defaultValue = "null", + category = "testing", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = + "Specifies a filter to forward to the test framework. Used to limit " + + "the tests run. Note that this does not affect which targets are built." + ) + public String testFilter; + + @Option( + name = "cache_test_results", + defaultValue = "auto", + category = "testing", + abbrev = 't', // it's useful to toggle this on/off quickly + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = + "If set to 'auto', Bazel reruns a test if and only if: " + + "(1) Bazel detects changes in the test or its dependencies, " + + "(2) the test is marked as external, " + + "(3) multiple test runs were requested with --runs_per_test, or" + + "(4) the test previously failed. " + + "If set to 'yes', Bazel caches all test results except for tests marked as " + + "external. If set to 'no', Bazel does not cache any test results." + ) + public TriState cacheTestResults; + + @Deprecated + @Option( + name = "test_result_expiration", + defaultValue = "-1", // No expiration by defualt. + category = "testing", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "This option is deprecated and has no effect." + ) + public int testResultExpiration; + + @Option( + name = "test_arg", + allowMultiple = true, + defaultValue = "", + category = "testing", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = + "Specifies additional options and arguments that should be passed to the test " + + "executable. Can be used multiple times to specify several arguments. " + + "If multiple tests are executed, each of them will receive identical arguments. " + + "Used only by the 'bazel test' command." + ) + public List<String> testArguments; + + @Option( + name = "test_sharding_strategy", + defaultValue = "explicit", + category = "testing", + converter = TestActionBuilder.ShardingStrategyConverter.class, + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = + "Specify strategy for test sharding: " + + "'explicit' to only use sharding if the 'shard_count' BUILD attribute is present. " + + "'disabled' to never use test sharding. " + + "'experimental_heuristic' to enable sharding on remotely executed tests without an " + + "explicit 'shard_count' attribute which link in a supported framework. Considered " + + "experimental." + ) + public TestActionBuilder.TestShardingStrategy testShardingStrategy; + + @Option( + name = "runs_per_test", + allowMultiple = true, + defaultValue = "1", + category = "testing", + converter = RunsPerTestConverter.class, + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = + "Specifies number of times to run each test. If any of those attempts " + + "fail for any reason, the whole test would be considered failed. " + + "Normally the value specified is just an integer. Example: --runs_per_test=3 " + + "will run all tests 3 times. " + + "Alternate syntax: regex_filter@runs_per_test. Where runs_per_test stands for " + + "an integer value and regex_filter stands " + + "for a list of include and exclude regular expression patterns (Also see " + + "--instrumentation_filter). Example: " + + "--runs_per_test=//foo/.*,-//foo/bar/.*@3 runs all tests in //foo/ " + + "except those under foo/bar three times. " + + "This option can be passed multiple times. " + ) + public List<PerLabelOptions> runsPerTest; + } + + /** Configuration loader for test options */ + public static class Loader implements ConfigurationFragmentFactory { + @Override + public Fragment create(ConfigurationEnvironment env, BuildOptions buildOptions) + throws InvalidConfigurationException { + return new TestConfiguration(buildOptions.get(TestOptions.class)); + } + + @Override + public Class<? extends Fragment> creates() { + return TestConfiguration.class; + } + + @Override + public ImmutableSet<Class<? extends FragmentOptions>> requiredOptions() { + return ImmutableSet.of(TestOptions.class); + } + } + + private final TestOptions options; + + TestConfiguration(TestOptions options) { + this.options = options; + } + + @Override + public void reportInvalidOptions(EventHandler reporter, BuildOptions buildOptions) { + if (options.testShardingStrategy + == TestActionBuilder.TestShardingStrategy.EXPERIMENTAL_HEURISTIC) { + reporter.handle( + Event.warn( + "Heuristic sharding is intended as a one-off experimentation tool for determing the " + + "benefit from sharding certain tests. Please don't keep this option in your " + + ".blazerc or continuous build")); + } + } + + public String getTestFilter() { + return options.testFilter; + } + + public TriState cacheTestResults() { + return options.cacheTestResults; + } + + public List<String> getTestArguments() { + return options.testArguments; + } + + public TestActionBuilder.TestShardingStrategy testShardingStrategy() { + return options.testShardingStrategy; + } + + /** + * @return number of times the given test should run. If the test doesn't match any of the + * filters, runs it once. + */ + public int getRunsPerTestForLabel(Label label) { + for (PerLabelOptions perLabelRuns : options.runsPerTest) { + if (perLabelRuns.isIncluded(label)) { + return Integer.parseInt(Iterables.getOnlyElement(perLabelRuns.getOptions())); + } + } + return 1; + } +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestEnvironmentProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestEnvironmentProvider.java new file mode 100644 index 0000000000..fb1125f31e --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestEnvironmentProvider.java @@ -0,0 +1,47 @@ +// Copyright 2015 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 com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; +import com.google.devtools.build.lib.packages.Info; +import com.google.devtools.build.lib.packages.NativeProvider; +import java.util.Map; + +/** Provider containing any additional environment variables for use in the test action. */ +@Immutable +public final class TestEnvironmentProvider extends Info { + + /** Skylark constructor and identifier for TestEnvironmentProvider. */ + public static final NativeProvider<TestEnvironmentProvider> SKYLARK_CONSTRUCTOR = + new NativeProvider<TestEnvironmentProvider>( + TestEnvironmentProvider.class, "TestEnvironment") {}; + + private final Map<String, String> environment; + + /** Constructs a new provider with the given variable name to variable value mapping. */ + public TestEnvironmentProvider(Map<String, String> environment) { + super(SKYLARK_CONSTRUCTOR, ImmutableMap.<String, Object>of("environment", environment)); + this.environment = Preconditions.checkNotNull(environment); + } + + /** + * Returns environment variables which should be set on the test action. + */ + public Map<String, String> getEnvironment() { + return environment; + } +} 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 new file mode 100644 index 0000000000..ce99d72e69 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestProvider.java @@ -0,0 +1,143 @@ +// 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 com.google.common.collect.ImmutableList; +import com.google.devtools.build.lib.actions.Artifact; +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; +import com.google.devtools.build.lib.packages.TestTimeout; +import java.util.List; + +/** + * A {@link TransitiveInfoProvider} for configured targets that implement test rules. + */ +@Immutable +public final class TestProvider implements TransitiveInfoProvider { + private final TestParams testParams; + private final ImmutableList<String> testTags; + + public TestProvider(TestParams testParams, ImmutableList<String> testTags) { + this.testParams = testParams; + this.testTags = testTags; + } + + /** + * Returns the {@link TestParams} object for the test represented by the corresponding configured + * target. + */ + public TestParams getTestParams() { + return testParams; + } + + /** + * Temporary hack to allow dependencies on test_suite targets to continue to work for the time + * being. + */ + public List<String> getTestTags() { + return testTags; + } + + /** + * Returns the test status artifacts for a specified configured target + * + * @param target the configured target. Should belong to a test rule. + * @return the test status artifacts + */ + public static ImmutableList<Artifact> getTestStatusArtifacts(TransitiveInfoCollection target) { + return target.getProvider(TestProvider.class).getTestParams().getTestStatusArtifacts(); + } + + /** + * A value class describing the properties of a test. + */ + public static class TestParams { + private final int runs; + private final int shards; + private final TestTimeout timeout; + private final String testRuleClass; + private final ImmutableList<Artifact> testStatusArtifacts; + private final ImmutableList<Artifact> coverageArtifacts; + private final Artifact coverageReportGenerator; + + /** + * Don't call this directly. Instead use + * {@link com.google.devtools.build.lib.analysis.test.TestActionBuilder}. + */ + TestParams(int runs, int shards, TestTimeout timeout, String testRuleClass, + ImmutableList<Artifact> testStatusArtifacts, + ImmutableList<Artifact> coverageArtifacts, + Artifact coverageReportGenerator) { + this.runs = runs; + this.shards = shards; + this.timeout = timeout; + this.testRuleClass = testRuleClass; + this.testStatusArtifacts = testStatusArtifacts; + this.coverageArtifacts = coverageArtifacts; + this.coverageReportGenerator = coverageReportGenerator; + } + + /** + * Returns the number of times this test should be run. + */ + public int getRuns() { + return runs; + } + + /** + * Returns the number of shards for this test. + */ + public int getShards() { + return shards; + } + + /** + * Returns the timeout of this test. + */ + public TestTimeout getTimeout() { + return timeout; + } + + /** + * Returns the test rule class. + */ + public String getTestRuleClass() { + return testRuleClass; + } + + /** + * Returns a list of test status artifacts that represent serialized test status protobuffers + * produced by testing this target. + */ + public ImmutableList<Artifact> getTestStatusArtifacts() { + return testStatusArtifacts; + } + + /** + * Returns the coverageArtifacts + */ + public ImmutableList<Artifact> getCoverageArtifacts() { + return coverageArtifacts; + } + + /** + * Returns the coverage report generator tool. + */ + public Artifact getCoverageReportGenerator() { + return coverageReportGenerator; + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestResult.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestResult.java new file mode 100644 index 0000000000..11f95a4656 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestResult.java @@ -0,0 +1,191 @@ +// 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 com.google.common.collect.ImmutableList; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.analysis.test.TestRunnerAction.ResolvedPaths; +import com.google.devtools.build.lib.buildeventstream.TestFileNameConstants; +import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; +import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; +import com.google.devtools.build.lib.util.Pair; +import com.google.devtools.build.lib.util.Preconditions; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus; +import com.google.devtools.build.lib.view.test.TestStatus.TestResultData; +import java.util.Collection; +import javax.annotation.Nullable; + +/** + * This is the event passed from the various test strategies to the {@code RecordingTestListener} + * upon test completion. + */ +@ThreadSafe +@Immutable +public class TestResult { + + private final TestRunnerAction testAction; + private final TestResultData data; + private final boolean cached; + @Nullable protected final Path execRoot; + + /** + * Construct the TestResult for the given test / status. + * + * @param testAction The test that was run. + * @param data test result protobuffer. + * @param cached true if this is a locally cached test result. + * @param execRoot The execution root in which the action was carried out; can be null, in which + * case everything depending on the execution root is ignored. + */ + public TestResult( + TestRunnerAction testAction, TestResultData data, boolean cached, @Nullable Path execRoot) { + this.testAction = Preconditions.checkNotNull(testAction); + this.data = data; + this.cached = cached; + this.execRoot = execRoot; + } + + public TestResult(TestRunnerAction testAction, TestResultData data, boolean cached) { + this(testAction, data, cached, null); + } + + public static boolean isBlazeTestStatusPassed(BlazeTestStatus status) { + return status == BlazeTestStatus.PASSED || status == BlazeTestStatus.FLAKY; + } + + /** + * @return The test action. + */ + public TestRunnerAction getTestAction() { + return testAction; + } + + /** + * @return The test log path. Note, that actual log file may no longer + * correspond to this artifact - use getActualLogPath() method if + * you need log location. + */ + public Path getTestLogPath() { + return testAction.getTestLog().getPath(); + } + + /** + * Return if result was loaded from local action cache. + */ + public final boolean isCached() { + return cached; + } + + /** + * @return Coverage data artifact, if available and null otherwise. + */ + public Path getCoverageData() { + if (data.getHasCoverage()) { + return testAction.getCoverageData().getPath(); + } + return null; + } + + /** + * @return The test status artifact. + */ + public Artifact getTestStatusArtifact() { + // these artifacts are used to keep track of the number of pending and completed tests. + return testAction.getCacheStatusArtifact(); + } + + + /** + * Gets the test name in a user-friendly format. + * Will generally include the target name and shard number, if applicable. + * + * @return The test name. + */ + public String getTestName() { + return testAction.getTestName(); + } + + /** + * @return The test label. + */ + public String getLabel() { + return Label.print(testAction.getOwner().getLabel()); + } + + /** + * @return The test shard number. + */ + public int getShardNum() { + return testAction.getShardNum(); + } + + /** + * @return Total number of test shards. 0 means + * no sharding, whereas 1 means degenerate sharding. + */ + public int getTotalShards() { + return testAction.getExecutionSettings().getTotalShards(); + } + + public TestResultData getData() { + return data; + } + + /** + * @return Collection of files created by the test, tagged by their name indicating usage (e.g., + * "test.log"). + */ + public Collection<Pair<String, Path>> getFiles() { + ImmutableList.Builder<Pair<String, Path>> builder = new ImmutableList.Builder<>(); + if (testAction.getTestLog().getPath().exists()) { + builder.add(Pair.of(TestFileNameConstants.TEST_LOG, testAction.getTestLog().getPath())); + } + if (execRoot != null) { + ResolvedPaths resolvedPaths = testAction.resolve(execRoot); + if (resolvedPaths.getXmlOutputPath().exists()) { + builder.add(Pair.of(TestFileNameConstants.TEST_XML, resolvedPaths.getXmlOutputPath())); + } + if (resolvedPaths.getSplitLogsPath().exists()) { + builder.add(Pair.of(TestFileNameConstants.SPLIT_LOGS, resolvedPaths.getSplitLogsPath())); + } + if (resolvedPaths.getTestWarningsPath().exists()) { + builder.add(Pair.of(TestFileNameConstants.TEST_WARNINGS, resolvedPaths.getSplitLogsPath())); + } + if (resolvedPaths.getUndeclaredOutputsZipPath().exists()) { + builder.add(Pair.of(TestFileNameConstants.UNDECLARED_OUTPUTS_ZIP, + resolvedPaths.getUndeclaredOutputsZipPath())); + } + if (resolvedPaths.getUndeclaredOutputsManifestPath().exists()) { + builder.add(Pair.of(TestFileNameConstants.UNDECLARED_OUTPUTS_MANIFEST, + resolvedPaths.getUndeclaredOutputsManifestPath())); + } + if (resolvedPaths.getUndeclaredOutputsAnnotationsPath().exists()) { + builder.add(Pair.of(TestFileNameConstants.UNDECLARED_OUTPUTS_ANNOTATIONS, + resolvedPaths.getUndeclaredOutputsManifestPath())); + } + if (resolvedPaths.getUnusedRunfilesLogPath().exists()) { + builder.add(Pair.of(TestFileNameConstants.UNUSED_RUNFILES_LOG, + resolvedPaths.getUnusedRunfilesLogPath())); + } + if (resolvedPaths.getInfrastructureFailureFile().exists()) { + builder.add(Pair.of(TestFileNameConstants.TEST_INFRASTRUCTURE_FAILURE, + resolvedPaths.getInfrastructureFailureFile())); + } + } + return builder.build(); + } +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java new file mode 100644 index 0000000000..bb572202cd --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java @@ -0,0 +1,802 @@ +// 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 com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.devtools.build.lib.actions.AbstractAction; +import com.google.devtools.build.lib.actions.ActionExecutionContext; +import com.google.devtools.build.lib.actions.ActionExecutionException; +import com.google.devtools.build.lib.actions.ActionInput; +import com.google.devtools.build.lib.actions.ActionInputHelper; +import com.google.devtools.build.lib.actions.ActionOwner; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.ExecException; +import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit; +import com.google.devtools.build.lib.actions.UserExecException; +import com.google.devtools.build.lib.analysis.RunfilesSupplierImpl; +import com.google.devtools.build.lib.analysis.config.BuildConfiguration; +import com.google.devtools.build.lib.analysis.config.RunUnder; +import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.collect.ImmutableIterable; +import com.google.devtools.build.lib.collect.nestedset.NestedSet; +import com.google.devtools.build.lib.util.Fingerprint; +import com.google.devtools.build.lib.util.LoggingUtil; +import com.google.devtools.build.lib.util.Preconditions; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.view.test.TestStatus.TestResultData; +import com.google.devtools.common.options.TriState; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import javax.annotation.Nullable; + +/** + * An Action representing a test with the associated environment (runfiles, + * environment variables, test result, etc). It consumes test executable and + * runfiles artifacts and produces test result and test status artifacts. + */ +// Not final so that we can mock it in tests. +public class TestRunnerAction extends AbstractAction implements NotifyOnActionCacheHit { + public static final PathFragment COVERAGE_TMP_ROOT = PathFragment.create("_coverage"); + + // Used for selecting subset of testcase / testmethods. + private static final String TEST_BRIDGE_TEST_FILTER_ENV = "TESTBRIDGE_TEST_ONLY"; + + private static final String GUID = "cc41f9d0-47a6-11e7-8726-eb6ce83a8cc8"; + + private final NestedSet<Artifact> runtime; + private final BuildConfiguration configuration; + private final TestConfiguration testConfiguration; + private final Artifact testLog; + private final Artifact cacheStatus; + private final PathFragment testWarningsPath; + private final PathFragment unusedRunfilesLogPath; + private final PathFragment splitLogsPath; + private final PathFragment splitLogsDir; + private final PathFragment undeclaredOutputsDir; + private final PathFragment undeclaredOutputsZipPath; + private final PathFragment undeclaredOutputsAnnotationsDir; + private final PathFragment undeclaredOutputsManifestPath; + private final PathFragment undeclaredOutputsAnnotationsPath; + private final PathFragment xmlOutputPath; + @Nullable + private final PathFragment testShard; + private final PathFragment testExitSafe; + private final PathFragment testStderr; + private final PathFragment testInfrastructureFailure; + private final PathFragment baseDir; + private final Artifact coverageData; + private final TestTargetProperties testProperties; + private final TestTargetExecutionSettings executionSettings; + private final int shardNum; + private final int runNumber; + private final String workspaceName; + private final boolean useTestRunner; + + // Mutable state related to test caching. Lazily initialized: null indicates unknown. + private Boolean unconditionalExecution; + + /** Any extra environment variables (and values) added by the rule that created this action. */ + private final ImmutableMap<String, String> extraTestEnv; + + /** + * The set of environment variables that are inherited from the client environment. These are + * handled explicitly by the ActionCacheChecker and so don't have to be included in the cache key. + */ + private final ImmutableIterable<String> requiredClientEnvVariables; + + private static ImmutableList<Artifact> list(Artifact... artifacts) { + ImmutableList.Builder<Artifact> builder = ImmutableList.builder(); + for (Artifact artifact : artifacts) { + if (artifact != null) { + builder.add(artifact); + } + } + return builder.build(); + } + + /** + * Create new TestRunnerAction instance. Should not be called directly. + * Use {@link TestActionBuilder} instead. + * + * @param shardNum The shard number. Must be 0 if totalShards == 0 + * (no sharding). Otherwise, must be >= 0 and < totalShards. + * @param runNumber test run number + */ + TestRunnerAction( + ActionOwner owner, + Iterable<Artifact> inputs, + NestedSet<Artifact> runtime, // Must be a subset of inputs + Artifact testLog, + Artifact cacheStatus, + Artifact coverageArtifact, + TestTargetProperties testProperties, + Map<String, String> extraTestEnv, + TestTargetExecutionSettings executionSettings, + int shardNum, + int runNumber, + BuildConfiguration configuration, + String workspaceName, + boolean useTestRunner) { + super( + owner, + inputs, + // Note that this action only cares about the runfiles, not the mapping. + new RunfilesSupplierImpl(PathFragment.create("runfiles"), executionSettings.getRunfiles()), + list(testLog, cacheStatus, coverageArtifact)); + this.runtime = runtime; + this.configuration = Preconditions.checkNotNull(configuration); + this.testConfiguration = + Preconditions.checkNotNull(configuration.getFragment(TestConfiguration.class)); + this.testLog = testLog; + this.cacheStatus = cacheStatus; + this.coverageData = coverageArtifact; + this.shardNum = shardNum; + this.runNumber = runNumber; + this.testProperties = Preconditions.checkNotNull(testProperties); + this.executionSettings = Preconditions.checkNotNull(executionSettings); + + this.baseDir = cacheStatus.getExecPath().getParentDirectory(); + + int totalShards = executionSettings.getTotalShards(); + Preconditions.checkState((totalShards == 0 && shardNum == 0) || + (totalShards > 0 && 0 <= shardNum && shardNum < totalShards)); + this.testExitSafe = baseDir.getChild("test.exited_prematurely"); + // testShard Path should be set only if sharding is enabled. + this.testShard = totalShards > 1 + ? baseDir.getChild("test.shard") + : null; + this.xmlOutputPath = baseDir.getChild("test.xml"); + this.testWarningsPath = baseDir.getChild("test.warnings"); + this.unusedRunfilesLogPath = baseDir.getChild("test.unused_runfiles_log"); + this.testStderr = baseDir.getChild("test.err"); + this.splitLogsDir = baseDir.getChild("test.raw_splitlogs"); + // See note in {@link #getSplitLogsPath} on the choice of file name. + this.splitLogsPath = splitLogsDir.getChild("test.splitlogs"); + this.undeclaredOutputsDir = baseDir.getChild("test.outputs"); + this.undeclaredOutputsZipPath = undeclaredOutputsDir.getChild("outputs.zip"); + this.undeclaredOutputsAnnotationsDir = baseDir.getChild("test.outputs_manifest"); + this.undeclaredOutputsManifestPath = undeclaredOutputsAnnotationsDir.getChild("MANIFEST"); + this.undeclaredOutputsAnnotationsPath = undeclaredOutputsAnnotationsDir.getChild("ANNOTATIONS"); + this.testInfrastructureFailure = baseDir.getChild("test.infrastructure_failure"); + this.workspaceName = workspaceName; + this.useTestRunner = useTestRunner; + + this.extraTestEnv = ImmutableMap.copyOf(extraTestEnv); + this.requiredClientEnvVariables = + ImmutableIterable.from(Iterables.concat( + configuration.getActionEnvironment().getInheritedEnv(), + configuration.getTestActionEnvironment().getInheritedEnv())); + } + + public BuildConfiguration getConfiguration() { + return configuration; + } + + public final PathFragment getBaseDir() { + return baseDir; + } + + @Override + public boolean showsOutputUnconditionally() { + return true; + } + + public List<ActionInput> getSpawnOutputs() { + final List<ActionInput> outputs = new ArrayList<>(); + outputs.add(ActionInputHelper.fromPath(getXmlOutputPath())); + outputs.add(ActionInputHelper.fromPath(getExitSafeFile())); + if (isSharded()) { + outputs.add(ActionInputHelper.fromPath(getTestShard())); + } + outputs.add(ActionInputHelper.fromPath(getTestWarningsPath())); + outputs.add(ActionInputHelper.fromPath(getSplitLogsPath())); + outputs.add(ActionInputHelper.fromPath(getUnusedRunfilesLogPath())); + outputs.add(ActionInputHelper.fromPath(getInfrastructureFailureFile())); + outputs.add(ActionInputHelper.fromPath(getUndeclaredOutputsZipPath())); + outputs.add(ActionInputHelper.fromPath(getUndeclaredOutputsManifestPath())); + outputs.add(ActionInputHelper.fromPath(getUndeclaredOutputsAnnotationsPath())); + if (isCoverageMode()) { + outputs.add(getCoverageData()); + } + return outputs; + } + + @Override + protected String computeKey() { + Fingerprint f = new Fingerprint(); + f.addString(GUID); + f.addStrings(executionSettings.getArgs().arguments()); + f.addString(executionSettings.getTestFilter() == null ? "" : executionSettings.getTestFilter()); + RunUnder runUnder = executionSettings.getRunUnder(); + f.addString(runUnder == null ? "" : runUnder.getValue()); + f.addStringMap(extraTestEnv); + // TODO(ulfjack): It might be better for performance to hash the action and test envs in config, + // and only add a hash here. + configuration.getActionEnvironment().addTo(f); + configuration.getTestActionEnvironment().addTo(f); + // The 'requiredClientEnvVariables' are handled by Skyframe and don't need to be added here. + f.addString(testProperties.getSize().toString()); + f.addString(testProperties.getTimeout().toString()); + f.addStrings(testProperties.getTags()); + f.addInt(testProperties.isLocal() ? 1 : 0); + f.addInt(shardNum); + f.addInt(executionSettings.getTotalShards()); + f.addInt(runNumber); + f.addInt(testConfiguration.getRunsPerTestForLabel(getOwner().getLabel())); + f.addInt(configuration.isCodeCoverageEnabled() ? 1 : 0); + return f.hexDigestAndReset(); + } + + @Override + public boolean executeUnconditionally() { + // Note: isVolatile must return true if executeUnconditionally can ever return true + // for this instance. + if (unconditionalExecution == null) { + unconditionalExecution = computeExecuteUnconditionallyFromTestStatus(); + } + return unconditionalExecution; + } + + @Override + public boolean isVolatile() { + return true; + } + + /** + * Saves cache status to disk. + */ + public void saveCacheStatus(TestResultData data) throws IOException { + try (OutputStream out = cacheStatus.getPath().getOutputStream()) { + data.writeTo(out); + } + } + + /** + * Returns the cache from disk, or null if the file doesn't exist or if there is an error. + */ + @Nullable + private TestResultData readCacheStatus() { + try (InputStream in = cacheStatus.getPath().getInputStream()) { + return TestResultData.parseFrom(in); + } catch (IOException expected) { + return null; + } + } + + private boolean computeExecuteUnconditionallyFromTestStatus() { + return !canBeCached( + testConfiguration.cacheTestResults(), + readCacheStatus(), + testProperties.isExternal(), + testConfiguration.getRunsPerTestForLabel(getOwner().getLabel())); + } + + @VisibleForTesting + static boolean canBeCached( + TriState cacheTestResults, TestResultData prevStatus, boolean isExternal, int runsPerTest) { + if (cacheTestResults == TriState.NO) { + return false; + } + if (isExternal) { + return false; + } + if (cacheTestResults == TriState.AUTO && (runsPerTest > 1)) { + return false; + } + // Test will not be executed unconditionally - check whether test result exists and is + // valid. If it is, method will return false and we will rely on the dependency checker + // to make a decision about test execution. + if (cacheTestResults == TriState.AUTO && prevStatus != null && !prevStatus.getTestPassed()) { + return false; + } + // Rely on the dependency checker to determine if the test can be cached. Note that the status + // is a declared output, so its non-existence also triggers a re-run. + return true; + } + + /** + * Returns whether caching has been deemed safe by looking at the previous test run + * (for local caching). If the previous run is not present, return "true" here, as + * remote execution caching should be safe. + */ + public boolean shouldCacheResult() { + return !executeUnconditionally(); + } + + @Override + public void actionCacheHit(ActionCachedContext executor) { + unconditionalExecution = null; + try { + executor.getEventBus().post( + executor.getContext(TestActionContext.class).newCachedTestResult( + executor.getExecRoot(), this, readCacheStatus())); + } catch (IOException e) { + LoggingUtil.logToRemote(Level.WARNING, "Failed creating cached protocol buffer", e); + } + } + + @Override + protected String getRawProgressMessage() { + return "Testing " + getTestName(); + } + + /** + * Deletes <b>all</b> possible test outputs. + * + * TestRunnerAction potentially can create many more non-declared outputs - xml output, + * coverage data file and logs for failed attempts. All those outputs are uniquely + * identified by the test log base name with arbitrary prefix and extension. + */ + @Override + protected void deleteOutputs(Path execRoot) throws IOException { + super.deleteOutputs(execRoot); + + // We do not rely on globs, as it causes quadratic behavior in --runs_per_test and test + // shard count. + + // We also need to remove *.(xml|data|shard|warnings|zip) files if they are present. + execRoot.getRelative(xmlOutputPath).delete(); + execRoot.getRelative(testWarningsPath).delete(); + execRoot.getRelative(unusedRunfilesLogPath).delete(); + // Note that splitLogsPath points to a file inside the splitLogsDir so + // it's not necessary to delete it explicitly. + FileSystemUtils.deleteTree(execRoot.getRelative(splitLogsDir)); + FileSystemUtils.deleteTree(execRoot.getRelative(undeclaredOutputsDir)); + FileSystemUtils.deleteTree(execRoot.getRelative(undeclaredOutputsAnnotationsDir)); + execRoot.getRelative(testStderr).delete(); + execRoot.getRelative(testExitSafe).delete(); + if (testShard != null) { + execRoot.getRelative(testShard).delete(); + } + execRoot.getRelative(testInfrastructureFailure).delete(); + + // Coverage files use "coverage" instead of "test". + String coveragePrefix = "coverage"; + + // We cannot use coverageData artifact since it may be null. Generate coverage name instead. + execRoot.getRelative(baseDir.getChild(coveragePrefix + ".dat")).delete(); + + // Delete files fetched from remote execution. + execRoot.getRelative(baseDir.getChild("test.zip")).delete(); + deleteTestAttemptsDirMaybe(execRoot.getRelative(baseDir), "test"); + } + + private void deleteTestAttemptsDirMaybe(Path outputDir, String namePrefix) throws IOException { + Path testAttemptsDir = outputDir.getChild(namePrefix + "_attempts"); + if (testAttemptsDir.exists()) { + // Normally we should have used deleteTree(testAttemptsDir). However, if test output is + // in a FUSE filesystem implemented with the high-level API, there may be .fuse??????? + // entries, which prevent removing the directory. As a workaround, code below will throw + // IOException if it will fail to remove something inside testAttemptsDir, but will + // silently suppress any exceptions when deleting testAttemptsDir itself. + FileSystemUtils.deleteTreesBelow(testAttemptsDir); + try { + testAttemptsDir.delete(); + } catch (IOException e) { + // Do nothing. + } + } + } + + public void setupEnvVariables(Map<String, String> env, Duration timeout) { + env.put("TEST_SIZE", getTestProperties().getSize().toString()); + env.put("TEST_TIMEOUT", Long.toString(timeout.getSeconds())); + env.put("TEST_WORKSPACE", getRunfilesPrefix()); + env.put( + "TEST_BINARY", + getExecutionSettings().getExecutable().getRootRelativePath().getCallablePathString()); + + // When we run test multiple times, set different TEST_RANDOM_SEED values for each run. + // Don't override any previous setting. + if (testConfiguration.getRunsPerTestForLabel(getOwner().getLabel()) > 1 + && !env.containsKey("TEST_RANDOM_SEED")) { + env.put("TEST_RANDOM_SEED", Integer.toString(getRunNumber() + 1)); + } + + String testFilter = getExecutionSettings().getTestFilter(); + if (testFilter != null) { + env.put(TEST_BRIDGE_TEST_FILTER_ENV, testFilter); + } + + env.put("TEST_WARNINGS_OUTPUT_FILE", getTestWarningsPath().getPathString()); + env.put("TEST_UNUSED_RUNFILES_LOG_FILE", getUnusedRunfilesLogPath().getPathString()); + + env.put("TEST_LOGSPLITTER_OUTPUT_FILE", getSplitLogsPath().getPathString()); + + env.put("TEST_UNDECLARED_OUTPUTS_ZIP", getUndeclaredOutputsZipPath().getPathString()); + env.put("TEST_UNDECLARED_OUTPUTS_DIR", getUndeclaredOutputsDir().getPathString()); + env.put("TEST_UNDECLARED_OUTPUTS_MANIFEST", getUndeclaredOutputsManifestPath().getPathString()); + env.put( + "TEST_UNDECLARED_OUTPUTS_ANNOTATIONS", + getUndeclaredOutputsAnnotationsPath().getPathString()); + env.put( + "TEST_UNDECLARED_OUTPUTS_ANNOTATIONS_DIR", + getUndeclaredOutputsAnnotationsDir().getPathString()); + + env.put("TEST_PREMATURE_EXIT_FILE", getExitSafeFile().getPathString()); + env.put("TEST_INFRASTRUCTURE_FAILURE_FILE", getInfrastructureFailureFile().getPathString()); + + if (isSharded()) { + env.put("TEST_SHARD_INDEX", Integer.toString(getShardNum())); + env.put("TEST_TOTAL_SHARDS", Integer.toString(getExecutionSettings().getTotalShards())); + env.put("TEST_SHARD_STATUS_FILE", getTestShard().getPathString()); + } + env.put("XML_OUTPUT_FILE", getXmlOutputPath().getPathString()); + + if (!isEnableRunfiles()) { + // If runfiles are disabled, tell remote-runtest.sh/local-runtest.sh about that. + env.put("RUNFILES_MANIFEST_ONLY", "1"); + } + + if (isCoverageMode()) { + // Instruct remote-runtest.sh/local-runtest.sh not to cd into the runfiles directory. + // TODO(ulfjack): Find a way to avoid setting this variable. + env.put("RUNTEST_PRESERVE_CWD", "1"); + + env.put("COVERAGE_MANIFEST", getCoverageManifest().getExecPathString()); + env.put("COVERAGE_DIR", getCoverageDirectory().getPathString()); + env.put("COVERAGE_OUTPUT_FILE", getCoverageData().getExecPathString()); + // TODO(elenairina): Remove this after the next blaze release (after 2017.07.30). + env.put("NEW_JAVA_COVERAGE_IMPL", "True"); + } + } + + /** + * Gets the test name in a user-friendly format. + * Will generally include the target name and run/shard numbers, if applicable. + */ + public String getTestName() { + String suffix = getTestSuffix(); + String label = Label.print(getOwner().getLabel()); + return suffix.isEmpty() ? label : label + " " + suffix; + } + + /** + * Gets the test suffix in a user-friendly format, eg "(shard 1 of 7)". + * Will include the target name and run/shard numbers, if applicable. + */ + public String getTestSuffix() { + int totalShards = executionSettings.getTotalShards(); + // Use a 1-based index for user friendliness. + int runsPerTest = testConfiguration.getRunsPerTestForLabel(getOwner().getLabel()); + if (totalShards > 1 && runsPerTest > 1) { + return String.format("(shard %d of %d, run %d of %d)", shardNum + 1, totalShards, + runNumber + 1, runsPerTest); + } else if (totalShards > 1) { + return String.format("(shard %d of %d)", shardNum + 1, totalShards); + } else if (runsPerTest > 1) { + return String.format("(run %d of %d)", runNumber + 1, runsPerTest); + } else { + return ""; + } + } + + public Artifact getTestLog() { + return testLog; + } + + /** + * Returns all environment variables which must be set in order to run this test. + */ + public Map<String, String> getExtraTestEnv() { + return extraTestEnv; + } + + @Override + public Iterable<String> getClientEnvironmentVariables() { + return requiredClientEnvVariables; + } + + public ResolvedPaths resolve(Path execRoot) { + return new ResolvedPaths(execRoot); + } + + public Artifact getCacheStatusArtifact() { + return cacheStatus; + } + + public PathFragment getTestWarningsPath() { + return testWarningsPath; + } + + public PathFragment getUnusedRunfilesLogPath() { + return unusedRunfilesLogPath; + } + + public PathFragment getSplitLogsPath() { + return splitLogsPath; + } + + public PathFragment getUndeclaredOutputsDir() { + return undeclaredOutputsDir; + } + + /** + * @return path to the optional zip file of undeclared test outputs. + */ + public PathFragment getUndeclaredOutputsZipPath() { + return undeclaredOutputsZipPath; + } + + /** + * @return path to the undeclared output manifest file. + */ + public PathFragment getUndeclaredOutputsManifestPath() { + return undeclaredOutputsManifestPath; + } + + public PathFragment getUndeclaredOutputsAnnotationsDir() { + return undeclaredOutputsAnnotationsDir; + } + + /** + * @return path to the undeclared output annotations file. + */ + public PathFragment getUndeclaredOutputsAnnotationsPath() { + return undeclaredOutputsAnnotationsPath; + } + + public PathFragment getTestShard() { + return testShard; + } + + public PathFragment getExitSafeFile() { + return testExitSafe; + } + + public PathFragment getInfrastructureFailureFile() { + return testInfrastructureFailure; + } + + /** + * @return path to the optionally created XML output file created by the test. + */ + public PathFragment getXmlOutputPath() { + return xmlOutputPath; + } + + /** + * @return coverage data artifact or null if code coverage was not requested. + */ + @Nullable public Artifact getCoverageData() { + return coverageData; + } + + @Nullable public Artifact getCoverageManifest() { + return getExecutionSettings().getInstrumentedFileManifest(); + } + + /** Returns true if coverage data should be gathered. */ + public boolean isCoverageMode() { + return coverageData != null; + } + + /** + * Returns a directory to temporarily store coverage results for the given action relative to the + * execution root. This directory is used to store all coverage results related to the test + * execution with exception of the locally generated *.gcda files. Those are stored separately + * using relative path within coverage directory. + * + * <p>The directory name for the given test runner action is constructed as: {@code + * _coverage/target_path/test_log_name} where {@code test_log_name} is usually a target name but + * potentially can include extra suffix, such as a shard number (if test execution was sharded). + */ + public PathFragment getCoverageDirectory() { + return COVERAGE_TMP_ROOT.getRelative( + FileSystemUtils.removeExtension(getTestLog().getRootRelativePath())); + } + + public TestTargetProperties getTestProperties() { + return testProperties; + } + + public TestTargetExecutionSettings getExecutionSettings() { + return executionSettings; + } + + public boolean useTestRunner() { + return useTestRunner; + } + + public boolean isSharded() { + return testShard != null; + } + + /** + * @return the shard number for this action. + * If getTotalShards() > 0, must be >= 0 and < getTotalShards(). + * Otherwise, must be 0. + */ + public int getShardNum() { + return shardNum; + } + + /** + * @return run number. + */ + public int getRunNumber() { + return runNumber; + } + + /** + * @return the workspace name. + */ + public String getRunfilesPrefix() { + return workspaceName; + } + + @Override + public Artifact getPrimaryOutput() { + return testLog; + } + + @Override + public void execute(ActionExecutionContext actionExecutionContext) + throws ActionExecutionException, InterruptedException { + TestActionContext context = actionExecutionContext.getContext(TestActionContext.class); + try { + context.exec(this, actionExecutionContext); + } catch (ExecException e) { + throw e.toActionExecutionException(this); + } finally { + unconditionalExecution = null; + } + } + + @Override + public String getMnemonic() { + return "TestRunner"; + } + + @Override + public ImmutableSet<Artifact> getMandatoryOutputs() { + return getOutputs(); + } + + public Artifact getRuntimeArtifact(String basename) throws ExecException { + for (Artifact runtimeArtifact : runtime) { + if (runtimeArtifact.getExecPath().getBaseName().equals(basename)) { + return runtimeArtifact; + } + } + + throw new UserExecException("'" + basename + "' not found in test runtime"); + } + + public PathFragment getShExecutable() { + return configuration.getShellExecutable(); + } + + public ImmutableMap<String, String> getLocalShellEnvironment() { + return configuration.getLocalShellEnvironment(); + } + + public boolean isEnableRunfiles() { + return configuration.runfilesEnabled(); + } + + /** + * The same set of paths as the parent test action, resolved against a given exec root. + */ + public final class ResolvedPaths { + private final Path execRoot; + + ResolvedPaths(Path execRoot) { + this.execRoot = Preconditions.checkNotNull(execRoot); + } + + private Path getPath(PathFragment relativePath) { + return execRoot.getRelative(relativePath); + } + + public final Path getBaseDir() { + return getPath(baseDir); + } + + /** + * In rare cases, error messages will be printed to stderr instead of stdout. The test action is + * responsible for appending anything in the stderr file to the real test.log. + */ + public Path getTestStderr() { + return getPath(testStderr); + } + + public Path getTestWarningsPath() { + return getPath(testWarningsPath); + } + + public Path getSplitLogsPath() { + return getPath(splitLogsPath); + } + + public Path getUnusedRunfilesLogPath() { + return getPath(unusedRunfilesLogPath); + } + + /** + * @return path to the directory containing the split logs (raw and proto file). + */ + public Path getSplitLogsDir() { + return getPath(splitLogsDir); + } + + /** + * @return path to the optional zip file of undeclared test outputs. + */ + public Path getUndeclaredOutputsZipPath() { + return getPath(undeclaredOutputsZipPath); + } + + /** + * @return path to the directory to hold undeclared test outputs. + */ + public Path getUndeclaredOutputsDir() { + return getPath(undeclaredOutputsDir); + } + + /** + * @return path to the directory to hold undeclared output annotations parts. + */ + public Path getUndeclaredOutputsAnnotationsDir() { + return getPath(undeclaredOutputsAnnotationsDir); + } + + /** + * @return path to the undeclared output manifest file. + */ + public Path getUndeclaredOutputsManifestPath() { + return getPath(undeclaredOutputsManifestPath); + } + + /** + * @return path to the undeclared output annotations file. + */ + public Path getUndeclaredOutputsAnnotationsPath() { + return getPath(undeclaredOutputsAnnotationsPath); + } + + @Nullable + public Path getTestShard() { + return testShard == null ? null : getPath(testShard); + } + + public Path getExitSafeFile() { + return getPath(testExitSafe); + } + + public Path getInfrastructureFailureFile() { + return getPath(testInfrastructureFailure); + } + + /** + * @return path to the optionally created XML output file created by the test. + */ + public Path getXmlOutputPath() { + return getPath(xmlOutputPath); + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetExecutionSettings.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetExecutionSettings.java new file mode 100644 index 0000000000..5a2c520dc0 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetExecutionSettings.java @@ -0,0 +1,139 @@ +// 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 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.RuleConfiguredTarget.Mode; +import com.google.devtools.build.lib.analysis.RuleContext; +import com.google.devtools.build.lib.analysis.Runfiles; +import com.google.devtools.build.lib.analysis.RunfilesSupport; +import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; +import com.google.devtools.build.lib.analysis.actions.CommandLine; +import com.google.devtools.build.lib.analysis.config.BuildConfiguration; +import com.google.devtools.build.lib.analysis.config.RunUnder; +import com.google.devtools.build.lib.packages.TargetUtils; +import com.google.devtools.build.lib.util.Preconditions; +import com.google.devtools.build.lib.vfs.Path; + +/** + * Container for common test execution settings shared by all + * all TestRunnerAction instances for the given test target. + */ +public final class TestTargetExecutionSettings { + + private final CommandLine testArguments; + private final String testFilter; + private final int totalShards; + private final RunUnder runUnder; + private final Artifact runUnderExecutable; + private final Artifact executable; + private final boolean runfilesSymlinksCreated; + private final Path runfilesDir; + private final Runfiles runfiles; + private final Artifact runfilesInputManifest; + private final Artifact instrumentedFileManifest; + + TestTargetExecutionSettings(RuleContext ruleContext, RunfilesSupport runfilesSupport, + Artifact executable, Artifact instrumentedFileManifest, int shards) { + Preconditions.checkArgument(TargetUtils.isTestRule(ruleContext.getRule())); + Preconditions.checkArgument(shards >= 0); + BuildConfiguration config = ruleContext.getConfiguration(); + TestConfiguration testConfig = config.getFragment(TestConfiguration.class); + + CommandLine targetArgs = runfilesSupport.getArgs(); + testArguments = + CommandLine.concat(targetArgs, ImmutableList.copyOf(testConfig.getTestArguments())); + + totalShards = shards; + runUnder = config.getRunUnder(); + runUnderExecutable = getRunUnderExecutable(ruleContext); + + this.testFilter = testConfig.getTestFilter(); + this.executable = executable; + this.runfilesSymlinksCreated = runfilesSupport.getCreateSymlinks(); + this.runfilesDir = runfilesSupport.getRunfilesDirectory(); + this.runfiles = runfilesSupport.getRunfiles(); + this.runfilesInputManifest = runfilesSupport.getRunfilesInputManifest(); + this.instrumentedFileManifest = instrumentedFileManifest; + } + + private static Artifact getRunUnderExecutable(RuleContext ruleContext) { + TransitiveInfoCollection runUnderTarget = ruleContext + .getPrerequisite(":run_under", Mode.DATA); + return runUnderTarget == null + ? null + : runUnderTarget.getProvider(FilesToRunProvider.class).getExecutable(); + } + + public CommandLine getArgs() { + return testArguments; + } + + public String getTestFilter() { + return testFilter; + } + + public int getTotalShards() { + return totalShards; + } + + public RunUnder getRunUnder() { + return runUnder; + } + + public Artifact getRunUnderExecutable() { + return runUnderExecutable; + } + + public Artifact getExecutable() { + return executable; + } + + /** @return whether or not the runfiles symlinks were created */ + public boolean getRunfilesSymlinksCreated() { + return runfilesSymlinksCreated; + } + + /** @return the directory of the runfiles */ + public Path getRunfilesDir() { + return runfilesDir; + } + + /** @return the runfiles for the test */ + public Runfiles getRunfiles() { + return runfiles; + } + + /** + * Returns the input runfiles manifest for this test. + * + * <p>This always returns the input manifest outside of the runfiles tree. + * + * @see com.google.devtools.build.lib.analysis.RunfilesSupport#getRunfilesInputManifest() + */ + public Artifact getInputManifest() { + return runfilesInputManifest; + } + + /** + * Returns instrumented file manifest or null if code coverage is not + * collected. + */ + public Artifact getInstrumentedFileManifest() { + return instrumentedFileManifest; + } +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetProperties.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetProperties.java new file mode 100644 index 0000000000..d55d066966 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetProperties.java @@ -0,0 +1,187 @@ +// 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 com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.devtools.build.lib.actions.ExecutionRequirements; +import com.google.devtools.build.lib.actions.ExecutionRequirements.ParseableRequirement.ValidationException; +import com.google.devtools.build.lib.actions.ResourceSet; +import com.google.devtools.build.lib.actions.Spawn; +import com.google.devtools.build.lib.actions.UserExecException; +import com.google.devtools.build.lib.analysis.RuleContext; +import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.packages.Rule; +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.syntax.Type; +import com.google.devtools.build.lib.util.Preconditions; +import java.util.List; +import java.util.Map; + +/** + * Container for test target properties available to the + * TestRunnerAction instance. + */ +public class TestTargetProperties { + + /** + * Resources used by local tests of various sizes. + * + * <p>When changing these values, remember to update the documentation at + * attributes/test/size.html. + */ + private static final ResourceSet SMALL_RESOURCES = ResourceSet.create(20, 0.9, 0.00, 1); + private static final ResourceSet MEDIUM_RESOURCES = ResourceSet.create(100, 0.9, 0.1, 1); + private static final ResourceSet LARGE_RESOURCES = ResourceSet.create(300, 0.8, 0.1, 1); + private static final ResourceSet ENORMOUS_RESOURCES = ResourceSet.create(800, 0.7, 0.4, 1); + private static final ResourceSet LOCAL_TEST_JOBS_BASED_RESOURCES = + ResourceSet.createWithLocalTestCount(1); + + private static ResourceSet getResourceSetFromSize(TestSize size) { + switch (size) { + case SMALL: return SMALL_RESOURCES; + case MEDIUM: return MEDIUM_RESOURCES; + case LARGE: return LARGE_RESOURCES; + default: return ENORMOUS_RESOURCES; + } + } + + private final TestSize size; + private final TestTimeout timeout; + private final List<String> tags; + private final boolean isLocal; + private final boolean isFlaky; + private final boolean isExternal; + private final String language; + private final ImmutableMap<String, String> executionInfo; + + /** + * Creates test target properties instance. Constructor expects that it + * will be called only for test configured targets. + */ + TestTargetProperties(RuleContext ruleContext, + ExecutionInfoProvider executionRequirements) { + Rule rule = ruleContext.getRule(); + + Preconditions.checkState(TargetUtils.isTestRule(rule)); + size = TestSize.getTestSize(rule); + timeout = TestTimeout.getTestTimeout(rule); + tags = ruleContext.attributes().get("tags", Type.STRING_LIST); + boolean isTaggedLocal = TargetUtils.isLocalTestRule(rule) + || TargetUtils.isExclusiveTestRule(rule); + + // We need to use method on ruleConfiguredTarget to perform validation. + isFlaky = ruleContext.attributes().get("flaky", Type.BOOLEAN); + isExternal = TargetUtils.isExternalTestRule(rule); + + Map<String, String> executionInfo = Maps.newLinkedHashMap(); + executionInfo.putAll(TargetUtils.getExecutionInfo(rule)); + if (isTaggedLocal) { + executionInfo.put("local", ""); + } + + boolean isRequestedLocalByProvider = false; + if (executionRequirements != null) { + // This will overwrite whatever TargetUtils put there, which might be confusing. + executionInfo.putAll(executionRequirements.getExecutionInfo()); + + // We also need to mark it as local if the execution requirements specifies it. + isRequestedLocalByProvider = executionRequirements.getExecutionInfo().containsKey("local"); + } + this.executionInfo = ImmutableMap.copyOf(executionInfo); + + isLocal = isTaggedLocal || isRequestedLocalByProvider; + + language = TargetUtils.getRuleLanguage(rule); + } + + public TestSize getSize() { + return size; + } + + public TestTimeout getTimeout() { + return timeout; + } + + public List<String> getTags() { + return tags; + } + + public boolean isLocal() { + return isLocal; + } + + public boolean isFlaky() { + return isFlaky; + } + + public boolean isExternal() { + return isExternal; + } + + public ResourceSet getLocalResourceUsage(Label label, boolean usingLocalTestJobs) + throws UserExecException { + if (usingLocalTestJobs) { + return LOCAL_TEST_JOBS_BASED_RESOURCES; + } + + ResourceSet testResourcesFromSize = TestTargetProperties.getResourceSetFromSize(size); + + // Tests can override their CPU reservation with a "cpus:<n>" tag. + ResourceSet testResourcesFromTag = null; + for (String tag : executionInfo.keySet()) { + try { + String cpus = ExecutionRequirements.CPU.parseIfMatches(tag); + if (cpus != null) { + if (testResourcesFromTag != null) { + throw new UserExecException( + String.format( + "%s has more than one '%s' tag, but duplicate tags aren't allowed", + label, ExecutionRequirements.CPU.userFriendlyName())); + } + testResourcesFromTag = + ResourceSet.create( + testResourcesFromSize.getMemoryMb(), + Float.parseFloat(cpus), + testResourcesFromSize.getIoUsage(), + testResourcesFromSize.getLocalTestCount()); + } + } catch (ValidationException e) { + throw new UserExecException( + String.format( + "%s has a '%s' tag, but its value '%s' didn't pass validation: %s", + label, + ExecutionRequirements.CPU.userFriendlyName(), + e.getTagValue(), + e.getMessage())); + } + } + + return testResourcesFromTag != null ? testResourcesFromTag : testResourcesFromSize; + } + + /** + * Returns a map of execution info. See {@link Spawn#getExecutionInfo}. + */ + public ImmutableMap<String, String> getExecutionInfo() { + return executionInfo; + } + + public String getLanguage() { + return language; + } +} |