| // 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 com.google.devtools.build.lib.actions.ActionInputHelper.asTreeFileArtifacts; |
| import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; |
| |
| import com.google.common.base.Predicate; |
| import com.google.common.base.Throwables; |
| 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.ActionInputHelper; |
| import com.google.devtools.build.lib.actions.ActionLookupData; |
| import com.google.devtools.build.lib.actions.ActionLookupValue; |
| import com.google.devtools.build.lib.actions.Actions; |
| import com.google.devtools.build.lib.actions.Artifact; |
| 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.ArtifactFileMetadata; |
| import com.google.devtools.build.lib.actions.ArtifactRoot; |
| import com.google.devtools.build.lib.actions.ArtifactSkyKey; |
| import com.google.devtools.build.lib.actions.BasicActionLookupValue; |
| import com.google.devtools.build.lib.actions.FileArtifactValue; |
| import com.google.devtools.build.lib.actions.MissingInputFileException; |
| import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException; |
| import com.google.devtools.build.lib.actions.cache.DigestUtils; |
| import com.google.devtools.build.lib.actions.util.TestAction.DummyAction; |
| import com.google.devtools.build.lib.events.NullEventHandler; |
| import com.google.devtools.build.lib.vfs.FileStatus; |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; |
| import com.google.devtools.build.lib.vfs.Path; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import com.google.devtools.build.skyframe.EvaluationContext; |
| import com.google.devtools.build.skyframe.EvaluationResult; |
| import com.google.devtools.build.skyframe.MemoizingEvaluator; |
| import com.google.devtools.build.skyframe.SkyFunction; |
| import com.google.devtools.build.skyframe.SkyFunctionException; |
| import com.google.devtools.build.skyframe.SkyFunctionException.Transience; |
| import com.google.devtools.build.skyframe.SkyKey; |
| import com.google.devtools.build.skyframe.SkyValue; |
| import java.io.IOException; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Random; |
| import java.util.stream.Collectors; |
| import java.util.stream.IntStream; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** |
| * Test the behavior of ActionMetadataHandler and ArtifactFunction |
| * with respect to TreeArtifacts. |
| */ |
| @RunWith(JUnit4.class) |
| public class TreeArtifactMetadataTest extends ArtifactFunctionTestCase { |
| |
| // A list of subpaths for the SetArtifact created by our custom ActionExecutionFunction. |
| private List<PathFragment> testTreeArtifactContents; |
| |
| @Before |
| public final void setUp() throws Exception { |
| delegateActionExecutionFunction = new TreeArtifactExecutionFunction(); |
| } |
| |
| private TreeArtifactValue evaluateTreeArtifact(Artifact treeArtifact, |
| Iterable<PathFragment> children) |
| throws Exception { |
| testTreeArtifactContents = ImmutableList.copyOf(children); |
| for (PathFragment child : children) { |
| file(treeArtifact.getPath().getRelative(child), child.toString()); |
| } |
| return (TreeArtifactValue) evaluateArtifactValue(treeArtifact, /*mandatory=*/ true); |
| } |
| |
| private TreeArtifactValue doTestTreeArtifacts(Iterable<PathFragment> children) throws Exception { |
| SpecialArtifact output = createTreeArtifact("output"); |
| return doTestTreeArtifacts(output, children); |
| } |
| |
| private TreeArtifactValue doTestTreeArtifacts( |
| SpecialArtifact tree, Iterable<PathFragment> children) throws Exception { |
| TreeArtifactValue value = evaluateTreeArtifact(tree, children); |
| assertThat(value.getChildPaths()).containsExactlyElementsIn(ImmutableSet.copyOf(children)); |
| assertThat(value.getChildren()).containsExactlyElementsIn( |
| asTreeFileArtifacts(tree, children)); |
| |
| // Assertions about digest. As of this writing this logic is essentially the same |
| // as that in TreeArtifact, but it's good practice to unit test anyway to guard against |
| // breaking changes. |
| Map<String, FileArtifactValue> digestBuilder = new HashMap<>(); |
| for (PathFragment child : children) { |
| FileArtifactValue subdigest = |
| FileArtifactValue.createShareable(tree.getPath().getRelative(child)); |
| digestBuilder.put(child.getPathString(), subdigest); |
| } |
| assertThat(DigestUtils.fromMetadata(digestBuilder).getDigestBytesUnsafe()) |
| .isEqualTo(value.getDigest()); |
| return value; |
| } |
| |
| @Test |
| public void testEmptyTreeArtifacts() throws Exception { |
| TreeArtifactValue value = doTestTreeArtifacts(ImmutableList.<PathFragment>of()); |
| // Additional test, only for this test method: we expect the FileArtifactValue is equal to |
| // the digest [0, 0, ...] |
| assertThat(value.getMetadata().getDigest()).isEqualTo(value.getDigest()); |
| // Java zero-fills arrays. |
| assertThat(value.getDigest()).isEqualTo(new byte[16]); |
| } |
| |
| @Test |
| public void testTreeArtifactOrdering() throws Exception { |
| int rangeSize = 100; |
| int attempts = 10; |
| List<PathFragment> children = |
| IntStream.range(0, rangeSize) |
| .mapToObj(i -> PathFragment.create("file" + i)) |
| .collect(Collectors.toList()); |
| |
| for (int i = 0; i < attempts; i++) { |
| Collections.shuffle(children, new Random()); |
| Artifact treeArtifact = createTreeArtifact("out"); |
| TreeArtifactValue value = evaluateTreeArtifact(treeArtifact, children); |
| assertThat(value.getChildPaths()).containsExactlyElementsIn(children); |
| assertThat(value.getChildPaths()).isInOrder(Comparator.naturalOrder()); |
| } |
| } |
| |
| @Test |
| public void testEqualTreeArtifacts() throws Exception { |
| Artifact treeArtifact = createTreeArtifact("out"); |
| ImmutableList<PathFragment> children = |
| ImmutableList.of(PathFragment.create("one"), PathFragment.create("two")); |
| TreeArtifactValue valueOne = evaluateTreeArtifact(treeArtifact, children); |
| MemoizingEvaluator evaluator = driver.getGraphForTesting(); |
| evaluator.delete(new Predicate<SkyKey>() { |
| @Override |
| public boolean apply(SkyKey key) { |
| // Delete action execution node to force our artifacts to be re-evaluated. |
| return actions.contains(key.argument()); |
| } |
| }); |
| TreeArtifactValue valueTwo = evaluateTreeArtifact(treeArtifact, children); |
| assertThat(valueOne.getDigest()).isNotSameInstanceAs(valueTwo.getDigest()); |
| assertThat(valueOne).isEqualTo(valueTwo); |
| } |
| |
| @Test |
| public void testTreeArtifactsWithDigests() throws Exception { |
| fastDigest = true; |
| doTestTreeArtifacts(ImmutableList.of(PathFragment.create("one"))); |
| } |
| |
| @Test |
| public void testTreeArtifactsWithoutDigests() throws Exception { |
| fastDigest = false; |
| doTestTreeArtifacts(ImmutableList.of(PathFragment.create("one"))); |
| } |
| |
| @Test |
| public void testTreeArtifactMultipleDigests() throws Exception { |
| doTestTreeArtifacts(ImmutableList.of(PathFragment.create("one"), PathFragment.create("two"))); |
| } |
| |
| @Test |
| public void testIdenticalTreeArtifactsProduceTheSameDigests() throws Exception { |
| // Make sure different root dirs for set artifacts don't produce different digests. |
| Artifact one = createTreeArtifact("outOne"); |
| Artifact two = createTreeArtifact("outTwo"); |
| ImmutableList<PathFragment> children = |
| ImmutableList.of(PathFragment.create("one"), PathFragment.create("two")); |
| TreeArtifactValue valueOne = evaluateTreeArtifact(one, children); |
| TreeArtifactValue valueTwo = evaluateTreeArtifact(two, children); |
| assertThat(valueOne.getDigest()).isEqualTo(valueTwo.getDigest()); |
| } |
| |
| /** |
| * Tests that ArtifactFunction rethrows transitive {@link IOException}s as |
| * {@link MissingInputFileException}s. |
| */ |
| @Test |
| public void testIOExceptionEndToEnd() throws Throwable { |
| final IOException exception = new IOException("boop"); |
| setupRoot( |
| new CustomInMemoryFs() { |
| @Override |
| public FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException { |
| if (path.getBaseName().equals("one")) { |
| throw exception; |
| } |
| return super.statIfFound(path, followSymlinks); |
| } |
| }); |
| Artifact artifact = createTreeArtifact("outOne"); |
| Exception e = |
| assertThrows( |
| Exception.class, |
| () -> evaluateTreeArtifact(artifact, ImmutableList.of(PathFragment.create("one")))); |
| assertThat(Throwables.getRootCause(e)).hasMessageThat().contains(exception.getMessage()); |
| } |
| |
| private void file(Path path, String contents) throws Exception { |
| FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); |
| writeFile(path, contents); |
| } |
| |
| private SpecialArtifact createTreeArtifact(String path) throws IOException { |
| PathFragment execPath = PathFragment.create("out").getRelative(path); |
| Path fullPath = root.getRelative(execPath); |
| SpecialArtifact output = |
| new SpecialArtifact( |
| ArtifactRoot.asDerivedRoot(root, root.getRelative("out")), |
| execPath, |
| ALL_OWNER, |
| SpecialArtifactType.TREE); |
| actions.add(new DummyAction(ImmutableList.<Artifact>of(), output)); |
| FileSystemUtils.createDirectoryAndParents(fullPath); |
| return output; |
| } |
| |
| private SkyValue evaluateArtifactValue(Artifact artifact, boolean mandatory) throws Exception { |
| SkyKey key = ArtifactSkyKey.key(artifact, mandatory); |
| EvaluationResult<SkyValue> result = evaluate(key); |
| if (result.hasError()) { |
| throw result.getError().getException(); |
| } |
| return result.get(key); |
| } |
| |
| private void setGeneratingActions() throws InterruptedException, ActionConflictException { |
| if (evaluator.getExistingValue(ALL_OWNER) == null) { |
| differencer.inject( |
| ImmutableMap.of( |
| ALL_OWNER, |
| new BasicActionLookupValue( |
| Actions.assignOwnersAndFilterSharedActionsAndThrowActionConflict( |
| actionKeyContext, |
| ImmutableList.copyOf(actions), |
| ALL_OWNER, |
| /*outputFiles=*/ null), |
| /*nonceVersion=*/ null))); |
| } |
| } |
| |
| private <E extends SkyValue> EvaluationResult<E> evaluate(SkyKey... keys) |
| throws InterruptedException, ActionConflictException { |
| setGeneratingActions(); |
| EvaluationContext evaluationContext = |
| EvaluationContext.newBuilder() |
| .setKeepGoing(false) |
| .setNumThreads(SkyframeExecutor.DEFAULT_THREAD_COUNT) |
| .setEventHander(NullEventHandler.INSTANCE) |
| .build(); |
| return driver.evaluate(Arrays.asList(keys), evaluationContext); |
| } |
| |
| private class TreeArtifactExecutionFunction implements SkyFunction { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| Map<Artifact, ArtifactFileMetadata> fileData = new HashMap<>(); |
| Map<TreeFileArtifact, FileArtifactValue> treeArtifactData = new HashMap<>(); |
| ActionLookupData actionLookupData = (ActionLookupData) skyKey.argument(); |
| ActionLookupValue actionLookupValue = |
| (ActionLookupValue) env.getValue(actionLookupData.getActionLookupKey()); |
| Action action = actionLookupValue.getAction(actionLookupData.getActionIndex()); |
| SpecialArtifact output = (SpecialArtifact) Iterables.getOnlyElement(action.getOutputs()); |
| for (PathFragment subpath : testTreeArtifactContents) { |
| try { |
| TreeFileArtifact suboutput = ActionInputHelper.treeFileArtifact(output, subpath); |
| ArtifactFileMetadata fileValue = |
| ActionMetadataHandler.fileMetadataFromArtifact(suboutput, null, null); |
| fileData.put(suboutput, fileValue); |
| treeArtifactData.put(suboutput, FileArtifactValue.create(suboutput, fileValue)); |
| } catch (IOException e) { |
| throw new SkyFunctionException(e, Transience.TRANSIENT) {}; |
| } |
| } |
| |
| TreeArtifactValue treeArtifactValue = TreeArtifactValue.create(treeArtifactData); |
| |
| return ActionExecutionValue.create( |
| fileData, |
| ImmutableMap.of(output, treeArtifactValue), |
| ImmutableMap.<Artifact, FileArtifactValue>of(), |
| /*outputSymlinks=*/ null, |
| /*discoveredModules=*/ null, |
| /*actionDependsOnBuildId=*/ false); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| } |
| } |