| // Copyright 2016 The Bazel Authors. All rights reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.devtools.build.lib.skyframe; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static org.junit.Assert.assertThrows; | |
| import com.google.common.base.Preconditions; | |
| 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.ActionAnalysisMetadata; | |
| import com.google.devtools.build.lib.actions.ActionExecutionContext; | |
| import com.google.devtools.build.lib.actions.ActionKeyContext; | |
| import com.google.devtools.build.lib.actions.ActionLookupData; | |
| import com.google.devtools.build.lib.actions.ActionLookupValue; | |
| import com.google.devtools.build.lib.actions.ActionLookupValue.ActionLookupKey; | |
| import com.google.devtools.build.lib.actions.ActionOwner; | |
| import com.google.devtools.build.lib.actions.ActionTemplate; | |
| import com.google.devtools.build.lib.actions.Actions; | |
| import com.google.devtools.build.lib.actions.Artifact; | |
| import com.google.devtools.build.lib.actions.Artifact.DerivedArtifact; | |
| import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact; | |
| import com.google.devtools.build.lib.actions.Artifact.SpecialArtifactType; | |
| import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact; | |
| import com.google.devtools.build.lib.actions.ArtifactOwner; | |
| import com.google.devtools.build.lib.actions.ArtifactPrefixConflictException; | |
| import com.google.devtools.build.lib.actions.ArtifactRoot; | |
| import com.google.devtools.build.lib.actions.FileArtifactValue; | |
| import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException; | |
| import com.google.devtools.build.lib.actions.util.ActionsTestUtil; | |
| import com.google.devtools.build.lib.actions.util.InjectedActionLookupKey; | |
| import com.google.devtools.build.lib.actions.util.TestAction.DummyAction; | |
| import com.google.devtools.build.lib.analysis.ConfiguredTarget; | |
| import com.google.devtools.build.lib.analysis.actions.CustomCommandLine; | |
| import com.google.devtools.build.lib.analysis.actions.SpawnActionTemplate; | |
| import com.google.devtools.build.lib.analysis.actions.SpawnActionTemplate.OutputPathMapper; | |
| 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.NullEventHandler; | |
| import com.google.devtools.build.lib.pkgcache.PathPackageLocator; | |
| import com.google.devtools.build.lib.skyframe.ActionTemplateExpansionValue.ActionTemplateExpansionKey; | |
| import com.google.devtools.build.lib.testutil.FoundationTestCase; | |
| import com.google.devtools.build.lib.util.Fingerprint; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.build.lib.vfs.Root; | |
| import com.google.devtools.build.skyframe.EvaluationContext; | |
| import com.google.devtools.build.skyframe.EvaluationResult; | |
| import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator; | |
| import com.google.devtools.build.skyframe.SequencedRecordingDifferencer; | |
| import com.google.devtools.build.skyframe.SequentialBuildDriver; | |
| import com.google.devtools.build.skyframe.SkyFunction; | |
| import com.google.devtools.build.skyframe.SkyKey; | |
| import com.google.devtools.build.skyframe.SkyValue; | |
| import java.util.LinkedHashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.UUID; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| import org.mockito.Mockito; | |
| /** Tests for {@link ActionTemplateExpansionFunction}. */ | |
| @RunWith(JUnit4.class) | |
| public final class ActionTemplateExpansionFunctionTest extends FoundationTestCase { | |
| private final Map<Artifact, TreeArtifactValue> artifactValueMap = new LinkedHashMap<>(); | |
| private final SequencedRecordingDifferencer differencer = new SequencedRecordingDifferencer(); | |
| private final SequentialBuildDriver driver = | |
| new SequentialBuildDriver( | |
| new InMemoryMemoizingEvaluator( | |
| ImmutableMap.of( | |
| Artifact.ARTIFACT, | |
| new DummyArtifactFunction(artifactValueMap), | |
| SkyFunctions.ACTION_TEMPLATE_EXPANSION, | |
| new ActionTemplateExpansionFunction(new ActionKeyContext())), | |
| differencer)); | |
| @Before | |
| public void setUp() { | |
| PrecomputedValue.BUILD_ID.set(differencer, UUID.randomUUID()); | |
| PrecomputedValue.PATH_PACKAGE_LOCATOR.set( | |
| differencer, | |
| new PathPackageLocator( | |
| rootDirectory.getFileSystem().getPath("/outputbase"), | |
| ImmutableList.of(Root.fromPath(rootDirectory)), | |
| BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY)); | |
| } | |
| @Test | |
| public void testActionTemplateExpansionFunction() throws Exception { | |
| SpecialArtifact inputTreeArtifact = | |
| createAndPopulateTreeArtifact("inputTreeArtifact", "child0", "child1", "child2"); | |
| SpecialArtifact outputTreeArtifact = createTreeArtifact("outputTreeArtifact"); | |
| SpawnActionTemplate spawnActionTemplate = ActionsTestUtil.createDummySpawnActionTemplate( | |
| inputTreeArtifact, outputTreeArtifact); | |
| List<Action> actions = evaluate(spawnActionTemplate); | |
| assertThat(actions).hasSize(3); | |
| ArtifactOwner owner = ActionTemplateExpansionValue.key(CTKEY, 0); | |
| int i = 0; | |
| for (Action action : actions) { | |
| String childName = "child" + i; | |
| assertThat(Artifact.asExecPaths(action.getInputs())) | |
| .contains("out/inputTreeArtifact/" + childName); | |
| assertThat(Artifact.asExecPaths(action.getOutputs())) | |
| .containsExactly("out/outputTreeArtifact/" + childName); | |
| assertThat(Iterables.getOnlyElement(action.getOutputs()).getArtifactOwner()).isEqualTo(owner); | |
| ++i; | |
| } | |
| } | |
| @Test | |
| public void testThrowsOnActionConflict() throws Exception { | |
| SpecialArtifact inputTreeArtifact = | |
| createAndPopulateTreeArtifact("inputTreeArtifact", "child0", "child1", "child2"); | |
| SpecialArtifact outputTreeArtifact = createTreeArtifact("outputTreeArtifact"); | |
| OutputPathMapper mapper = new OutputPathMapper() { | |
| @Override | |
| public PathFragment parentRelativeOutputPath(TreeFileArtifact inputTreeFileArtifact) { | |
| return PathFragment.create("conflict_path"); | |
| } | |
| }; | |
| SpawnActionTemplate spawnActionTemplate = | |
| new SpawnActionTemplate.Builder(inputTreeArtifact, outputTreeArtifact) | |
| .setExecutable(PathFragment.create("/bin/cp")) | |
| .setCommandLineTemplate(CustomCommandLine.builder().build()) | |
| .setOutputPathMapper(mapper) | |
| .build(ActionsTestUtil.NULL_ACTION_OWNER); | |
| assertThrows(ActionConflictException.class, () -> evaluate(spawnActionTemplate)); | |
| } | |
| @Test | |
| public void testThrowsOnArtifactPrefixConflict() throws Exception { | |
| SpecialArtifact inputTreeArtifact = | |
| createAndPopulateTreeArtifact("inputTreeArtifact", "child0", "child1", "child2"); | |
| SpecialArtifact outputTreeArtifact = createTreeArtifact("outputTreeArtifact"); | |
| OutputPathMapper mapper = new OutputPathMapper() { | |
| private int i = 0; | |
| @Override | |
| public PathFragment parentRelativeOutputPath(TreeFileArtifact inputTreeFileArtifact) { | |
| PathFragment path; | |
| switch (i) { | |
| case 0: | |
| path = PathFragment.create("path_prefix"); | |
| break; | |
| case 1: | |
| path = PathFragment.create("path_prefix/conflict"); | |
| break; | |
| default: | |
| path = inputTreeFileArtifact.getParentRelativePath(); | |
| } | |
| ++i; | |
| return path; | |
| } | |
| }; | |
| SpawnActionTemplate spawnActionTemplate = | |
| new SpawnActionTemplate.Builder(inputTreeArtifact, outputTreeArtifact) | |
| .setExecutable(PathFragment.create("/bin/cp")) | |
| .setCommandLineTemplate(CustomCommandLine.builder().build()) | |
| .setOutputPathMapper(mapper) | |
| .build(ActionsTestUtil.NULL_ACTION_OWNER); | |
| assertThrows(ArtifactPrefixConflictException.class, () -> evaluate(spawnActionTemplate)); | |
| } | |
| @Test | |
| public void cannotDeclareNonTreeOutput() throws Exception { | |
| SpecialArtifact inputTree = createAndPopulateTreeArtifact("input", "child"); | |
| SpecialArtifact outputTree = createTreeArtifact("output"); | |
| ActionTemplate<DummyAction> template = | |
| new TestActionTemplate(inputTree, outputTree) { | |
| @Override | |
| public ImmutableList<DummyAction> generateActionsForInputArtifacts( | |
| ImmutableSet<TreeFileArtifact> inputTreeFileArtifacts, | |
| ActionLookupKey artifactOwner) { | |
| return ImmutableList.of(); | |
| } | |
| @Override | |
| public ImmutableSet<Artifact> getOutputs() { | |
| return ImmutableSet.of( | |
| outputTree, | |
| new DerivedArtifact( | |
| outputTree.getRoot(), | |
| outputTree.getRoot().getExecPath().getRelative("not_tree"), | |
| outputTree.getArtifactOwner())); | |
| } | |
| }; | |
| Exception e = assertThrows(RuntimeException.class, () -> evaluate(template)); | |
| assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class); | |
| assertThat(e) | |
| .hasCauseThat() | |
| .hasMessageThat() | |
| .contains(template + " declares an output which is not a tree artifact"); | |
| } | |
| @Test | |
| public void cannotGenerateOutputWithWrongOwner() throws Exception { | |
| SpecialArtifact inputTree = createAndPopulateTreeArtifact("input", "child"); | |
| SpecialArtifact outputTree = createTreeArtifact("output"); | |
| ActionTemplate<DummyAction> template = | |
| new TestActionTemplate(inputTree, outputTree) { | |
| @Override | |
| public ImmutableList<DummyAction> generateActionsForInputArtifacts( | |
| ImmutableSet<TreeFileArtifact> inputTreeFileArtifacts, | |
| ActionLookupKey artifactOwner) { | |
| TreeFileArtifact input = Iterables.getOnlyElement(inputTreeFileArtifacts); | |
| TreeFileArtifact outputWithWrongOwner = | |
| TreeFileArtifact.createTemplateExpansionOutput( | |
| outputTree, "child", ActionsTestUtil.NULL_TEMPLATE_EXPANSION_ARTIFACT_OWNER); | |
| assertThat(outputWithWrongOwner.getArtifactOwner()).isNotEqualTo(artifactOwner); | |
| return ImmutableList.of(new DummyAction(input, outputWithWrongOwner)); | |
| } | |
| }; | |
| Exception e = assertThrows(RuntimeException.class, () -> evaluate(template)); | |
| assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class); | |
| assertThat(e) | |
| .hasCauseThat() | |
| .hasMessageThat() | |
| .contains(template + " generated an action with an output owned by the wrong owner"); | |
| } | |
| @Test | |
| public void cannotGenerateNonTreeFileArtifactOutput() throws Exception { | |
| SpecialArtifact inputTree = createAndPopulateTreeArtifact("input", "child"); | |
| SpecialArtifact outputTree = createTreeArtifact("output"); | |
| ActionTemplate<DummyAction> template = | |
| new TestActionTemplate(inputTree, outputTree) { | |
| @Override | |
| public ImmutableList<DummyAction> generateActionsForInputArtifacts( | |
| ImmutableSet<TreeFileArtifact> inputTreeFileArtifacts, | |
| ActionLookupKey artifactOwner) { | |
| TreeFileArtifact input = Iterables.getOnlyElement(inputTreeFileArtifacts); | |
| Artifact notTreeFileArtifact = | |
| new DerivedArtifact( | |
| input.getRoot(), | |
| input.getRoot().getExecPath().getRelative("a.txt"), | |
| artifactOwner); | |
| assertThat(notTreeFileArtifact.isTreeArtifact()).isFalse(); | |
| return ImmutableList.of(new DummyAction(input, notTreeFileArtifact)); | |
| } | |
| }; | |
| Exception e = assertThrows(RuntimeException.class, () -> evaluate(template)); | |
| assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class); | |
| assertThat(e) | |
| .hasCauseThat() | |
| .hasMessageThat() | |
| .contains(template + " generated an action which outputs a non-TreeFileArtifact"); | |
| } | |
| @Test | |
| public void cannotGenerateOutputUnderUndeclaredTree() throws Exception { | |
| SpecialArtifact inputTree = createAndPopulateTreeArtifact("input", "child"); | |
| SpecialArtifact outputTree = createTreeArtifact("output"); | |
| ActionTemplate<DummyAction> template = | |
| new TestActionTemplate(inputTree, outputTree) { | |
| @Override | |
| public ImmutableList<DummyAction> generateActionsForInputArtifacts( | |
| ImmutableSet<TreeFileArtifact> inputTreeFileArtifacts, | |
| ActionLookupKey artifactOwner) { | |
| TreeFileArtifact input = Iterables.getOnlyElement(inputTreeFileArtifacts); | |
| TreeFileArtifact outputUnderWrongTree = | |
| TreeFileArtifact.createTemplateExpansionOutput( | |
| createTreeArtifact("undeclared"), "child", artifactOwner); | |
| return ImmutableList.of(new DummyAction(input, outputUnderWrongTree)); | |
| } | |
| }; | |
| Exception e = assertThrows(RuntimeException.class, () -> evaluate(template)); | |
| assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class); | |
| assertThat(e) | |
| .hasCauseThat() | |
| .hasMessageThat() | |
| .contains(template + " generated an action with an output under an undeclared tree"); | |
| } | |
| @Test | |
| public void canGenerateOutputUnderAdditionalDeclaredTree() throws Exception { | |
| SpecialArtifact inputTree = createAndPopulateTreeArtifact("input", "child"); | |
| SpecialArtifact outputTree = createTreeArtifact("output"); | |
| SpecialArtifact additionalOutputTree = createTreeArtifact("additional_output"); | |
| ActionTemplate<DummyAction> template = | |
| new TestActionTemplate(inputTree, outputTree) { | |
| @Override | |
| public ImmutableList<DummyAction> generateActionsForInputArtifacts( | |
| ImmutableSet<TreeFileArtifact> inputTreeFileArtifacts, | |
| ActionLookupKey artifactOwner) { | |
| TreeFileArtifact input = Iterables.getOnlyElement(inputTreeFileArtifacts); | |
| return ImmutableList.of( | |
| new DummyAction( | |
| input, | |
| TreeFileArtifact.createTemplateExpansionOutput( | |
| outputTree, "child", artifactOwner)), | |
| new DummyAction( | |
| input, | |
| TreeFileArtifact.createTemplateExpansionOutput( | |
| additionalOutputTree, "additional_child", artifactOwner))); | |
| } | |
| @Override | |
| public ImmutableSet<Artifact> getOutputs() { | |
| return ImmutableSet.of(outputTree, additionalOutputTree); | |
| } | |
| }; | |
| evaluate(template); | |
| } | |
| private static final ActionLookupValue.ActionLookupKey CTKEY = new InjectedActionLookupKey("key"); | |
| private ImmutableList<Action> evaluate(ActionTemplate<?> actionTemplate) throws Exception { | |
| ConfiguredTargetValue ctValue = createConfiguredTargetValue(actionTemplate); | |
| differencer.inject(CTKEY, ctValue); | |
| ActionTemplateExpansionKey templateKey = ActionTemplateExpansionValue.key(CTKEY, 0); | |
| EvaluationContext evaluationContext = | |
| EvaluationContext.newBuilder() | |
| .setKeepGoing(false) | |
| .setNumThreads(SkyframeExecutor.DEFAULT_THREAD_COUNT) | |
| .setEventHander(NullEventHandler.INSTANCE) | |
| .build(); | |
| EvaluationResult<ActionTemplateExpansionValue> result = | |
| driver.evaluate(ImmutableList.of(templateKey), evaluationContext); | |
| if (result.hasError()) { | |
| throw result.getError().getException(); | |
| } | |
| ActionTemplateExpansionValue actionTemplateExpansionValue = result.get(templateKey); | |
| ImmutableList.Builder<Action> actionList = ImmutableList.builder(); | |
| for (int i = 0; i < actionTemplateExpansionValue.getNumActions(); i++) { | |
| actionList.add(actionTemplateExpansionValue.getAction(i)); | |
| } | |
| return actionList.build(); | |
| } | |
| private static ConfiguredTargetValue createConfiguredTargetValue( | |
| ActionTemplate<?> actionTemplate) { | |
| return new NonRuleConfiguredTargetValue( | |
| Mockito.mock(ConfiguredTarget.class), | |
| Actions.GeneratingActions.fromSingleAction(actionTemplate, CTKEY), | |
| NestedSetBuilder.emptySet(Order.STABLE_ORDER)); | |
| } | |
| private SpecialArtifact createTreeArtifact(String path) { | |
| PathFragment execPath = PathFragment.create("out").getRelative(path); | |
| return new SpecialArtifact( | |
| ArtifactRoot.asDerivedRoot(rootDirectory, "out"), | |
| execPath, | |
| CTKEY, | |
| SpecialArtifactType.TREE); | |
| } | |
| private SpecialArtifact createAndPopulateTreeArtifact(String path, String... childRelativePaths) | |
| throws Exception { | |
| SpecialArtifact treeArtifact = createTreeArtifact(path); | |
| treeArtifact.setGeneratingActionKey(ActionLookupData.create(CTKEY, /*actionIndex=*/ 0)); | |
| Map<TreeFileArtifact, FileArtifactValue> treeFileArtifactMap = new LinkedHashMap<>(); | |
| for (String childRelativePath : childRelativePaths) { | |
| TreeFileArtifact treeFileArtifact = | |
| TreeFileArtifact.createTreeOutput(treeArtifact, childRelativePath); | |
| scratch.file(treeFileArtifact.getPath().toString(), childRelativePath); | |
| // We do not care about the FileArtifactValues in this test. | |
| treeFileArtifactMap.put( | |
| treeFileArtifact, FileArtifactValue.createForTesting(treeFileArtifact)); | |
| } | |
| artifactValueMap.put( | |
| treeArtifact, TreeArtifactValue.create(ImmutableMap.copyOf(treeFileArtifactMap))); | |
| return treeArtifact; | |
| } | |
| /** Dummy ArtifactFunction that just returns injected values */ | |
| private static final class DummyArtifactFunction implements SkyFunction { | |
| private final Map<Artifact, TreeArtifactValue> artifactValueMap; | |
| DummyArtifactFunction(Map<Artifact, TreeArtifactValue> artifactValueMap) { | |
| this.artifactValueMap = artifactValueMap; | |
| } | |
| @Override | |
| public SkyValue compute(SkyKey skyKey, Environment env) { | |
| return Preconditions.checkNotNull(artifactValueMap.get(skyKey)); | |
| } | |
| @Override | |
| public String extractTag(SkyKey skyKey) { | |
| return null; | |
| } | |
| } | |
| private abstract static class TestActionTemplate implements ActionTemplate<DummyAction> { | |
| private final SpecialArtifact inputTreeArtifact; | |
| private final SpecialArtifact outputTreeArtifact; | |
| TestActionTemplate(SpecialArtifact inputTreeArtifact, SpecialArtifact outputTreeArtifact) { | |
| Preconditions.checkArgument(inputTreeArtifact.isTreeArtifact(), inputTreeArtifact); | |
| Preconditions.checkArgument(outputTreeArtifact.isTreeArtifact(), outputTreeArtifact); | |
| this.inputTreeArtifact = inputTreeArtifact; | |
| this.outputTreeArtifact = outputTreeArtifact; | |
| } | |
| @Override | |
| public SpecialArtifact getInputTreeArtifact() { | |
| return inputTreeArtifact; | |
| } | |
| @Override | |
| public SpecialArtifact getOutputTreeArtifact() { | |
| return outputTreeArtifact; | |
| } | |
| @Override | |
| public ActionOwner getOwner() { | |
| return ActionsTestUtil.NULL_ACTION_OWNER; | |
| } | |
| @Override | |
| public boolean isShareable() { | |
| return false; | |
| } | |
| @Override | |
| public String getMnemonic() { | |
| return "TestActionTemplate"; | |
| } | |
| @Override | |
| public String getKey(ActionKeyContext actionKeyContext) { | |
| Fingerprint fp = new Fingerprint(); | |
| fp.addPath(inputTreeArtifact.getPath()); | |
| fp.addPath(outputTreeArtifact.getPath()); | |
| return fp.hexDigestAndReset(); | |
| } | |
| @Override | |
| public String prettyPrint() { | |
| return "TestActionTemplate for " + outputTreeArtifact; | |
| } | |
| @Override | |
| public NestedSet<Artifact> getTools() { | |
| return NestedSetBuilder.emptySet(Order.STABLE_ORDER); | |
| } | |
| @Override | |
| public NestedSet<Artifact> getInputs() { | |
| return NestedSetBuilder.create(Order.STABLE_ORDER, inputTreeArtifact); | |
| } | |
| @Override | |
| public Iterable<String> getClientEnvironmentVariables() { | |
| return ImmutableList.of(); | |
| } | |
| @Override | |
| public NestedSet<Artifact> getInputFilesForExtraAction( | |
| ActionExecutionContext actionExecutionContext) { | |
| return NestedSetBuilder.emptySet(Order.STABLE_ORDER); | |
| } | |
| @Override | |
| public ImmutableSet<Artifact> getMandatoryOutputs() { | |
| return ImmutableSet.of(); | |
| } | |
| @Override | |
| public NestedSet<Artifact> getMandatoryInputs() { | |
| return NestedSetBuilder.create(Order.STABLE_ORDER, inputTreeArtifact); | |
| } | |
| @Override | |
| public boolean shouldReportPathPrefixConflict(ActionAnalysisMetadata action) { | |
| return false; | |
| } | |
| @Override | |
| public MiddlemanType getActionType() { | |
| return MiddlemanType.NORMAL; | |
| } | |
| @Override | |
| public String toString() { | |
| return prettyPrint(); | |
| } | |
| } | |
| } |