// 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 org.junit.Assert.fail; 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.ActionAnalysisMetadata; 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.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.MissingInputFileException; import com.google.devtools.build.lib.actions.Root; import com.google.devtools.build.lib.actions.cache.DigestUtils; import com.google.devtools.build.lib.actions.cache.Metadata; 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.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.HashMap; import java.util.List; import java.util.Map; 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 { Artifact output = createTreeArtifact("output"); return doTestTreeArtifacts(output, children); } private TreeArtifactValue doTestTreeArtifacts(Artifact 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, Metadata> digestBuilder = new HashMap<>(); for (PathFragment child : children) { Metadata subdigest = new Metadata(tree.getPath().getRelative(child).getMD5Digest()); 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 Metadata is equal to // the digest [0, 0, ...] assertThat(value.getMetadata().digest).isEqualTo(value.getDigest()); // Java zero-fills arrays. assertThat(value.getDigest()).isEqualTo(new byte[16]); } @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()).isNotSameAs(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 stat(Path path, boolean followSymlinks) throws IOException { if (path.getBaseName().equals("one")) { throw exception; } return super.stat(path, followSymlinks); } }); try { Artifact artifact = createTreeArtifact("outOne"); TreeArtifactValue value = evaluateTreeArtifact(artifact, ImmutableList.of(PathFragment.create("one"))); fail("MissingInputFileException expected, got " + value); } catch (Exception e) { assertThat(Throwables.getRootCause(e).getMessage()).contains(exception.getMessage()); } } private void file(Path path, String contents) throws Exception { FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); writeFile(path, contents); } private Artifact createTreeArtifact(String path) throws IOException { PathFragment execPath = PathFragment.create("out").getRelative(path); Path fullPath = root.getRelative(execPath); Artifact output = new SpecialArtifact( fullPath, Root.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() { if (evaluator.getExistingValueForTesting(OWNER_KEY) == null) { differencer.inject( ImmutableMap.of( OWNER_KEY, new ActionLookupValue(ImmutableList.<ActionAnalysisMetadata>copyOf(actions), false))); } } private <E extends SkyValue> EvaluationResult<E> evaluate(SkyKey... keys) throws InterruptedException { setGeneratingActions(); return driver.evaluate( Arrays.asList(keys), /*keepGoing=*/ false, SkyframeExecutor.DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE); } private class TreeArtifactExecutionFunction implements SkyFunction { @Override public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException, InterruptedException { Map<Artifact, FileValue> fileData = new HashMap<>(); Map<TreeFileArtifact, FileArtifactValue> treeArtifactData = new HashMap<>(); ActionLookupData actionLookupData = (ActionLookupData) skyKey.argument(); ActionLookupValue actionLookupValue = (ActionLookupValue) env.getValue(actionLookupData.getActionLookupNode()); Action action = actionLookupValue.getAction(actionLookupData.getActionIndex()); Artifact output = Iterables.getOnlyElement(action.getOutputs()); for (PathFragment subpath : testTreeArtifactContents) { try { TreeFileArtifact suboutput = ActionInputHelper.treeFileArtifact(output, subpath); FileValue fileValue = ActionMetadataHandler.fileValueFromArtifact( 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 new ActionExecutionValue( fileData, ImmutableMap.of(output, treeArtifactValue), ImmutableMap.<Artifact, FileArtifactValue>of()); } @Override public String extractTag(SkyKey skyKey) { return null; } } }