diff options
author | Googler <noreply@google.com> | 2016-02-11 17:55:41 +0000 |
---|---|---|
committer | David Chen <dzc@google.com> | 2016-02-11 22:23:29 +0000 |
commit | ece75724364be3caad5a7a21c09885b35357c0ca (patch) | |
tree | bc4a1d77d1763f8e3933dcc094b0fe609b5076d7 /src/test/java/com/google | |
parent | 7b03d63b7ecc6e75ecf8a4a0ff6b0dd16a16c8b3 (diff) |
Rollback of commit a0eefb52f529b73c6cb92f0a762853646ea2eae6.
*** Reason for rollback ***
Rolling forward with the restored logic to avoid stat calls on injected Metadata.
*** Original change description ***
Automated [] rollback of commit df03e10f6552566982399b8779fe7bc7a17d75dc.
--
MOS_MIGRATED_REVID=114447944
Diffstat (limited to 'src/test/java/com/google')
-rw-r--r-- | src/test/java/com/google/devtools/build/lib/actions/util/TestAction.java | 2 | ||||
-rw-r--r-- | src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java | 773 |
2 files changed, 774 insertions, 1 deletions
diff --git a/src/test/java/com/google/devtools/build/lib/actions/util/TestAction.java b/src/test/java/com/google/devtools/build/lib/actions/util/TestAction.java index 4b5485dba4..1877b7252f 100644 --- a/src/test/java/com/google/devtools/build/lib/actions/util/TestAction.java +++ b/src/test/java/com/google/devtools/build/lib/actions/util/TestAction.java @@ -45,7 +45,7 @@ public class TestAction extends AbstractAction { private static final ResourceSet RESOURCES = ResourceSet.createWithRamCpuIo(/*memoryMb=*/1.0, /*cpu=*/0.1, /*io=*/0.0); - private final Callable<Void> effect; + protected final Callable<Void> effect; /** Use this constructor if the effect can't throw exceptions. */ public TestAction(Runnable effect, diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java new file mode 100644 index 0000000000..18d54e96c1 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java @@ -0,0 +1,773 @@ +// 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.base.Throwables.getRootCause; +import static com.google.common.truth.Truth.assertThat; +import static com.google.devtools.build.lib.actions.ActionInputHelper.artifactFile; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.hash.Hashing; +import com.google.common.util.concurrent.Runnables; +import com.google.devtools.build.lib.actions.ActionExecutionContext; +import com.google.devtools.build.lib.actions.ActionExecutionException; +import com.google.devtools.build.lib.actions.ActionInput; +import com.google.devtools.build.lib.actions.ActionInputHelper; +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.ArtifactFile; +import com.google.devtools.build.lib.actions.BuildFailedException; +import com.google.devtools.build.lib.actions.Root; +import com.google.devtools.build.lib.actions.TestExecException; +import com.google.devtools.build.lib.actions.cache.InjectedStat; +import com.google.devtools.build.lib.actions.cache.MetadataHandler; +import com.google.devtools.build.lib.actions.util.TestAction; +import com.google.devtools.build.lib.testutil.TestUtils; +import com.google.devtools.build.lib.util.AbruptExitException; +import com.google.devtools.build.lib.vfs.FileStatus; +import com.google.devtools.build.lib.vfs.FileSystem; +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.lib.vfs.Symlinks; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import javax.annotation.Nullable; + +/** Timestamp builder tests for TreeArtifacts. */ +@RunWith(JUnit4.class) +public class TreeArtifactBuildTest extends TimestampBuilderTestCase { + // Common Artifacts, ArtifactFiles, and Buttons. These aren't all used in all tests, but they're + // used often enough that we can save ourselves a lot of copy-pasted code by creating them + // in setUp(). + + Artifact in; + + Artifact outOne; + ArtifactFile outOneFileOne; + ArtifactFile outOneFileTwo; + Button buttonOne = new Button(); + + Artifact outTwo; + ArtifactFile outTwoFileOne; + ArtifactFile outTwoFileTwo; + Button buttonTwo = new Button(); + + @Before + public void setUp() throws Exception { + in = createSourceArtifact("input"); + writeFile(in, "input_content"); + + outOne = createTreeArtifact("outputOne"); + outOneFileOne = artifactFile(outOne, "out_one_file_one"); + outOneFileTwo = artifactFile(outOne, "out_one_file_two"); + + outTwo = createTreeArtifact("outputTwo"); + outTwoFileOne = artifactFile(outTwo, "out_one_file_one"); + outTwoFileTwo = artifactFile(outTwo, "out_one_file_two"); + } + + /** Simple smoke test. If this isn't passing, something is very wrong... */ + @Test + public void testTreeArtifactSimpleCase() throws Exception { + TouchingTestAction action = new TouchingTestAction(outOneFileOne, outOneFileTwo); + registerAction(action); + buildArtifact(action.getSoleOutput()); + + assertTrue(outOneFileOne.getPath().exists()); + assertTrue(outOneFileTwo.getPath().exists()); + } + + /** Simple test for the case with dependencies. */ + @Test + public void testDependentTreeArtifacts() throws Exception { + TouchingTestAction actionOne = new TouchingTestAction(outOneFileOne, outOneFileTwo); + registerAction(actionOne); + + CopyTreeAction actionTwo = new CopyTreeAction( + ImmutableList.of(outOneFileOne, outOneFileTwo), + ImmutableList.of(outTwoFileOne, outTwoFileTwo)); + registerAction(actionTwo); + + buildArtifact(outTwo); + + assertTrue(outOneFileOne.getPath().exists()); + assertTrue(outOneFileTwo.getPath().exists()); + assertTrue(outTwoFileOne.getPath().exists()); + assertTrue(outTwoFileTwo.getPath().exists()); + } + + /** Unchanged TreeArtifact outputs should not cause reexecution. */ + @Test + public void testCacheCheckingForTreeArtifactsDoesNotCauseReexecution() throws Exception { + Artifact outOne = createTreeArtifact("outputOne"); + Button buttonOne = new Button(); + + Artifact outTwo = createTreeArtifact("outputTwo"); + Button buttonTwo = new Button(); + + TouchingTestAction actionOne = new TouchingTestAction( + buttonOne, outOne, "file_one", "file_two"); + registerAction(actionOne); + + CopyTreeAction actionTwo = new CopyTreeAction( + buttonTwo, outOne, outTwo, "file_one", "file_two"); + registerAction(actionTwo); + + buttonOne.pressed = buttonTwo.pressed = false; + buildArtifact(outTwo); + assertTrue(buttonOne.pressed); // built + assertTrue(buttonTwo.pressed); // built + + buttonOne.pressed = buttonTwo.pressed = false; + buildArtifact(outTwo); + assertFalse(buttonOne.pressed); // not built + assertFalse(buttonTwo.pressed); // not built + } + + /** + * Test rebuilding TreeArtifacts for inputs, outputs, and dependents. + * Also a test for caching. + */ + @Test + public void testTransitiveReexecutionForTreeArtifacts() throws Exception { + WriteInputToFilesAction actionOne = new WriteInputToFilesAction( + buttonOne, + in, + outOneFileOne, outOneFileTwo); + registerAction(actionOne); + + CopyTreeAction actionTwo = new CopyTreeAction( + buttonTwo, + ImmutableList.of(outOneFileOne, outOneFileTwo), + ImmutableList.of(outTwoFileOne, outTwoFileTwo)); + registerAction(actionTwo); + + buttonOne.pressed = buttonTwo.pressed = false; + buildArtifact(outTwo); + assertTrue(buttonOne.pressed); // built + assertTrue(buttonTwo.pressed); // built + + buttonOne.pressed = buttonTwo.pressed = false; + writeFile(in, "modified_input"); + buildArtifact(outTwo); + assertTrue(buttonOne.pressed); // built + assertTrue(buttonTwo.pressed); // not built + + buttonOne.pressed = buttonTwo.pressed = false; + writeFile(outOneFileOne, "modified_output"); + buildArtifact(outTwo); + assertTrue(buttonOne.pressed); // built + assertFalse(buttonTwo.pressed); // should have been cached + + buttonOne.pressed = buttonTwo.pressed = false; + writeFile(outTwoFileOne, "more_modified_output"); + buildArtifact(outTwo); + assertFalse(buttonOne.pressed); // not built + assertTrue(buttonTwo.pressed); // built + } + + /** Tests that changing a TreeArtifact directory should cause reexeuction. */ + @Test + public void testDirectoryContentsCachingForTreeArtifacts() throws Exception { + WriteInputToFilesAction actionOne = new WriteInputToFilesAction( + buttonOne, + in, + outOneFileOne, outOneFileTwo); + registerAction(actionOne); + + CopyTreeAction actionTwo = new CopyTreeAction( + buttonTwo, + ImmutableList.of(outOneFileOne, outOneFileTwo), + ImmutableList.of(outTwoFileOne, outTwoFileTwo)); + registerAction(actionTwo); + + buttonOne.pressed = buttonTwo.pressed = false; + buildArtifact(outTwo); + // just a smoke test--if these aren't built we have bigger problems! + assertTrue(buttonOne.pressed); + assertTrue(buttonTwo.pressed); + + // Adding a file to a directory should cause reexecution. + buttonOne.pressed = buttonTwo.pressed = false; + Path spuriousOutputOne = outOne.getPath().getRelative("spuriousOutput"); + touchFile(spuriousOutputOne); + buildArtifact(outTwo); + // Should re-execute, and delete spurious output + assertFalse(spuriousOutputOne.exists()); + assertTrue(buttonOne.pressed); + assertFalse(buttonTwo.pressed); // should have been cached + + buttonOne.pressed = buttonTwo.pressed = false; + Path spuriousOutputTwo = outTwo.getPath().getRelative("anotherSpuriousOutput"); + touchFile(spuriousOutputTwo); + buildArtifact(outTwo); + assertFalse(spuriousOutputTwo.exists()); + assertFalse(buttonOne.pressed); + assertTrue(buttonTwo.pressed); + + // Deleting should cause reexecution. + buttonOne.pressed = buttonTwo.pressed = false; + deleteFile(outOneFileOne); + buildArtifact(outTwo); + assertTrue(outOneFileOne.getPath().exists()); + assertTrue(buttonOne.pressed); + assertFalse(buttonTwo.pressed); // should have been cached + + buttonOne.pressed = buttonTwo.pressed = false; + deleteFile(outTwoFileOne); + buildArtifact(outTwo); + assertTrue(outTwoFileOne.getPath().exists()); + assertFalse(buttonOne.pressed); + assertTrue(buttonTwo.pressed); + } + + /** + * TreeArtifacts don't care about mtime, even when the file is empty. + * However, actions taking input non-Tree artifacts still care about mtime + * (although this behavior should go away). + */ + @Test + public void testMTimeForTreeArtifactsDoesNotMatter() throws Exception { + // For this test, we only touch the input file. + Artifact in = createSourceArtifact("touchable_input"); + touchFile(in); + + WriteInputToFilesAction actionOne = new WriteInputToFilesAction( + buttonOne, + in, + outOneFileOne, outOneFileTwo); + registerAction(actionOne); + + CopyTreeAction actionTwo = new CopyTreeAction( + buttonTwo, + ImmutableList.of(outOneFileOne, outOneFileTwo), + ImmutableList.of(outTwoFileOne, outTwoFileTwo)); + registerAction(actionTwo); + + buttonOne.pressed = buttonTwo.pressed = false; + buildArtifact(outTwo); + assertTrue(buttonOne.pressed); // built + assertTrue(buttonTwo.pressed); // built + + buttonOne.pressed = buttonTwo.pressed = false; + touchFile(in); + buildArtifact(outTwo); + // Per existing behavior, mtime matters for empty file Artifacts. + assertTrue(buttonOne.pressed); + // But this should be cached. + assertFalse(buttonTwo.pressed); + + // None of the below following should result in anything being built. + buttonOne.pressed = buttonTwo.pressed = false; + touchFile(outOneFileOne); + buildArtifact(outTwo); + // Nothing should be built. + assertFalse(buttonOne.pressed); + assertFalse(buttonTwo.pressed); + + buttonOne.pressed = buttonTwo.pressed = false; + touchFile(outOneFileTwo); + buildArtifact(outTwo); + // Nothing should be built. + assertFalse(buttonOne.pressed); + assertFalse(buttonTwo.pressed); + } + + /** Tests that the declared order of TreeArtifact contents does not matter. */ + @Test + public void testOrderIndependenceOfTreeArtifactContents() throws Exception { + WriteInputToFilesAction actionOne = new WriteInputToFilesAction( + in, + // The design of WritingTestAction is s.t. + // these files will be registered in the given order. + outOneFileTwo, outOneFileOne); + registerAction(actionOne); + + CopyTreeAction actionTwo = new CopyTreeAction( + ImmutableList.of(outOneFileOne, outOneFileTwo), + ImmutableList.of(outTwoFileOne, outTwoFileTwo)); + registerAction(actionTwo); + + buildArtifact(outTwo); + } + + @Test + public void testActionExpansion() throws Exception { + WriteInputToFilesAction action = new WriteInputToFilesAction(in, outOneFileOne, outOneFileTwo); + + CopyTreeAction actionTwo = new CopyTreeAction( + ImmutableList.of(outOneFileOne, outOneFileTwo), + ImmutableList.of(outTwoFileOne, outTwoFileTwo)) { + @Override + public void executeTestBehavior(ActionExecutionContext actionExecutionContext) + throws ActionExecutionException { + super.executeTestBehavior(actionExecutionContext); + + Collection<ActionInput> expanded = + ActionInputHelper.expandArtifacts(ImmutableList.of(outOne), + actionExecutionContext.getArtifactExpander()); + // Only files registered should show up here. + assertThat(expanded).containsExactly(outOneFileOne, outOneFileTwo); + } + }; + + registerAction(action); + registerAction(actionTwo); + + buildArtifact(outTwo); // should not fail + } + + @Test + public void testInvalidOutputRegistrations() throws Exception { + TreeArtifactTestAction failureOne = new TreeArtifactTestAction( + Runnables.doNothing(), outOneFileOne, outOneFileTwo) { + @Override + public void executeTestBehavior(ActionExecutionContext actionExecutionContext) + throws ActionExecutionException { + try { + writeFile(outOneFileOne, "one"); + writeFile(outOneFileTwo, "two"); + // In this test case, we only register one output. This will fail. + registerOutput(actionExecutionContext, "one"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + + registerAction(failureOne); + try { + buildArtifact(outOne); + fail(); // Should have thrown + } catch (Exception e) { + assertThat(getRootCause(e).getMessage()).contains("not present on disk"); + } + + TreeArtifactTestAction failureTwo = new TreeArtifactTestAction( + Runnables.doNothing(), outTwoFileOne, outTwoFileTwo) { + @Override + public void executeTestBehavior(ActionExecutionContext actionExecutionContext) + throws ActionExecutionException { + try { + writeFile(outTwoFileOne, "one"); + writeFile(outTwoFileTwo, "two"); + // In this test case, register too many outputs. This will fail. + registerOutput(actionExecutionContext, "one"); + registerOutput(actionExecutionContext, "two"); + registerOutput(actionExecutionContext, "three"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + + registerAction(failureTwo); + try { + buildArtifact(outTwo); + fail(); // Should have thrown + } catch (Exception e) { + assertThat(getRootCause(e).getMessage()).contains("not present on disk"); + } + } + + private static void checkDirectoryPermissions(Path path) throws IOException { + assertTrue(path.isDirectory()); + assertTrue(path.isExecutable()); + assertTrue(path.isReadable()); + assertFalse(path.isWritable()); + } + + private static void checkFilePermissions(Path path) throws IOException { + assertFalse(path.isDirectory()); + assertTrue(path.isExecutable()); + assertTrue(path.isReadable()); + assertFalse(path.isWritable()); + } + + @Test + public void testOutputsAreReadOnlyAndExecutable() throws Exception { + final Artifact out = createTreeArtifact("output"); + + TreeArtifactTestAction action = new TreeArtifactTestAction(out) { + @Override + public void execute(ActionExecutionContext actionExecutionContext) + throws ActionExecutionException { + try { + writeFile(out.getPath().getChild("one"), "one"); + writeFile(out.getPath().getChild("two"), "two"); + writeFile(out.getPath().getChild("three").getChild("four"), "three/four"); + registerOutput(actionExecutionContext, "one"); + registerOutput(actionExecutionContext, "two"); + registerOutput(actionExecutionContext, "three/four"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + + registerAction(action); + + buildArtifact(action.getSoleOutput()); + + checkDirectoryPermissions(out.getPath()); + checkFilePermissions(out.getPath().getChild("one")); + checkFilePermissions(out.getPath().getChild("two")); + checkDirectoryPermissions(out.getPath().getChild("three")); + checkFilePermissions(out.getPath().getChild("three").getChild("four")); + } + + // This is more a smoke test than anything, because it turns out that: + // 1) there is no easy way to turn fast digests on/off for these test cases, and + // 2) injectDigest() doesn't really complain if you inject bad digests or digests + // for nonexistent files. Instead some weird error shows up down the line. + // In fact, there are no tests for injectDigest anywhere in the codebase. + // So all we're really testing here is that injectDigest() doesn't throw a weird exception. + // TODO(bazel-team): write real tests for injectDigest, here and elsewhere. + @Test + public void testDigestInjection() throws Exception { + TreeArtifactTestAction action = new TreeArtifactTestAction(outOne) { + @Override + public void execute(ActionExecutionContext actionExecutionContext) + throws ActionExecutionException { + try { + writeFile(outOneFileOne, "one"); + writeFile(outOneFileTwo, "two"); + + MetadataHandler md = actionExecutionContext.getMetadataHandler(); + FileStatus stat = outOneFileOne.getPath().stat(Symlinks.NOFOLLOW); + md.injectDigest(outOneFileOne, + new InjectedStat(stat.getLastModifiedTime(), stat.getSize(), stat.getNodeId()), + Hashing.md5().hashString("one", Charset.forName("UTF-8")).asBytes()); + + stat = outOneFileTwo.getPath().stat(Symlinks.NOFOLLOW); + md.injectDigest(outOneFileTwo, + new InjectedStat(stat.getLastModifiedTime(), stat.getSize(), stat.getNodeId()), + Hashing.md5().hashString("two", Charset.forName("UTF-8")).asBytes()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + + registerAction(action); + buildArtifact(action.getSoleOutput()); + } + + /** + * A generic test action that takes at most one input TreeArtifact, + * exactly one output TreeArtifact, and some path fragment inputs/outputs. + */ + private abstract static class TreeArtifactTestAction extends TestAction { + final Iterable<ArtifactFile> inputFiles; + final Iterable<ArtifactFile> outputFiles; + + TreeArtifactTestAction(final Artifact output, final String... subOutputs) { + this(Runnables.doNothing(), + null, + ImmutableList.<ArtifactFile>of(), + output, + Collections2.transform( + Arrays.asList(subOutputs), + new Function<String, ArtifactFile>() { + @Nullable + @Override + public ArtifactFile apply(String s) { + return ActionInputHelper.artifactFile(output, s); + } + })); + } + + TreeArtifactTestAction(Runnable effect, ArtifactFile... outputFiles) { + this(effect, Arrays.asList(outputFiles)); + } + + TreeArtifactTestAction(Runnable effect, Collection<ArtifactFile> outputFiles) { + this(effect, null, ImmutableList.<ArtifactFile>of(), + outputFiles.iterator().next().getParent(), outputFiles); + } + + TreeArtifactTestAction(Runnable effect, Artifact inputFile, + Collection<ArtifactFile> outputFiles) { + this(effect, inputFile, ImmutableList.<ArtifactFile>of(), + outputFiles.iterator().next().getParent(), outputFiles); + } + + TreeArtifactTestAction(Runnable effect, Collection<ArtifactFile> inputFiles, + Collection<ArtifactFile> outputFiles) { + this(effect, inputFiles.iterator().next().getParent(), inputFiles, + outputFiles.iterator().next().getParent(), outputFiles); + } + + TreeArtifactTestAction( + Runnable effect, + @Nullable Artifact input, + Collection<ArtifactFile> inputFiles, + Artifact output, + Collection<ArtifactFile> outputFiles) { + super(effect, + input == null ? ImmutableList.<Artifact>of() : ImmutableList.of(input), + ImmutableList.of(output)); + Preconditions.checkArgument( + inputFiles.isEmpty() || (input != null && input.isTreeArtifact())); + Preconditions.checkArgument(output == null || output.isTreeArtifact()); + this.inputFiles = ImmutableList.copyOf(inputFiles); + this.outputFiles = ImmutableList.copyOf(outputFiles); + for (ArtifactFile inputFile : inputFiles) { + Preconditions.checkState(inputFile.getParent().equals(input)); + } + for (ArtifactFile outputFile : outputFiles) { + Preconditions.checkState(outputFile.getParent().equals(output)); + } + } + + @Override + public void execute(ActionExecutionContext actionExecutionContext) + throws ActionExecutionException { + if (getInputs().iterator().hasNext()) { + // Sanity check--verify all inputs exist. + Artifact input = getSoleInput(); + if (!input.getPath().exists()) { + throw new IllegalStateException("action's input Artifact does not exist: " + + input.getPath()); + } + for (ArtifactFile inputFile : inputFiles) { + if (!inputFile.getPath().exists()) { + throw new IllegalStateException("action's input does not exist: " + inputFile); + } + } + } + + Artifact output = getSoleOutput(); + assertTrue(output.getPath().exists()); + try { + effect.call(); + executeTestBehavior(actionExecutionContext); + for (ArtifactFile outputFile : outputFiles) { + actionExecutionContext.getMetadataHandler().addExpandedTreeOutput(outputFile); + } + } catch (RuntimeException e) { + throw new RuntimeException(e); + } catch (Exception e) { + throw new ActionExecutionException("TestAction failed due to exception", + e, this, false); + } + } + + void executeTestBehavior(ActionExecutionContext c) throws ActionExecutionException { + // Default: do nothing + } + + /** Checks there's exactly one input, and returns it. */ + // This prevents us from making testing mistakes, like + // assuming there's only one input when this isn't actually true. + Artifact getSoleInput() { + Iterator<Artifact> it = getInputs().iterator(); + Artifact r = it.next(); + Preconditions.checkNotNull(r); + Preconditions.checkState(!it.hasNext()); + return r; + } + + /** Checks there's exactly one output, and returns it. */ + Artifact getSoleOutput() { + Iterator<Artifact> it = getOutputs().iterator(); + Artifact r = it.next(); + Preconditions.checkNotNull(r); + Preconditions.checkState(!it.hasNext()); + Preconditions.checkState(r.equals(getPrimaryOutput())); + return r; + } + + void registerOutput(ActionExecutionContext context, String outputName) throws IOException { + context.getMetadataHandler().addExpandedTreeOutput( + artifactFile(getSoleOutput(), new PathFragment(outputName))); + } + + static List<ArtifactFile> asArtifactFiles(final Artifact parent, String... files) { + return Lists.transform( + Arrays.asList(files), + new Function<String, ArtifactFile>() { + @Nullable + @Override + public ArtifactFile apply(String s) { + return ActionInputHelper.artifactFile(parent, s); + } + }); + } + } + + /** An action that touches some output ArtifactFiles. Takes no inputs. */ + private static class TouchingTestAction extends TreeArtifactTestAction { + TouchingTestAction(ArtifactFile... outputPaths) { + super(Runnables.doNothing(), outputPaths); + } + + TouchingTestAction(Runnable effect, Artifact output, String... outputPaths) { + super(effect, asArtifactFiles(output, outputPaths)); + } + + @Override + public void executeTestBehavior(ActionExecutionContext actionExecutionContext) + throws ActionExecutionException { + try { + for (ArtifactFile file : outputFiles) { + touchFile(file); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + /** Takes an input file and populates several copies inside a TreeArtifact. */ + private static class WriteInputToFilesAction extends TreeArtifactTestAction { + WriteInputToFilesAction(Artifact input, ArtifactFile... outputs) { + this(Runnables.doNothing(), input, outputs); + } + + WriteInputToFilesAction( + Runnable effect, + Artifact input, + ArtifactFile... outputs) { + super(effect, input, Arrays.asList(outputs)); + Preconditions.checkArgument(!input.isTreeArtifact()); + } + + @Override + public void executeTestBehavior(ActionExecutionContext actionExecutionContext) + throws ActionExecutionException { + try { + for (ArtifactFile file : outputFiles) { + FileSystemUtils.createDirectoryAndParents(file.getPath().getParentDirectory()); + FileSystemUtils.copyFile(getSoleInput().getPath(), file.getPath()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + /** Copies the given ArtifactFile inputs to the given outputs, in respective order. */ + private static class CopyTreeAction extends TreeArtifactTestAction { + + CopyTreeAction(Runnable effect, Artifact input, Artifact output, String... sourcesAndDests) { + super(effect, input, asArtifactFiles(input, sourcesAndDests), output, + asArtifactFiles(output, sourcesAndDests)); + } + + CopyTreeAction( + Collection<ArtifactFile> inputPaths, + Collection<ArtifactFile> outputPaths) { + super(Runnables.doNothing(), inputPaths, outputPaths); + } + + CopyTreeAction( + Runnable effect, + Collection<ArtifactFile> inputPaths, + Collection<ArtifactFile> outputPaths) { + super(effect, inputPaths, outputPaths); + } + + @Override + public void executeTestBehavior(ActionExecutionContext actionExecutionContext) + throws ActionExecutionException { + Iterator<ArtifactFile> inputIterator = inputFiles.iterator(); + Iterator<ArtifactFile> outputIterator = outputFiles.iterator(); + + try { + while (inputIterator.hasNext() || outputIterator.hasNext()) { + ArtifactFile input = inputIterator.next(); + ArtifactFile output = outputIterator.next(); + FileSystemUtils.createDirectoryAndParents(output.getPath().getParentDirectory()); + FileSystemUtils.copyFile(input.getPath(), output.getPath()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + // both iterators must be of the same size + assertFalse(inputIterator.hasNext()); + assertFalse(inputIterator.hasNext()); + } + } + + private Artifact createTreeArtifact(String name) { + FileSystem fs = scratch.getFileSystem(); + Path execRoot = fs.getPath(TestUtils.tmpDir()); + PathFragment execPath = new PathFragment("out").getRelative(name); + Path path = execRoot.getRelative(execPath); + return new SpecialArtifact( + path, Root.asDerivedRoot(execRoot, execRoot.getRelative("out")), execPath, ALL_OWNER, + SpecialArtifactType.TREE); + } + + private void buildArtifact(Artifact artifact) + throws InterruptedException, BuildFailedException, TestExecException, AbruptExitException { + buildArtifacts(cachingBuilder(), artifact); + } + + private static void writeFile(Path path, String contents) throws IOException { + FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); + // sometimes we write read-only files + if (path.exists()) { + path.setWritable(true); + } + FileSystemUtils.writeContentAsLatin1(path, contents); + } + + private static void writeFile(ArtifactFile file, String contents) throws IOException { + writeFile(file.getPath(), contents); + } + + private static void touchFile(Path path) throws IOException { + FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); + path.getParentDirectory().setWritable(true); + FileSystemUtils.touchFile(path); + } + + private static void touchFile(ArtifactFile file) throws IOException { + touchFile(file.getPath()); + } + + private static void deleteFile(ArtifactFile file) throws IOException { + Path path = file.getPath(); + // sometimes we write read-only files + if (path.exists()) { + path.setWritable(true); + // work around the sticky bit (this might depend on the behavior of the OS?) + path.getParentDirectory().setWritable(true); + path.delete(); + } + } +} |