aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/test/java/com
diff options
context:
space:
mode:
authorGravatar Han-Wen Nienhuys <hanwen@google.com>2015-10-28 15:48:36 +0000
committerGravatar Dmitry Lomov <dslomov@google.com>2015-10-28 16:05:47 +0000
commiteb71eccefb02ca17a87ff25435f2a8e170e03c6b (patch)
tree4c48687969c09ba8957c32cd45b91e7bfaecb68f /src/test/java/com
parentde332aeb49802b66a9bd855c864a25ee84394c96 (diff)
Open source FileFunctionTest.
-- MOS_MIGRATED_REVID=106499960
Diffstat (limited to 'src/test/java/com')
-rw-r--r--src/test/java/com/google/devtools/build/lib/skyframe/FileFunctionTest.java1450
1 files changed, 1450 insertions, 0 deletions
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/FileFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/FileFunctionTest.java
new file mode 100644
index 0000000000..8899d79d4d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/FileFunctionTest.java
@@ -0,0 +1,1450 @@
+// Copyright 2015 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.skyframe.SkyframeExecutor.DEFAULT_THREAD_COUNT;
+import static org.junit.Assert.assertArrayEquals;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+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.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.testing.EqualsTester;
+import com.google.devtools.build.lib.events.NullEventHandler;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.testutil.ManualClock;
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+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.RootedPath;
+import com.google.devtools.build.lib.vfs.UnixFileSystem;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+import com.google.devtools.build.skyframe.ErrorInfo;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator;
+import com.google.devtools.build.skyframe.MemoizingEvaluator;
+import com.google.devtools.build.skyframe.RecordingDifferencer;
+import com.google.devtools.build.skyframe.SequentialBuildDriver;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * Tests for {@link FileFunction}.
+ */
+public class FileFunctionTest extends TestCase {
+ private CustomInMemoryFs fs;
+ private Path pkgRoot;
+ private PathPackageLocator pkgLocator;
+ private TimestampGranularityMonitor tsgm;
+ private boolean fastMd5;
+ private ManualClock manualClock;
+ private RecordingDifferencer differencer;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ fastMd5 = true;
+ manualClock = new ManualClock();
+ createFsAndRoot(new CustomInMemoryFs(manualClock));
+ tsgm = new TimestampGranularityMonitor(BlazeClock.instance());
+ }
+
+ private void createFsAndRoot(CustomInMemoryFs fs) throws IOException {
+ this.fs = fs;
+ pkgRoot = fs.getRootDirectory().getRelative("root");
+ pkgLocator = new PathPackageLocator(pkgRoot);
+ FileSystemUtils.createDirectoryAndParents(pkgRoot);
+ }
+
+ private SequentialBuildDriver makeDriver() {
+ return makeDriver(/*errorOnExternalFiles=*/ false);
+ }
+
+ private SequentialBuildDriver makeDriver(boolean errorOnExternalFiles) {
+ AtomicReference<PathPackageLocator> pkgLocatorRef = new AtomicReference<>(pkgLocator);
+ ExternalFilesHelper externalFilesHelper =
+ new ExternalFilesHelper(pkgLocatorRef, errorOnExternalFiles);
+ differencer = new RecordingDifferencer();
+ MemoizingEvaluator evaluator =
+ new InMemoryMemoizingEvaluator(
+ ImmutableMap.of(
+ SkyFunctions.FILE_STATE, new FileStateFunction(tsgm, externalFilesHelper),
+ SkyFunctions.FILE_SYMLINK_CYCLE_UNIQUENESS,
+ new FileSymlinkCycleUniquenessFunction(),
+ SkyFunctions.FILE_SYMLINK_INFINITE_EXPANSION_UNIQUENESS,
+ new FileSymlinkInfiniteExpansionUniquenessFunction(),
+ SkyFunctions.FILE, new FileFunction(pkgLocatorRef, tsgm, externalFilesHelper)),
+ differencer);
+ PrecomputedValue.BUILD_ID.set(differencer, UUID.randomUUID());
+ return new SequentialBuildDriver(evaluator);
+ }
+
+ private FileValue valueForPath(Path path) throws InterruptedException {
+ return valueForPathHelper(pkgRoot, path);
+ }
+
+ private FileValue valueForPathOutsidePkgRoot(Path path) throws InterruptedException {
+ return valueForPathHelper(fs.getRootDirectory(), path);
+ }
+
+ private FileValue valueForPathHelper(Path root, Path path) throws InterruptedException {
+ PathFragment pathFragment = path.relativeTo(root);
+ RootedPath rootedPath = RootedPath.toRootedPath(root, pathFragment);
+ SequentialBuildDriver driver = makeDriver();
+ SkyKey key = FileValue.key(rootedPath);
+ EvaluationResult<FileValue> result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ assertFalse(result.hasError());
+ return result.get(key);
+ }
+
+ public void testFileValueHashCodeAndEqualsContract() throws Exception {
+ Path pathA = file(pkgRoot + "a", "a");
+ Path pathB = file(pkgRoot + "b", "b");
+ FileValue valueA1 = valueForPathOutsidePkgRoot(pathA);
+ FileValue valueA2 = valueForPathOutsidePkgRoot(pathA);
+ FileValue valueB1 = valueForPathOutsidePkgRoot(pathB);
+ FileValue valueB2 = valueForPathOutsidePkgRoot(pathB);
+ new EqualsTester()
+ .addEqualityGroup(valueA1, valueA2)
+ .addEqualityGroup(valueB1, valueB2)
+ .testEquals();
+ }
+
+ public void testIsDirectory() throws Exception {
+ assertFalse(valueForPath(file("a")).isDirectory());
+ assertFalse(valueForPath(path("nonexistent")).isDirectory());
+ assertTrue(valueForPath(directory("dir")).isDirectory());
+
+ assertFalse(valueForPath(symlink("sa", "a")).isDirectory());
+ assertFalse(valueForPath(symlink("smissing", "missing")).isDirectory());
+ assertTrue(valueForPath(symlink("sdir", "dir")).isDirectory());
+ assertTrue(valueForPath(symlink("ssdir", "sdir")).isDirectory());
+ }
+
+ public void testIsFile() throws Exception {
+ assertTrue(valueForPath(file("a")).isFile());
+ assertFalse(valueForPath(path("nonexistent")).isFile());
+ assertFalse(valueForPath(directory("dir")).isFile());
+
+ assertTrue(valueForPath(symlink("sa", "a")).isFile());
+ assertFalse(valueForPath(symlink("smissing", "missing")).isFile());
+ assertFalse(valueForPath(symlink("sdir", "dir")).isFile());
+ assertTrue(valueForPath(symlink("ssfile", "sa")).isFile());
+ }
+
+ public void testSimpleIndependentFiles() throws Exception {
+ file("a");
+ file("b");
+
+ Set<RootedPath> seenFiles = Sets.newHashSet();
+ seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("a", false, "b"));
+ seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("b", false, "a"));
+ assertThat(seenFiles).containsExactly(rootedPath("a"), rootedPath("b"), rootedPath(""));
+ }
+
+ public void testSimpleSymlink() throws Exception {
+ symlink("a", "b");
+ file("b");
+
+ assertValueChangesIfContentsOfFileChanges("a", false, "b");
+ assertValueChangesIfContentsOfFileChanges("b", true, "a");
+ }
+
+ public void testTransitiveSymlink() throws Exception {
+ symlink("a", "b");
+ symlink("b", "c");
+ file("c");
+
+ assertValueChangesIfContentsOfFileChanges("a", false, "b");
+ assertValueChangesIfContentsOfFileChanges("a", false, "c");
+ assertValueChangesIfContentsOfFileChanges("b", true, "a");
+ assertValueChangesIfContentsOfFileChanges("c", true, "b");
+ assertValueChangesIfContentsOfFileChanges("c", true, "a");
+ }
+
+ public void testFileUnderDirectorySymlink() throws Exception {
+ symlink("a", "b/c");
+ symlink("b", "d");
+ assertValueChangesIfContentsOfDirectoryChanges("b", true, "a/e");
+ }
+
+ public void testSymlinkInDirectory() throws Exception {
+ symlink("a/aa", "ab");
+ file("a/ab");
+
+ assertValueChangesIfContentsOfFileChanges("a/aa", false, "a/ab");
+ assertValueChangesIfContentsOfFileChanges("a/ab", true, "a/aa");
+ }
+
+ public void testRelativeSymlink() throws Exception {
+ symlink("a/aa/aaa", "../ab/aba");
+ file("a/ab/aba");
+ assertValueChangesIfContentsOfFileChanges("a/ab/aba", true, "a/aa/aaa");
+ }
+
+ public void testDoubleRelativeSymlink() throws Exception {
+ symlink("a/b/c/d", "../../e/f");
+ file("a/e/f");
+ assertValueChangesIfContentsOfFileChanges("a/e/f", true, "a/b/c/d");
+ }
+
+ public void testExternalRelativeSymlink() throws Exception {
+ symlink("a", "../outside");
+ file("b");
+ file("../outside");
+ Set<RootedPath> seenFiles = Sets.newHashSet();
+ seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("b", false, "a"));
+ seenFiles.addAll(
+ getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("../outside", true, "a"));
+ assertThat(seenFiles)
+ .containsExactly(
+ rootedPath("a"),
+ rootedPath(""),
+ RootedPath.toRootedPath(fs.getRootDirectory(), PathFragment.EMPTY_FRAGMENT),
+ RootedPath.toRootedPath(fs.getRootDirectory(), new PathFragment("outside")));
+ }
+
+ public void testAbsoluteSymlink() throws Exception {
+ symlink("a", "/absolute");
+ file("b");
+ file("/absolute");
+ Set<RootedPath> seenFiles = Sets.newHashSet();
+ seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("b", false, "a"));
+ seenFiles.addAll(
+ getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("/absolute", true, "a"));
+ assertThat(seenFiles)
+ .containsExactly(
+ rootedPath("a"),
+ rootedPath(""),
+ RootedPath.toRootedPath(fs.getRootDirectory(), PathFragment.EMPTY_FRAGMENT),
+ RootedPath.toRootedPath(fs.getRootDirectory(), new PathFragment("absolute")));
+ }
+
+ public void testSymlinkAsAncestor() throws Exception {
+ file("a/b/c/d");
+ symlink("f", "a/b/c");
+ assertValueChangesIfContentsOfFileChanges("a/b/c/d", true, "f/d");
+ }
+
+ public void testSymlinkAsAncestorNested() throws Exception {
+ file("a/b/c/d");
+ symlink("f", "a/b");
+ assertValueChangesIfContentsOfFileChanges("a/b/c/d", true, "f/c/d");
+ }
+
+ public void testTwoSymlinksInAncestors() throws Exception {
+ file("a/aa/aaa/aaaa");
+ symlink("b/ba/baa", "../../a/aa");
+ symlink("c/ca", "../b/ba");
+
+ assertValueChangesIfContentsOfFileChanges("c/ca", true, "c/ca/baa/aaa/aaaa");
+ assertValueChangesIfContentsOfFileChanges("b/ba/baa", true, "c/ca/baa/aaa/aaaa");
+ assertValueChangesIfContentsOfFileChanges("a/aa/aaa/aaaa", true, "c/ca/baa/aaa/aaaa");
+ }
+
+ public void testSelfReferencingSymlink() throws Exception {
+ symlink("a", "a");
+ assertError("a");
+ }
+
+ public void testMutuallyReferencingSymlinks() throws Exception {
+ symlink("a", "b");
+ symlink("b", "a");
+ assertError("a");
+ }
+
+ public void testRecursiveNestingSymlink() throws Exception {
+ symlink("a/a", "../a");
+ assertError("a/a/b");
+ }
+
+ public void testBrokenSymlink() throws Exception {
+ symlink("a", "b");
+ Set<RootedPath> seenFiles = Sets.newHashSet();
+ seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("b", true, "a"));
+ seenFiles.addAll(getFilesSeenAndAssertValueChangesIfContentsOfFileChanges("a", false, "b"));
+ assertThat(seenFiles).containsExactly(rootedPath("a"), rootedPath("b"), rootedPath(""));
+ }
+
+ public void testBrokenDirectorySymlink() throws Exception {
+ symlink("a", "b");
+ file("c");
+
+ assertValueChangesIfContentsOfDirectoryChanges("a", true, "a/aa");
+ // This just creates the directory "b", which doesn't change the value for "a/aa", since "a/aa"
+ // still has real path "b/aa" and still doesn't exist.
+ assertValueChangesIfContentsOfDirectoryChanges("b", false, "a/aa");
+ assertValueChangesIfContentsOfFileChanges("c", false, "a/aa");
+ }
+
+ public void testTraverseIntoVirtualNonDirectory() throws Exception {
+ file("dir/a");
+ symlink("vdir", "dir");
+ // The following evaluation should not throw IOExceptions.
+ assertNoError("vdir/a/aa/aaa");
+ }
+
+ public void testFileCreation() throws Exception {
+ FileValue a = valueForPath(path("file"));
+ Path p = file("file");
+ FileValue b = valueForPath(p);
+ assertFalse(a.equals(b));
+ }
+
+ public void testEmptyFile() throws Exception {
+ final byte[] digest = new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+ createFsAndRoot(
+ new CustomInMemoryFs(manualClock) {
+ @Override
+ protected String getFastDigestFunctionType(Path path) {
+ return "magic";
+ }
+
+ @Override
+ protected byte[] getFastDigest(Path path) throws IOException {
+ return digest;
+ }
+ });
+ Path p = file("file");
+ p.setLastModifiedTime(0L);
+ FileValue a = valueForPath(p);
+ p.setLastModifiedTime(1L);
+ assertThat(valueForPath(p)).isNotEqualTo(a);
+ p.setLastModifiedTime(0L);
+ assertEquals(a, valueForPath(p));
+ FileSystemUtils.writeContentAsLatin1(p, "content");
+ // Same digest, but now non-empty.
+ assertThat(valueForPath(p)).isNotEqualTo(a);
+ }
+
+ public void testFileModificationModTime() throws Exception {
+ fastMd5 = false;
+ Path p = file("file");
+ FileValue a = valueForPath(p);
+ p.setLastModifiedTime(42);
+ FileValue b = valueForPath(p);
+ assertFalse(a.equals(b));
+ }
+
+ public void testFileModificationDigest() throws Exception {
+ fastMd5 = true;
+ Path p = file("file");
+ FileValue a = valueForPath(p);
+ FileSystemUtils.writeContentAsLatin1(p, "goop");
+ FileValue b = valueForPath(p);
+ assertFalse(a.equals(b));
+ }
+
+ public void testModTimeVsDigest() throws Exception {
+ Path p = file("somefile", "fizzley");
+
+ fastMd5 = true;
+ FileValue aMd5 = valueForPath(p);
+ fastMd5 = false;
+ FileValue aModTime = valueForPath(p);
+ assertThat(aModTime).isNotEqualTo(aMd5);
+ new EqualsTester().addEqualityGroup(aMd5).addEqualityGroup(aModTime).testEquals();
+ }
+
+ public void testFileDeletion() throws Exception {
+ Path p = file("file");
+ FileValue a = valueForPath(p);
+ p.delete();
+ FileValue b = valueForPath(p);
+ assertFalse(a.equals(b));
+ }
+
+ public void testFileTypeChange() throws Exception {
+ Path p = file("file");
+ FileValue a = valueForPath(p);
+ p.delete();
+ p = symlink("file", "foo");
+ FileValue b = valueForPath(p);
+ p.delete();
+ FileSystemUtils.createDirectoryAndParents(pkgRoot.getRelative("file"));
+ FileValue c = valueForPath(p);
+ assertFalse(a.equals(b));
+ assertFalse(b.equals(c));
+ assertFalse(a.equals(c));
+ }
+
+ public void testSymlinkTargetChange() throws Exception {
+ Path p = symlink("symlink", "foo");
+ FileValue a = valueForPath(p);
+ p.delete();
+ p = symlink("symlink", "bar");
+ FileValue b = valueForPath(p);
+ assertThat(b).isNotEqualTo(a);
+ }
+
+ public void testSymlinkTargetContentsChangeModTime() throws Exception {
+ fastMd5 = false;
+ Path fooPath = file("foo");
+ FileSystemUtils.writeContentAsLatin1(fooPath, "foo");
+ Path p = symlink("symlink", "foo");
+ FileValue a = valueForPath(p);
+ fooPath.setLastModifiedTime(88);
+ FileValue b = valueForPath(p);
+ assertThat(b).isNotEqualTo(a);
+ }
+
+ public void testSymlinkTargetContentsChangeDigest() throws Exception {
+ fastMd5 = true;
+ Path fooPath = file("foo");
+ FileSystemUtils.writeContentAsLatin1(fooPath, "foo");
+ Path p = symlink("symlink", "foo");
+ FileValue a = valueForPath(p);
+ FileSystemUtils.writeContentAsLatin1(fooPath, "bar");
+ FileValue b = valueForPath(p);
+ assertThat(b).isNotEqualTo(a);
+ }
+
+ public void testRealPath() throws Exception {
+ file("file");
+ directory("directory");
+ file("directory/file");
+ symlink("directory/link", "file");
+ symlink("directory/doublelink", "link");
+ symlink("directory/parentlink", "../file");
+ symlink("directory/doubleparentlink", "../link");
+ symlink("link", "file");
+ symlink("deadlink", "missing_file");
+ symlink("dirlink", "directory");
+ symlink("doublelink", "link");
+ symlink("doubledirlink", "dirlink");
+
+ checkRealPath("file");
+ checkRealPath("link");
+ checkRealPath("doublelink");
+
+ for (String dir : new String[] {"directory", "dirlink", "doubledirlink"}) {
+ checkRealPath(dir);
+ checkRealPath(dir + "/file");
+ checkRealPath(dir + "/link");
+ checkRealPath(dir + "/doublelink");
+ checkRealPath(dir + "/parentlink");
+ }
+
+ assertRealPath("missing", "missing");
+ assertRealPath("deadlink", "missing_file");
+ }
+
+ public void testRealPathRelativeSymlink() throws Exception {
+ directory("dir");
+ symlink("dir/link", "../dir2");
+ directory("dir2");
+ symlink("dir2/filelink", "../dest");
+ file("dest");
+
+ checkRealPath("dir/link/filelink");
+ }
+
+ public void testSymlinkAcrossPackageRoots() throws Exception {
+ Path otherPkgRoot = fs.getRootDirectory().getRelative("other_root");
+ pkgLocator = new PathPackageLocator(pkgRoot, otherPkgRoot);
+ symlink("a", "/other_root/b");
+ assertValueChangesIfContentsOfFileChanges("/other_root/b", true, "a");
+ }
+
+ public void testFilesOutsideRootHasDepOnBuildID() throws Exception {
+ Path file = file("/outsideroot");
+ SequentialBuildDriver driver = makeDriver();
+ SkyKey key = skyKey("/outsideroot");
+ EvaluationResult<SkyValue> result;
+ result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ if (result.hasError()) {
+ fail(String.format("Evaluation error for %s: %s", key, result.getError()));
+ }
+ FileValue oldValue = (FileValue) result.get(key);
+ assertTrue(oldValue.exists());
+
+ file.delete();
+ result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ if (result.hasError()) {
+ fail(String.format("Evaluation error for %s: %s", key, result.getError()));
+ }
+ FileValue newValue = (FileValue) result.get(key);
+ assertSame(oldValue, newValue);
+
+ PrecomputedValue.BUILD_ID.set(differencer, UUID.randomUUID());
+ result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ if (result.hasError()) {
+ fail(String.format("Evaluation error for %s: %s", key, result.getError()));
+ }
+ newValue = (FileValue) result.get(key);
+ assertNotSame(oldValue, newValue);
+ assertFalse(newValue.exists());
+ }
+
+ public void testFilesOutsideRootWhenExternalDisallowed() throws Exception {
+ file("/outsideroot");
+
+ SequentialBuildDriver driver = makeDriver(/*errorOnExternalFiles=*/ true);
+ SkyKey key = skyKey("/outsideroot");
+ EvaluationResult<SkyValue> result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+
+ assertTrue(result.hasError());
+ assertThat(result.getError(key).getException())
+ .isInstanceOf(FileOutsidePackageRootsException.class);
+ }
+
+ public void testAbsoluteSymlinksToFilesOutsideRootWhenExternalDisallowed() throws Exception {
+ file("/outsideroot");
+ symlink("a", "/outsideroot");
+
+ SequentialBuildDriver driver = makeDriver(/*errorOnExternalFiles=*/ true);
+ SkyKey key = skyKey("a");
+ EvaluationResult<SkyValue> result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+
+ assertTrue(result.hasError());
+ assertThat(result.getError(key).getException())
+ .isInstanceOf(FileOutsidePackageRootsException.class);
+ }
+
+ public void testRelativeSymlinksToFilesOutsideRootWhenExternalDisallowed() throws Exception {
+ file("../outsideroot");
+ symlink("a", "../outsideroot");
+ SequentialBuildDriver driver = makeDriver(/*errorOnExternalFiles=*/ true);
+ SkyKey key = skyKey("a");
+ EvaluationResult<SkyValue> result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ assertTrue(result.hasError());
+ assertThat(result.getError(key).getException())
+ .isInstanceOf(FileOutsidePackageRootsException.class);
+ }
+
+ public void testAbsoluteSymlinksBackIntoSourcesOkWhenExternalDisallowed() throws Exception {
+ Path file = file("insideroot");
+ symlink("a", file.getPathString());
+
+ SequentialBuildDriver driver = makeDriver(/*allowExternalReferences=*/ false);
+ SkyKey key = skyKey("a");
+ EvaluationResult<SkyValue> result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+
+ assertFalse(result.hasError());
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static Set<RootedPath> filesSeen(MemoizingEvaluator graph) {
+ return ImmutableSet.copyOf(
+ (Iterable<RootedPath>)
+ (Iterable)
+ Iterables.transform(
+ Iterables.filter(
+ graph.getValues().keySet(),
+ SkyFunctionName.functionIs(SkyFunctions.FILE_STATE)),
+ SkyKey.NODE_NAME));
+ }
+
+ public void testSize() throws Exception {
+ Path file = file("file");
+ int fileSize = 20;
+ FileSystemUtils.writeContentAsLatin1(file, Strings.repeat("a", fileSize));
+ assertEquals(fileSize, valueForPath(file).getSize());
+ Path dir = directory("directory");
+ file(dir.getChild("child").getPathString());
+ try {
+ valueForPath(dir).getSize();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected.
+ }
+ Path nonexistent = fs.getPath("/root/noexist");
+ try {
+ valueForPath(nonexistent).getSize();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected.
+ }
+ Path symlink = symlink("link", "/root/file");
+ // Symlink stores size of target, not link.
+ assertEquals(fileSize, valueForPath(symlink).getSize());
+ assertTrue(symlink.delete());
+ symlink = symlink("link", "/root/directory");
+ try {
+ valueForPath(symlink).getSize();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected.
+ }
+ assertTrue(symlink.delete());
+ symlink = symlink("link", "/root/noexist");
+ try {
+ valueForPath(symlink).getSize();
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected.
+ }
+ }
+
+ public void testDigest() throws Exception {
+ final AtomicInteger digestCalls = new AtomicInteger(0);
+ int expectedCalls = 0;
+ fs =
+ new CustomInMemoryFs(manualClock) {
+ @Override
+ protected byte[] getMD5Digest(Path path) throws IOException {
+ digestCalls.incrementAndGet();
+ return super.getMD5Digest(path);
+ }
+ };
+ pkgRoot = fs.getRootDirectory().getRelative("root");
+ Path file = file("file");
+ FileSystemUtils.writeContentAsLatin1(file, Strings.repeat("a", 20));
+ byte[] digest = file.getMD5Digest();
+ expectedCalls++;
+ assertEquals(expectedCalls, digestCalls.get());
+ FileValue value = valueForPath(file);
+ expectedCalls++;
+ assertEquals(expectedCalls, digestCalls.get());
+ assertArrayEquals(digest, value.getDigest());
+ // Digest is cached -- no filesystem access.
+ assertEquals(expectedCalls, digestCalls.get());
+ fastMd5 = false;
+ digestCalls.set(0);
+ value = valueForPath(file);
+ // No new digest calls.
+ assertEquals(0, digestCalls.get());
+ assertNull(value.getDigest());
+ assertEquals(0, digestCalls.get());
+ fastMd5 = true;
+ Path dir = directory("directory");
+ try {
+ assertNull(valueForPath(dir).getDigest());
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected.
+ }
+ assertEquals(0, digestCalls.get()); // No digest calls made for directory.
+ Path nonexistent = fs.getPath("/root/noexist");
+ try {
+ assertNull(valueForPath(nonexistent).getDigest());
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected.
+ }
+ assertEquals(0, digestCalls.get()); // No digest calls made for nonexistent file.
+ Path symlink = symlink("link", "/root/file");
+ value = valueForPath(symlink);
+ assertEquals(1, digestCalls.get());
+ // Symlink stores digest of target, not link.
+ assertArrayEquals(digest, value.getDigest());
+ assertEquals(1, digestCalls.get());
+ digestCalls.set(0);
+ assertTrue(symlink.delete());
+ symlink = symlink("link", "/root/directory");
+ // Symlink stores digest of target, not link, for directories too.
+ try {
+ assertNull(valueForPath(symlink).getDigest());
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected.
+ }
+ assertEquals(0, digestCalls.get());
+ }
+
+ public void testFilesystemInconsistencies_ParentDoesntExistAndChildIsSymlink() throws Exception {
+ symlink("a/b", "doesntmatter");
+ // Our custom filesystem says "a/b" exists but "a" does not exist.
+ fs.stubStat(path("a"), null);
+ SequentialBuildDriver driver = makeDriver();
+ SkyKey skyKey = skyKey("a/b");
+ EvaluationResult<FileValue> result =
+ driver.evaluate(
+ ImmutableList.of(skyKey), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ assertTrue(result.hasError());
+ ErrorInfo errorInfo = result.getError(skyKey);
+ assertThat(errorInfo.getException()).isInstanceOf(InconsistentFilesystemException.class);
+ assertThat(errorInfo.getException().getMessage())
+ .contains(
+ "/root/a/b was a symlink to doesntmatter but others made us think it was a "
+ + "nonexistent path");
+ }
+
+ public void testFilesystemInconsistencies_ParentIsntADirectory() throws Exception {
+ file("a/b");
+ // Our custom filesystem says "a/b" exists but its parent "a" is a file.
+ FileStatus inconsistentParentFileStatus =
+ new FileStatus() {
+ @Override
+ public boolean isFile() {
+ return true;
+ }
+
+ @Override
+ public boolean isSpecialFile() {
+ return false;
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return false;
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ return false;
+ }
+
+ @Override
+ public long getSize() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public long getLastModifiedTime() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public long getLastChangeTime() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public long getNodeId() throws IOException {
+ return 0;
+ }
+ };
+ fs.stubStat(path("a"), inconsistentParentFileStatus);
+ // Disable fast-path md5 so that we don't try try to md5 the "a" (since it actually physically
+ // is a directory).
+ fastMd5 = false;
+ SequentialBuildDriver driver = makeDriver();
+ SkyKey skyKey = skyKey("a/b");
+ EvaluationResult<FileValue> result =
+ driver.evaluate(
+ ImmutableList.of(skyKey), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ assertTrue(result.hasError());
+ ErrorInfo errorInfo = result.getError(skyKey);
+ assertThat(errorInfo.getException()).isInstanceOf(InconsistentFilesystemException.class);
+ assertThat(errorInfo.getException().getMessage())
+ .contains("file /root/a/b exists but its parent path /root/a isn't an existing directory");
+ }
+
+ public void testFilesystemInconsistencies_GetFastDigest() throws Exception {
+ file("a");
+ // Our custom filesystem says "a/b" exists but "a" does not exist.
+ fs.stubFastDigestError(path("a"), new IOException("nope"));
+ SequentialBuildDriver driver = makeDriver();
+ SkyKey skyKey = skyKey("a");
+ EvaluationResult<FileValue> result =
+ driver.evaluate(
+ ImmutableList.of(skyKey), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ assertTrue(result.hasError());
+ ErrorInfo errorInfo = result.getError(skyKey);
+ assertThat(errorInfo.getException()).isInstanceOf(InconsistentFilesystemException.class);
+ assertThat(errorInfo.getException().getMessage()).contains("encountered error 'nope'");
+ assertThat(errorInfo.getException().getMessage()).contains("/root/a is no longer a file");
+ }
+
+ private void runTestSymlinkCycle(boolean ancestorCycle, boolean startInCycle) throws Exception {
+ symlink("a", "b");
+ symlink("b", "c");
+ symlink("c", "d");
+ symlink("d", "e");
+ symlink("e", "c");
+ // We build multiple keys at once to make sure the cycle is reported exactly once.
+ Map<RootedPath, ImmutableList<RootedPath>> startToCycleMap =
+ ImmutableMap.<RootedPath, ImmutableList<RootedPath>>builder()
+ .put(
+ rootedPath("a"),
+ ImmutableList.of(rootedPath("c"), rootedPath("d"), rootedPath("e")))
+ .put(
+ rootedPath("b"),
+ ImmutableList.of(rootedPath("c"), rootedPath("d"), rootedPath("e")))
+ .put(
+ rootedPath("d"),
+ ImmutableList.<RootedPath>of(rootedPath("d"), rootedPath("e"), rootedPath("c")))
+ .put(
+ rootedPath("e"),
+ ImmutableList.<RootedPath>of(rootedPath("e"), rootedPath("c"), rootedPath("d")))
+ .put(
+ rootedPath("a/some/descendant"),
+ ImmutableList.of(rootedPath("c"), rootedPath("d"), rootedPath("e")))
+ .put(
+ rootedPath("b/some/descendant"),
+ ImmutableList.of(rootedPath("c"), rootedPath("d"), rootedPath("e")))
+ .put(
+ rootedPath("d/some/descendant"),
+ ImmutableList.<RootedPath>of(rootedPath("d"), rootedPath("e"), rootedPath("c")))
+ .put(
+ rootedPath("e/some/descendant"),
+ ImmutableList.<RootedPath>of(rootedPath("e"), rootedPath("c"), rootedPath("d")))
+ .build();
+ Map<RootedPath, ImmutableList<RootedPath>> startToPathToCycleMap =
+ ImmutableMap.<RootedPath, ImmutableList<RootedPath>>builder()
+ .put(rootedPath("a"), ImmutableList.of(rootedPath("a"), rootedPath("b")))
+ .put(rootedPath("b"), ImmutableList.of(rootedPath("b")))
+ .put(rootedPath("d"), ImmutableList.<RootedPath>of())
+ .put(rootedPath("e"), ImmutableList.<RootedPath>of())
+ .put(
+ rootedPath("a/some/descendant"), ImmutableList.of(rootedPath("a"), rootedPath("b")))
+ .put(rootedPath("b/some/descendant"), ImmutableList.of(rootedPath("b")))
+ .put(rootedPath("d/some/descendant"), ImmutableList.<RootedPath>of())
+ .put(rootedPath("e/some/descendant"), ImmutableList.<RootedPath>of())
+ .build();
+ ImmutableList<SkyKey> keys;
+ if (ancestorCycle && startInCycle) {
+ keys = ImmutableList.of(skyKey("d/some/descendant"), skyKey("e/some/descendant"));
+ } else if (ancestorCycle && !startInCycle) {
+ keys = ImmutableList.of(skyKey("a/some/descendant"), skyKey("b/some/descendant"));
+ } else if (!ancestorCycle && startInCycle) {
+ keys = ImmutableList.of(skyKey("d"), skyKey("e"));
+ } else {
+ keys = ImmutableList.of(skyKey("a"), skyKey("b"));
+ }
+ StoredEventHandler eventHandler = new StoredEventHandler();
+ SequentialBuildDriver driver = makeDriver();
+ EvaluationResult<FileValue> result =
+ driver.evaluate(keys, /*keepGoing=*/ true, DEFAULT_THREAD_COUNT, eventHandler);
+ assertTrue(result.hasError());
+ for (SkyKey key : keys) {
+ ErrorInfo errorInfo = result.getError(key);
+ // FileFunction detects symlink cycles explicitly.
+ assertThat(errorInfo.getCycleInfo()).isEmpty();
+ FileSymlinkCycleException fsce = (FileSymlinkCycleException) errorInfo.getException();
+ RootedPath start = (RootedPath) key.argument();
+ assertThat(fsce.getPathToCycle())
+ .containsExactlyElementsIn(startToPathToCycleMap.get(start))
+ .inOrder();
+ assertThat(fsce.getCycle()).containsExactlyElementsIn(startToCycleMap.get(start)).inOrder();
+ }
+ // Check that the unique cycle was reported exactly once.
+ assertThat(eventHandler.getEvents()).hasSize(1);
+ assertThat(Iterables.getOnlyElement(eventHandler.getEvents()).getMessage())
+ .contains("circular symlinks detected");
+ }
+
+ public void testSymlinkCycle_AncestorCycle_StartInCycle() throws Exception {
+ runTestSymlinkCycle(/*ancestorCycle=*/ true, /*startInCycle=*/ true);
+ }
+
+ public void testSymlinkCycle_AncestorCycle_StartOutOfCycle() throws Exception {
+ runTestSymlinkCycle(/*ancestorCycle=*/ true, /*startInCycle=*/ false);
+ }
+
+ public void testSymlinkCycle_RegularCycle_StartInCycle() throws Exception {
+ runTestSymlinkCycle(/*ancestorCycle=*/ false, /*startInCycle=*/ true);
+ }
+
+ public void testSymlinkCycle_RegularCycle_StartOutOfCycle() throws Exception {
+ runTestSymlinkCycle(/*ancestorCycle=*/ false, /*startInCycle=*/ false);
+ }
+
+ public void testSerialization() throws Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(bos);
+
+ FileSystem oldFileSystem = Path.getFileSystemForSerialization();
+ try {
+ FileSystem fs = UnixFileSystem.INSTANCE; // InMemoryFS is not supported for serialization.
+ Path.setFileSystemForSerialization(fs);
+ pkgRoot = fs.getRootDirectory();
+
+ FileValue a = valueForPath(fs.getPath("/"));
+
+ Path tmp = fs.getPath(TestUtils.tmpDirFile().getAbsoluteFile() + "/file.txt");
+
+ FileSystemUtils.writeContentAsLatin1(tmp, "test contents");
+
+ FileValue b = valueForPath(tmp);
+ Preconditions.checkState(b.isFile());
+ FileValue c = valueForPath(fs.getPath("/does/not/exist"));
+ oos.writeObject(a);
+ oos.writeObject(b);
+ oos.writeObject(c);
+
+ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+ ObjectInputStream ois = new ObjectInputStream(bis);
+
+ FileValue a2 = (FileValue) ois.readObject();
+ FileValue b2 = (FileValue) ois.readObject();
+ FileValue c2 = (FileValue) ois.readObject();
+
+ assertEquals(a, a2);
+ assertEquals(b, b2);
+ assertEquals(c, c2);
+ assertFalse(a2.equals(b2));
+ } finally {
+ Path.setFileSystemForSerialization(oldFileSystem);
+ }
+ }
+
+ public void testFileStateEquality() throws Exception {
+ file("a");
+ symlink("b1", "a");
+ symlink("b2", "a");
+ symlink("b3", "zzz");
+ directory("d1");
+ directory("d2");
+ SkyKey file = fileStateSkyKey("a");
+ SkyKey symlink1 = fileStateSkyKey("b1");
+ SkyKey symlink2 = fileStateSkyKey("b2");
+ SkyKey symlink3 = fileStateSkyKey("b3");
+ SkyKey missing1 = fileStateSkyKey("c1");
+ SkyKey missing2 = fileStateSkyKey("c2");
+ SkyKey directory1 = fileStateSkyKey("d1");
+ SkyKey directory2 = fileStateSkyKey("d2");
+ ImmutableList<SkyKey> keys =
+ ImmutableList.of(
+ file, symlink1, symlink2, symlink3, missing1, missing2, directory1, directory2);
+
+ SequentialBuildDriver driver = makeDriver();
+ EvaluationResult<SkyValue> result =
+ driver.evaluate(keys, false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+
+ new EqualsTester()
+ .addEqualityGroup(result.get(file))
+ .addEqualityGroup(result.get(symlink1), result.get(symlink2))
+ .addEqualityGroup(result.get(symlink3))
+ .addEqualityGroup(result.get(missing1), result.get(missing2))
+ .addEqualityGroup(result.get(directory1), result.get(directory2))
+ .testEquals();
+ }
+
+ public void testSymlinkToPackagePathBoundary() throws Exception {
+ Path path = path("this/is/a/path");
+ FileSystemUtils.ensureSymbolicLink(path, pkgRoot);
+ assertError("this/is/a/path");
+ }
+
+ private void runTestInfiniteSymlinkExpansion(boolean symlinkToAncestor, boolean absoluteSymlink)
+ throws Exception {
+ Path otherPath = path("other");
+ RootedPath otherRootedPath = RootedPath.toRootedPath(pkgRoot, otherPath.relativeTo(pkgRoot));
+ Path ancestorPath = path("a");
+ RootedPath ancestorRootedPath =
+ RootedPath.toRootedPath(pkgRoot, ancestorPath.relativeTo(pkgRoot));
+ FileSystemUtils.ensureSymbolicLink(otherPath, ancestorPath);
+ Path intermediatePath = path("inter");
+ RootedPath intermediateRootedPath =
+ RootedPath.toRootedPath(pkgRoot, intermediatePath.relativeTo(pkgRoot));
+ Path descendantPath = path("a/b/c/d/e");
+ RootedPath descendantRootedPath =
+ RootedPath.toRootedPath(pkgRoot, descendantPath.relativeTo(pkgRoot));
+ if (symlinkToAncestor) {
+ FileSystemUtils.ensureSymbolicLink(descendantPath, intermediatePath);
+ if (absoluteSymlink) {
+ FileSystemUtils.ensureSymbolicLink(intermediatePath, ancestorPath);
+ } else {
+ FileSystemUtils.ensureSymbolicLink(intermediatePath, ancestorRootedPath.getRelativePath());
+ }
+ } else {
+ FileSystemUtils.ensureSymbolicLink(ancestorPath, intermediatePath);
+ if (absoluteSymlink) {
+ FileSystemUtils.ensureSymbolicLink(intermediatePath, descendantPath);
+ } else {
+ FileSystemUtils.ensureSymbolicLink(
+ intermediatePath, descendantRootedPath.getRelativePath());
+ }
+ }
+ StoredEventHandler eventHandler = new StoredEventHandler();
+ SequentialBuildDriver driver = makeDriver();
+ SkyKey ancestorPathKey = FileValue.key(ancestorRootedPath);
+ SkyKey descendantPathKey = FileValue.key(descendantRootedPath);
+ SkyKey otherPathKey = FileValue.key(otherRootedPath);
+ ImmutableList<SkyKey> keys;
+ ImmutableList<SkyKey> errorKeys;
+ ImmutableList<RootedPath> expectedChain;
+ if (symlinkToAncestor) {
+ keys = ImmutableList.of(descendantPathKey, otherPathKey);
+ errorKeys = ImmutableList.of(descendantPathKey);
+ expectedChain =
+ ImmutableList.of(descendantRootedPath, intermediateRootedPath, ancestorRootedPath);
+ } else {
+ keys = ImmutableList.of(ancestorPathKey, otherPathKey);
+ errorKeys = keys;
+ expectedChain =
+ ImmutableList.of(ancestorRootedPath, intermediateRootedPath, descendantRootedPath);
+ }
+ EvaluationResult<FileValue> result =
+ driver.evaluate(keys, /*keepGoing=*/ true, DEFAULT_THREAD_COUNT, eventHandler);
+ assertTrue(result.hasError());
+ for (SkyKey key : errorKeys) {
+ ErrorInfo errorInfo = result.getError(key);
+ // FileFunction detects infinite symlink expansion explicitly.
+ assertThat(errorInfo.getCycleInfo()).isEmpty();
+ FileSymlinkInfiniteExpansionException fsiee =
+ (FileSymlinkInfiniteExpansionException) errorInfo.getException();
+ assertThat(fsiee.getMessage()).contains("Infinite symlink expansion");
+ assertThat(fsiee.getChain()).containsExactlyElementsIn(expectedChain).inOrder();
+ }
+ // Check that the unique symlink expansion error was reported exactly once.
+ assertThat(eventHandler.getEvents()).hasSize(1);
+ assertThat(Iterables.getOnlyElement(eventHandler.getEvents()).getMessage())
+ .contains("infinite symlink expansion detected");
+ }
+
+ public void testInfiniteSymlinkExpansion_AbsoluteSymlinkToDescendant() throws Exception {
+ runTestInfiniteSymlinkExpansion(/*ancestor=*/ false, /*absoluteSymlink=*/ true);
+ }
+
+ public void testInfiniteSymlinkExpansion_RelativeSymlinkToDescendant() throws Exception {
+ runTestInfiniteSymlinkExpansion(/*ancestor=*/ false, /*absoluteSymlink=*/ false);
+ }
+
+ public void testInfiniteSymlinkExpansion_AbsoluteSymlinkToAncestor() throws Exception {
+ runTestInfiniteSymlinkExpansion(/*ancestor=*/ true, /*absoluteSymlink=*/ true);
+ }
+
+ public void testInfiniteSymlinkExpansion_RelativeSymlinkToAncestor() throws Exception {
+ runTestInfiniteSymlinkExpansion(/*ancestor=*/ true, /*absoluteSymlink=*/ false);
+ }
+
+ public void testChildOfNonexistentParent() throws Exception {
+ Path ancestor = directory("this/is/an/ancestor");
+ Path parent = ancestor.getChild("parent");
+ Path child = parent.getChild("child");
+ assertFalse(valueForPath(parent).exists());
+ assertFalse(valueForPath(child).exists());
+ }
+
+ private void checkRealPath(String pathString) throws Exception {
+ Path realPath = pkgRoot.getRelative(pathString).resolveSymbolicLinks();
+ assertRealPath(pathString, realPath.relativeTo(pkgRoot).toString());
+ }
+
+ private void assertRealPath(String pathString, String expectedRealPathString) throws Exception {
+ SequentialBuildDriver driver = makeDriver();
+ SkyKey key = skyKey(pathString);
+ EvaluationResult<SkyValue> result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ if (result.hasError()) {
+ fail(String.format("Evaluation error for %s: %s", key, result.getError()));
+ }
+ FileValue fileValue = (FileValue) result.get(key);
+ assertEquals(
+ pkgRoot.getRelative(expectedRealPathString).toString(),
+ fileValue.realRootedPath().asPath().toString());
+ }
+
+ /**
+ * Returns a callback that, when executed, deletes the given path.
+ * Not meant to be called directly by tests.
+ */
+ private Runnable makeDeletePathCallback(final Path toDelete) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ try {
+ toDelete.delete();
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a callback that, when executed, writes the given bytes to the given file path.
+ * Not meant to be called directly by tests.
+ */
+ private Runnable makeWriteFileContentCallback(final Path toChange, final byte[] contents) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ OutputStream outputStream;
+ try {
+ outputStream = toChange.getOutputStream();
+ outputStream.write(contents);
+ outputStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a callback that, when executed, creates the given directory path.
+ * Not meant to be called directly by tests.
+ */
+ private Runnable makeCreateDirectoryCallback(final Path toCreate) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ try {
+ toCreate.createDirectory();
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a callback that, when executed, makes {@code toLink} a symlink to {@code toTarget}.
+ * Not meant to be called directly by tests.
+ */
+ private Runnable makeSymlinkCallback(final Path toLink, final PathFragment toTarget) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ try {
+ FileSystemUtils.ensureSymbolicLink(toLink, toTarget);
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns the files that would be changed/created if {@code path} were to be changed/created.
+ */
+ private ImmutableList<String> filesTouchedIfTouched(Path path) {
+ List<String> filesToBeTouched = Lists.newArrayList();
+ do {
+ filesToBeTouched.add(path.getPathString());
+ path = path.getParentDirectory();
+ } while (!path.exists());
+ return ImmutableList.copyOf(filesToBeTouched);
+ }
+
+ /**
+ * Changes the contents of the FileValue for the given file in some way e.g.
+ * <ul>
+ * <li> If it's a regular file, the contents will be changed.
+ * <li> If it's a non-existent file, it will be created.
+ * <ul>
+ * and then returns the file(s) changed paired with a callback to undo the change. Not meant to
+ * be called directly by tests.
+ */
+ private Pair<ImmutableList<String>, Runnable> changeFile(String fileStringToChange)
+ throws Exception {
+ Path fileToChange = path(fileStringToChange);
+ if (fileToChange.exists()) {
+ final byte[] oldContents = FileSystemUtils.readContent(fileToChange);
+ OutputStream outputStream = fileToChange.getOutputStream(/*append=*/ true);
+ outputStream.write(new byte[] {(byte) 42}, 0, 1);
+ outputStream.close();
+ return Pair.of(
+ ImmutableList.of(fileStringToChange),
+ makeWriteFileContentCallback(fileToChange, oldContents));
+ } else {
+ ImmutableList<String> filesTouched = filesTouchedIfTouched(fileToChange);
+ file(fileStringToChange, "new stuff");
+ return Pair.of(ImmutableList.copyOf(filesTouched), makeDeletePathCallback(fileToChange));
+ }
+ }
+
+ /**
+ * Changes the contents of the FileValue for the given directory in some way e.g.
+ * <ul>
+ * <li> If it exists, the directory will be deleted.
+ * <li> If it doesn't exist, the directory will be created.
+ * <ul>
+ * and then returns the file(s) changed paired with a callback to undo the change. Not meant to
+ * be called directly by tests.
+ */
+ private Pair<ImmutableList<String>, Runnable> changeDirectory(String directoryStringToChange)
+ throws Exception {
+ final Path directoryToChange = path(directoryStringToChange);
+ if (directoryToChange.exists()) {
+ directoryToChange.delete();
+ return Pair.of(
+ ImmutableList.of(directoryStringToChange),
+ makeCreateDirectoryCallback(directoryToChange));
+ } else {
+ directoryToChange.createDirectory();
+ return Pair.of(
+ ImmutableList.of(directoryStringToChange), makeDeletePathCallback(directoryToChange));
+ }
+ }
+
+ /**
+ * Performs filesystem operations to change the file or directory denoted by
+ * {@code changedPathString} and returns the file(s) changed paired with a callback to undo the
+ * change.
+ * Not meant to be called directly by tests.
+ *
+ * @param isSupposedToBeFile whether the path denoted by the given string is supposed to be a
+ * file or a directory. This is needed is the path doesn't exist yet,
+ * and so the filesystem doesn't know.
+ */
+ private Pair<ImmutableList<String>, Runnable> change(
+ String changedPathString, boolean isSupposedToBeFile) throws Exception {
+ final Path changedPath = path(changedPathString);
+ if (changedPath.isSymbolicLink()) {
+ ImmutableList<String> filesTouched = filesTouchedIfTouched(changedPath);
+ PathFragment oldTarget = changedPath.readSymbolicLink();
+ FileSystemUtils.ensureSymbolicLink(changedPath, oldTarget.getChild("__different_target__"));
+ return Pair.of(filesTouched, makeSymlinkCallback(changedPath, oldTarget));
+ } else if (isSupposedToBeFile) {
+ return changeFile(changedPathString);
+ } else {
+ return changeDirectory(changedPathString);
+ }
+ }
+
+ /**
+ * Asserts that if the contents of {@code changedPathString} changes, then the FileValue
+ * corresponding to {@code pathString} will change. Not meant to be called directly by tests.
+ */
+ private void assertValueChangesIfContentsOfFileChanges(
+ String changedPathString, boolean changes, String pathString) throws Exception {
+ getFilesSeenAndAssertValueChangesIfContentsOfFileChanges(
+ changedPathString, changes, pathString);
+ }
+
+ /**
+ * Asserts that if the contents of {@code changedPathString} changes, then the FileValue
+ * corresponding to {@code pathString} will change. Returns the paths of all files seen.
+ */
+ private Set<RootedPath> getFilesSeenAndAssertValueChangesIfContentsOfFileChanges(
+ String changedPathString, boolean changes, String pathString) throws Exception {
+ return assertChangesIfChanges(changedPathString, true, changes, pathString);
+ }
+
+ /**
+ * Asserts that if the directory {@code changedPathString} changes, then the FileValue
+ * corresponding to {@code pathString} will change. Returns the paths of all files seen.
+ */
+ private Set<RootedPath> assertValueChangesIfContentsOfDirectoryChanges(
+ String changedPathString, boolean changes, String pathString) throws Exception {
+ return assertChangesIfChanges(changedPathString, false, changes, pathString);
+ }
+
+ /**
+ * Asserts that if the contents of {@code changedPathString} changes, then the FileValue
+ * corresponding to {@code pathString} will change. Returns the paths of all files seen.
+ * Not meant to be called directly by tests.
+ */
+ private Set<RootedPath> assertChangesIfChanges(
+ String changedPathString, boolean isFile, boolean changes, String pathString)
+ throws Exception {
+ SequentialBuildDriver driver = makeDriver();
+ SkyKey key = skyKey(pathString);
+ EvaluationResult<SkyValue> result;
+ result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ if (result.hasError()) {
+ fail(String.format("Evaluation error for %s: %s", key, result.getError()));
+ }
+ SkyValue oldValue = result.get(key);
+
+ Pair<ImmutableList<String>, Runnable> changeResult = change(changedPathString, isFile);
+ ImmutableList<String> changedPathStrings = changeResult.first;
+ Runnable undoCallback = changeResult.second;
+ differencer.invalidate(
+ Iterables.transform(
+ changedPathStrings,
+ new Function<String, SkyKey>() {
+ @Override
+ public SkyKey apply(String input) {
+ return fileStateSkyKey(input);
+ }
+ }));
+
+ result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ if (result.hasError()) {
+ fail(String.format("Evaluation error for %s: %s", key, result.getError()));
+ }
+
+ SkyValue newValue = result.get(key);
+ assertTrue(
+ String.format(
+ "Changing the contents of %s %s should%s change the value for file %s.",
+ isFile ? "file" : "directory",
+ changedPathString,
+ changes ? "" : " not",
+ pathString),
+ changes != newValue.equals(oldValue));
+
+ // Restore the original file.
+ undoCallback.run();
+ return filesSeen(driver.getGraphForTesting());
+ }
+
+ /**
+ * Asserts that trying to construct a FileValue for {@code path} succeeds. Returns the paths of
+ * all files seen.
+ */
+ private Set<RootedPath> assertNoError(String pathString) throws Exception {
+ SequentialBuildDriver driver = makeDriver();
+ SkyKey key = skyKey(pathString);
+ EvaluationResult<FileValue> result;
+ result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ assertFalse(
+ "Did not expect error while evaluating " + pathString + ", got " + result.get(key),
+ result.hasError());
+ return filesSeen(driver.getGraphForTesting());
+ }
+
+ /**
+ * Asserts that trying to construct a FileValue for {@code path} fails. Returns the paths of all
+ * files seen.
+ */
+ private Set<RootedPath> assertError(String pathString) throws Exception {
+ SequentialBuildDriver driver = makeDriver();
+ SkyKey key = skyKey(pathString);
+ EvaluationResult<FileValue> result;
+ result =
+ driver.evaluate(
+ ImmutableList.of(key), false, DEFAULT_THREAD_COUNT, NullEventHandler.INSTANCE);
+ assertTrue(
+ "Expected error while evaluating " + pathString + ", got " + result.get(key),
+ result.hasError());
+ assertTrue(
+ !Iterables.isEmpty(result.getError().getCycleInfo())
+ || result.getError().getException() != null);
+ return filesSeen(driver.getGraphForTesting());
+ }
+
+ private Path file(String fileName) throws Exception {
+ Path path = path(fileName);
+ FileSystemUtils.createDirectoryAndParents(path.getParentDirectory());
+ FileSystemUtils.createEmptyFile(path);
+ return path;
+ }
+
+ private Path file(String fileName, String contents) throws Exception {
+ Path path = path(fileName);
+ FileSystemUtils.createDirectoryAndParents(path.getParentDirectory());
+ FileSystemUtils.writeContentAsLatin1(path, contents);
+ return path;
+ }
+
+ private Path directory(String directoryName) throws Exception {
+ Path path = path(directoryName);
+ FileSystemUtils.createDirectoryAndParents(path);
+ return path;
+ }
+
+ private Path symlink(String link, String target) throws Exception {
+ Path path = path(link);
+ FileSystemUtils.createDirectoryAndParents(path.getParentDirectory());
+ path.createSymbolicLink(new PathFragment(target));
+ return path;
+ }
+
+ private Path path(String rootRelativePath) {
+ return pkgRoot.getRelative(new PathFragment(rootRelativePath));
+ }
+
+ private RootedPath rootedPath(String pathString) {
+ Path path = path(pathString);
+ for (Path root : pkgLocator.getPathEntries()) {
+ if (path.startsWith(root)) {
+ return RootedPath.toRootedPath(root, path);
+ }
+ }
+ return RootedPath.toRootedPath(fs.getRootDirectory(), path);
+ }
+
+ private SkyKey skyKey(String pathString) {
+ return FileValue.key(rootedPath(pathString));
+ }
+
+ private SkyKey fileStateSkyKey(String pathString) {
+ return FileStateValue.key(rootedPath(pathString));
+ }
+
+ private class CustomInMemoryFs extends InMemoryFileSystem {
+
+ private Map<Path, FileStatus> stubbedStats = Maps.newHashMap();
+ private Map<Path, IOException> stubbedFastDigestErrors = Maps.newHashMap();
+
+ public CustomInMemoryFs(ManualClock manualClock) {
+ super(manualClock);
+ }
+
+ @Override
+ protected String getFastDigestFunctionType(Path path) {
+ return fastMd5 ? "MD5" : null;
+ }
+
+ public void stubFastDigestError(Path path, IOException error) {
+ stubbedFastDigestErrors.put(path, error);
+ }
+
+ @Override
+ protected byte[] getFastDigest(Path path) throws IOException {
+ if (stubbedFastDigestErrors.containsKey(path)) {
+ throw stubbedFastDigestErrors.get(path);
+ }
+ return fastMd5 ? getMD5Digest(path) : null;
+ }
+
+ public void stubStat(Path path, @Nullable FileStatus stubbedResult) {
+ stubbedStats.put(path, stubbedResult);
+ }
+
+ @Override
+ public FileStatus stat(Path path, boolean followSymlinks) throws IOException {
+ if (stubbedStats.containsKey(path)) {
+ return stubbedStats.get(path);
+ }
+ return super.stat(path, followSymlinks);
+ }
+ }
+}