|  | // 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.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.ActionAnalysisMetadata; | 
|  | 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.ArtifactRoot; | 
|  | import com.google.devtools.build.lib.actions.ArtifactRoot.RootType; | 
|  | 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.util.ActionsTestUtil; | 
|  | import com.google.devtools.build.lib.actions.util.TestAction.DummyAction; | 
|  | 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.vfs.FileStatus; | 
|  | import com.google.devtools.build.lib.vfs.FileStatusWithDigestAdapter; | 
|  | import com.google.devtools.build.lib.vfs.Path; | 
|  | import com.google.devtools.build.lib.vfs.PathFragment; | 
|  | import com.google.devtools.build.lib.vfs.Symlinks; | 
|  | import com.google.devtools.build.lib.vfs.SyscallCache; | 
|  | import com.google.devtools.build.skyframe.Differencer.DiffWithDelta.Delta; | 
|  | import com.google.devtools.build.skyframe.EvaluationContext; | 
|  | import com.google.devtools.build.skyframe.EvaluationResult; | 
|  | 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.List; | 
|  | 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 final class TreeArtifactMetadataTest extends ArtifactFunctionTestCase { | 
|  |  | 
|  | // A list of subpaths for the SetArtifact created by our custom ActionExecutionFunction. | 
|  | private List<PathFragment> testTreeArtifactContents; | 
|  |  | 
|  | @Before | 
|  | public void setUp() { | 
|  | 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); | 
|  | } | 
|  |  | 
|  | 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( | 
|  | Iterables.transform(children, child -> TreeFileArtifact.createTreeOutput(tree, child))); | 
|  | return value; | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void testEmptyTreeArtifacts() throws Exception { | 
|  | TreeArtifactValue value = doTestTreeArtifacts(ImmutableList.of()); | 
|  | // Additional test, only for this test method: we expect the FileArtifactValue is equal to | 
|  | // the digest [0] | 
|  | assertThat(value.getMetadata().getDigest()).isEqualTo(value.getDigest()); | 
|  | // Java zero-fills arrays. | 
|  | assertThat(value.getDigest()).isEqualTo(new byte[1]); | 
|  | } | 
|  |  | 
|  | @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); | 
|  | // Delete action execution node to force our artifacts to be re-evaluated. | 
|  | evaluator.delete(key -> 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(PathFragment 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 static void file(Path path, String contents) throws Exception { | 
|  | path.getParentDirectory().createDirectoryAndParents(); | 
|  | writeFile(path, contents); | 
|  | } | 
|  |  | 
|  | private SpecialArtifact createTreeArtifact(String path) throws IOException { | 
|  | PathFragment execPath = PathFragment.create("out").getRelative(path); | 
|  | Path fullPath = root.getRelative(execPath); | 
|  | SpecialArtifact output = | 
|  | SpecialArtifact.create( | 
|  | ArtifactRoot.asDerivedRoot(root, RootType.Output, "out"), | 
|  | execPath, | 
|  | ALL_OWNER, | 
|  | SpecialArtifactType.TREE); | 
|  | actions.add(new DummyAction(NestedSetBuilder.emptySet(Order.STABLE_ORDER), output)); | 
|  | fullPath.createDirectoryAndParents(); | 
|  | return output; | 
|  | } | 
|  |  | 
|  | private SkyValue evaluateArtifactValue(Artifact artifact) throws Exception { | 
|  | SkyKey key = Artifact.key(artifact); | 
|  | EvaluationResult<SkyValue> result = evaluate(key); | 
|  | if (result.hasError()) { | 
|  | throw result.getError().getException(); | 
|  | } | 
|  | return result.get(key); | 
|  | } | 
|  |  | 
|  | private void setGeneratingActions() | 
|  | throws InterruptedException, ActionConflictException, | 
|  | Actions.ArtifactGeneratedByOtherRuleException { | 
|  | if (evaluator.getExistingValue(ALL_OWNER) == null) { | 
|  | ImmutableList<ActionAnalysisMetadata> generatingActions = ImmutableList.copyOf(actions); | 
|  | Actions.assignOwnersAndThrowIfConflictToleratingSharedActions( | 
|  | actionKeyContext, generatingActions, ALL_OWNER); | 
|  | differencer.inject( | 
|  | ImmutableMap.of(ALL_OWNER, Delta.justNew(new BasicActionLookupValue(generatingActions)))); | 
|  | } | 
|  | } | 
|  |  | 
|  | private <E extends SkyValue> EvaluationResult<E> evaluate(SkyKey... keys) | 
|  | throws InterruptedException, ActionConflictException, | 
|  | Actions.ArtifactGeneratedByOtherRuleException { | 
|  | setGeneratingActions(); | 
|  | EvaluationContext evaluationContext = | 
|  | EvaluationContext.newBuilder() | 
|  | .setKeepGoing(false) | 
|  | .setParallelism(SkyframeExecutor.DEFAULT_THREAD_COUNT) | 
|  | .setEventHandler(NullEventHandler.INSTANCE) | 
|  | .build(); | 
|  | return evaluator.evaluate(Arrays.asList(keys), evaluationContext); | 
|  | } | 
|  |  | 
|  | private class TreeArtifactExecutionFunction implements SkyFunction { | 
|  | @Override | 
|  | public SkyValue compute(SkyKey skyKey, Environment env) | 
|  | throws SkyFunctionException, InterruptedException { | 
|  | 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()); | 
|  | TreeArtifactValue.Builder tree = TreeArtifactValue.newBuilder(output); | 
|  | for (PathFragment subpath : testTreeArtifactContents) { | 
|  | try { | 
|  | TreeFileArtifact suboutput = TreeFileArtifact.createTreeOutput(output, subpath); | 
|  | Path path = suboutput.getPath(); | 
|  | FileArtifactValue noDigest = | 
|  | ActionMetadataHandler.fileArtifactValueFromArtifact( | 
|  | suboutput, | 
|  | FileStatusWithDigestAdapter.maybeAdapt(path.statIfFound(Symlinks.NOFOLLOW)), | 
|  | SyscallCache.NO_CACHE, | 
|  | null); | 
|  | FileArtifactValue withDigest = | 
|  | FileArtifactValue.createFromInjectedDigest(noDigest, path.getDigest()); | 
|  | tree.putChild(suboutput, withDigest); | 
|  | } catch (IOException e) { | 
|  | throw new SkyFunctionException(e, Transience.TRANSIENT) {}; | 
|  | } | 
|  | } | 
|  |  | 
|  | return ActionsTestUtil.createActionExecutionValue( | 
|  | /* artifactData= */ ImmutableMap.of(), ImmutableMap.of(output, tree.build())); | 
|  | } | 
|  | } | 
|  | } |