diff options
author | 2015-02-25 14:18:14 +0000 | |
---|---|---|
committer | 2015-02-25 14:18:14 +0000 | |
commit | d5baac0122a72209553a338ea28648d9184ca37e (patch) | |
tree | 8d73a4dbac72f1e2f99ec0910e6ba3c848b81fcb /src/test/java/com/google/devtools/build/lib | |
parent | 7d6a4f618488a778f4e5780a1ad6f8a3e27a6ec9 (diff) |
Open-source BuildViewTestCase and the infrastructure required by it, as well as
the tests under analysis/actions. They don't run yet, because the mock client
setup is still missing.
--
MOS_MIGRATED_REVID=87149625
Diffstat (limited to 'src/test/java/com/google/devtools/build/lib')
14 files changed, 3064 insertions, 0 deletions
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/BuildViewTestCase.java b/src/test/java/com/google/devtools/build/lib/analysis/BuildViewTestCase.java new file mode 100644 index 0000000000..7566f2f80f --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/analysis/BuildViewTestCase.java @@ -0,0 +1,1341 @@ +// Copyright 2015 Google Inc. 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; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +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.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.eventbus.EventBus; +import com.google.devtools.build.lib.actions.Action; +import com.google.devtools.build.lib.actions.ActionInput; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.ArtifactOwner; +import com.google.devtools.build.lib.actions.Executor; +import com.google.devtools.build.lib.actions.MapBasedActionGraph; +import com.google.devtools.build.lib.actions.MiddlemanFactory; +import com.google.devtools.build.lib.actions.MutableActionGraph; +import com.google.devtools.build.lib.actions.ResourceManager; +import com.google.devtools.build.lib.actions.ResourceSet; +import com.google.devtools.build.lib.actions.Root; +import com.google.devtools.build.lib.actions.util.ActionsTestUtil; +import com.google.devtools.build.lib.analysis.BuildView.AnalysisResult; +import com.google.devtools.build.lib.analysis.actions.SpawnAction; +import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey; +import com.google.devtools.build.lib.analysis.config.BinTools; +import com.google.devtools.build.lib.analysis.config.BuildConfiguration; +import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection; +import com.google.devtools.build.lib.analysis.config.BuildConfigurationKey; +import com.google.devtools.build.lib.analysis.config.BuildOptions; +import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider; +import com.google.devtools.build.lib.analysis.config.ConfigurationFactory; +import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException; +import com.google.devtools.build.lib.analysis.util.AnalysisMock; +import com.google.devtools.build.lib.analysis.util.AnalysisTestUtil; +import com.google.devtools.build.lib.buildtool.BuildRequest; +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.events.Event; +import com.google.devtools.build.lib.events.EventHandler; +import com.google.devtools.build.lib.events.StoredEventHandler; +import com.google.devtools.build.lib.exec.ExecutionOptions; +import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition; +import com.google.devtools.build.lib.packages.AttributeMap; +import com.google.devtools.build.lib.packages.ConstantRuleVisibility; +import com.google.devtools.build.lib.packages.NoSuchPackageException; +import com.google.devtools.build.lib.packages.NoSuchTargetException; +import com.google.devtools.build.lib.packages.OutputFile; +import com.google.devtools.build.lib.packages.PackageFactory; +import com.google.devtools.build.lib.packages.PackageFactory.EnvironmentExtension; +import com.google.devtools.build.lib.packages.PackageIdentifier; +import com.google.devtools.build.lib.packages.PackageSpecification; +import com.google.devtools.build.lib.packages.Preprocessor; +import com.google.devtools.build.lib.packages.Rule; +import com.google.devtools.build.lib.packages.Target; +import com.google.devtools.build.lib.packages.util.MockToolsConfig; +import com.google.devtools.build.lib.pkgcache.LoadingPhaseRunner; +import com.google.devtools.build.lib.pkgcache.LoadingPhaseRunner.LoadingResult; +import com.google.devtools.build.lib.pkgcache.PackageCacheOptions; +import com.google.devtools.build.lib.pkgcache.PackageManager; +import com.google.devtools.build.lib.pkgcache.PathPackageLocator; +import com.google.devtools.build.lib.pkgcache.TransitivePackageLoader; +import com.google.devtools.build.lib.rules.test.BaselineCoverageAction; +import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey; +import com.google.devtools.build.lib.skyframe.DiffAwareness; +import com.google.devtools.build.lib.skyframe.PrecomputedValue; +import com.google.devtools.build.lib.skyframe.SequencedSkyframeExecutor; +import com.google.devtools.build.lib.syntax.Label; +import com.google.devtools.build.lib.syntax.Label.SyntaxException; +import com.google.devtools.build.lib.testutil.FoundationTestCase; +import com.google.devtools.build.lib.testutil.TestConstants; +import com.google.devtools.build.lib.testutil.TestRuleClassProvider; +import com.google.devtools.build.lib.util.BlazeClock; +import com.google.devtools.build.lib.util.FileType; +import com.google.devtools.build.lib.util.StringUtil; +import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.ModifiedFileSet; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.skyframe.SkyFunction; +import com.google.devtools.build.skyframe.SkyFunctionName; +import com.google.devtools.common.options.Options; +import com.google.devtools.common.options.OptionsParser; +import com.google.devtools.common.options.OptionsParsingException; + +import org.mockito.Mockito; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +/** + * Common test code that creates a BuildView instance. + */ +public abstract class BuildViewTestCase extends FoundationTestCase { + protected static final int LOADING_PHASE_THREADS = 20; + + protected ConfiguredRuleClassProvider ruleClassProvider; + protected ConfigurationFactory configurationFactory; + protected BuildView view; + + private SequencedSkyframeExecutor skyframeExecutor; + + protected BlazeDirectories directories; + protected BinTools binTools; + + // Note that these configurations are virtual (they use only VFS) + protected BuildConfigurationCollection masterConfig; + protected BuildConfiguration targetConfig; // "target" or "build" config + + protected OptionsParser optionsParser; + private PackageCacheOptions packageCacheOptions; + + protected MockToolsConfig mockToolsConfig; + + protected WorkspaceStatusAction.Factory workspaceStatusActionFactory; + + private MutableActionGraph mutableActionGraph; + + @Override + protected void setUp() throws Exception { + super.setUp(); + AnalysisMock mock = getAnalysisMock(); + directories = new BlazeDirectories(outputBase, outputBase, rootDirectory); + binTools = BinTools.forUnitTesting(directories, TestConstants.EMBEDDED_TOOLS); + mockToolsConfig = new MockToolsConfig(rootDirectory, false); + mock.setupMockClient(mockToolsConfig); + configurationFactory = mock.createConfigurationFactory(); + packageCacheOptions = parsePackageCacheOptions(); + workspaceStatusActionFactory = + new AnalysisTestUtil.DummyWorkspaceStatusActionFactory(directories); + mutableActionGraph = new MapBasedActionGraph(); + ruleClassProvider = getRuleClassProvider(); + skyframeExecutor = SequencedSkyframeExecutor.create(reporter, + new PackageFactory(ruleClassProvider, getEnvironmentExtensions()), + new TimestampGranularityMonitor(BlazeClock.instance()), directories, + workspaceStatusActionFactory, + ruleClassProvider.getBuildInfoFactories(), + ImmutableSet.<Path>of(), + ImmutableList.<DiffAwareness.Factory>of(), + Predicates.<PathFragment>alwaysFalse(), + getPreprocessorFactorySupplier(), + ImmutableMap.<SkyFunctionName, SkyFunction>of(), + getPrecomputedValues() + ); + skyframeExecutor.preparePackageLoading( + new PathPackageLocator(rootDirectory), ConstantRuleVisibility.PUBLIC, true, "", + UUID.randomUUID()); + useConfiguration(); + setUpSkyframe(); + // Also initializes ResourceManager. + ResourceManager.instance().setAvailableResources(getStartingResources()); + } + + protected AnalysisMock getAnalysisMock() { + try { + Class<?> providerClass = Class.forName(TestConstants.TEST_ANALYSIS_MOCK); + Field instanceField = providerClass.getField("INSTANCE"); + return (AnalysisMock) instanceField.get(null); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + /** Creates or retrieves the rule class provider used in this test. */ + protected ConfiguredRuleClassProvider getRuleClassProvider() { + return TestRuleClassProvider.getRuleClassProvider(); + } + + protected Iterable<EnvironmentExtension> getEnvironmentExtensions() { + return ImmutableList.<EnvironmentExtension>of(); + } + + protected ImmutableList<PrecomputedValue.Injected> getPrecomputedValues() { + return ImmutableList.of(); + } + + protected Preprocessor.Factory.Supplier getPreprocessorFactorySupplier() { + return Preprocessor.Factory.Supplier.NullSupplier.INSTANCE; + } + + protected ResourceSet getStartingResources() { + // Effectively disable ResourceManager by default. + return ResourceSet.createWithRamCpuIo(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE); + } + + protected final BuildConfigurationCollection createConfigurations(String... args) + throws Exception { + optionsParser = OptionsParser.newOptionsParser(Iterables.concat(Arrays.asList( + ExecutionOptions.class, + BuildRequest.BuildRequestOptions.class), + ruleClassProvider.getConfigurationOptions())); + try { + List<String> configurationArgs = new ArrayList<>(); + configurationArgs.add("--experimental_extended_sanity_checks"); + configurationArgs.addAll(getAnalysisMock().getOptionOverrides()); + + optionsParser.parse(configurationArgs); + optionsParser.parse(args); + + configurationFactory.forbidSanityCheck(); + BuildOptions buildOptions = ruleClassProvider.createBuildOptions(optionsParser); + ensureTargetsVisited(buildOptions.getAllLabels().values()); + BuildConfigurationKey key = new BuildConfigurationKey( + buildOptions, directories, + ImmutableMap.<String, String>of()); + skyframeExecutor.invalidateConfigurationCollection(); + return skyframeExecutor.createConfigurations(configurationFactory, key); + } catch (InvalidConfigurationException | OptionsParsingException e) { + throw new IllegalArgumentException(e); + } + } + + protected Target getTarget(String label) + throws NoSuchPackageException, NoSuchTargetException, + Label.SyntaxException, InterruptedException { + return getTarget(Label.parseAbsolute(label)); + } + + protected Target getTarget(Label label) + throws NoSuchPackageException, NoSuchTargetException, InterruptedException { + return getPackageManager().getTarget(reporter, label); + } + + private void setUpSkyframe() { + PathPackageLocator pkgLocator = PathPackageLocator.create( + packageCacheOptions.packagePath, reporter, rootDirectory, rootDirectory); + skyframeExecutor.preparePackageLoading(pkgLocator, + packageCacheOptions.defaultVisibility, true, + ruleClassProvider.getDefaultsPackageContent(optionsParser), + UUID.randomUUID()); + skyframeExecutor.setDeletedPackages(ImmutableSet.copyOf(packageCacheOptions.deletedPackages)); + } + + protected void setPackageCacheOptions(String... options) throws Exception { + packageCacheOptions = parsePackageCacheOptions(options); + setUpSkyframe(); + } + + private PackageCacheOptions parsePackageCacheOptions(String... options) throws Exception { + OptionsParser parser = OptionsParser.newOptionsParser(PackageCacheOptions.class); + parser.parse("--default_visibility=public"); + parser.parse(options); + return parser.getOptions(PackageCacheOptions.class); + } + + /** Used by skyframe-only tests. */ + protected SequencedSkyframeExecutor getSkyframeExecutor() { + return Preconditions.checkNotNull(skyframeExecutor); + } + + protected PackageManager getPackageManager() { + return skyframeExecutor.getPackageManager(); + } + + protected AnalysisHooks getAnalysisHooks() { + return new AnalysisHooks() { + @Override + public PackageManager getPackageManager() { + return BuildViewTestCase.this.getPackageManager(); + } + + @Override + public ConfiguredTarget getExistingConfiguredTarget(Target target, + BuildConfiguration configuration) { + return view.getExistingConfiguredTarget(target, configuration); + } + + }; + } + + /** + * Invalidates all existing packages. + * @throws InterruptedException + */ + protected void invalidatePackages() throws InterruptedException { + skyframeExecutor.invalidateFilesUnderPathForTesting(ModifiedFileSet.EVERYTHING_MODIFIED, + rootDirectory); + } + + /** + * Sets host and target configuration using the specified options, falling back to the default + * options for unspecified ones, and recreates the build view. + * + * @throws IllegalArgumentException + */ + protected final void useConfiguration(String... args) throws Exception { + masterConfig = createConfigurations(args); + targetConfig = getTargetConfiguration(); + createBuildView(); + } + + /** + * Creates BuildView using current hostConfig/targetConfig values. + * Ensures that hostConfig is either identical to the targetConfig or has + * 'host' short name. + */ + protected final void createBuildView() throws Exception { + Preconditions.checkNotNull(masterConfig); + Preconditions.checkState(getHostConfiguration() == getTargetConfiguration() + || getHostConfiguration().getShortName().equals("host"), + "Host configuration %s does not have name 'host' " + + "and does not match target configuration %s", + getHostConfiguration(), getTargetConfiguration()); + + String defaultsPackageContent = ruleClassProvider.getDefaultsPackageContent(optionsParser); + skyframeExecutor.setupDefaultPackage(defaultsPackageContent); + skyframeExecutor.dropConfiguredTargets(); + + view = new BuildView(directories, getPackageManager(), ruleClassProvider, skyframeExecutor, + binTools, null); + view.setConfigurationsForTesting(masterConfig); + + view.setArtifactRoots( + ImmutableMap.of(PackageIdentifier.createInDefaultRepo(""), rootDirectory)); + simulateLoadingPhase(); + } + + protected CachingAnalysisEnvironment getTestAnalysisEnvironment() { + return new CachingAnalysisEnvironment(view.getArtifactFactory(), + ArtifactOwner.NULL_OWNER, /*isSystemEnv=*/true, /*extendedSanityChecks*/false, reporter, + /*skyframeEnv=*/ null, /*actionsEnabled=*/true, binTools); + } + + /** + * Allows access to the prerequisites of a configured target. This is currently used in some tests + * to reach into the internals of RuleCT for white box testing. In principle, this should not be + * used; instead tests should only assert on properties of the exposed provider instances and / or + * the action graph. + */ + protected Iterable<ConfiguredTarget> getDirectPrerequisites(ConfiguredTarget target) { + return view.getDirectPrerequisites(target); + } + + /** + * Creates and returns a rule context that is equivalent to the one that was used to create the + * given configured target. + */ + protected RuleContext getRuleContext(ConfiguredTarget target) { + return new RuleContext.Builder( + new StubAnalysisEnvironment(), (Rule) target.getTarget(), + target.getConfiguration(), ruleClassProvider.getPrerequisiteValidator()) + .setVisibility(NestedSetBuilder.<PackageSpecification>create( + Order.STABLE_ORDER, PackageSpecification.EVERYTHING)) + .setPrerequisites(view.getPrerequisiteMapForTesting(target)) + .setConfigConditions(ImmutableSet.<ConfigMatchingProvider>of()) + .build(); + } + + /** + * Creates and returns a rule context to use for Skylark tests that is equivalent to the one + * that was used to create the given configured target. + */ + protected RuleContext getRuleContextForSkylark(ConfiguredTarget target) { + // TODO(bazel-team): we need this horrible workaround because CachingAnalysisEnvironment + // only works with StoredErrorEventListener despite the fact it accepts the interface + // ErrorEventListener, so it's not possible to create it with reporter. + // See BuildView.getRuleContextForTesting(). + StoredEventHandler eventHandler = new StoredEventHandler() { + @Override + public synchronized void handle(Event e) { + super.handle(e); + reporter.handle(e); + } + }; + return view.getRuleContextForTesting(target, eventHandler); + } + + /** + * Allows access to the prerequisites of a configured target. This is currently used in some tests + * to reach into the internals of RuleCT for white box testing. In principle, this should not be + * used; instead tests should only assert on properties of the exposed provider instances and / or + * the action graph. + */ + protected List<? extends TransitiveInfoCollection> getPrerequisites(ConfiguredTarget target, + String attributeName) { + return getRuleContext(target).getConfiguredTargetMap().get(attributeName); + } + + /** + * Allows access to the prerequisites of a configured target. This is currently used in some tests + * to reach into the internals of RuleCT for white box testing. In principle, this should not be + * used; instead tests should only assert on properties of the exposed provider instances and / or + * the action graph. + */ + protected <C extends TransitiveInfoProvider> Iterable<C> getPrerequisites( + ConfiguredTarget target, String attributeName, Class<C> classType) { + return AnalysisUtils.getProviders(getPrerequisites(target, attributeName), classType); + } + + /** + * Allows access to the prerequisites of a configured target. This is currently used in some tests + * to reach into the internals of RuleCT for white box testing. In principle, this should not be + * used; instead tests should only assert on properties of the exposed provider instances and / or + * the action graph. + */ + protected ImmutableList<Artifact> getPrerequisiteArtifacts( + ConfiguredTarget target, String attributeName) { + Set<Artifact> result = new LinkedHashSet<>(); + for (FileProvider provider : getPrerequisites(target, attributeName, FileProvider.class)) { + Iterables.addAll(result, provider.getFilesToBuild()); + } + return ImmutableList.copyOf(result); + } + + protected final Action getGeneratingAction(Artifact artifact) { + Preconditions.checkNotNull(artifact); + Action action = mutableActionGraph.getGeneratingAction(artifact); + if (action != null) { + return action; + } + return view.getActionGraph().getGeneratingAction(artifact); + } + + protected void simulateLoadingPhase() { + try { + ensureTargetsVisited(targetConfig.getAllLabels().values()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected ActionsTestUtil actionsTestUtil() { + return new ActionsTestUtil(view.getActionGraph()); + } + + private Set<Target> getTargets(Iterable<Label> labels) throws InterruptedException, + NoSuchTargetException, NoSuchPackageException{ + Set<Target> targets = Sets.newHashSet(); + for (Label label : labels) { + targets.add(skyframeExecutor.getPackageManager().getTarget(reporter, label)); + } + return targets; + } + + // Get a MutableActionGraph for testing purposes. + protected MutableActionGraph getMutableActionGraph() { + return mutableActionGraph; + } + + protected TransitivePackageLoader makeVisitor() { + setUpSkyframe(); + return skyframeExecutor.pkgLoader(); + } + + /** + * Construct the containing package of the specified labels, and all of its transitive + * dependencies. This must be done prior to configuration, as the latter is intolerant of + * NoSuchTargetExceptions. + */ + protected boolean ensureTargetsVisited(TransitivePackageLoader visitor, + Collection<Label> targets, Collection<Label> labels, boolean keepGoing) + throws InterruptedException, NoSuchTargetException, NoSuchPackageException { + boolean success = visitor.sync(reporter, + ImmutableSet.copyOf(getTargets(targets)), + ImmutableSet.copyOf(labels), + keepGoing, + /*parallelThreads=*/4, + /*maxDepth=*/Integer.MAX_VALUE); + return success; + } + + protected boolean ensureTargetsVisited(Collection<Label> labels) + throws InterruptedException, NoSuchTargetException, NoSuchPackageException { + return ensureTargetsVisited(makeVisitor(), ImmutableSet.<Label>of(), labels, + /*keepGoing=*/false); + } + + protected boolean ensureTargetsVisited(Label label) + throws InterruptedException, NoSuchTargetException, NoSuchPackageException { + return ensureTargetsVisited(ImmutableList.of(label)); + } + + protected boolean ensureTargetsVisited(String... labels) + throws InterruptedException, NoSuchTargetException, NoSuchPackageException, SyntaxException { + List<Label> actualLabels = new ArrayList<>(); + for (String label : labels) { + actualLabels.add(Label.parseAbsolute(label)); + } + return ensureTargetsVisited(actualLabels); + } + + /** + * Returns the ConfiguredTarget for the specified label, configured for the + * "build" (aka "target") configuration. + */ + protected ConfiguredTarget getConfiguredTarget(String label) + throws NoSuchPackageException, NoSuchTargetException, + Label.SyntaxException, InterruptedException { + return getConfiguredTarget(label, targetConfig); + } + + /** + * Returns the ConfiguredTarget for the specified label, using the + * given build configuration. + */ + protected ConfiguredTarget getConfiguredTarget(String label, BuildConfiguration config) + throws NoSuchPackageException, NoSuchTargetException, + Label.SyntaxException, InterruptedException { + ensureTargetsVisited(label); + return view.getConfiguredTargetForTesting(getTarget(label), config); + } + + /** + * Returns the ConfiguredTarget for the specified label, using the + * given build configuration. + */ + protected ConfiguredTarget getConfiguredTarget(Label label, BuildConfiguration config) + throws NoSuchPackageException, NoSuchTargetException, InterruptedException { + ensureTargetsVisited(label); + return view.getConfiguredTargetForTesting(getTarget(label), config); + } + + /** + * Returns the ConfiguredTarget for the specified file label, configured for + * the "build" (aka "target") configuration. + */ + protected FileConfiguredTarget getFileConfiguredTarget(String label) + throws NoSuchPackageException, NoSuchTargetException, + Label.SyntaxException, InterruptedException { + return (FileConfiguredTarget) getConfiguredTarget(label, targetConfig); + } + + /** + * Returns the ConfiguredTarget for the specified label, configured for + * the "host" configuration. + */ + protected ConfiguredTarget getHostConfiguredTarget(String label) + throws NoSuchPackageException, NoSuchTargetException, + Label.SyntaxException, InterruptedException { + return getConfiguredTarget(label, getHostConfiguration()); + } + + /** + * Returns the ConfiguredTarget for the specified file label, configured for + * the "host" configuration. + */ + protected FileConfiguredTarget getHostFileConfiguredTarget(String label) + throws NoSuchPackageException, NoSuchTargetException, + Label.SyntaxException, InterruptedException { + return (FileConfiguredTarget) getHostConfiguredTarget(label); + } + + /** + * Create and return a configured scratch rule. + * + * @param packageName the package name ofthe rule. + * @param ruleName the name of the rule. + * @param lines the text of the rule. + * @return the configured target instance for the created rule. + * @throws IOException + * @throws Exception + */ + protected ConfiguredTarget scratchConfiguredTarget(String packageName, + String ruleName, + String... lines) + throws IOException, Exception { + return scratchConfiguredTarget(packageName, ruleName, targetConfig, lines); + } + + /** + * Create and return a scratch rule. + * + * @param packageName the package name of the rule. + * @param ruleName the name of the rule. + * @param lines the text of the rule. + * @return the rule instance for the created rule. + * @throws IOException + * @throws Exception + */ + protected Rule scratchRule(String packageName, String ruleName, String... lines) + throws Exception { + scratchFile("/" + TestConstants.TEST_WORKSPACE_DIRECTORY + "/" + packageName + "/BUILD", lines); + return (Rule) getTarget("//" + packageName + ":" + ruleName); + } + + /** + * Create and return a configured scratch rule. + * + * @param packageName the package name of the rule. + * @param ruleName the name of the rule. + * @param config the configuration to use to construct the configured rule. + * @param lines the text of the rule. + * @return the configured target instance for the created rule. + * @throws IOException + * @throws Exception + */ + protected ConfiguredTarget scratchConfiguredTarget(String packageName, + String ruleName, + BuildConfiguration config, + String... lines) + throws IOException, Exception { + Target rule = scratchRule(packageName, ruleName, lines); + if (ensureTargetsVisited(rule.getLabel())) { + return view.getConfiguredTargetForTesting(rule, config); + } else { + return null; + } + } + + /** + * Check that configuration of the target named 'ruleName' in the + * specified BUILD file fails with an error message ending in + * 'expectedErrorMessage'. + * + * @param packageName the package name of the generated BUILD file + * @param ruleName the rule name for the rule in the generated BUILD file + * @param expectedErrorMessage the expected error message. + * @param lines the text of the rule. + * @return the found error. + */ + protected Event checkError(String packageName, + String ruleName, + String expectedErrorMessage, + String... lines) throws Exception { + eventCollector.clear(); + reporter.removeHandler(failFastHandler); // expect errors + ConfiguredTarget target = scratchConfiguredTarget(packageName, ruleName, lines); + if (target != null) { + assertTrue("Rule '" + "//" + packageName + ":" + ruleName + "' did not contain an error", + view.hasErrors(target)); + } + return assertContainsEvent(expectedErrorMessage); + } + + /** + * Check that configuration of the target named 'ruleName' in the + * specified BUILD file reports a warning message ending in + * 'expectedWarningMessage', and that no errors were reported. + * + * @param packageName the package name of the generated BUILD file + * @param ruleName the rule name for the rule in the generated BUILD file + * @param expectedWarningMessage the expected warning message. + * @param lines the text of the rule. + * @return the found error. + */ + protected Event checkWarning(String packageName, + String ruleName, + String expectedWarningMessage, + String... lines) throws Exception { + eventCollector.clear(); + ConfiguredTarget target = scratchConfiguredTarget(packageName, ruleName, + lines); + assertFalse("Rule '" + "//" + packageName + ":" + ruleName + + "' did contain an error", + view.hasErrors(target)); + return assertContainsEvent(expectedWarningMessage); + } + + /** + * Given a collection of Artifacts, returns a corresponding set of strings of + * the form "[root] [relpath]", such as "bin x/libx.a". Such strings make + * assertions easier to write. + * + * <p>The returned set preserves the order of the input. + */ + protected Set<String> artifactsToStrings(Iterable<Artifact> artifacts) { + return AnalysisTestUtil.artifactsToStrings(masterConfig, artifacts); + } + + /** + * Asserts that targetName's outputs are exactly expectedOuts. + * + * @param targetName The label of a rule. + * @param expectedOuts The labels of the expected outputs of the rule. + */ + protected void assertOuts(String targetName, String... expectedOuts) throws Exception { + Rule ruleTarget = (Rule) getTarget(targetName); + for (String expectedOut : expectedOuts) { + Target outTarget = getTarget(expectedOut); + if (!(outTarget instanceof OutputFile)) { + fail("Target " + outTarget + " is not an output"); + assertSame(ruleTarget, ((OutputFile) outTarget).getGeneratingRule()); + // This ensures that the output artifact is wired up in the action graph + getConfiguredTarget(expectedOut); + } + } + + Collection<OutputFile> outs = ruleTarget.getOutputFiles(); + assertEquals("Mismatched outputs: " + outs, expectedOuts.length, outs.size()); + } + + /** + * Asserts that there exists a configured target file for the given label. + */ + protected void assertConfiguredTargetExists(String label) throws Exception { + assertNotNull(getFileConfiguredTarget(label)); + } + + /** + * Assert that the first label and the second label are both generated + * by the same command. + */ + protected void assertSameGeneratingAction(String labelA, String labelB) + throws Exception { + assertSame( + "Action for " + labelA + " did not match " + labelB, + getGeneratingActionForLabel(labelA), + getGeneratingActionForLabel(labelB)); + } + + protected Artifact getSourceArtifact(PathFragment rootRelativePath, Root root) { + return view.getArtifactFactory().getSourceArtifact(rootRelativePath, root); + } + + protected Artifact getSourceArtifact(String name) throws IOException { + return getSourceArtifact(new PathFragment(name), + Root.asSourceRoot(scratchDir("/" + TestConstants.TEST_WORKSPACE_DIRECTORY))); + } + + /** + * Gets a derived artifact, creating it if necessary. {@code ArtifactOwner} should be a genuine + * {@link LabelAndConfiguration} corresponding to a {@link ConfiguredTarget}. If called from a + * test that does not exercise the analysis phase, the convenience methods {@link + * #getBinArtifactWithNoOwner} or {@link #getGenfilesArtifactWithNoOwner} should be used instead. + */ + protected Artifact getDerivedArtifact(PathFragment rootRelativePath, Root root, + ArtifactOwner owner) { + return view.getArtifactFactory().getDerivedArtifact(rootRelativePath, root, owner); + } + + /** + * Gets a derived Artifact for testing with path of the form + * root/owner.getPackageFragment()/packageRelativePath. + * + * @see #getDerivedArtifact(PathFragment, Root, ArtifactOwner) + */ + private Artifact getPackageRelativeDerivedArtifact(String packageRelativePath, Root root, + ArtifactOwner owner) { + return getDerivedArtifact( + owner.getLabel().getPackageFragment().getRelative(packageRelativePath), + root, owner); + } + + /** + * Gets a derived Artifact for testing in the {@link BuildConfiguration#getBinDirectory()}. This + * method should only be used for tests that do no analysis, and so there is no ConfiguredTarget + * to own this artifact. If the test runs the analysis phase, {@link + * #getBinArtifact(String, ArtifactOwner)} or its convenience methods should be + * used instead. + */ + protected Artifact getBinArtifactWithNoOwner(String rootRelativePath) { + return getDerivedArtifact(new PathFragment(rootRelativePath), targetConfig.getBinDirectory(), + ActionsTestUtil.NULL_ARTIFACT_OWNER); + } + + /** + * Gets a derived Artifact for testing in the subdirectory of the {@link + * BuildConfiguration#getBinDirectory()} corresponding to the package of {@code owner}. So + * to specify a file foo/foo.o owned by target //foo:foo, {@code packageRelativePath} should just + * be "foo.o". + */ + protected Artifact getBinArtifact(String packageRelativePath, String owner) { + return getBinArtifact(packageRelativePath, makeLabelAndConfiguration(owner)); + } + + /** + * Gets a derived Artifact for testing in the subdirectory of the {@link + * BuildConfiguration#getBinDirectory()} corresponding to the package of {@code owner}. So + * to specify a file foo/foo.o owned by target //foo:foo, {@code packageRelativePath} should just + * be "foo.o". + */ + protected Artifact getBinArtifact(String packageRelativePath, ConfiguredTarget owner) { + return getPackageRelativeDerivedArtifact(packageRelativePath, + owner.getConfiguration().getBinDirectory(), new ConfiguredTargetKey(owner)); + } + + /** + * Gets a derived Artifact for testing in the subdirectory of the {@link + * BuildConfiguration#getBinDirectory()} corresponding to the package of {@code owner}. So + * to specify a file foo/foo.o owned by target //foo:foo, {@code packageRelativePath} should just + * be "foo.o". + */ + private Artifact getBinArtifact(String packageRelativePath, ArtifactOwner owner) { + return getPackageRelativeDerivedArtifact(packageRelativePath, targetConfig.getBinDirectory(), + owner); + } + + /** + * Gets a derived Artifact for testing in the {@link BuildConfiguration#getGenfilesDirectory()}. + * This method should only be used for tests that do no analysis, and so there is no + * ConfiguredTarget to own this artifact. If the test runs the analysis phase, {@link + * #getGenfilesArtifact(String, ArtifactOwner)} or its convenience methods should be used instead. + */ + protected Artifact getGenfilesArtifactWithNoOwner(String rootRelativePath) { + return getDerivedArtifact(new PathFragment(rootRelativePath), + targetConfig.getGenfilesDirectory(), ActionsTestUtil.NULL_ARTIFACT_OWNER); + } + + /** + * Gets a derived Artifact for testing in the subdirectory of the {@link + * BuildConfiguration#getGenfilesDirectory()} corresponding to the package of {@code owner}. + * So to specify a file foo/foo.o owned by target //foo:foo, {@code packageRelativePath} should + * just be "foo.o". + */ + protected Artifact getGenfilesArtifact(String packageRelativePath, String owner) { + return getGenfilesArtifact(packageRelativePath, makeLabelAndConfiguration(owner)); + } + + /** + * Gets a derived Artifact for testing in the subdirectory of the {@link + * BuildConfiguration#getGenfilesDirectory()} corresponding to the package of {@code owner}. + * So to specify a file foo/foo.o owned by target //foo:foo, {@code packageRelativePath} should + * just be "foo.o". + */ + protected Artifact getGenfilesArtifact(String packageRelativePath, ConfiguredTarget owner) { + return getGenfilesArtifact(packageRelativePath, new ConfiguredTargetKey(owner)); + } + + /** + * Gets a derived Artifact for testing in the subdirectory of the {@link + * BuildConfiguration#getGenfilesDirectory()} corresponding to the package of {@code owner}. + * So to specify a file foo/foo.o owned by target //foo:foo, {@code packageRelativePath} should + * just be "foo.o". + */ + private Artifact getGenfilesArtifact(String packageRelativePath, ArtifactOwner owner) { + return getPackageRelativeDerivedArtifact(packageRelativePath, + targetConfig.getGenfilesDirectory(), + owner); + } + + protected Action getGeneratingActionForLabel(String label) throws Exception { + return getGeneratingAction(getFileConfiguredTarget(label).getArtifact()); + } + + protected String fileName(Artifact artifact) { + return artifact.getExecPathString(); + } + + protected String fileName(FileConfiguredTarget target) { + return fileName(target.getArtifact()); + } + + protected String fileName(String name) throws Exception { + return fileName(getFileConfiguredTarget(name)); + } + + protected Path getOutputPath() { + return directories.getOutputPath(); + } + + /** + * Verifies whether the rule checks the 'srcs' attribute validity. + * + * <p>At the call site it expects the {@code packageName} to contain: + * <ol> + * <li>{@code :gvalid} - genrule that outputs a valid file</li> + * <li>{@code :ginvalid} - genrule that outputs an invalid file</li> + * <li>{@code :gmix} - genrule that outputs a mix of valid and invalid + * files</li> + * <li>{@code :valid} - rule of type {@code ruleType} that has a valid + * file, {@code :gvalid} and {@code :gmix} in the srcs</li> + * <li>{@code :invalid} - rule of type {@code ruleType} that has an invalid + * file, {@code :ginvalid} in the srcs</li> + * <li>{@code :mix} - rule of type {@code ruleType} that has a valid and an + * invalid file in the srcs</li> + * </ol> + * + * @param packageName the package where the rules under test are located + * @param ruleType rules under test types + * @param expectedTypes expected file types + */ + protected void assertSrcsValidityForRuleType(String packageName, String ruleType, + String expectedTypes) throws Exception { + reporter.removeHandler(failFastHandler); + String descriptionSingle = ruleType + " srcs file (expected " + expectedTypes + ")"; + String descriptionPlural = ruleType + " srcs files (expected " + expectedTypes + ")"; + String descriptionPluralFile = "(expected " + expectedTypes + ")"; + assertSrcsValidity(ruleType, packageName + ":valid", false, + "need at least one " + descriptionSingle, + "'" + packageName + ":gvalid' does not produce any " + descriptionPlural, + "'" + packageName + ":gmix' does not produce any " + descriptionPlural); + assertSrcsValidity(ruleType, packageName + ":invalid", true, + "file '" + packageName + ":a.foo' is misplaced here " + descriptionPluralFile, + "'" + packageName + ":ginvalid' does not produce any " + descriptionPlural); + assertSrcsValidity(ruleType, packageName + ":mix", true, + "'" + packageName + ":a.foo' does not produce any " + descriptionPlural); + } + + protected void assertSrcsValidity(String ruleType, String targetName, boolean expectedError, + String... expectedMessages) throws Exception{ + ConfiguredTarget target = getConfiguredTarget(targetName); + if (expectedError) { + assertTrue(view.hasErrors(target)); + for (String expectedMessage : expectedMessages) { + String message = "in srcs attribute of " + ruleType + " rule " + targetName + ": " + + expectedMessage; + assertContainsEvent(message); + } + } else { + assertFalse(view.hasErrors(target)); + for (String expectedMessage : expectedMessages) { + String message = "in srcs attribute of " + ruleType + " rule " + target.getLabel() + ": " + + expectedMessage; + assertDoesNotContainEvent(message); + } + } + } + + private static Label makeLabel(String label) { + try { + return Label.parseAbsolute(label); + } catch (SyntaxException e) { + throw new IllegalStateException(e); + } + } + + private ConfiguredTargetKey makeLabelAndConfiguration(String label) { + return new ConfiguredTargetKey(makeLabel(label), targetConfig); + } + + protected static List<String> actionInputsToPaths(Iterable<? extends ActionInput> actionInputs) { + return ImmutableList.copyOf( + Iterables.transform(actionInputs, new Function<ActionInput, String>() { + @Override + public String apply(ActionInput actionInput) { + return actionInput.getExecPathString(); + } + })); + } + + protected String readContentAsLatin1String(Artifact artifact) throws IOException { + return new String(FileSystemUtils.readContentAsLatin1(artifact.getPath())); + } + + /** + * Asserts that the predecessor closure of the given Artifact contains the same elements as those + * in expectedPredecessors, plus the given common predecessors. Only looks at predecessors of + * the given file type. + */ + public void assertPredecessorClosureSameContents( + Artifact artifact, FileType fType, Iterable<String> common, String... expectedPredecessors) { + assertSameContentsWithCommonElements( + actionsTestUtil().predecessorClosureAsCollection(artifact, fType), + expectedPredecessors, common); + } + + /** + * Utility method for asserting that the contents of one collection are the + * same as those in a second plus some set of common elements. + */ + protected void assertSameContentsWithCommonElements(Iterable<Artifact> artifacts, + Iterable<String> common, String... expectedInputs) { + assertSameContents(Iterables.concat(Lists.newArrayList(expectedInputs), common), + ActionsTestUtil.prettyArtifactNames(artifacts)); + } + + /** + * Utility method for asserting that the contents of one collection are the + * same as those in a second plus some set of common elements. + */ + protected void assertSameContentsWithCommonElements(Iterable<String> artifacts, + String[] expectedInputs, Iterable<String> common) { + assertSameContents(Iterables.concat(Lists.newArrayList(expectedInputs), common), artifacts); + } + + /** + * Utility method for asserting that a list contains the elements of a + * sublist This is useful for checking that a list of arguments contains a + * particular set of arguments. + */ + protected void assertContainsSublist(List<String> list, List<String> sublist) { + assertContainsSublist(null, list, sublist); + } + + /** + * Utility method for asserting that a list contains the elements of a + * sublist This is useful for checking that a list of arguments contains a + * particular set of arguments. + */ + protected void assertContainsSublist(String message, List<String> list, List<String> sublist) { + if (Collections.indexOfSubList(list, sublist) == -1) { + fail((message == null ? "" : (message + ' ')) + + "expected: <" + list + "> to contain sublist: <" + sublist + ">"); + } + } + + protected void assertContainsSelfEdgeEvent(String label) { + assertContainsEvent(label + " [self-edge]"); + } + + protected Iterable<Artifact> collectRunfiles(ConfiguredTarget target) { + RunfilesProvider runfilesProvider = target.getProvider(RunfilesProvider.class); + if (runfilesProvider != null) { + return runfilesProvider.getDefaultRunfiles().getAllArtifacts(); + } else { + return Runfiles.EMPTY.getAllArtifacts(); + } + } + + protected NestedSet<Artifact> getFilesToBuild(TransitiveInfoCollection target) { + return target.getProvider(FileProvider.class).getFilesToBuild(); + } + + /** + * Returns all extra actions for that target (no transitive actions), no duplicate actions. + */ + protected ImmutableList<Action> getExtraActionActions(ConfiguredTarget target) { + LinkedHashSet<Action> result = new LinkedHashSet<>(); + for (Artifact artifact : getExtraActionArtifacts(target)) { + result.add(getGeneratingAction(artifact)); + } + return ImmutableList.copyOf(result); + } + + protected ImmutableList<Action> getFilesToBuildActions(ConfiguredTarget target) { + List<Action> result = new ArrayList<>(); + for (Artifact artifact : getFilesToBuild(target)) { + Action action = getGeneratingAction(artifact); + if (action != null) { + result.add(action); + } + } + return ImmutableList.copyOf(result); + } + + protected NestedSet<Artifact> getOutputGroup( + TransitiveInfoCollection target, String outputGroup) { + TopLevelArtifactProvider provider = target.getProvider(TopLevelArtifactProvider.class); + return provider == null + ? NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER) + : provider.getOutputGroup(outputGroup); + } + + protected ImmutableList<Artifact> getExtraActionArtifacts(ConfiguredTarget target) { + return target.getProvider(ExtraActionArtifactsProvider.class).getExtraActionArtifacts(); + } + + protected Artifact getExecutable(String label) throws Exception { + return getConfiguredTarget(label).getProvider(FilesToRunProvider.class).getExecutable(); + } + + protected Artifact getExecutable(TransitiveInfoCollection target) { + return target.getProvider(FilesToRunProvider.class).getExecutable(); + } + + protected ImmutableList<Artifact> getFilesToRun(TransitiveInfoCollection target) { + return target.getProvider(FilesToRunProvider.class).getFilesToRun(); + } + + protected ImmutableList<Artifact> getFilesToRun(Label label) throws Exception { + return getConfiguredTarget(label, targetConfig) + .getProvider(FilesToRunProvider.class).getFilesToRun(); + } + + protected ImmutableList<Artifact> getFilesToRun(String label) throws Exception { + return getConfiguredTarget(label).getProvider(FilesToRunProvider.class).getFilesToRun(); + } + + protected RunfilesSupport getRunfilesSupport(String label) throws Exception { + return getConfiguredTarget(label).getProvider(FilesToRunProvider.class).getRunfilesSupport(); + } + + protected RunfilesSupport getRunfilesSupport(TransitiveInfoCollection target) { + return target.getProvider(FilesToRunProvider.class).getRunfilesSupport(); + } + + protected static Runfiles getDefaultRunfiles(ConfiguredTarget target) { + return target.getProvider(RunfilesProvider.class).getDefaultRunfiles(); + } + + protected static Runfiles getDataRunfiles(ConfiguredTarget target) { + return target.getProvider(RunfilesProvider.class).getDataRunfiles(); + } + + protected BuildConfiguration getTargetConfiguration() { + return Iterables.getOnlyElement(masterConfig.getTargetConfigurations()); + } + + protected BuildConfiguration getDataConfiguration() { + return getTargetConfiguration().getConfiguration(ConfigurationTransition.DATA); + } + + protected BuildConfiguration getHostConfiguration() { + return getTargetConfiguration().getConfiguration(ConfigurationTransition.HOST); + } + + /** + * Returns an attribute value retriever for the given rule for the target configuration. + + */ + protected AttributeMap attributes(RuleConfiguredTarget ct) { + return ConfiguredAttributeMapper.of(ct); + } + + protected AttributeMap attributes(ConfiguredTarget rule) { + return attributes((RuleConfiguredTarget) rule); + } + + protected AnalysisResult update(List<String> targets, + boolean keepGoing, + int loadingPhaseThreads, + boolean doAnalysis, + EventBus eventBus) throws Exception { + + LoadingPhaseRunner.Options loadingOptions = + Options.getDefaults(LoadingPhaseRunner.Options.class); + loadingOptions.loadingPhaseThreads = loadingPhaseThreads; + + BuildView.Options viewOptions = Options.getDefaults(BuildView.Options.class); + viewOptions.keepGoing = keepGoing; + + LoadingPhaseRunner runner = new LoadingPhaseRunner(getPackageManager(), + Collections.unmodifiableSet(ruleClassProvider.getRuleClassMap().keySet())); + LoadingResult loadingResult = runner.execute(reporter, eventBus, targets, loadingOptions, + getTargetConfiguration().getAllLabels(), + viewOptions.keepGoing, /*determineTests=*/false, /*callback=*/null); + if (!doAnalysis) { + // TODO(bazel-team): What's supposed to happen in this case? + return null; + } + return view.update(loadingResult, masterConfig, viewOptions, + TopLevelArtifactContext.DEFAULT, reporter, eventBus); + } + + protected static Predicate<Artifact> artifactNamed(final String name) { + return new Predicate<Artifact>() { + @Override + public boolean apply(Artifact input) { + return name.equals(input.prettyPrint()); + } + }; + } + + /** + * Utility method for tests. Converts an array of strings into a set of labels. + * + * @param strings the set of strings to be converted to labels. + * @throws SyntaxException if there are any syntax errors in the strings. + */ + public static Set<Label> asLabelSet(String... strings) throws SyntaxException { + return asLabelSet(ImmutableList.copyOf(strings)); + } + + /** + * Utility method for tests. Converts an array of strings into a set of labels. + * + * @param strings the set of strings to be converted to labels. + * @throws SyntaxException if there are any syntax errors in the strings. + */ + public static Set<Label> asLabelSet(Iterable<String> strings) throws SyntaxException { + Set<Label> result = Sets.newTreeSet(); + for (String s : strings) { + result.add(Label.parseAbsolute(s)); + } + return result; + } + + protected SpawnAction getGeneratingAction(ConfiguredTarget target, + String outputName) { + Artifact found = Iterables.find(getFilesToBuild(target), + artifactNamed(outputName)); + return (SpawnAction) getGeneratingAction(found); + } + + protected String getErrorMsgSingleFile(String attrName, String ruleType, String ruleName, + String depRuleName) { + return "in " + attrName + " attribute of " + ruleType + " rule " + ruleName + ": '" + + depRuleName + "' must produce a single file"; + } + + protected String getErrorMsgNoGoodFiles(String attrName, String ruleType, String ruleName, + String depRuleName) { + return "in " + attrName + " attribute of " + ruleType + " rule " + ruleName + ": '" + + depRuleName + "' does not produce any " + ruleType + " " + attrName + " files"; + } + + protected String getErrorMsgMisplacedFiles(String attrName, String ruleType, String ruleName, + String fileName) { + return "in " + attrName + " attribute of " + ruleType + " rule " + ruleName + ": file '" + + fileName + "' is misplaced here"; + } + + protected String getErrorNonExistingTarget(String attrName, String ruleType, String ruleName, + String targetName) { + return "in " + attrName + " attribute of " + ruleType + " rule " + ruleName + ": target '" + + targetName + "' does not exist"; + } + + protected String getErrorNonExistingRule(String attrName, String ruleType, String ruleName, + String targetName) { + return "in " + attrName + " attribute of " + ruleType + " rule " + ruleName + ": rule '" + + targetName + "' does not exist"; + } + + protected String getErrorMsgMisplacedRules(String attrName, String ruleType, String ruleName, + String depRuleType, String depRuleName) { + return "in " + attrName + " attribute of " + ruleType + " rule " + ruleName + ": " + + depRuleType + " rule '" + depRuleName + "' is misplaced here"; + } + + public static String getErrorMsgNonEmptyList(String attrName, String ruleType, String ruleName) { + return "non empty attribute '" + attrName + "' in '" + ruleType + + "' rule '" + ruleName + "' has to have at least one value"; + } + + protected String getErrorMsgMandatoryMissing(String attrName, String ruleType) { + return "missing value for mandatory attribute '" + attrName + "' in '" + ruleType + "' rule"; + } + + protected String getErrorMsgWrongAttributeValue(String value, String... expected) { + return String.format("has to be one of %s instead of '%s'", + StringUtil.joinEnglishList(ImmutableSet.copyOf(expected), "or", "'"), value); + } + + private class StubAnalysisEnvironment implements AnalysisEnvironment { + + @Override + public void registerAction(Action... action) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasErrors() { + return false; + } + + @Override + public Artifact getEmbeddedToolArtifact(String embeddedPath) { + throw new UnsupportedOperationException(); + } + + @Override + public Artifact getConstantMetadataArtifact(PathFragment rootRelativePath, Root root) { + throw new UnsupportedOperationException(); + } + + @Override + public EventHandler getEventHandler() { + return reporter; + } + + @Override + public MiddlemanFactory getMiddlemanFactory() { + throw new UnsupportedOperationException(); + } + + @Override + public Action getLocalGeneratingAction(Artifact artifact) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterable<Action> getRegisteredActions() { + throw new UnsupportedOperationException(); + } + + @Override + public SkyFunction.Environment getSkyframeEnv() { + throw new UnsupportedOperationException(); + } + + @Override + public Artifact getFilesetArtifact(PathFragment rootRelativePath, Root root) { + throw new UnsupportedOperationException(); + } + + @Override + public Artifact getDerivedArtifact(PathFragment rootRelativePath, Root root) { + throw new UnsupportedOperationException(); + } + + @Override + public Artifact getStableWorkspaceStatusArtifact() { + throw new UnsupportedOperationException(); + } + + @Override + public Artifact getVolatileWorkspaceStatusArtifact() { + throw new UnsupportedOperationException(); + } + + @Override + public ImmutableList<Artifact> getBuildInfo(RuleContext ruleContext, BuildInfoKey key) { + throw new UnsupportedOperationException(); + } + + @Override + public ArtifactOwner getOwner() { + throw new UnsupportedOperationException(); + } + + @Override + public ImmutableSet<Artifact> getOrphanArtifacts() { + throw new UnsupportedOperationException(); + } + } + + protected Iterable<String> baselineCoverageArtifactBasenames(ConfiguredTarget target) + throws Exception { + Artifact baselineCoverage = Iterables.getOnlyElement(target + .getProvider(TopLevelArtifactProvider.class) + .getOutputGroup(TopLevelArtifactProvider.BASELINE_COVERAGE)); + BaselineCoverageAction baselineAction = + (BaselineCoverageAction) getGeneratingAction(baselineCoverage); + + EventBus eventBus = new EventBus(); + Executor mockExecutor = Mockito.mock(Executor.class); + Mockito.when(mockExecutor.getEventBus()).thenReturn(eventBus); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + baselineAction.newDeterministicWriter(reporter, mockExecutor).writeOutputFile(bytes); + + ImmutableList.Builder<String> basenames = ImmutableList.builder(); + for (String line : new String(bytes.toByteArray(), StandardCharsets.UTF_8).split("\n")) { + if (line.startsWith("SF:")) { + String basename = line.substring(line.lastIndexOf("/") + 1); + basenames.add(basename); + } + } + + return basenames.build(); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/BinaryFileWriteActionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/BinaryFileWriteActionTest.java new file mode 100644 index 0000000000..22e6bcf30d --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/BinaryFileWriteActionTest.java @@ -0,0 +1,50 @@ +// Copyright 2015 Google Inc. 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.actions; + +import com.google.common.io.ByteSource; +import com.google.devtools.build.lib.actions.Action; +import com.google.devtools.build.lib.actions.ActionOwner; +import com.google.devtools.build.lib.actions.Artifact; + +import java.nio.charset.StandardCharsets; + +public class BinaryFileWriteActionTest extends FileWriteActionTestCase { + @Override + protected Action createAction( + ActionOwner actionOwner, Artifact outputArtifact, String data, boolean makeExecutable) { + return new BinaryFileWriteAction(actionOwner, outputArtifact, + ByteSource.wrap(data.getBytes(StandardCharsets.UTF_8)), makeExecutable); + } + + public void testNoInputs() { + checkNoInputsByDefault(); + } + + public void testDestinationArtifactIsOutput() { + checkDestinationArtifactIsOutput(); + } + + public void testCanWriteNonExecutableFile() throws Exception { + checkCanWriteNonExecutableFile(); + } + + public void testCanWriteExecutableFile() throws Exception { + checkCanWriteExecutableFile(); + } + + public void testComputesConsistentKeys() throws Exception { + checkComputesConsistentKeys(); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/CustomCommandLineTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/CustomCommandLineTest.java new file mode 100644 index 0000000000..d3b8723fa5 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/CustomCommandLineTest.java @@ -0,0 +1,78 @@ +// Copyright 2015 Google Inc. 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.actions; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.analysis.BuildViewTestCase; +import com.google.devtools.build.lib.vfs.PathFragment; + +public class CustomCommandLineTest extends BuildViewTestCase { + public void testAddBeforeEachPath() { + CustomCommandLine commandLine = new CustomCommandLine.Builder() + .add("foo") + .addBeforeEachPath( + "-I", ImmutableList.of(new PathFragment("/path1"), new PathFragment("/path2"))) + .add("bar") + .addBeforeEachPath("-I", ImmutableList.<PathFragment>of()) + .add("baz") + .build(); + assertThat(commandLine.arguments()) + .containsExactly("foo", "-I", "/path1", "-I", "/path2", "bar", "baz") + .inOrder(); + } + + public void testAddBeforeEach() { + CustomCommandLine commandLine = new CustomCommandLine.Builder() + .add("foo") + .addBeforeEach("-D", ImmutableList.<String>of()) + .add("bar") + .addBeforeEach("-D", ImmutableList.of("DEBUG=42", "ENABLE_QUANTUM", "__OBJC__")) + .add("baz") + .build(); + assertThat(commandLine.arguments()) + .containsExactly( + "foo", "bar", "-D", "DEBUG=42", "-D", "ENABLE_QUANTUM", "-D", "__OBJC__", "baz") + .inOrder(); + } + + public void testAddBeforeEachExecPath() throws Exception { + CustomCommandLine commandLine = new CustomCommandLine.Builder() + .add("foo") + .addBeforeEachExecPath("-l", + ImmutableList.of(getSourceArtifact("pkg/util.a"), getSourceArtifact("pkg2/extra.a"))) + .add("bar") + .addBeforeEachExecPath("-l", ImmutableList.<Artifact>of()) + .add("baz") + .build(); + assertThat(commandLine.arguments()) + .containsExactly("foo", "-l", "pkg/util.a", "-l", "pkg2/extra.a", "bar", "baz") + .inOrder(); + } + + public void testAddFormatEach() { + CustomCommandLine commandLine = new CustomCommandLine.Builder() + .add("foo") + .addFormatEach("-X'%s'", ImmutableList.<String>of()) + .add("bar") + .addFormatEach("-X'%s'", ImmutableList.of("42", "1011")) + .add("baz") + .build(); + assertThat(commandLine.arguments()) + .containsExactly("foo", "bar", "-X'42'", "-X'1011'", "baz") + .inOrder(); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/FileWriteActionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/FileWriteActionTest.java new file mode 100644 index 0000000000..1bc136fcd5 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/FileWriteActionTest.java @@ -0,0 +1,46 @@ +// Copyright 2015 Google Inc. 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.actions; + +import com.google.devtools.build.lib.actions.ActionOwner; +import com.google.devtools.build.lib.actions.Artifact; + +public class FileWriteActionTest extends FileWriteActionTestCase { + + @Override + protected FileWriteAction createAction( + ActionOwner actionOwner, Artifact outputArtifact, String data, boolean makeExecutable) { + return new FileWriteAction(actionOwner, outputArtifact, data, makeExecutable); + } + + public void testNoInputs() { + checkNoInputsByDefault(); + } + + public void testDestinationArtifactIsOutput() { + checkDestinationArtifactIsOutput(); + } + + public void testCanWriteNonExecutableFile() throws Exception { + checkCanWriteNonExecutableFile(); + } + + public void testCanWriteExecutableFile() throws Exception { + checkCanWriteExecutableFile(); + } + + public void testComputesConsistentKeys() throws Exception { + checkComputesConsistentKeys(); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/FileWriteActionTestCase.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/FileWriteActionTestCase.java new file mode 100644 index 0000000000..ff0ed2f2d1 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/FileWriteActionTestCase.java @@ -0,0 +1,93 @@ +// Copyright 2015 Google Inc. 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.actions; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.devtools.build.lib.actions.util.ActionsTestUtil.NULL_ACTION_OWNER; + +import com.google.devtools.build.lib.actions.Action; +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.Executor; +import com.google.devtools.build.lib.analysis.BuildViewTestCase; +import com.google.devtools.build.lib.analysis.util.ActionTester; +import com.google.devtools.build.lib.exec.util.TestExecutorBuilder; +import com.google.devtools.build.lib.util.io.FileOutErr; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.Path; + +import java.util.Collection; + +public abstract class FileWriteActionTestCase extends BuildViewTestCase { + + private Action action; + private Artifact outputArtifact; + private Path output; + private Executor executor; + private ActionExecutionContext context; + + @Override + public void setUp() throws Exception { + super.setUp(); + outputArtifact = getBinArtifactWithNoOwner("destination.txt"); + output = outputArtifact.getPath(); + FileSystemUtils.createDirectoryAndParents(output.getParentDirectory()); + action = createAction(NULL_ACTION_OWNER, outputArtifact, "Hello World", false); + executor = new TestExecutorBuilder(directories, binTools).build(); + context = new ActionExecutionContext(executor, null, null, new FileOutErr(), null); + } + + protected abstract Action createAction( + ActionOwner actionOwner, Artifact outputArtifact, String data, boolean makeExecutable); + + protected void checkNoInputsByDefault() { + assertThat(action.getInputs()).isEmpty(); + assertNull(action.getPrimaryInput()); + } + + protected void checkDestinationArtifactIsOutput() { + Collection<Artifact> outputs = action.getOutputs(); + assertEquals(asSet(outputArtifact), asSet(outputs)); + assertEquals(outputArtifact, action.getPrimaryOutput()); + } + + protected void checkCanWriteNonExecutableFile() throws Exception { + action.execute(context); + String content = new String(FileSystemUtils.readContentAsLatin1(output)); + assertEquals("Hello World", content); + assertFalse(output.isExecutable()); + } + + protected void checkCanWriteExecutableFile() throws Exception { + Artifact outputArtifact = getBinArtifactWithNoOwner("hello"); + Path output = outputArtifact.getPath(); + Action action = createAction(NULL_ACTION_OWNER, outputArtifact, "echo 'Hello World'", true); + action.execute(context); + String content = new String(FileSystemUtils.readContentAsLatin1(output)); + assertEquals("echo 'Hello World'", content); + assertTrue(output.isExecutable()); + } + + protected void checkComputesConsistentKeys() throws Exception { + ActionTester.runTest(4, new ActionTester.ActionCombinationFactory() { + @Override + public Action generate(int i) { + return createAction(NULL_ACTION_OWNER, outputArtifact, + (i & 1) == 0 ? "0" : "1", + (i & 2) == 0); + } + }); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTest.java new file mode 100644 index 0000000000..6ff4a8db4f --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTest.java @@ -0,0 +1,377 @@ +// Copyright 2015 Google Inc. 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.actions; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.util.Arrays.asList; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.devtools.build.lib.actions.AbstractAction; +import com.google.devtools.build.lib.actions.Action; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType; +import com.google.devtools.build.lib.actions.extra.EnvironmentVariable; +import com.google.devtools.build.lib.actions.extra.ExtraActionInfo; +import com.google.devtools.build.lib.actions.extra.SpawnInfo; +import com.google.devtools.build.lib.actions.util.ActionsTestUtil; +import com.google.devtools.build.lib.analysis.BuildViewTestCase; +import com.google.devtools.build.lib.analysis.util.ActionTester; +import com.google.devtools.build.lib.analysis.util.ActionTester.ActionCombinationFactory; +import com.google.devtools.build.lib.analysis.util.AnalysisTestUtil; +import com.google.devtools.build.lib.vfs.PathFragment; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Tests {@link SpawnAction}. + */ +public class SpawnActionTest extends BuildViewTestCase { + private Artifact welcomeArtifact; + private Artifact destinationArtifact; + private Artifact jarArtifact; + private AnalysisTestUtil.CollectingAnalysisEnvironment collectingAnalysisEnvironment; + + private SpawnAction.Builder builder() { + return new SpawnAction.Builder(); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + collectingAnalysisEnvironment = new AnalysisTestUtil.CollectingAnalysisEnvironment( + getTestAnalysisEnvironment()); + welcomeArtifact = getSourceArtifact("pkg/welcome.txt"); + jarArtifact = getSourceArtifact("pkg/exe.jar"); + destinationArtifact = getBinArtifactWithNoOwner("dir/destination.txt"); + } + + private SpawnAction createCopyFromWelcomeToDestination() { + PathFragment cp = new PathFragment("/bin/cp"); + List<String> arguments = asList(welcomeArtifact.getExecPath().getPathString(), + destinationArtifact.getExecPath().getPathString()); + + Action[] actions = builder() + .addInput(welcomeArtifact) + .addOutput(destinationArtifact) + .setExecutionInfo(ImmutableMap.<String, String>of("local", "")) + .setExecutable(cp) + .addArguments(arguments) + .setProgressMessage("hi, mom!") + .setMnemonic("Dummy") + .build(ActionsTestUtil.NULL_ACTION_OWNER, collectingAnalysisEnvironment, targetConfig); + collectingAnalysisEnvironment.registerAction(actions); + return (SpawnAction) actions[0]; + } + + public void testWelcomeArtifactIsInput() { + SpawnAction copyFromWelcomeToDestination = createCopyFromWelcomeToDestination(); + Iterable<Artifact> inputs = copyFromWelcomeToDestination.getInputs(); + assertEquals(asSet(welcomeArtifact), asSet(inputs)); + } + + public void testDestinationArtifactIsOutput() { + SpawnAction copyFromWelcomeToDestination = createCopyFromWelcomeToDestination(); + Collection<Artifact> outputs = copyFromWelcomeToDestination.getOutputs(); + assertEquals(asSet(destinationArtifact), asSet(outputs)); + } + + public void testBuilder() throws Exception { + Artifact input = getSourceArtifact("input"); + Artifact output = getBinArtifactWithNoOwner("output"); + Action[] actions = builder() + .addInput(input) + .addOutput(output) + .setExecutable(scratchFile("/bin/xxx").asFragment()) + .setProgressMessage("Test") + .build(ActionsTestUtil.NULL_ACTION_OWNER, collectingAnalysisEnvironment, targetConfig); + collectingAnalysisEnvironment.registerAction(actions); + SpawnAction action = (SpawnAction) actions[0]; + assertEquals(ActionsTestUtil.NULL_ACTION_OWNER.getLabel(), + action.getOwner().getLabel()); + assertSameContents(asList(input), action.getInputs()); + assertSameContents(asList(output), action.getOutputs()); + assertEquals(AbstractAction.DEFAULT_RESOURCE_SET, action.getSpawn().getLocalResources()); + assertSameContents(asList("/bin/xxx"), action.getArguments()); + assertEquals("Test", action.getProgressMessage()); + } + + public void testBuilderWithExecutable() throws Exception { + Action[] actions = builder() + .setExecutable(welcomeArtifact) + .addOutput(destinationArtifact) + .build(ActionsTestUtil.NULL_ACTION_OWNER, collectingAnalysisEnvironment, targetConfig); + collectingAnalysisEnvironment.registerAction(actions); + SpawnAction action = (SpawnAction) actions[0]; + assertSameContents(asList(welcomeArtifact.getExecPath().getPathString()), + action.getArguments()); + } + + public void testBuilderWithJavaExecutable() throws Exception { + Action[] actions = builder() + .addOutput(destinationArtifact) + .setJavaExecutable(scratchFile("/bin/java").asFragment(), + jarArtifact, "MyMainClass", asList("-jvmarg")) + .build(ActionsTestUtil.NULL_ACTION_OWNER, collectingAnalysisEnvironment, targetConfig); + collectingAnalysisEnvironment.registerAction(actions); + SpawnAction action = (SpawnAction) actions[0]; + assertEquals(asList("/bin/java", "-Xverify:none", "-jvmarg", "-cp", + "pkg/exe.jar", "MyMainClass"), action.getArguments()); + } + + public void testBuilderWithJavaExecutableAndParameterFile() throws Exception { + useConfiguration("--min_param_file_size=0"); + collectingAnalysisEnvironment = new AnalysisTestUtil.CollectingAnalysisEnvironment( + getTestAnalysisEnvironment()); + Artifact output = getBinArtifactWithNoOwner("output"); + Artifact paramFile = getBinArtifactWithNoOwner("output-2.params"); + Action[] actions = builder() + .addOutput(output) + .setJavaExecutable( + scratchFile("/bin/java").asFragment(), jarArtifact, "MyMainClass", asList("-jvmarg")) + .addArgument("-X") + .useParameterFile(ParameterFileType.UNQUOTED) + .build(ActionsTestUtil.NULL_ACTION_OWNER, collectingAnalysisEnvironment, targetConfig); + collectingAnalysisEnvironment.registerAction(actions); + SpawnAction action = (SpawnAction) actions[0]; + if (getMutableActionGraph() != null) { + // Otherwise, CachingAnalysisEnvironment.registerAction() registers the action. We cannot + // use STUB_ANALYSIS_ENVIRONMENT here because we also need a BuildConfiguration. + collectingAnalysisEnvironment.registerWith(getMutableActionGraph()); + } + assertEquals(asList("/bin/java", "-Xverify:none", "-jvmarg", "-cp", + "pkg/exe.jar", "MyMainClass", "@" + paramFile.getExecPathString()), + action.getArguments()); + assertEquals(ImmutableList.of("-X"), + ImmutableList.copyOf( + ((ParameterFileWriteAction) getGeneratingAction(paramFile)).getContents())); + assertContainsSublist(actionInputsToPaths(action.getSpawn().getInputFiles()), + "pkg/exe.jar"); + } + + public void testBuilderWithJavaExecutableAndParameterFileAndParameterFileFlag() throws Exception { + useConfiguration("--min_param_file_size=0"); + collectingAnalysisEnvironment = new AnalysisTestUtil.CollectingAnalysisEnvironment( + getTestAnalysisEnvironment()); + + Artifact output = getBinArtifactWithNoOwner("output"); + Artifact paramFile = getBinArtifactWithNoOwner("output-2.params"); + Action[] actions = builder() + .addOutput(output) + .setJavaExecutable( + scratchFile("/bin/java").asFragment(), jarArtifact, "MyMainClass", asList("-jvmarg")) + .addArgument("-X") + .useParameterFile(ParameterFileType.UNQUOTED, ISO_8859_1, "--flagfile=") + .build(ActionsTestUtil.NULL_ACTION_OWNER, collectingAnalysisEnvironment, targetConfig); + collectingAnalysisEnvironment.registerAction(actions); + SpawnAction action = (SpawnAction) actions[0]; + if (getMutableActionGraph() != null) { + // Otherwise, CachingAnalysisEnvironment.registerAction() registers the action. We cannot + // use STUB_ANALYSIS_ENVIRONMENT here because we also need a BuildConfiguration. + collectingAnalysisEnvironment.registerWith(getMutableActionGraph()); + } + assertEquals(asList("/bin/java", "-Xverify:none", "-jvmarg", "-cp", + "pkg/exe.jar", "MyMainClass", "--flagfile=" + paramFile.getExecPathString()), + ImmutableList.copyOf(action.getArguments())); + assertEquals(Arrays.asList("-X"), + ImmutableList.copyOf( + ((ParameterFileWriteAction) getGeneratingAction(paramFile)).getContents())); + assertContainsSublist(actionInputsToPaths(action.getSpawn().getInputFiles()), + "pkg/exe.jar"); + } + + public void testBuilderWithExtraExecutableArguments() throws Exception { + Action[] actions = builder() + .addOutput(destinationArtifact) + .setJavaExecutable( + scratchFile("/bin/java").asFragment(), jarArtifact, "MyMainClass", asList("-jvmarg")) + .addExecutableArguments("execArg1", "execArg2") + .addArguments("arg1") + .build(ActionsTestUtil.NULL_ACTION_OWNER, collectingAnalysisEnvironment, targetConfig); + collectingAnalysisEnvironment.registerAction(actions); + SpawnAction action = (SpawnAction) actions[0]; + assertEquals(asList("/bin/java", "-Xverify:none", "-jvmarg", "-cp", + "pkg/exe.jar", "MyMainClass", "execArg1", "execArg2", "arg1"), + action.getArguments()); + } + + public void testBuilderWithExtraExecutableArgumentsAndParameterFile() throws Exception { + useConfiguration("--min_param_file_size=0"); + collectingAnalysisEnvironment = new AnalysisTestUtil.CollectingAnalysisEnvironment( + getTestAnalysisEnvironment()); + Artifact output = getBinArtifactWithNoOwner("output"); + Artifact paramFile = getBinArtifactWithNoOwner("output-2.params"); + Action[] actions = builder() + .addOutput(output) + .setJavaExecutable( + scratchFile("/bin/java").asFragment(), jarArtifact, "MyMainClass", asList("-jvmarg")) + .addExecutableArguments("execArg1", "execArg2") + .addArguments("arg1", "arg2", "arg3") + .useParameterFile(ParameterFileType.UNQUOTED) + .build(ActionsTestUtil.NULL_ACTION_OWNER, collectingAnalysisEnvironment, targetConfig); + collectingAnalysisEnvironment.registerAction(actions); + SpawnAction action = (SpawnAction) actions[0]; + if (getMutableActionGraph() != null) { + // Otherwise, CachingAnalysisEnvironment.registerAction() registers the action. We cannot + // use STUB_ANALYSIS_ENVIRONMENT here because we also need a BuildConfiguration. + collectingAnalysisEnvironment.registerWith(getMutableActionGraph()); + } + assertEquals(asList("/bin/java", "-Xverify:none", "-jvmarg", "-cp", + "pkg/exe.jar", "MyMainClass", "execArg1", "execArg2", + "@" + paramFile.getExecPathString()), action.getSpawn().getArguments()); + assertEquals(asList("/bin/java", "-Xverify:none", "-jvmarg", "-cp", + "pkg/exe.jar", "MyMainClass", "execArg1", "execArg2", + "@" + paramFile.getExecPathString()), ImmutableList.copyOf(action.getArguments())); + assertEquals(Arrays.asList("arg1", "arg2", "arg3"), + ImmutableList.copyOf( + ((ParameterFileWriteAction) getGeneratingAction(paramFile)).getContents())); + } + + public void testParameterFiles() throws Exception { + Artifact output1 = getBinArtifactWithNoOwner("output1"); + Artifact output2 = getBinArtifactWithNoOwner("output2"); + Artifact paramFile = getBinArtifactWithNoOwner("output1-2.params"); + PathFragment executable = new PathFragment("/bin/executable"); + + useConfiguration("--min_param_file_size=500"); + + String longOption = Strings.repeat("x", 1000); + SpawnAction spawnAction = ((SpawnAction) builder() + .addOutput(output1) + .setExecutable(executable) + .useParameterFile(ParameterFileType.UNQUOTED) + .addArgument(longOption) + .build(ActionsTestUtil.NULL_ACTION_OWNER, collectingAnalysisEnvironment, targetConfig)[0]); + assertThat(spawnAction.getRemainingArguments()).containsExactly( + "@" + paramFile.getExecPathString()).inOrder(); + + useConfiguration("--min_param_file_size=1500"); + spawnAction = ((SpawnAction) builder() + .addOutput(output2) + .setExecutable(executable) + .useParameterFile(ParameterFileType.UNQUOTED) + .addArgument(longOption) + .build(ActionsTestUtil.NULL_ACTION_OWNER, collectingAnalysisEnvironment, targetConfig)[0]); + assertThat(spawnAction.getRemainingArguments()).containsExactly(longOption).inOrder(); + } + + public void testExtraActionInfo() throws Exception { + SpawnAction copyFromWelcomeToDestination = createCopyFromWelcomeToDestination(); + ExtraActionInfo.Builder builder = copyFromWelcomeToDestination.getExtraActionInfo(); + ExtraActionInfo info = builder.build(); + assertEquals("Dummy", info.getMnemonic()); + + SpawnInfo spawnInfo = info.getExtension(SpawnInfo.spawnInfo); + assertNotNull(spawnInfo); + + assertSameContents(copyFromWelcomeToDestination.getArguments(), spawnInfo.getArgumentList()); + + Iterable<String> inputPaths = Artifact.toExecPaths( + copyFromWelcomeToDestination.getInputs()); + Iterable<String> outputPaths = Artifact.toExecPaths( + copyFromWelcomeToDestination.getOutputs()); + + assertSameContents(inputPaths, spawnInfo.getInputFileList()); + assertSameContents(outputPaths, spawnInfo.getOutputFileList()); + Map<String, String> environment = copyFromWelcomeToDestination.getEnvironment(); + assertEquals(environment.size(), spawnInfo.getVariableCount()); + + for (EnvironmentVariable variable : spawnInfo.getVariableList()) { + assertEquals(variable.getValue(), environment.get(variable.getName())); + } + } + + public void testInputManifest() throws Exception { + Artifact manifest = getSourceArtifact("MANIFEST"); + Action[] actions = builder() + .addInput(manifest) + .addInputManifest(manifest, new PathFragment("/destination/")) + .addOutput(getBinArtifactWithNoOwner("output")) + .setExecutable(scratchFile("/bin/xxx").asFragment()) + .setProgressMessage("Test") + .build(ActionsTestUtil.NULL_ACTION_OWNER, collectingAnalysisEnvironment, targetConfig); + collectingAnalysisEnvironment.registerAction(actions); + SpawnAction action = (SpawnAction) actions[0]; + List<String> inputFiles = actionInputsToPaths(action.getSpawn().getInputFiles()); + assertTrue(inputFiles.isEmpty()); + } + + public void testComputeKey() throws Exception { + final Artifact artifactA = getSourceArtifact("a"); + final Artifact artifactB = getSourceArtifact("b"); + + ActionTester.runTest(64, new ActionCombinationFactory() { + @Override + public Action generate(int i) { + SpawnAction.Builder builder = builder(); + builder.addOutput(destinationArtifact); + + PathFragment executable = (i & 1) == 0 ? artifactA.getExecPath() : artifactB.getExecPath(); + if ((i & 2) == 0) { + builder.setExecutable(executable); + } else { + builder.setJavaExecutable(executable, jarArtifact, "Main", ImmutableList.<String>of()); + } + + builder.setMnemonic((i & 4) == 0 ? "a" : "b"); + + if ((i & 8) == 0) { + builder.addInputManifest(artifactA, new PathFragment("a")); + } else { + builder.addInputManifest(artifactB, new PathFragment("a")); + } + + if ((i & 16) == 0) { + builder.addInputManifest(artifactA, new PathFragment("aa")); + } else { + builder.addInputManifest(artifactA, new PathFragment("ab")); + } + + Map<String, String> env = new HashMap<>(); + if ((i & 32) == 0) { + env.put("foo", "bar"); + } + builder.setEnvironment(env); + + Action[] actions = builder.build(ActionsTestUtil.NULL_ACTION_OWNER, + collectingAnalysisEnvironment, targetConfig); + collectingAnalysisEnvironment.registerAction(actions); + return actions[0]; + } + }); + } + + public void testMnemonicMustNotContainSpaces() { + SpawnAction.Builder builder = builder(); + try { + builder.setMnemonic("contains space"); + fail("Expected exception"); + } catch (IllegalArgumentException expected) {} + try { + builder.setMnemonic("contains\nnewline"); + fail("Expected exception"); + } catch (IllegalArgumentException expected) {} + try { + builder.setMnemonic("contains/slash"); + fail("Expected exception"); + } catch (IllegalArgumentException expected) {} + } +} diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/SymlinkActionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/SymlinkActionTest.java new file mode 100644 index 0000000000..4ab2c12fae --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/SymlinkActionTest.java @@ -0,0 +1,92 @@ +// Copyright 2015 Google Inc. 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.actions; + +import static com.google.devtools.build.lib.actions.util.ActionsTestUtil.NULL_ACTION_OWNER; + +import com.google.devtools.build.lib.actions.ActionExecutionContext; +import com.google.devtools.build.lib.actions.ActionExecutionException; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.Executor; +import com.google.devtools.build.lib.analysis.BuildViewTestCase; +import com.google.devtools.build.lib.exec.util.TestExecutorBuilder; +import com.google.devtools.build.lib.testutil.MoreAsserts; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.Path; + +/** + * Tests {@link SymlinkAction}. + */ +public class SymlinkActionTest extends BuildViewTestCase { + + private Path input; + private Artifact inputArtifact; + private Path output; + private Artifact outputArtifact; + private SymlinkAction action; + + @Override + public void setUp() throws Exception { + super.setUp(); + input = scratchFile("/workspace/input.txt", "Hello, world."); + inputArtifact = getSourceArtifact("input.txt"); + Path linkedInput = directories.getExecRoot().getRelative("input.txt"); + FileSystemUtils.createDirectoryAndParents(linkedInput.getParentDirectory()); + linkedInput.createSymbolicLink(input); + outputArtifact = getBinArtifactWithNoOwner("destination.txt"); + output = outputArtifact.getPath(); + FileSystemUtils.createDirectoryAndParents(output.getParentDirectory()); + action = new SymlinkAction(NULL_ACTION_OWNER, + inputArtifact, outputArtifact, "Symlinking test"); + } + + public void testInputArtifactIsInput() { + Iterable<Artifact> inputs = action.getInputs(); + assertEquals(asSet(inputArtifact), asSet(inputs)); + } + + public void testDestinationArtifactIsOutput() { + Iterable<Artifact> outputs = action.getOutputs(); + assertEquals(asSet(outputArtifact), asSet(outputs)); + } + + public void testSymlink() throws Exception { + Executor executor = new TestExecutorBuilder(directories, null).build(); + action.execute(new ActionExecutionContext(executor, null, null, null, null)); + assertTrue(output.isSymbolicLink()); + assertEquals(input, output.resolveSymbolicLinks()); + assertEquals(inputArtifact, action.getPrimaryInput()); + assertEquals(outputArtifact, action.getPrimaryOutput()); + } + + public void testExecutableSymlink() throws Exception { + Executor executor = new TestExecutorBuilder(directories, null).build(); + outputArtifact = getBinArtifactWithNoOwner("destination2.txt"); + output = outputArtifact.getPath(); + action = new ExecutableSymlinkAction(NULL_ACTION_OWNER, inputArtifact, outputArtifact); + assertFalse(input.isExecutable()); + ActionExecutionContext actionExecutionContext = + new ActionExecutionContext(executor, null, null, null, null); + try { + action.execute(actionExecutionContext); + fail("Expected ActionExecutionException"); + } catch (ActionExecutionException e) { + MoreAsserts.assertContainsRegex("'input.txt' is not executable", e.getMessage()); + } + input.setExecutable(true); + action.execute(actionExecutionContext); + assertTrue(output.isSymbolicLink()); + assertEquals(input, output.resolveSymbolicLinks()); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/TemplateExpansionActionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/TemplateExpansionActionTest.java new file mode 100644 index 0000000000..ec8935eabe --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/TemplateExpansionActionTest.java @@ -0,0 +1,169 @@ +// Copyright 2015 Google Inc. 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.actions; + +import static com.google.devtools.build.lib.actions.util.ActionsTestUtil.NULL_ACTION_OWNER; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.devtools.build.lib.actions.ActionExecutionContext; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.Executor; +import com.google.devtools.build.lib.actions.Root; +import com.google.devtools.build.lib.analysis.BlazeDirectories; +import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Substitution; +import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Template; +import com.google.devtools.build.lib.analysis.config.BinTools; +import com.google.devtools.build.lib.exec.util.TestExecutorBuilder; +import com.google.devtools.build.lib.testutil.FoundationTestCase; +import com.google.devtools.build.lib.util.io.FileOutErr; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.Path; + +import java.util.List; + +/** + * Tests {@link TemplateExpansionAction}. + */ +public class TemplateExpansionActionTest extends FoundationTestCase { + + private static final String TEMPLATE = Joiner.on('\n').join("key=%key%", "value=%value%"); + + private Root outputRoot; + private Artifact inputArtifact; + private Artifact outputArtifact; + private Path output; + private List<Substitution> substitutions; + private BlazeDirectories directories; + private BinTools binTools; + + @Override + protected void setUp() throws Exception { + super.setUp(); + Root workspace = Root.asSourceRoot(scratchDir("/workspace")); + outputRoot = Root.asDerivedRoot(scratchDir("/workspace"), scratchDir("/workspace/out")); + Path input = scratchFile("/workspace/input.txt", TEMPLATE); + inputArtifact = new Artifact(input, workspace); + output = scratchFS().getPath("/workspace/out/destination.txt"); + outputArtifact = new Artifact(output, outputRoot); + substitutions = Lists.newArrayList(); + substitutions.add(Substitution.of("%key%", "foo")); + substitutions.add(Substitution.of("%value%", "bar")); + directories = new BlazeDirectories( + scratchFS().getPath("/install"), + scratchFS().getPath("/base"), + scratchFS().getPath("/workspace")); + binTools = BinTools.empty(directories); + } + + private TemplateExpansionAction create() { + TemplateExpansionAction result = new TemplateExpansionAction(NULL_ACTION_OWNER, + outputArtifact, Template.forString(TEMPLATE), substitutions, false); + return result; + } + + public void testInputsIsEmpty() { + assertTrue(Iterables.isEmpty(create().getInputs())); + } + + public void testDestinationArtifactIsOutput() { + assertEquals(ImmutableSet.of(outputArtifact), create().getOutputs()); + } + + public void testExpansion() throws Exception { + Executor executor = new TestExecutorBuilder(directories, binTools).build(); + create().execute(createContext(executor)); + String content = new String(FileSystemUtils.readContentAsLatin1(output)); + String expected = Joiner.on('\n').join("key=foo", "value=bar"); + assertEquals(expected, content); + } + + public void testKeySameIfSame() throws Exception { + Artifact outputArtifact2 = new Artifact(scratchFS().getPath("/workspace/out/destination.txt"), + outputRoot); + TemplateExpansionAction a = new TemplateExpansionAction(NULL_ACTION_OWNER, + outputArtifact, Template.forString(TEMPLATE), + ImmutableList.of(Substitution.of("%key%", "foo")), false); + TemplateExpansionAction b = new TemplateExpansionAction(NULL_ACTION_OWNER, + outputArtifact2, Template.forString(TEMPLATE), + ImmutableList.of(Substitution.of("%key%", "foo")), false); + assertEquals(a.computeKey(), b.computeKey()); + } + + public void testKeyDiffersForSubstitution() throws Exception { + Artifact outputArtifact2 = new Artifact(scratchFS().getPath("/workspace/out/destination.txt"), + outputRoot); + TemplateExpansionAction a = new TemplateExpansionAction(NULL_ACTION_OWNER, + outputArtifact, Template.forString(TEMPLATE), + ImmutableList.of(Substitution.of("%key%", "foo")), false); + TemplateExpansionAction b = new TemplateExpansionAction(NULL_ACTION_OWNER, + outputArtifact2, Template.forString(TEMPLATE), + ImmutableList.of(Substitution.of("%key%", "foo2")), false); + assertFalse(a.computeKey().equals(b.computeKey())); + } + + public void testKeyDiffersForExecutable() throws Exception { + Artifact outputArtifact2 = new Artifact(scratchFS().getPath("/workspace/out/destination.txt"), + outputRoot); + TemplateExpansionAction a = new TemplateExpansionAction(NULL_ACTION_OWNER, + outputArtifact, Template.forString(TEMPLATE), + ImmutableList.of(Substitution.of("%key%", "foo")), false); + TemplateExpansionAction b = new TemplateExpansionAction(NULL_ACTION_OWNER, + outputArtifact2, Template.forString(TEMPLATE), + ImmutableList.of(Substitution.of("%key%", "foo")), true); + assertFalse(a.computeKey().equals(b.computeKey())); + } + + public void testKeyDiffersForTemplates() throws Exception { + Artifact outputArtifact2 = new Artifact(scratchFS().getPath("/workspace/out/destination.txt"), + outputRoot); + TemplateExpansionAction a = new TemplateExpansionAction(NULL_ACTION_OWNER, + outputArtifact, Template.forString(TEMPLATE), + ImmutableList.of(Substitution.of("%key%", "foo")), false); + TemplateExpansionAction b = new TemplateExpansionAction(NULL_ACTION_OWNER, + outputArtifact2, Template.forString(TEMPLATE + " "), + ImmutableList.of(Substitution.of("%key%", "foo")), false); + assertFalse(a.computeKey().equals(b.computeKey())); + } + + private TemplateExpansionAction createWithArtifact() { + TemplateExpansionAction result = new TemplateExpansionAction(NULL_ACTION_OWNER, + inputArtifact, outputArtifact, substitutions, false); + return result; + } + + public void testArtifactTemplateHasInput() { + assertEquals(ImmutableList.of(inputArtifact), createWithArtifact().getInputs()); + } + + public void testArtifactTemplateHasOutput() { + assertEquals(ImmutableSet.of(outputArtifact), createWithArtifact().getOutputs()); + } + + public void testArtifactTemplateExpansion() throws Exception { + Executor executor = new TestExecutorBuilder(directories, binTools).build(); + createWithArtifact().execute(createContext(executor)); + String content = new String(FileSystemUtils.readContentAsLatin1(output)); + // The trailing "" is needed because scratchFile implicitly appends "\n". + String expected = Joiner.on('\n').join("key=foo", "value=bar", ""); + assertEquals(expected, content); + } + + private ActionExecutionContext createContext(Executor executor) { + return new ActionExecutionContext(executor, null, null, new FileOutErr(), null); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/ActionTester.java b/src/test/java/com/google/devtools/build/lib/analysis/util/ActionTester.java new file mode 100644 index 0000000000..2dfa0eae3d --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/analysis/util/ActionTester.java @@ -0,0 +1,76 @@ +// Copyright 2015 Google Inc. 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.util; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import com.google.devtools.build.lib.actions.Action; +import com.google.devtools.build.lib.actions.Actions; + +/** + * Test helper for testing {@link Action} implementations. + */ +public class ActionTester { + + /** + * A generator for action instances. + */ + public interface ActionCombinationFactory { + + /** + * Returns a new action instance. The parameter {@code i} is used to vary the parameters used to + * create the action. Implementations should do something like this: + * <code><pre> + * return new MyAction(owner, inputs, outputs, configuration, + * (i & 1) == 0 ? a1 : a2, + * (i & 2) == 0 ? b1 : b2, + * (i & 4) == 0 ? c1 : c2); + * (i & 16) == 0 ? d1 : d2); + * </pre></code> + * + * <p>The wrap-around (in this case at 32) is intentional and is checked for by the testing + * method. + * + * <p>To reduce the combinatorial complexity of testing an action class, all elements that are + * only used to change the executed command line should go into a single parameter, and the key + * computation should take the generated command line into account. + * + * <p>Furthermore, when called with identical parameters, this method should return different + * instances (i.e. according to {@code ==}), but they should have the same key. + */ + Action generate(int i); + } + + /** + * Tests that different actions have different keys. The count should specify how many different + * permutations the {@link ActionCombinationFactory} can generate. + */ + public static void runTest(int count, ActionCombinationFactory factory) throws Exception { + Action[] actions = new Action[count]; + for (int i = 0; i < actions.length; i++) { + actions[i] = factory.generate(i); + } + // Sanity check that the count is correct. + assertThat(Actions.canBeShared(actions[0], factory.generate(count))).isTrue(); + + for (int i = 0; i < actions.length; i++) { + assertThat(Actions.canBeShared(actions[i], factory.generate(i))).isTrue(); + for (int j = i + 1; j < actions.length; j++) { + assertWithMessage(i + " and " + j).that(Actions.canBeShared(actions[i], actions[j])) + .isFalse(); + } + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisMock.java b/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisMock.java new file mode 100644 index 0000000000..c411695605 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisMock.java @@ -0,0 +1,59 @@ +// Copyright 2015 Google Inc. 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.util; + +import com.google.devtools.build.lib.analysis.config.ConfigurationFactory; +import com.google.devtools.build.lib.packages.util.MockToolsConfig; + +import java.io.IOException; +import java.util.Collection; + +/** + * Create a mock client for the analysis phase, as well as a configuration factory. + */ +public abstract class AnalysisMock { + + /** + * This is called from test setup to create the mock directory layout needed to create the + * configuration. + */ + public abstract void setupMockClient(MockToolsConfig mockToolsConfig) throws IOException; + + public abstract ConfigurationFactory createConfigurationFactory(); + + public abstract Collection<String> getOptionOverrides(); + + public static class Delegate extends AnalysisMock { + private final AnalysisMock delegate; + + public Delegate(AnalysisMock delegate) { + this.delegate = delegate; + } + + @Override + public void setupMockClient(MockToolsConfig mockToolsConfig) throws IOException { + delegate.setupMockClient(mockToolsConfig); + } + + @Override + public ConfigurationFactory createConfigurationFactory() { + return delegate.createConfigurationFactory(); + } + + @Override + public Collection<String> getOptionOverrides() { + return delegate.getOptionOverrides(); + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisTestUtil.java b/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisTestUtil.java new file mode 100644 index 0000000000..3d75939dd9 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisTestUtil.java @@ -0,0 +1,430 @@ +// Copyright 2015 Google Inc. 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.util; + +import com.google.common.base.Supplier; +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.Action; +import com.google.devtools.build.lib.actions.ActionContextProvider; +import com.google.devtools.build.lib.actions.ActionExecutionContext; +import com.google.devtools.build.lib.actions.ActionExecutionException; +import com.google.devtools.build.lib.actions.ActionGraph; +import com.google.devtools.build.lib.actions.ActionInputFileCache; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.ArtifactFactory; +import com.google.devtools.build.lib.actions.ArtifactOwner; +import com.google.devtools.build.lib.actions.ExecutionStrategy; +import com.google.devtools.build.lib.actions.Executor; +import com.google.devtools.build.lib.actions.Executor.ActionContext; +import com.google.devtools.build.lib.actions.ExecutorInitException; +import com.google.devtools.build.lib.actions.MiddlemanFactory; +import com.google.devtools.build.lib.actions.MutableActionGraph; +import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException; +import com.google.devtools.build.lib.actions.ResourceSet; +import com.google.devtools.build.lib.actions.Root; +import com.google.devtools.build.lib.actions.util.ActionsTestUtil; +import com.google.devtools.build.lib.analysis.AnalysisEnvironment; +import com.google.devtools.build.lib.analysis.BlazeDirectories; +import com.google.devtools.build.lib.analysis.BuildInfoHelper; +import com.google.devtools.build.lib.analysis.RuleContext; +import com.google.devtools.build.lib.analysis.WorkspaceStatusAction; +import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Key; +import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey; +import com.google.devtools.build.lib.analysis.config.BuildConfiguration; +import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection; +import com.google.devtools.build.lib.events.EventHandler; +import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.skyframe.SkyFunction; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public final class AnalysisTestUtil { + + /** + * An {@link AnalysisEnvironment} implementation that collects the actions registered. + */ + public static class CollectingAnalysisEnvironment implements AnalysisEnvironment { + private final List<Action> actions = new ArrayList<>(); + private final AnalysisEnvironment original; + + public CollectingAnalysisEnvironment(AnalysisEnvironment original) { + this.original = original; + } + + public void clear() { + actions.clear(); + } + + @Override + public void registerAction(Action... actions) { + for (Action action : actions) { + this.actions.add(action); + } + original.registerAction(actions); + } + + /** Calls {@link MutableActionGraph#registerAction} for all collected actions. */ + public void registerWith(MutableActionGraph actionGraph) { + for (Action action : actions) { + try { + actionGraph.registerAction(action); + } catch (ActionConflictException e) { + throw new ActionsTestUtil.UncheckedActionConflictException(e); + } + } + } + + @Override + public EventHandler getEventHandler() { + return original.getEventHandler(); + } + + @Override + public boolean hasErrors() { + return original.hasErrors(); + } + + @Override + public Artifact getDerivedArtifact(PathFragment rootRelativePath, Root root) { + return original.getDerivedArtifact(rootRelativePath, root); + } + + @Override + public Artifact getConstantMetadataArtifact(PathFragment rootRelativePath, Root root) { + return original.getConstantMetadataArtifact(rootRelativePath, root); + } + + @Override + public Artifact getFilesetArtifact(PathFragment rootRelativePath, Root root) { + return original.getFilesetArtifact(rootRelativePath, root); + } + + @Override + public Artifact getEmbeddedToolArtifact(String embeddedPath) { + return original.getEmbeddedToolArtifact(embeddedPath); + } + + @Override + public MiddlemanFactory getMiddlemanFactory() { + return original.getMiddlemanFactory(); + } + + @Override + public Action getLocalGeneratingAction(Artifact artifact) { + return original.getLocalGeneratingAction(artifact); + } + + @Override + public Iterable<Action> getRegisteredActions() { + return original.getRegisteredActions(); + } + + @Override + public SkyFunction.Environment getSkyframeEnv() { + return null; + } + + @Override + public Artifact getStableWorkspaceStatusArtifact() { + return original.getStableWorkspaceStatusArtifact(); + } + + @Override + public Artifact getVolatileWorkspaceStatusArtifact() { + return original.getVolatileWorkspaceStatusArtifact(); + } + + @Override + public ImmutableList<Artifact> getBuildInfo(RuleContext ruleContext, BuildInfoKey key) { + return original.getBuildInfo(ruleContext, key); + } + + @Override + public ArtifactOwner getOwner() { + return original.getOwner(); + } + + @Override + public ImmutableSet<Artifact> getOrphanArtifacts() { + return original.getOrphanArtifacts(); + } + } + + public static class DummyWorkspaceStatusAction extends WorkspaceStatusAction { + private final String key; + private final Artifact stableStatus; + private final Artifact volatileStatus; + + public DummyWorkspaceStatusAction(String key, + Artifact stableStatus, Artifact volatileStatus) { + super( + BuildInfoHelper.BUILD_INFO_ACTION_OWNER, + ImmutableList.<Artifact>of(), + ImmutableList.of(stableStatus, volatileStatus)); + this.key = key; + this.stableStatus = stableStatus; + this.volatileStatus = volatileStatus; + } + + @Override + public String describeStrategy(Executor executor) { + return ""; + } + + @Override + public void execute(ActionExecutionContext actionExecutionContext) + throws ActionExecutionException { + try { + FileSystemUtils.writeContent(stableStatus.getPath(), new byte[] {}); + FileSystemUtils.writeContent(volatileStatus.getPath(), new byte[] {}); + } catch (IOException e) { + throw new ActionExecutionException(e, this, true); + } + } + + @Override + public String getMnemonic() { + return "DummyBuildInfoAction" + key; + } + + @Override + public ResourceSet estimateResourceConsumption(Executor executor) { + return ResourceSet.ZERO; + } + + @Override + public String computeKey() { + return ""; + } + + @Override + public Artifact getVolatileStatus() { + return volatileStatus; + } + + @Override + public Artifact getStableStatus() { + return stableStatus; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof DummyWorkspaceStatusAction)) { + return false; + } + + DummyWorkspaceStatusAction that = (DummyWorkspaceStatusAction) o; + return that.key.equals(this.key); + } + } + + @ExecutionStrategy(contextType = WorkspaceStatusAction.Context.class) + public static class DummyWorkspaceStatusActionContext implements WorkspaceStatusAction.Context { + @Override + public ImmutableMap<String, Key> getStableKeys() { + return ImmutableMap.of(); + } + + @Override + public ImmutableMap<String, Key> getVolatileKeys() { + return ImmutableMap.of(); + } + } + + public static class DummyWorkspaceActionContextProvider implements ActionContextProvider { + @Override + public Iterable<ActionContext> getActionContexts() { + return ImmutableList.<ActionContext>of(new DummyWorkspaceStatusActionContext()); + } + + @Override + public void executorCreated(Iterable<ActionContext> usedContexts) throws ExecutorInitException { + } + + @Override + public void executionPhaseStarting(ActionInputFileCache actionInputFileCache, + ActionGraph actionGraph, + Iterable<Artifact> topLevelArtifacts) throws ExecutorInitException, InterruptedException { + } + + @Override + public void executionPhaseEnding() { + } + } + + /** + * A workspace status action factory that does not do any interaction with the environment. + */ + public static class DummyWorkspaceStatusActionFactory implements WorkspaceStatusAction.Factory { + private final BlazeDirectories directories; + private String key; + + public DummyWorkspaceStatusActionFactory(BlazeDirectories directories) { + this.directories = directories; + this.key = ""; + } + + public void setKey(String key) { + this.key = key; + } + + @Override + public WorkspaceStatusAction createWorkspaceStatusAction( + ArtifactFactory artifactFactory, ArtifactOwner artifactOwner, Supplier<UUID> buildId) { + Artifact stableStatus = artifactFactory.getDerivedArtifact( + new PathFragment("build-info.txt"), + directories.getBuildDataDirectory(), artifactOwner); + Artifact volatileStatus = artifactFactory.getConstantMetadataArtifact( + new PathFragment("build-changelist.txt"), + directories.getBuildDataDirectory(), artifactOwner); + return new DummyWorkspaceStatusAction(key, stableStatus, volatileStatus); + } + + @Override + public Map<String, String> createDummyWorkspaceStatus() { + return ImmutableMap.of(); + } + } + + public static final AnalysisEnvironment STUB_ANALYSIS_ENVIRONMENT = new AnalysisEnvironment() { + @Override + public void registerAction(Action... action) { + } + + @Override + public boolean hasErrors() { + return false; + } + + @Override + public Artifact getEmbeddedToolArtifact(String embeddedPath) { + return null; + } + + @Override + public Artifact getConstantMetadataArtifact(PathFragment rootRelativePath, Root root) { + return null; + } + + @Override + public EventHandler getEventHandler() { + return null; + } + + @Override + public MiddlemanFactory getMiddlemanFactory() { + return null; + } + + @Override + public Action getLocalGeneratingAction(Artifact artifact) { + return null; + } + + @Override + public Iterable<Action> getRegisteredActions() { + return ImmutableList.of(); + } + + @Override + public SkyFunction.Environment getSkyframeEnv() { + return null; + } + + @Override + public Artifact getFilesetArtifact(PathFragment rootRelativePath, Root root) { + return null; + } + + @Override + public Artifact getDerivedArtifact(PathFragment rootRelativePath, Root root) { + return null; + } + + @Override + public Artifact getStableWorkspaceStatusArtifact() { + return null; + } + + @Override + public Artifact getVolatileWorkspaceStatusArtifact() { + return null; + } + + @Override + public ImmutableList<Artifact> getBuildInfo(RuleContext ruleContext, BuildInfoKey key) { + return ImmutableList.of(); + } + + @Override + public ArtifactOwner getOwner() { + return ArtifactOwner.NULL_OWNER; + } + + @Override + public ImmutableSet<Artifact> getOrphanArtifacts() { + return ImmutableSet.<Artifact>of(); + } + }; + + /** + * Given a collection of Artifacts, returns a corresponding set of strings of + * the form "{root} {relpath}", such as "bin x/libx.a". Such strings make + * assertions easier to write. + * + * <p>The returned set preserves the order of the input. + */ + public static Set<String> artifactsToStrings(BuildConfigurationCollection configurations, + Iterable<Artifact> artifacts) { + Map<Root, String> rootMap = new HashMap<>(); + BuildConfiguration targetConfiguration = + Iterables.getOnlyElement(configurations.getTargetConfigurations()); + BuildConfiguration hostConfiguration = + targetConfiguration.getConfiguration(ConfigurationTransition.HOST); + rootMap.put(targetConfiguration.getBinDirectory(), "bin"); + rootMap.put(targetConfiguration.getGenfilesDirectory(), "genfiles"); + rootMap.put(targetConfiguration.getMiddlemanDirectory(), "internal"); + rootMap.put(hostConfiguration.getBinDirectory(), "bin(host)"); + rootMap.put(hostConfiguration.getGenfilesDirectory(), "genfiles(host)"); + rootMap.put(hostConfiguration.getMiddlemanDirectory(), "internal(host)"); + + Set<String> files = new LinkedHashSet<>(); + for (Artifact artifact : artifacts) { + Root root = artifact.getRoot(); + if (root.isSourceRoot()) { + files.add("src " + artifact.getRootRelativePath()); + } else { + String name = rootMap.get(root); + if (name == null) { + name = "/"; + } + files.add(name + " " + artifact.getRootRelativePath()); + } + } + return files; + } + +} diff --git a/src/test/java/com/google/devtools/build/lib/exec/util/TestExecutorBuilder.java b/src/test/java/com/google/devtools/build/lib/exec/util/TestExecutorBuilder.java new file mode 100644 index 0000000000..1743349d9a --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/exec/util/TestExecutorBuilder.java @@ -0,0 +1,111 @@ +// Copyright 2009 Google Inc. 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.exec.util; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.eventbus.EventBus; +import com.google.devtools.build.lib.actions.ActionContextProvider; +import com.google.devtools.build.lib.actions.BlazeExecutor; +import com.google.devtools.build.lib.actions.Executor.ActionContext; +import com.google.devtools.build.lib.actions.ExecutorInitException; +import com.google.devtools.build.lib.actions.SpawnActionContext; +import com.google.devtools.build.lib.analysis.BlazeDirectories; +import com.google.devtools.build.lib.analysis.config.BinTools; +import com.google.devtools.build.lib.events.Reporter; +import com.google.devtools.build.lib.exec.ExecutionOptions; +import com.google.devtools.build.lib.exec.FileWriteStrategy; +import com.google.devtools.build.lib.exec.SourceManifestActionContextImpl; +import com.google.devtools.build.lib.exec.SymlinkTreeStrategy; +import com.google.devtools.build.lib.runtime.CommonCommandOptions; +import com.google.devtools.build.lib.testutil.TestConstants; +import com.google.devtools.build.lib.util.BlazeClock; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.common.options.OptionsBase; +import com.google.devtools.common.options.OptionsParser; +import com.google.devtools.common.options.OptionsParsingException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Builder for the test instance of the {@link BlazeExecutor} class. + */ +public class TestExecutorBuilder { + public static final List<Class<? extends OptionsBase>> DEFAULT_OPTIONS = ImmutableList.of( + ExecutionOptions.class, CommonCommandOptions.class); + private final BlazeDirectories directories; + private Reporter reporter = new Reporter(); + private EventBus bus = new EventBus(); + private OptionsParser optionsParser = OptionsParser.newOptionsParser(DEFAULT_OPTIONS); + private List<ActionContext> strategies = new ArrayList<>(); + private Map<String, SpawnActionContext> spawnStrategyMap = new HashMap<>(); + + public TestExecutorBuilder(BlazeDirectories directories, BinTools binTools) { + this.directories = directories; + + strategies.add(new SourceManifestActionContextImpl( + new PathFragment(TestConstants.RUNFILES_PREFIX))); + strategies.add(new FileWriteStrategy()); + strategies.add(new SymlinkTreeStrategy(null, binTools)); + } + + public TestExecutorBuilder setReporter(Reporter reporter) { + this.reporter = reporter; + return this; + } + + public TestExecutorBuilder setBus(EventBus bus) { + this.bus = bus; + return this; + } + + public TestExecutorBuilder setOptionsParser(OptionsParser optionsParser) { + this.optionsParser = optionsParser; + return this; + } + + public TestExecutorBuilder parseOptions(String... options) throws OptionsParsingException { + this.optionsParser.parse(options); + return this; + } + + public TestExecutorBuilder addStrategy(ActionContext strategy) { + strategies.add(strategy); + return this; + } + + public TestExecutorBuilder addStrategyFactory(ActionContextProvider factory) { + Iterables.addAll(strategies, factory.getActionContexts()); + return this; + } + + public TestExecutorBuilder setExecution(String mnemonic, SpawnActionContext strategy) { + spawnStrategyMap.put(mnemonic, strategy); + strategies.add(strategy); + return this; + } + + public BlazeExecutor build() throws ExecutorInitException { + return new BlazeExecutor(directories.getExecRoot(), directories.getOutputPath(), reporter, bus, + BlazeClock.instance(), optionsParser, + optionsParser.getOptions(ExecutionOptions.class).verboseFailures, + optionsParser.getOptions(ExecutionOptions.class).showSubcommands, + strategies, + ImmutableMap.copyOf(spawnStrategyMap), ImmutableList.<ActionContextProvider>of()); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/MockToolsConfig.java b/src/test/java/com/google/devtools/build/lib/packages/util/MockToolsConfig.java new file mode 100644 index 0000000000..fcf9b6f562 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/packages/util/MockToolsConfig.java @@ -0,0 +1,137 @@ +// Copyright 2015 Google Inc. 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.packages.util; + +import com.google.common.base.Preconditions; +import com.google.devtools.build.lib.testutil.BlazeTestUtils; +import com.google.devtools.build.lib.testutil.TestConstants; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.Path; + +import java.io.IOException; + +import javax.annotation.Nullable; + +/** + * Configuration for the mock client setup that we use for testing. + */ +public final class MockToolsConfig { + + private final Path rootDirectory; + private final boolean realFileSystem; + + // Allow the injection of the runfiles directory where actual tools are found. + // TestUtil.getRunfilesDir() caches the value of the "TEST_SRCDIR" system property, which makes + // it impossible to change if it doesn't get set early in test configuration setup. + private final Path runfilesDirectory; + + public MockToolsConfig(Path rootDirectory) { + this(rootDirectory, false, null); + } + + public MockToolsConfig(Path rootDirectory, boolean realFileSystem) { + this(rootDirectory, realFileSystem, null); + } + + public MockToolsConfig(Path rootDirectory, boolean realFileSystem, + @Nullable Path runfilesDirectoryOpt) { + this.rootDirectory = rootDirectory; + this.realFileSystem = realFileSystem; + if (runfilesDirectoryOpt == null) { + this.runfilesDirectory = rootDirectory.getRelative(BlazeTestUtils.runfilesDir()); + } else { + this.runfilesDirectory = runfilesDirectoryOpt; + } + } + + public boolean isRealFileSystem() { + return realFileSystem; + } + + public Path getPath(String relativePath) { + return rootDirectory.getRelative(relativePath); + } + + public Path create(String relativePath, String... lines) throws IOException { + Path path = rootDirectory.getRelative(relativePath); + if (!path.exists()) { + FileSystemUtils.writeIsoLatin1(path, lines); + } else if (lines.length > 0) { + String existingContent = new String(FileSystemUtils.readContentAsLatin1(path)); + + StringBuilder newContent = new StringBuilder(); + for (String line : lines) { + newContent.append(line); + newContent.append("\n"); + } + + if (!newContent.toString().equals(existingContent)) { + throw new IOException("Conflict: '" + relativePath + "':\n'" + newContent + "'\n vs \n'" + + existingContent + "'"); + } + } + return path; + } + + /** + * Links a tool into the workspace by creating a symbolic link to a real file. The target location + * in the workspace uses the same relative path as the given path to the tool in the runfiles + * tree. Use this if you do not need to rename or relocate the file, i.e., if the location in the + * workspace and the runfiles tree matches. Otherwise use {@link #linkTool(String, String)}. + * + * @param relativePath the relative path within the runfiles tree of the current test + * @throws IOException + */ + public void linkTool(String relativePath) throws IOException { + Preconditions.checkState(realFileSystem); + linkTool(relativePath, relativePath); + } + + /** + * Links a tool into the workspace by creating a symbolic link to a real file. + * + * @param relativePath the relative path within the runfiles tree of the current test + * @param dest the relative path in the mock client + * @throws IOException + */ + public void linkTool(String relativePath, String dest) throws IOException { + Preconditions.checkState(realFileSystem); + Path target = runfilesDirectory.getRelative(TestConstants.RUNFILES_PREFIX + "/" + relativePath); + if (!target.exists()) { + // In some cases we run tests in a special client with a ../READONLY/ path where we may also + // find the runfiles. Try that, too. + Path readOnlyClientPath = rootDirectory.getRelative( + "../READONLY/" + TestConstants.RUNFILES_PREFIX + "/" + relativePath); + if (!readOnlyClientPath.exists()) { + throw new IOException("target does not exist " + target); + } else { + target = readOnlyClientPath; + } + } + Path path = rootDirectory.getRelative(dest); + FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); + path.delete(); + path.createSymbolicLink(target); + } + + /** + * Convenience method to link multiple tools. Same as calling {@link #linkTool(String)} for each + * parameter. + */ + public void linkTools(String... tools) throws IOException { + for (String tool : tools) { + linkTool(tool); + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java b/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java index 48e948c6d5..28ca6bf7f8 100644 --- a/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java +++ b/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java @@ -42,6 +42,11 @@ public class TestConstants { public static final String RUNFILES_PREFIX = "DOES-NOT-WORK-YET"; /** + * Name of a class with an INSTANCE field of type AnalysisMock to be used for analysis tests. + */ + public static final String TEST_ANALYSIS_MOCK = "DOES-NOT-WORK-YET"; + + /** * Directory where we can find bazel's Java tests, relative to a test's runfiles directory. */ public static final String JAVATESTS_ROOT = "src/test/java/"; |