Update from Google.

--
MOE_MIGRATED_REVID=85702957
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemConcurrencyTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemConcurrencyTest.java
new file mode 100644
index 0000000..c90de18
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemConcurrencyTest.java
@@ -0,0 +1,97 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.vfs.util.FileSystems;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * (Slow) tests of FileSystem under concurrency.
+ *
+ * These tests are nondeterministic but provide good coverage nonetheless.
+ */
+@RunWith(JUnit4.class)
+public class FileSystemConcurrencyTest {
+
+  Path workingDir;
+
+  @Before
+  public void setUp() throws Exception {
+    FileSystem testFS = FileSystems.initDefaultAsNative();
+
+    // Resolve symbolic links in the temp dir:
+    workingDir = testFS.getPath(new File(TestUtils.tmpDir()).getCanonicalPath());
+  }
+
+  @Test
+  public void testConcurrentSymlinkModifications() throws Exception {
+    final Path xFile = workingDir.getRelative("file");
+    FileSystemUtils.createEmptyFile(xFile);
+
+    final Path xLinkToFile = workingDir.getRelative("link");
+
+    // "Boxed" for pass-by-reference.
+    final boolean[] run = { true };
+    final IOException[] exception = { null };
+    Thread createThread = new Thread() {
+      @Override
+      public void run() {
+        while (run[0]) {
+          if (!xLinkToFile.exists()) {
+            try {
+              xLinkToFile.createSymbolicLink(xFile);
+            } catch (IOException e) {
+              exception[0] = e;
+              return;
+            }
+          }
+        }
+      }
+    };
+    Thread deleteThread = new Thread() {
+      @Override
+      public void run() {
+        while (run[0]) {
+          if (xLinkToFile.exists(Symlinks.NOFOLLOW)) {
+            try {
+              xLinkToFile.delete();
+            } catch (IOException e) {
+              exception[0] = e;
+              return;
+            }
+          }
+        }
+      }
+    };
+    createThread.start();
+    deleteThread.start();
+    Thread.sleep(1000);
+    run[0] = false;
+    createThread.join(0);
+    deleteThread.join(0);
+
+    if (exception[0] != null) {
+      throw exception[0];
+    }
+  }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java
new file mode 100644
index 0000000..b6e88d8
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java
@@ -0,0 +1,1356 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Preconditions;
+import com.google.common.io.BaseEncoding;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.util.Fingerprint;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This class handles the generic tests that any filesystem must pass.
+ *
+ * <p>Each filesystem-test should inherit from this class, thereby obtaining
+ * all the tests.
+ */
+public abstract class FileSystemTest {
+
+  private long savedTime;
+  protected FileSystem testFS;
+  protected boolean supportsSymlinks;
+  protected Path workingDir;
+
+  // Some useful examples of various kinds of files (mnemonic: "x" = "eXample")
+  protected Path xNothing;
+  protected Path xFile;
+  protected Path xNonEmptyDirectory;
+  protected Path xNonEmptyDirectoryFoo;
+  protected Path xEmptyDirectory;
+
+  @Before
+  public void setUp() throws Exception {
+    testFS = getFreshFileSystem();
+    workingDir = testFS.getPath(getTestTmpDir());
+    cleanUpWorkingDirectory(workingDir);
+    supportsSymlinks = testFS.supportsSymbolicLinks();
+
+    // % ls -lR
+    // -rw-rw-r-- xFile
+    // drwxrwxr-x xNonEmptyDirectory
+    // -rw-rw-r-- xNonEmptyDirectory/foo
+    // drwxrwxr-x xEmptyDirectory
+
+    xNothing = absolutize("xNothing");
+    xFile = absolutize("xFile");
+    xNonEmptyDirectory = absolutize("xNonEmptyDirectory");
+    xNonEmptyDirectoryFoo = xNonEmptyDirectory.getChild("foo");
+    xEmptyDirectory = absolutize("xEmptyDirectory");
+
+    FileSystemUtils.createEmptyFile(xFile);
+    xNonEmptyDirectory.createDirectory();
+    FileSystemUtils.createEmptyFile(xNonEmptyDirectoryFoo);
+    xEmptyDirectory.createDirectory();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    destroyFileSystem(testFS);
+  }
+
+  /**
+   * Returns an instance of the file system to test.
+   */
+  protected abstract FileSystem getFreshFileSystem() throws IOException;
+
+  protected boolean isSymbolicLink(File file) {
+    return com.google.devtools.build.lib.unix.FilesystemUtils.isSymbolicLink(file);
+  }
+
+  protected void setWritable(File file) throws IOException {
+    com.google.devtools.build.lib.unix.FilesystemUtils.setWritable(file);
+  }
+
+  protected void setExecutable(File file) throws IOException {
+    com.google.devtools.build.lib.unix.FilesystemUtils.setExecutable(file);
+  }
+
+  private static final Pattern STAT_SUBDIR_ERROR = Pattern.compile("(.*) \\(Not a directory\\)");
+
+  // Test that file is not present, using statIfFound. Base implementation throws an exception, but
+  // subclasses may override statIfFound to return null, in which case their tests should override
+  // this method.
+  @SuppressWarnings("unused") // Subclasses may throw.
+  protected void expectNotFound(Path path) throws IOException {
+    try {
+      assertNull(path.statIfFound());
+    } catch (IOException e) {
+      // May be because of a non-directory path component. Parse exception to check this.
+      Matcher matcher = STAT_SUBDIR_ERROR.matcher(e.getMessage());
+      if (!matcher.matches() || !path.getPathString().startsWith(matcher.group(1))) {
+        // Throw if this doesn't match what an ENOTDIR error looks like.
+        throw e;
+      }
+    }
+  }
+
+  /**
+   * Removes all stuff from the test filesystem.
+   */
+  protected void destroyFileSystem(FileSystem fileSystem) throws IOException {
+    Preconditions.checkArgument(fileSystem.equals(workingDir.getFileSystem()));
+    cleanUpWorkingDirectory(workingDir);
+  }
+
+  /**
+   * Cleans up the working directory by removing everything.
+   */
+  protected void cleanUpWorkingDirectory(Path workingPath)
+      throws IOException {
+    if (workingPath.exists()) {
+      removeEntireDirectory(workingPath.getPathFile()); // uses java.io.File!
+    }
+    FileSystemUtils.createDirectoryAndParents(workingPath);
+  }
+
+  /**
+   * This function removes an entire directory and all of its contents.
+   * Much like rm -rf directoryToRemove
+   */
+  protected void removeEntireDirectory(File directoryToRemove)
+      throws IOException {
+    // make sure that we do not remove anything outside the test directory
+    Path testDirPath = testFS.getPath(getTestTmpDir());
+    if (!testFS.getPath(directoryToRemove.getAbsolutePath()).startsWith(testDirPath)) {
+      throw new IOException("trying to remove files outside of the testdata directory");
+    }
+    // Some tests set the directories read-only and/or non-executable, so
+    // override that:
+    setWritable(directoryToRemove);
+    setExecutable(directoryToRemove);
+
+    File[] files = directoryToRemove.listFiles();
+    if (files != null) {
+      for (File currentFile : files) {
+        boolean isSymbolicLink = isSymbolicLink(currentFile);
+        if (!isSymbolicLink && currentFile.isDirectory()) {
+          removeEntireDirectory(currentFile);
+        } else {
+          if (!isSymbolicLink) {
+            setWritable(currentFile);
+          }
+          if (!currentFile.delete()) {
+            throw new IOException("Failed to delete '" + currentFile + "'");
+          }
+        }
+      }
+    }
+    if (!directoryToRemove.delete()) {
+      throw new IOException("Failed to delete '" + directoryToRemove + "'");
+    }
+  }
+
+  /**
+   * Returns the directory to use as the FileSystem's working directory.
+   * Canonicalized to make tests hermetic against symbolic links in TEST_TMPDIR.
+   */
+  protected final String getTestTmpDir() throws IOException {
+    return new File(TestUtils.tmpDir()).getCanonicalPath() + "/testdir";
+  }
+
+  /**
+   * Indirection to create links so we can test FileSystems that do not support
+   * link creation.  For example, JavaFileSystemTest overrides this method
+   * and creates the link with an alternate FileSystem.
+   */
+  protected void createSymbolicLink(Path link, Path target) throws IOException {
+    createSymbolicLink(link, target.asFragment());
+  }
+
+  /**
+   * Indirection to create links so we can test FileSystems that do not support
+   * link creation.  For example, JavaFileSystemTest overrides this method
+   * and creates the link with an alternate FileSystem.
+   */
+  protected void createSymbolicLink(Path link, PathFragment target) throws IOException {
+    link.createSymbolicLink(target);
+  }
+
+  /**
+   * Indirection to setReadOnly(false) on FileSystems that do not
+   * support setReadOnly(false).  For example, JavaFileSystemTest overrides this
+   * method and makes the Path writable with an alternate FileSystem.
+   */
+  protected void makeWritable(Path target) throws IOException {
+    target.setWritable(true);
+  }
+
+  /**
+   * Indirection to {@link Path#setExecutable(boolean)} on FileSystems that do
+   * not support setExecutable.  For example, JavaFileSystemTest overrides this
+   * method and makes the Path executable with an alternate FileSystem.
+   */
+  protected void setExecutable(Path target, boolean mode) throws IOException {
+    target.setExecutable(mode);
+  }
+
+  // TODO(bazel-team): (2011) Put in a setLastModifiedTime into the various objects
+  // and clobber the current time of the object we're currently handling.
+  // Otherwise testing the thing might get a little hard, depending on the clock.
+  void storeReferenceTime(long timeToMark) {
+    savedTime = timeToMark;
+  }
+
+  boolean isLaterThanreferenceTime(long testTime) {
+    return (savedTime <= testTime);
+  }
+
+  Path getTestFile() throws IOException {
+    Path tempPath = absolutize("test-file");
+    FileSystemUtils.createEmptyFile(tempPath);
+    return tempPath;
+  }
+
+  protected Path absolutize(String relativePathName) {
+    return workingDir.getRelative(relativePathName);
+  }
+
+  // Here the tests begin.
+
+  @Test
+  public void testIsFileForNonexistingPath() {
+    Path nonExistingPath = testFS.getPath("/something/strange");
+    assertFalse(nonExistingPath.isFile());
+  }
+
+  @Test
+  public void testIsDirectoryForNonexistingPath() {
+    Path nonExistingPath = testFS.getPath("/something/strange");
+    assertFalse(nonExistingPath.isDirectory());
+  }
+
+  @Test
+  public void testIsLinkForNonexistingPath() {
+    Path nonExistingPath = testFS.getPath("/something/strange");
+    assertFalse(nonExistingPath.isSymbolicLink());
+  }
+
+  @Test
+  public void testExistsForNonexistingPath() throws Exception {
+    Path nonExistingPath = testFS.getPath("/something/strange");
+    assertFalse(nonExistingPath.exists());
+    expectNotFound(nonExistingPath);
+  }
+
+  @Test
+  public void testBadPermissionsThrowsExceptionOnStatIfFound() throws Exception {
+    Path inaccessible = absolutize("inaccessible");
+    inaccessible.createDirectory();
+    Path child = inaccessible.getChild("child");
+    FileSystemUtils.createEmptyFile(child);
+    inaccessible.setExecutable(false);
+    assertFalse(child.exists());
+    try {
+      child.statIfFound();
+      fail();
+    } catch (IOException expected) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testStatIfFoundReturnsNullForChildOfNonDir() throws Exception {
+    Path foo = absolutize("foo");
+    foo.createDirectory();
+    Path nonDir = foo.getRelative("bar");
+    FileSystemUtils.createEmptyFile(nonDir);
+    assertNull(nonDir.getRelative("file").statIfFound());
+  }
+
+  // The following tests check the handling of the current working directory.
+  @Test
+  public void testCreatePathRelativeToWorkingDirectory() {
+    Path relativeCreatedPath = absolutize("some-file");
+    Path expectedResult = workingDir.getRelative(new PathFragment("some-file"));
+
+    assertEquals(expectedResult, relativeCreatedPath);
+  }
+
+  // The following tests check the handling of the root directory
+  @Test
+  public void testRootIsDirectory() {
+    Path rootPath = testFS.getPath("/");
+    assertTrue(rootPath.isDirectory());
+  }
+
+  @Test
+  public void testRootHasNoParent() {
+    Path rootPath = testFS.getPath("/");
+    assertNull(rootPath.getParentDirectory());
+  }
+
+  // The following functions test the creation of files/links/directories.
+  @Test
+  public void testFileExists() throws Exception {
+    Path someFile = absolutize("some-file");
+    FileSystemUtils.createEmptyFile(someFile);
+    assertTrue(someFile.exists());
+    assertNotNull(someFile.statIfFound());
+  }
+
+  @Test
+  public void testFileIsFile() throws Exception {
+    Path someFile = absolutize("some-file");
+    FileSystemUtils.createEmptyFile(someFile);
+    assertTrue(someFile.isFile());
+  }
+
+  @Test
+  public void testFileIsNotDirectory() throws Exception {
+    Path someFile = absolutize("some-file");
+    FileSystemUtils.createEmptyFile(someFile);
+    assertFalse(someFile.isDirectory());
+  }
+
+  @Test
+  public void testFileIsNotSymbolicLink() throws Exception {
+    Path someFile = absolutize("some-file");
+    FileSystemUtils.createEmptyFile(someFile);
+    assertFalse(someFile.isSymbolicLink());
+  }
+
+  @Test
+  public void testDirectoryExists() throws Exception {
+    Path someDirectory = absolutize("some-dir");
+    someDirectory.createDirectory();
+    assertTrue(someDirectory.exists());
+    assertNotNull(someDirectory.statIfFound());
+  }
+
+  @Test
+  public void testDirectoryIsDirectory() throws Exception {
+    Path someDirectory = absolutize("some-dir");
+    someDirectory.createDirectory();
+    assertTrue(someDirectory.isDirectory());
+  }
+
+  @Test
+  public void testDirectoryIsNotFile() throws Exception {
+    Path someDirectory = absolutize("some-dir");
+    someDirectory.createDirectory();
+    assertFalse(someDirectory.isFile());
+  }
+
+  @Test
+  public void testDirectoryIsNotSymbolicLink() throws Exception {
+    Path someDirectory = absolutize("some-dir");
+    someDirectory.createDirectory();
+    assertFalse(someDirectory.isSymbolicLink());
+  }
+
+  @Test
+  public void testSymbolicFileLinkExists() throws Exception {
+    if (supportsSymlinks) {
+      Path someLink = absolutize("some-link");
+      someLink.createSymbolicLink(xFile);
+      assertTrue(someLink.exists());
+      assertNotNull(someLink.statIfFound());
+    }
+  }
+
+  @Test
+  public void testSymbolicFileLinkIsSymbolicLink() throws Exception {
+    if (supportsSymlinks) {
+      Path someLink = absolutize("some-link");
+      someLink.createSymbolicLink(xFile);
+      assertTrue(someLink.isSymbolicLink());
+    }
+  }
+
+  @Test
+  public void testSymbolicFileLinkIsFile() throws Exception {
+    if (supportsSymlinks) {
+      Path someLink = absolutize("some-link");
+      someLink.createSymbolicLink(xFile);
+      assertTrue(someLink.isFile());
+    }
+  }
+
+  @Test
+  public void testSymbolicFileLinkIsNotDirectory() throws Exception {
+    if (supportsSymlinks) {
+      Path someLink = absolutize("some-link");
+      someLink.createSymbolicLink(xFile);
+      assertFalse(someLink.isDirectory());
+    }
+  }
+
+  @Test
+  public void testSymbolicDirLinkExists() throws Exception {
+    if (supportsSymlinks) {
+      Path someLink = absolutize("some-link");
+      someLink.createSymbolicLink(xEmptyDirectory);
+      assertTrue(someLink.exists());
+      assertNotNull(someLink.statIfFound());
+    }
+  }
+
+  @Test
+  public void testSymbolicDirLinkIsSymbolicLink() throws Exception {
+    if (supportsSymlinks) {
+      Path someLink = absolutize("some-link");
+      someLink.createSymbolicLink(xEmptyDirectory);
+      assertTrue(someLink.isSymbolicLink());
+    }
+  }
+
+  @Test
+  public void testSymbolicDirLinkIsDirectory() throws Exception {
+    if (supportsSymlinks) {
+      Path someLink = absolutize("some-link");
+      someLink.createSymbolicLink(xEmptyDirectory);
+      assertTrue(someLink.isDirectory());
+    }
+  }
+
+  @Test
+  public void testSymbolicDirLinkIsNotFile() throws Exception {
+    if (supportsSymlinks) {
+      Path someLink = absolutize("some-link");
+      someLink.createSymbolicLink(xEmptyDirectory);
+      assertFalse(someLink.isFile());
+    }
+  }
+
+  @Test
+  public void testChildOfNonDirectory() throws Exception {
+    Path somePath = absolutize("file-name");
+    FileSystemUtils.createEmptyFile(somePath);
+    Path childOfNonDir = somePath.getChild("child");
+    assertFalse(childOfNonDir.exists());
+    expectNotFound(childOfNonDir);
+  }
+
+  @Test
+  public void testCreateDirectoryIsEmpty() throws Exception {
+    Path newPath = xEmptyDirectory.getChild("new-dir");
+    newPath.createDirectory();
+    assertEquals(newPath.getDirectoryEntries().size(), 0);
+  }
+
+  @Test
+  public void testCreateDirectoryIsOnlyChildInParent() throws Exception {
+    Path newPath = xEmptyDirectory.getChild("new-dir");
+    newPath.createDirectory();
+    assertEquals(1, newPath.getParentDirectory().getDirectoryEntries().size());
+    assertThat(newPath.getParentDirectory().getDirectoryEntries()).containsExactly(newPath);
+  }
+
+  @Test
+  public void testCreateDirectories() throws Exception {
+    Path newPath = absolutize("new-dir/sub/directory");
+    assertTrue(FileSystemUtils.createDirectoryAndParents(newPath));
+  }
+
+  @Test
+  public void testCreateDirectoriesIsDirectory() throws Exception {
+    Path newPath = absolutize("new-dir/sub/directory");
+    FileSystemUtils.createDirectoryAndParents(newPath);
+    assertTrue(newPath.isDirectory());
+  }
+
+  @Test
+  public void testCreateDirectoriesIsNotFile() throws Exception {
+    Path newPath = absolutize("new-dir/sub/directory");
+    FileSystemUtils.createDirectoryAndParents(newPath);
+    assertFalse(newPath.isFile());
+  }
+
+  @Test
+  public void testCreateDirectoriesIsNotSymbolicLink() throws Exception {
+    Path newPath = absolutize("new-dir/sub/directory");
+    FileSystemUtils.createDirectoryAndParents(newPath);
+    assertFalse(newPath.isSymbolicLink());
+  }
+
+  @Test
+  public void testCreateDirectoriesIsEmpty() throws Exception {
+    Path newPath = absolutize("new-dir/sub/directory");
+    FileSystemUtils.createDirectoryAndParents(newPath);
+    assertEquals(newPath.getDirectoryEntries().size(), 0);
+  }
+
+  @Test
+  public void testCreateDirectoriesIsOnlyChildInParent() throws Exception {
+    Path newPath = absolutize("new-dir/sub/directory");
+    FileSystemUtils.createDirectoryAndParents(newPath);
+    assertEquals(1, newPath.getParentDirectory().getDirectoryEntries().size());
+    assertThat(newPath.getParentDirectory().getDirectoryEntries()).containsExactly(newPath);
+  }
+
+  @Test
+  public void testCreateEmptyFileIsEmpty() throws Exception {
+    Path newPath = xEmptyDirectory.getChild("new-file");
+    FileSystemUtils.createEmptyFile(newPath);
+
+    assertEquals(newPath.getFileSize(), 0);
+  }
+
+  @Test
+  public void testCreateFileIsOnlyChildInParent() throws Exception {
+    Path newPath = xEmptyDirectory.getChild("new-file");
+    FileSystemUtils.createEmptyFile(newPath);
+    assertEquals(1, newPath.getParentDirectory().getDirectoryEntries().size());
+    assertThat(newPath.getParentDirectory().getDirectoryEntries()).containsExactly(newPath);
+  }
+
+  // The following functions test the behavior if errors occur during the
+  // creation of files/links/directories.
+  @Test
+  public void testCreateDirectoryWhereDirectoryAlreadyExists() throws Exception {
+    assertFalse(xEmptyDirectory.createDirectory());
+  }
+
+  @Test
+  public void testCreateDirectoryWhereFileAlreadyExists() {
+    try {
+      xFile.createDirectory();
+      fail();
+    } catch (IOException e) {
+      assertEquals(xFile + " (File exists)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotCreateDirectoryWithoutExistingParent() throws Exception {
+    Path newPath = testFS.getPath("/deep/new-dir");
+    try {
+      newPath.createDirectory();
+      fail();
+    } catch (FileNotFoundException e) {
+      MoreAsserts.assertEndsWith(" (No such file or directory)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotCreateDirectoryWithReadOnlyParent() throws Exception {
+    xEmptyDirectory.setWritable(false);
+    Path xChildOfReadonlyDir = xEmptyDirectory.getChild("x");
+    try {
+      xChildOfReadonlyDir.createDirectory();
+      fail();
+    } catch (IOException e) {
+      assertEquals(xChildOfReadonlyDir + " (Permission denied)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotCreateFileWithoutExistingParent() throws Exception {
+    Path newPath = testFS.getPath("/non-existing-dir/new-file");
+    try {
+      FileSystemUtils.createEmptyFile(newPath);
+      fail();
+    } catch (FileNotFoundException e) {
+      MoreAsserts.assertEndsWith(" (No such file or directory)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotCreateFileWithReadOnlyParent() throws Exception {
+    xEmptyDirectory.setWritable(false);
+    Path xChildOfReadonlyDir = xEmptyDirectory.getChild("x");
+    try {
+      FileSystemUtils.createEmptyFile(xChildOfReadonlyDir);
+      fail();
+    } catch (IOException e) {
+      assertEquals(xChildOfReadonlyDir + " (Permission denied)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotCreateFileWithinFile() throws Exception {
+    Path newFilePath = absolutize("some-file");
+    FileSystemUtils.createEmptyFile(newFilePath);
+    Path wrongPath = absolutize("some-file/new-file");
+    try {
+      FileSystemUtils.createEmptyFile(wrongPath);
+      fail();
+    } catch (IOException e) {
+      MoreAsserts.assertEndsWith(" (Not a directory)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotCreateDirectoryWithinFile() throws Exception {
+    Path newFilePath = absolutize("some-file");
+    FileSystemUtils.createEmptyFile(newFilePath);
+    Path wrongPath = absolutize("some-file/new-file");
+    try {
+      wrongPath.createDirectory();
+      fail();
+    } catch (IOException e) {
+      MoreAsserts.assertEndsWith(" (Not a directory)", e.getMessage());
+    }
+  }
+
+  // Test directory contents
+  @Test
+  public void testCreateMultipleChildren() throws Exception {
+    Path theDirectory = absolutize("foo/");
+    theDirectory.createDirectory();
+    Path newPath1 = absolutize("foo/new-file-1");
+    Path newPath2 = absolutize("foo/new-file-2");
+    Path newPath3 = absolutize("foo/new-file-3");
+
+    FileSystemUtils.createEmptyFile(newPath1);
+    FileSystemUtils.createEmptyFile(newPath2);
+    FileSystemUtils.createEmptyFile(newPath3);
+
+    assertThat(theDirectory.getDirectoryEntries()).containsExactly(newPath1, newPath2, newPath3);
+  }
+
+  @Test
+  public void testGetDirectoryEntriesThrowsExceptionWhenRunOnFile() throws Exception {
+    try {
+      xFile.getDirectoryEntries();
+      fail("No Exception thrown.");
+    } catch (IOException ex) {
+      if (ex instanceof FileNotFoundException) {
+        fail("The method should throw an object of class IOException.");
+      }
+      assertEquals(xFile + " (Not a directory)", ex.getMessage());
+    }
+  }
+
+  @Test
+  public void testGetDirectoryEntriesThrowsExceptionForNonexistingPath() {
+    Path somePath = testFS.getPath("/non-existing-path");
+    try {
+      somePath.getDirectoryEntries();
+      fail("FileNotFoundException not thrown.");
+    } catch (Exception x) {
+      assertEquals(somePath + " (No such file or directory)", x.getMessage());
+    }
+  }
+
+  // Test the removal of items
+  @Test
+  public void testDeleteDirectory() throws Exception {
+    assertTrue(xEmptyDirectory.delete());
+  }
+
+  @Test
+  public void testDeleteDirectoryIsNotDirectory() throws Exception {
+    xEmptyDirectory.delete();
+    assertFalse(xEmptyDirectory.isDirectory());
+  }
+
+  @Test
+  public void testDeleteDirectoryParentSize() throws Exception {
+    int parentSize = workingDir.getDirectoryEntries().size();
+    xEmptyDirectory.delete();
+    assertEquals(workingDir.getDirectoryEntries().size(), parentSize - 1);
+  }
+
+  @Test
+  public void testDeleteFile() throws Exception {
+    assertTrue(xFile.delete());
+  }
+
+  @Test
+  public void testDeleteFileIsNotFile() throws Exception {
+    xFile.delete();
+    assertFalse(xEmptyDirectory.isFile());
+  }
+
+  @Test
+  public void testDeleteFileParentSize() throws Exception {
+    int parentSize = workingDir.getDirectoryEntries().size();
+    xFile.delete();
+    assertEquals(workingDir.getDirectoryEntries().size(), parentSize - 1);
+  }
+
+  @Test
+  public void testDeleteRemovesCorrectFile() throws Exception {
+    Path newPath1 = xEmptyDirectory.getChild("new-file-1");
+    Path newPath2 = xEmptyDirectory.getChild("new-file-2");
+    Path newPath3 = xEmptyDirectory.getChild("new-file-3");
+
+    FileSystemUtils.createEmptyFile(newPath1);
+    FileSystemUtils.createEmptyFile(newPath2);
+    FileSystemUtils.createEmptyFile(newPath3);
+
+    assertTrue(newPath2.delete());
+    assertThat(xEmptyDirectory.getDirectoryEntries()).containsExactly(newPath1, newPath3);
+  }
+
+  @Test
+  public void testDeleteNonExistingDir() throws Exception {
+    Path path = xEmptyDirectory.getRelative("non-existing-dir");
+    assertFalse(path.delete());
+  }
+
+  @Test
+  public void testDeleteNotADirectoryPath() throws Exception {
+    Path path = xFile.getChild("new-file");
+    assertFalse(path.delete());
+  }
+
+  // Here we test the situations where delete should throw exceptions.
+  @Test
+  public void testDeleteNonEmptyDirectoryThrowsException() throws Exception {
+    try {
+      xNonEmptyDirectory.delete();
+      fail();
+    } catch (IOException e) {
+      assertEquals(xNonEmptyDirectory + " (Directory not empty)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testDeleteNonEmptyDirectoryNotDeletedDirectory() throws Exception {
+    try {
+      xNonEmptyDirectory.delete();
+      fail();
+    } catch (IOException e) {
+      // Expected
+    }
+
+    assertTrue(xNonEmptyDirectory.isDirectory());
+  }
+
+  @Test
+  public void testDeleteNonEmptyDirectoryNotDeletedFile() throws Exception {
+    try {
+      xNonEmptyDirectory.delete();
+      fail();
+    } catch (IOException e) {
+      // Expected
+    }
+
+    assertTrue(xNonEmptyDirectoryFoo.isFile());
+  }
+
+  @Test
+  public void testCannotRemoveRoot() {
+    Path rootDirectory = testFS.getRootDirectory();
+    try {
+      rootDirectory.delete();
+      fail();
+    } catch (IOException e) {
+      String msg = e.getMessage();
+      assertTrue(String.format("got %s want EBUSY or ENOTEMPTY", msg),
+          msg.endsWith(" (Directory not empty)")
+          || msg.endsWith(" (Device or resource busy)")
+          || msg.endsWith(" (Is a directory)"));  // Happens on OS X.
+    }
+  }
+
+  // Test the date functions
+  @Test
+  public void testCreateFileChangesTimeOfDirectory() throws Exception {
+    storeReferenceTime(workingDir.getLastModifiedTime());
+    Path newPath = absolutize("new-file");
+    FileSystemUtils.createEmptyFile(newPath);
+    assertTrue(isLaterThanreferenceTime(workingDir.getLastModifiedTime()));
+  }
+
+  @Test
+  public void testRemoveFileChangesTimeOfDirectory() throws Exception {
+    Path newPath = absolutize("new-file");
+    FileSystemUtils.createEmptyFile(newPath);
+    storeReferenceTime(workingDir.getLastModifiedTime());
+    newPath.delete();
+    assertTrue(isLaterThanreferenceTime(workingDir.getLastModifiedTime()));
+  }
+
+  // This test is a little bit strange, as we cannot test the progression
+  // of the time directly. As the Java time and the OS time are slightly different.
+  // Therefore, we first create an unrelated file to get a notion
+  // of the current OS time and use that as a baseline.
+  @Test
+  public void testCreateFileTimestamp() throws Exception {
+    Path syncFile = absolutize("sync-file");
+    FileSystemUtils.createEmptyFile(syncFile);
+
+    Path newFile = absolutize("new-file");
+    storeReferenceTime(syncFile.getLastModifiedTime());
+    FileSystemUtils.createEmptyFile(newFile);
+    assertTrue(isLaterThanreferenceTime(newFile.getLastModifiedTime()));
+  }
+
+  @Test
+  public void testCreateDirectoryTimestamp() throws Exception {
+    Path syncFile = absolutize("sync-file");
+    FileSystemUtils.createEmptyFile(syncFile);
+
+    Path newPath = absolutize("new-dir");
+    storeReferenceTime(syncFile.getLastModifiedTime());
+    assertTrue(newPath.createDirectory());
+    assertTrue(isLaterThanreferenceTime(newPath.getLastModifiedTime()));
+  }
+
+  @Test
+  public void testWriteChangesModifiedTime() throws Exception {
+    storeReferenceTime(xFile.getLastModifiedTime());
+    FileSystemUtils.writeContentAsLatin1(xFile, "abc19");
+    assertTrue(isLaterThanreferenceTime(xFile.getLastModifiedTime()));
+  }
+
+  @Test
+  public void testGetLastModifiedTimeThrowsExceptionForNonexistingPath() throws Exception {
+    Path newPath = testFS.getPath("/non-existing-dir");
+    try {
+      newPath.getLastModifiedTime();
+      fail("FileNotFoundException not thrown!");
+    } catch (FileNotFoundException x) {
+      assertEquals(newPath + " (No such file or directory)", x.getMessage());
+    }
+  }
+
+  // Test file size
+  @Test
+  public void testFileSizeThrowsExceptionForNonexistingPath() throws Exception {
+    Path newPath = testFS.getPath("/non-existing-file");
+    try {
+      newPath.getFileSize();
+      fail("FileNotFoundException not thrown.");
+    } catch (FileNotFoundException e) {
+      assertEquals(newPath + " (No such file or directory)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testFileSizeAfterWrite() throws Exception {
+    String testData = "abc19";
+
+    FileSystemUtils.writeContentAsLatin1(xFile, testData);
+    assertEquals(testData.length(), xFile.getFileSize());
+  }
+
+  // Testing the input/output routines
+  @Test
+  public void testFileWriteAndReadAsLatin1() throws Exception {
+    String testData = "abc19";
+
+    FileSystemUtils.writeContentAsLatin1(xFile, testData);
+    String resultData = new String(FileSystemUtils.readContentAsLatin1(xFile));
+
+    assertEquals(testData,resultData);
+  }
+
+  @Test
+  public void testInputAndOutputStreamEOF() throws Exception {
+    OutputStream outStream = xFile.getOutputStream();
+    outStream.write(1);
+    outStream.close();
+
+    InputStream inStream = xFile.getInputStream();
+    inStream.read();
+    assertEquals(-1, inStream.read());
+    inStream.close();
+  }
+
+  @Test
+  public void testInputAndOutputStream() throws Exception {
+    OutputStream outStream = xFile.getOutputStream();
+    for (int i = 33; i < 126; i++) {
+      outStream.write(i);
+    }
+    outStream.close();
+
+    InputStream inStream = xFile.getInputStream();
+    for (int i = 33; i < 126; i++) {
+      int readValue = inStream.read();
+      assertEquals(i,readValue);
+    }
+    inStream.close();
+  }
+
+  @Test
+  public void testInputAndOutputStreamAppend() throws Exception {
+    OutputStream outStream = xFile.getOutputStream();
+    for (int i = 33; i < 126; i++) {
+      outStream.write(i);
+    }
+    outStream.close();
+
+    OutputStream appendOut = xFile.getOutputStream(true);
+    for (int i = 126; i < 155; i++) {
+      appendOut.write(i);
+    }
+    appendOut.close();
+
+    InputStream inStream = xFile.getInputStream();
+    for (int i = 33; i < 155; i++) {
+      int readValue = inStream.read();
+      assertEquals(i,readValue);
+    }
+    inStream.close();
+  }
+
+  @Test
+  public void testInputAndOutputStreamNoAppend() throws Exception {
+    OutputStream outStream = xFile.getOutputStream();
+    outStream.write(1);
+    outStream.close();
+
+    OutputStream noAppendOut = xFile.getOutputStream(false);
+    noAppendOut.close();
+
+    InputStream inStream = xFile.getInputStream();
+    assertEquals(-1, inStream.read());
+    inStream.close();
+  }
+
+  @Test
+  public void testGetOutputStreamCreatesFile() throws Exception {
+    Path newFile = absolutize("does_not_exist_yet.txt");
+
+    OutputStream out = newFile.getOutputStream();
+    out.write(42);
+    out.close();
+
+    assertTrue(newFile.isFile());
+  }
+
+  @Test
+  public void testInpuStreamThrowExceptionOnDirectory() throws Exception {
+    try {
+      xEmptyDirectory.getOutputStream();
+      fail("The Exception was not thrown!");
+    } catch (IOException ex) {
+      assertEquals(xEmptyDirectory + " (Is a directory)", ex.getMessage());
+    }
+  }
+
+  @Test
+  public void testOutputStreamThrowExceptionOnDirectory() throws Exception {
+    try {
+      xEmptyDirectory.getInputStream();
+      fail("The Exception was not thrown!");
+    } catch (IOException ex) {
+      assertEquals(xEmptyDirectory + " (Is a directory)", ex.getMessage());
+    }
+  }
+
+  // Test renaming
+  @Test
+  public void testCanRenameToUnusedName() throws Exception {
+    xFile.renameTo(xNothing);
+    assertFalse(xFile.exists());
+    assertTrue(xNothing.isFile());
+  }
+
+  @Test
+  public void testCanRenameFileToExistingFile() throws Exception {
+    Path otherFile = absolutize("otherFile");
+    FileSystemUtils.createEmptyFile(otherFile);
+    xFile.renameTo(otherFile); // succeeds
+    assertFalse(xFile.exists());
+    assertTrue(otherFile.isFile());
+  }
+
+  @Test
+  public void testCanRenameDirToExistingEmptyDir() throws Exception {
+    xNonEmptyDirectory.renameTo(xEmptyDirectory); // succeeds
+    assertFalse(xNonEmptyDirectory.exists());
+    assertTrue(xEmptyDirectory.isDirectory());
+    assertFalse(xEmptyDirectory.getDirectoryEntries().isEmpty());
+  }
+
+  @Test
+  public void testCantRenameDirToExistingNonEmptyDir() throws Exception {
+    try {
+      xEmptyDirectory.renameTo(xNonEmptyDirectory);
+      fail();
+    } catch (IOException e) {
+      MoreAsserts.assertEndsWith(" (Directory not empty)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCantRenameDirToExistingNonEmptyDirNothingChanged() throws Exception {
+    try {
+      xEmptyDirectory.renameTo(xNonEmptyDirectory);
+      fail();
+    } catch (IOException e) {
+      // Expected
+    }
+
+    assertTrue(xNonEmptyDirectory.isDirectory());
+    assertTrue(xEmptyDirectory.isDirectory());
+    assertTrue(xEmptyDirectory.getDirectoryEntries().isEmpty());
+    assertFalse(xNonEmptyDirectory.getDirectoryEntries().isEmpty());
+  }
+
+  @Test
+  public void testCantRenameDirToExistingFile() {
+    try {
+      xEmptyDirectory.renameTo(xFile);
+      fail();
+    } catch (IOException e) {
+      assertEquals(xEmptyDirectory + " -> " + xFile + " (Not a directory)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCantRenameDirToExistingFileNothingChanged() {
+    try {
+      xEmptyDirectory.renameTo(xFile);
+      fail();
+    } catch (IOException e) {
+      // Expected
+    }
+
+    assertTrue(xEmptyDirectory.isDirectory());
+    assertTrue(xFile.isFile());
+  }
+
+  @Test
+  public void testCantRenameFileToExistingDir() {
+    try {
+      xFile.renameTo(xEmptyDirectory);
+      fail();
+    } catch (IOException e) {
+      assertEquals(xFile + " -> " + xEmptyDirectory + " (Is a directory)",
+                   e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCantRenameFileToExistingDirNothingChanged() {
+    try {
+      xFile.renameTo(xEmptyDirectory);
+      fail();
+    } catch (IOException e) {
+      // Expected
+    }
+
+    assertTrue(xEmptyDirectory.isDirectory());
+    assertTrue(xFile.isFile());
+  }
+
+  @Test
+  public void testMoveOnNonExistingFileThrowsException() throws Exception {
+    Path nonExistingPath = absolutize("non-existing");
+    Path targetPath = absolutize("does-not-matter");
+    try {
+      nonExistingPath.renameTo(targetPath);
+      fail();
+    } catch (FileNotFoundException e) {
+      MoreAsserts.assertEndsWith(" (No such file or directory)", e.getMessage());
+    }
+  }
+
+  // Test the Paths
+  @Test
+  public void testGetPathOnlyAcceptsAbsolutePath() {
+    try {
+      testFS.getPath("not-absolute");
+      fail("The expected Exception was not thrown.");
+    } catch (IllegalArgumentException ex) {
+      assertEquals("not-absolute (not an absolute path)", ex.getMessage());
+    }
+  }
+
+  @Test
+  public void testGetPathOnlyAcceptsAbsolutePathFragment() {
+    try {
+      testFS.getPath(new PathFragment("not-absolute"));
+      fail("The expected Exception was not thrown.");
+    } catch (IllegalArgumentException ex) {
+      assertEquals("not-absolute (not an absolute path)", ex.getMessage());
+    }
+  }
+
+  // Test the access permissions
+  @Test
+  public void testNewFilesAreWritable() throws Exception {
+    assertTrue(xFile.isWritable());
+  }
+
+  @Test
+  public void testNewFilesAreReadable() throws Exception {
+    assertTrue(xFile.isReadable());
+  }
+
+  @Test
+  public void testNewDirsAreWritable() throws Exception {
+    assertTrue(xEmptyDirectory.isWritable());
+  }
+
+  @Test
+  public void testNewDirsAreReadable() throws Exception {
+    assertTrue(xEmptyDirectory.isReadable());
+  }
+
+  @Test
+  public void testNewDirsAreExecutable() throws Exception {
+    assertTrue(xEmptyDirectory.isExecutable());
+  }
+
+  @Test
+  public void testCannotGetExecutableOnNonexistingFile() throws Exception {
+    try {
+      xNothing.isExecutable();
+      fail("No exception thrown.");
+    } catch (FileNotFoundException ex) {
+      assertEquals(xNothing + " (No such file or directory)", ex.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotSetExecutableOnNonexistingFile() throws Exception {
+    try {
+      xNothing.setExecutable(true);
+      fail("No exception thrown.");
+    } catch (FileNotFoundException ex) {
+      assertEquals(xNothing + " (No such file or directory)", ex.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotGetWritableOnNonexistingFile() throws Exception {
+    try {
+      xNothing.isWritable();
+      fail("No exception thrown.");
+    } catch (FileNotFoundException ex) {
+      assertEquals(xNothing + " (No such file or directory)", ex.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotSetWritableOnNonexistingFile() throws Exception {
+    try {
+      xNothing.setWritable(false);
+      fail("No exception thrown.");
+    } catch (FileNotFoundException ex) {
+      assertEquals(xNothing + " (No such file or directory)", ex.getMessage());
+    }
+  }
+
+  @Test
+  public void testSetReadableOnFile() throws Exception {
+    xFile.setReadable(false);
+    assertFalse(xFile.isReadable());
+    xFile.setReadable(true);
+    assertTrue(xFile.isReadable());
+  }
+
+  @Test
+  public void testSetWritableOnFile() throws Exception {
+    xFile.setWritable(false);
+    assertFalse(xFile.isWritable());
+    xFile.setWritable(true);
+    assertTrue(xFile.isWritable());
+  }
+
+  @Test
+  public void testSetExecutableOnFile() throws Exception {
+    xFile.setExecutable(true);
+    assertTrue(xFile.isExecutable());
+    xFile.setExecutable(false);
+    assertFalse(xFile.isExecutable());
+  }
+
+  @Test
+  public void testSetExecutableOnDirectory() throws Exception {
+    setExecutable(xNonEmptyDirectory, false);
+
+    try {
+      // We can't map names->inodes in a non-executable directory:
+      xNonEmptyDirectoryFoo.isWritable(); // i.e. stat
+      fail();
+    } catch (IOException e) {
+      MoreAsserts.assertEndsWith(" (Permission denied)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testWritingToReadOnlyFileThrowsException() throws Exception {
+    xFile.setWritable(false);
+    try {
+      FileSystemUtils.writeContent(xFile, "hello, world!".getBytes());
+      fail("No exception thrown.");
+    } catch (IOException e) {
+      assertEquals(xFile + " (Permission denied)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testReadingFromUnreadableFileThrowsException() throws Exception {
+    FileSystemUtils.writeContent(xFile, "hello, world!".getBytes());
+    xFile.setReadable(false);
+    try {
+      FileSystemUtils.readContent(xFile);
+      fail("No exception thrown.");
+    } catch (IOException e) {
+      assertEquals(xFile + " (Permission denied)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotCreateFileInReadOnlyDirectory() throws Exception {
+    Path xNonEmptyDirectoryBar = xNonEmptyDirectory.getChild("bar");
+    xNonEmptyDirectory.setWritable(false);
+
+    try {
+      FileSystemUtils.createEmptyFile(xNonEmptyDirectoryBar);
+      fail("No exception thrown.");
+    } catch (IOException e) {
+      assertEquals(xNonEmptyDirectoryBar + " (Permission denied)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotCreateDirectoryInReadOnlyDirectory() throws Exception {
+    Path xNonEmptyDirectoryBar = xNonEmptyDirectory.getChild("bar");
+    xNonEmptyDirectory.setWritable(false);
+
+    try {
+      xNonEmptyDirectoryBar.createDirectory();
+      fail("No exception thrown.");
+    } catch (IOException e) {
+      assertEquals(xNonEmptyDirectoryBar + " (Permission denied)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotMoveIntoReadOnlyDirectory() throws Exception {
+    Path xNonEmptyDirectoryBar = xNonEmptyDirectory.getChild("bar");
+    xNonEmptyDirectory.setWritable(false);
+
+    try {
+      xFile.renameTo(xNonEmptyDirectoryBar);
+      fail("No exception thrown.");
+    } catch (IOException e) {
+      MoreAsserts.assertEndsWith(" (Permission denied)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotMoveFromReadOnlyDirectory() throws Exception {
+    xNonEmptyDirectory.setWritable(false);
+
+    try {
+      xNonEmptyDirectoryFoo.renameTo(xNothing);
+      fail("No exception thrown.");
+    } catch (IOException e) {
+      MoreAsserts.assertEndsWith(" (Permission denied)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotDeleteInReadOnlyDirectory() throws Exception {
+    xNonEmptyDirectory.setWritable(false);
+
+    try {
+      xNonEmptyDirectoryFoo.delete();
+      fail("No exception thrown.");
+    } catch (IOException e) {
+      assertEquals(xNonEmptyDirectoryFoo + " (Permission denied)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotCreatSymbolicLinkInReadOnlyDirectory() throws Exception {
+    Path xNonEmptyDirectoryBar = xNonEmptyDirectory.getChild("bar");
+    xNonEmptyDirectory.setWritable(false);
+
+    if (supportsSymlinks) {
+      try {
+        createSymbolicLink(xNonEmptyDirectoryBar, xNonEmptyDirectoryFoo);
+        fail("No exception thrown.");
+      } catch (IOException e) {
+        assertEquals(xNonEmptyDirectoryBar + " (Permission denied)", e.getMessage());
+      }
+    }
+  }
+
+  @Test
+  public void testGetMD5DigestForEmptyFile() throws Exception {
+    Fingerprint fp = new Fingerprint();
+    fp.addBytes(new byte[0]);
+    assertEquals(BaseEncoding.base16().lowerCase().encode(xFile.getMD5Digest()),
+        fp.hexDigestAndReset());
+  }
+
+  @Test
+  public void testGetMD5Digest() throws Exception {
+    byte[] buffer = new byte[500000];
+    for (int i = 0; i < buffer.length; ++i) {
+      buffer[i] = 1;
+    }
+    FileSystemUtils.writeContent(xFile, buffer);
+    Fingerprint fp = new Fingerprint();
+    fp.addBytes(buffer);
+    assertEquals(BaseEncoding.base16().lowerCase().encode(xFile.getMD5Digest()),
+        fp.hexDigestAndReset());
+  }
+
+  @Test
+  public void testStatFailsFastOnNonExistingFiles() throws Exception {
+    try {
+      xNothing.stat();
+      fail("Expected IOException");
+    } catch(IOException e) {
+      // Do nothing.
+    }
+  }
+
+  @Test
+  public void testStatNullableFailsFastOnNonExistingFiles() throws Exception {
+    assertNull(xNothing.statNullable());
+  }
+
+  @Test
+  public void testResolveSymlinks() throws Exception {
+    if (supportsSymlinks) {
+      createSymbolicLink(xNothing, xFile);
+      FileSystemUtils.createEmptyFile(xFile);
+      assertEquals(xFile.asFragment(), testFS.resolveOneLink(xNothing));
+      assertEquals(xFile, xNothing.resolveSymbolicLinks());
+    }
+  }
+
+  @Test
+  public void testResolveNonSymlinks() throws Exception {
+    if (supportsSymlinks) {
+      assertEquals(null, testFS.resolveOneLink(xFile));
+      assertEquals(xFile, xFile.resolveSymbolicLinks());
+    }
+  }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java
new file mode 100644
index 0000000..21ca39b
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java
@@ -0,0 +1,878 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.appendWithoutExtension;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.commonAncestor;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.copyFile;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.copyTool;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.createDirectoryAndParents;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.deleteTree;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.deleteTreesBelowNotPrefixed;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.longestPathPrefix;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.plantLinkForest;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.relativePath;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.removeExtension;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.touchFile;
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.traverseTree;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.testutil.BlazeTestUtils;
+import com.google.devtools.build.lib.testutil.ManualClock;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This class tests the file system utilities.
+ */
+@RunWith(JUnit4.class)
+public class FileSystemUtilsTest {
+  private ManualClock clock;
+  private FileSystem fileSystem;
+  private Path workingDir;
+
+  @Before
+  public void setUp() throws Exception {
+    clock = new ManualClock();
+    fileSystem = new InMemoryFileSystem(clock);
+    workingDir = fileSystem.getPath("/workingDir");
+  }
+
+  Path topDir;
+  Path file1;
+  Path file2;
+  Path aDir;
+  Path file3;
+  Path innerDir;
+  Path link1;
+  Path dirLink;
+  Path file4;
+
+  /*
+   * Build a directory tree that looks like:
+   *   top-dir/
+   *     file-1
+   *     file-2
+   *     a-dir/
+   *       file-3
+   *       inner-dir/
+   *         link-1 => file-4
+   *         dir-link => a-dir
+   *   file-4
+   */
+  private void createTestDirectoryTree() throws IOException {
+    topDir = fileSystem.getPath("/top-dir");
+    file1 = fileSystem.getPath("/top-dir/file-1");
+    file2 = fileSystem.getPath("/top-dir/file-2");
+    aDir = fileSystem.getPath("/top-dir/a-dir");
+    file3 = fileSystem.getPath("/top-dir/a-dir/file-3");
+    innerDir = fileSystem.getPath("/top-dir/a-dir/inner-dir");
+    link1 = fileSystem.getPath("/top-dir/a-dir/inner-dir/link-1");
+    dirLink = fileSystem.getPath("/top-dir/a-dir/inner-dir/dir-link");
+    file4 = fileSystem.getPath("/file-4");
+
+    topDir.createDirectory();
+    FileSystemUtils.createEmptyFile(file1);
+    FileSystemUtils.createEmptyFile(file2);
+    aDir.createDirectory();
+    FileSystemUtils.createEmptyFile(file3);
+    innerDir.createDirectory();
+    link1.createSymbolicLink(file4);  // simple symlink
+    dirLink.createSymbolicLink(aDir); // creates link loop
+    FileSystemUtils.createEmptyFile(file4);
+  }
+
+  private void checkTestDirectoryTreesBelow(Path toPath) throws IOException {
+    Path copiedFile1 = toPath.getChild("file-1");
+    assertTrue(copiedFile1.exists());
+    assertTrue(copiedFile1.isFile());
+
+    Path copiedFile2 = toPath.getChild("file-2");
+    assertTrue(copiedFile2.exists());
+    assertTrue(copiedFile2.isFile());
+
+    Path copiedADir = toPath.getChild("a-dir");
+    assertTrue(copiedADir.exists());
+    assertTrue(copiedADir.isDirectory());
+    Collection<Path> aDirEntries = copiedADir.getDirectoryEntries();
+    assertEquals(2, aDirEntries.size());
+
+    Path copiedFile3 = copiedADir.getChild("file-3");
+    assertTrue(copiedFile3.exists());
+    assertTrue(copiedFile3.isFile());
+
+    Path copiedInnerDir = copiedADir.getChild("inner-dir");
+    assertTrue(copiedInnerDir.exists());
+    assertTrue(copiedInnerDir.isDirectory());
+
+    Path copiedLink1 = copiedInnerDir.getChild("link-1");
+    assertTrue(copiedLink1.exists());
+    assertTrue(copiedLink1.isSymbolicLink());
+    assertEquals(copiedLink1.resolveSymbolicLinks(), file4);
+
+    Path copiedDirLink = copiedInnerDir.getChild("dir-link");
+    assertTrue(copiedDirLink.exists());
+    assertTrue(copiedDirLink.isSymbolicLink());
+    assertEquals(copiedDirLink.resolveSymbolicLinks(), aDir);
+  }
+
+  // tests
+
+  @Test
+  public void testChangeModtime() throws IOException {
+    Path file = fileSystem.getPath("/my-file");
+    try {
+      BlazeTestUtils.changeModtime(file);
+      fail();
+    } catch (FileNotFoundException e) {
+      /* ok */
+    }
+    FileSystemUtils.createEmptyFile(file);
+    long prevMtime = file.getLastModifiedTime();
+    BlazeTestUtils.changeModtime(file);
+    assertFalse(prevMtime == file.getLastModifiedTime());
+  }
+
+  @Test
+  public void testCommonAncestor() {
+    assertEquals(topDir, commonAncestor(topDir, topDir));
+    assertEquals(topDir, commonAncestor(file1, file3));
+    assertEquals(topDir, commonAncestor(file1, dirLink));
+  }
+
+  @Test
+  public void testRelativePath() throws IOException {
+    createTestDirectoryTree();
+    assertEquals("file-1", relativePath(topDir, file1).getPathString());
+    assertEquals(".", relativePath(topDir, topDir).getPathString());
+    assertEquals("a-dir/inner-dir/dir-link", relativePath(topDir, dirLink).getPathString());
+    assertEquals("../file-4", relativePath(topDir, file4).getPathString());
+    assertEquals("../../../file-4", relativePath(innerDir, file4).getPathString());
+  }
+
+  private String longestPathPrefixStr(String path, String... prefixStrs) {
+    Set<PathFragment> prefixes = new HashSet<>();
+    for (String prefix : prefixStrs) {
+      prefixes.add(new PathFragment(prefix));
+    }
+    PathFragment longest = longestPathPrefix(new PathFragment(path), prefixes);
+    return longest != null ? longest.getPathString() : null;
+  }
+
+  @Test
+  public void testLongestPathPrefix() {
+    assertEquals("A", longestPathPrefixStr("A/b", "A", "B")); // simple parent
+    assertEquals("A", longestPathPrefixStr("A", "A", "B")); // self
+    assertEquals("A/B", longestPathPrefixStr("A/B/c", "A", "A/B"));  // want longest
+    assertNull(longestPathPrefixStr("C/b", "A", "B"));  // not found in other parents
+    assertNull(longestPathPrefixStr("A", "A/B", "B"));  // not found in child
+    assertEquals("A/B/C", longestPathPrefixStr("A/B/C/d/e/f.h", "A/B/C", "B/C/d"));
+  }
+
+  @Test
+  public void testRemoveExtension_Strings() throws Exception {
+    assertEquals("foo", removeExtension("foo.c"));
+    assertEquals("a/foo", removeExtension("a/foo.c"));
+    assertEquals("a.b/foo", removeExtension("a.b/foo"));
+    assertEquals("foo", removeExtension("foo"));
+    assertEquals("foo", removeExtension("foo."));
+  }
+
+  @Test
+  public void testRemoveExtension_Paths() throws Exception {
+    assertPath("/foo", removeExtension(fileSystem.getPath("/foo.c")));
+    assertPath("/a/foo", removeExtension(fileSystem.getPath("/a/foo.c")));
+    assertPath("/a.b/foo", removeExtension(fileSystem.getPath("/a.b/foo")));
+    assertPath("/foo", removeExtension(fileSystem.getPath("/foo")));
+    assertPath("/foo", removeExtension(fileSystem.getPath("/foo.")));
+  }
+
+  private static void assertPath(String expected, PathFragment actual) {
+    assertEquals(expected, actual.getPathString());
+  }
+
+  private static void assertPath(String expected, Path actual) {
+    assertEquals(expected, actual.getPathString());
+  }
+
+  @Test
+  public void testReplaceExtension_Path() throws Exception {
+    assertPath("/foo/bar.baz",
+               FileSystemUtils.replaceExtension(fileSystem.getPath("/foo/bar"), ".baz"));
+    assertPath("/foo/bar.baz",
+               FileSystemUtils.replaceExtension(fileSystem.getPath("/foo/bar.cc"), ".baz"));
+    assertPath("/foo.baz", FileSystemUtils.replaceExtension(fileSystem.getPath("/foo/"), ".baz"));
+    assertPath("/foo.baz",
+               FileSystemUtils.replaceExtension(fileSystem.getPath("/foo.cc/"), ".baz"));
+    assertPath("/foo.baz", FileSystemUtils.replaceExtension(fileSystem.getPath("/foo"), ".baz"));
+    assertPath("/foo.baz", FileSystemUtils.replaceExtension(fileSystem.getPath("/foo.cc"), ".baz"));
+    assertPath("/.baz", FileSystemUtils.replaceExtension(fileSystem.getPath("/.cc"), ".baz"));
+    assertEquals(null, FileSystemUtils.replaceExtension(fileSystem.getPath("/"), ".baz"));
+  }
+
+  @Test
+  public void testReplaceExtension_PathFragment() throws Exception {
+    assertPath("foo/bar.baz",
+               FileSystemUtils.replaceExtension(new PathFragment("foo/bar"), ".baz"));
+    assertPath("foo/bar.baz",
+               FileSystemUtils.replaceExtension(new PathFragment("foo/bar.cc"), ".baz"));
+    assertPath("/foo/bar.baz",
+               FileSystemUtils.replaceExtension(new PathFragment("/foo/bar"), ".baz"));
+    assertPath("/foo/bar.baz",
+               FileSystemUtils.replaceExtension(new PathFragment("/foo/bar.cc"), ".baz"));
+    assertPath("foo.baz", FileSystemUtils.replaceExtension(new PathFragment("foo/"), ".baz"));
+    assertPath("foo.baz", FileSystemUtils.replaceExtension(new PathFragment("foo.cc/"), ".baz"));
+    assertPath("/foo.baz", FileSystemUtils.replaceExtension(new PathFragment("/foo/"), ".baz"));
+    assertPath("/foo.baz",
+               FileSystemUtils.replaceExtension(new PathFragment("/foo.cc/"), ".baz"));
+    assertPath("foo.baz", FileSystemUtils.replaceExtension(new PathFragment("foo"), ".baz"));
+    assertPath("foo.baz", FileSystemUtils.replaceExtension(new PathFragment("foo.cc"), ".baz"));
+    assertPath("/foo.baz", FileSystemUtils.replaceExtension(new PathFragment("/foo"), ".baz"));
+    assertPath("/foo.baz",
+               FileSystemUtils.replaceExtension(new PathFragment("/foo.cc"), ".baz"));
+    assertPath(".baz", FileSystemUtils.replaceExtension(new PathFragment(".cc"), ".baz"));
+    assertEquals(null, FileSystemUtils.replaceExtension(new PathFragment("/"), ".baz"));
+    assertEquals(null, FileSystemUtils.replaceExtension(new PathFragment(""), ".baz"));
+    assertPath("foo/bar.baz",
+        FileSystemUtils.replaceExtension(new PathFragment("foo/bar.pony"), ".baz", ".pony"));
+    assertPath("foo/bar.baz",
+        FileSystemUtils.replaceExtension(new PathFragment("foo/bar"), ".baz", ""));
+    assertEquals(null, FileSystemUtils.replaceExtension(new PathFragment(""), ".baz", ".pony"));
+    assertEquals(null,
+        FileSystemUtils.replaceExtension(new PathFragment("foo/bar.pony"), ".baz", ".unicorn"));
+  }
+
+  @Test
+  public void testAppendWithoutExtension() throws Exception {
+    assertPath("libfoo-src.jar",
+        appendWithoutExtension(new PathFragment("libfoo.jar"), "-src"));
+    assertPath("foo/libfoo-src.jar",
+        appendWithoutExtension(new PathFragment("foo/libfoo.jar"), "-src"));
+    assertPath("java/com/google/foo/libfoo-src.jar",
+        appendWithoutExtension(new PathFragment("java/com/google/foo/libfoo.jar"), "-src"));
+    assertPath("libfoo.bar-src.jar",
+        appendWithoutExtension(new PathFragment("libfoo.bar.jar"), "-src"));
+    assertPath("libfoo-src",
+        appendWithoutExtension(new PathFragment("libfoo"), "-src"));
+    assertPath("libfoo-src.jar",
+        appendWithoutExtension(new PathFragment("libfoo.jar/"), "-src"));
+    assertPath("libfoo.src.jar",
+        appendWithoutExtension(new PathFragment("libfoo.jar"), ".src"));
+    assertEquals(null, appendWithoutExtension(new PathFragment("/"), "-src"));
+    assertEquals(null, appendWithoutExtension(new PathFragment(""), "-src"));
+  }
+
+  @Test
+  public void testReplaceSegments() {
+    assertPath(
+        "poo/bar/baz.cc",
+        FileSystemUtils.replaceSegments(new PathFragment("foo/bar/baz.cc"), "foo", "poo", true));
+    assertPath(
+        "poo/poo/baz.cc",
+        FileSystemUtils.replaceSegments(new PathFragment("foo/foo/baz.cc"), "foo", "poo", true));
+    assertPath(
+        "poo/foo/baz.cc",
+        FileSystemUtils.replaceSegments(new PathFragment("foo/foo/baz.cc"), "foo", "poo", false));
+    assertPath(
+        "foo/bar/baz.cc",
+        FileSystemUtils.replaceSegments(new PathFragment("foo/bar/baz.cc"), "boo", "poo", true));
+  }
+
+  @Test
+  public void testGetWorkingDirectory() {
+    String userDir = System.getProperty("user.dir");
+
+    assertEquals(FileSystemUtils.getWorkingDirectory(fileSystem),
+        fileSystem.getPath(System.getProperty("user.dir", "/")));
+
+    System.setProperty("user.dir", "/blah/blah/blah");
+    assertEquals(FileSystemUtils.getWorkingDirectory(fileSystem),
+        fileSystem.getPath("/blah/blah/blah"));
+
+    System.setProperty("user.dir", userDir);
+  }
+
+  @Test
+  public void testResolveRelativeToFilesystemWorkingDir() {
+    PathFragment relativePath = new PathFragment("relative/path");
+    assertEquals(workingDir.getRelative(relativePath),
+                 workingDir.getRelative(relativePath));
+
+    PathFragment absolutePath = new PathFragment("/absolute/path");
+    assertEquals(fileSystem.getPath(absolutePath),
+                 workingDir.getRelative(absolutePath));
+  }
+
+  @Test
+  public void testTouchFileCreatesFile() throws IOException {
+    createTestDirectoryTree();
+    Path nonExistingFile = fileSystem.getPath("/previously-non-existing");
+    assertFalse(nonExistingFile.exists());
+    touchFile(nonExistingFile);
+
+    assertTrue(nonExistingFile.exists());
+  }
+
+  @Test
+  public void testTouchFileAdjustsFileTime() throws IOException {
+    createTestDirectoryTree();
+    Path testFile = file4;
+    long oldTime = testFile.getLastModifiedTime();
+    testFile.setLastModifiedTime(42);
+    touchFile(testFile);
+
+    assertTrue(testFile.getLastModifiedTime() >= oldTime);
+  }
+
+  @Test
+  public void testCopyFile() throws IOException {
+    createTestDirectoryTree();
+    Path originalFile = file1;
+    byte[] content = new byte[] { 'a', 'b', 'c', 23, 42 };
+    FileSystemUtils.writeContent(originalFile, content);
+
+    Path copyTarget = file2;
+
+    copyFile(originalFile, copyTarget);
+
+    assertTrue(Arrays.equals(content, FileSystemUtils.readContent(copyTarget)));
+  }
+
+  @Test
+  public void testReadContentWithLimit() throws IOException {
+    createTestDirectoryTree();
+    String str = "this is a test of readContentWithLimit method";
+    FileSystemUtils.writeContent(file1, StandardCharsets.ISO_8859_1, str);
+    assertEquals(readStringFromFile(file1, 0), "");
+    assertEquals(readStringFromFile(file1, 10), str.substring(0, 10));
+    assertEquals(readStringFromFile(file1, 1000000), str);
+  }
+
+  private String readStringFromFile(Path file, int limit) throws IOException {
+    byte[] bytes = FileSystemUtils.readContentWithLimit(file, limit);
+    return new String(bytes, StandardCharsets.ISO_8859_1);
+  }
+
+  @Test
+  public void testAppend() throws IOException {
+    createTestDirectoryTree();
+    FileSystemUtils.writeIsoLatin1(file1, "nobody says ");
+    FileSystemUtils.writeIsoLatin1(file1, "mary had");
+    FileSystemUtils.appendIsoLatin1(file1, "a little lamb");
+    assertEquals(
+        "mary had\na little lamb\n",
+        new String(FileSystemUtils.readContentAsLatin1(file1)));
+  }
+
+  @Test
+  public void testCopyFileAttributes() throws IOException {
+    createTestDirectoryTree();
+    Path originalFile = file1;
+    byte[] content = new byte[] { 'a', 'b', 'c', 23, 42 };
+    FileSystemUtils.writeContent(originalFile, content);
+    file1.setLastModifiedTime(12345L);
+    file1.setWritable(false);
+    file1.setExecutable(false);
+
+    Path copyTarget = file2;
+    copyFile(originalFile, copyTarget);
+
+    assertEquals(12345L, file2.getLastModifiedTime());
+    assertFalse(file2.isExecutable());
+    assertFalse(file2.isWritable());
+
+    file1.setWritable(true);
+    file1.setExecutable(true);
+
+    copyFile(originalFile, copyTarget);
+
+    assertEquals(12345L, file2.getLastModifiedTime());
+    assertTrue(file2.isExecutable());
+    assertTrue(file2.isWritable());
+
+  }
+
+  @Test
+  public void testCopyFileThrowsExceptionIfTargetCantBeDeleted() throws IOException {
+    createTestDirectoryTree();
+    Path originalFile = file1;
+    byte[] content = new byte[] { 'a', 'b', 'c', 23, 42 };
+    FileSystemUtils.writeContent(originalFile, content);
+
+    try {
+      copyFile(originalFile, aDir);
+      fail();
+    } catch (IOException ex) {
+      assertEquals("error copying file: couldn't delete destination: "
+                   + aDir + " (Directory not empty)",
+                   ex.getMessage());
+    }
+  }
+
+  @Test
+  public void testCopyTool() throws IOException {
+    createTestDirectoryTree();
+    Path originalFile = file1;
+    byte[] content = new byte[] { 'a', 'b', 'c', 23, 42 };
+    FileSystemUtils.writeContent(originalFile, content);
+
+    Path copyTarget = copyTool(topDir.getRelative("file-1"), aDir.getRelative("file-1"));
+
+    assertTrue(Arrays.equals(content, FileSystemUtils.readContent(copyTarget)));
+    assertEquals(file1.isWritable(), copyTarget.isWritable());
+    assertEquals(file1.isExecutable(), copyTarget.isExecutable());
+    assertEquals(file1.getLastModifiedTime(), copyTarget.getLastModifiedTime());
+  }
+
+  @Test
+  public void testCopyTreesBelow() throws IOException {
+    createTestDirectoryTree();
+    Path toPath = fileSystem.getPath("/copy-here");
+    toPath.createDirectory();
+
+    FileSystemUtils.copyTreesBelow(topDir, toPath);
+    checkTestDirectoryTreesBelow(toPath);
+  }
+
+  @Test
+  public void testCopyTreesBelowWithOverriding() throws IOException {
+    createTestDirectoryTree();
+    Path toPath = fileSystem.getPath("/copy-here");
+    toPath.createDirectory();
+    toPath.getChild("file-2");
+
+    FileSystemUtils.copyTreesBelow(topDir, toPath);
+    checkTestDirectoryTreesBelow(toPath);
+  }
+
+  @Test
+  public void testCopyTreesBelowToSubtree() throws IOException {
+    createTestDirectoryTree();
+    try {
+      FileSystemUtils.copyTreesBelow(topDir, aDir);
+      fail("Should not be able to copy a directory to a subdir");
+    } catch (IllegalArgumentException expected) {
+      assertEquals("/top-dir/a-dir is a subdirectory of /top-dir", expected.getMessage());
+    }
+  }
+
+  @Test
+  public void testCopyFileAsDirectoryTree() throws IOException {
+    createTestDirectoryTree();
+    try {
+      FileSystemUtils.copyTreesBelow(file1, aDir);
+      fail("Should not be able to copy a file with copyDirectory method");
+    } catch (IOException expected) {
+      assertEquals("/top-dir/file-1 (Not a directory)", expected.getMessage());
+    }
+  }
+
+  @Test
+  public void testCopyTreesBelowToFile() throws IOException {
+    createTestDirectoryTree();
+    Path copyDir = fileSystem.getPath("/my-dir");
+    Path copySubDir = fileSystem.getPath("/my-dir/subdir");
+    FileSystemUtils.createDirectoryAndParents(copySubDir);
+    try {
+      FileSystemUtils.copyTreesBelow(copyDir, file4);
+      fail("Should not be able to copy a directory to a file");
+    } catch (IOException expected) {
+      assertEquals("/file-4 (Not a directory)", expected.getMessage());
+    }
+  }
+
+  @Test
+  public void testCopyTreesBelowFromUnexistingDir() throws IOException {
+    createTestDirectoryTree();
+
+    try {
+      Path unexistingDir = fileSystem.getPath("/unexisting-dir");
+      FileSystemUtils.copyTreesBelow(unexistingDir, aDir);
+      fail("Should not be able to copy from an unexisting path");
+    } catch (FileNotFoundException expected) {
+      assertEquals("/unexisting-dir (No such file or directory)", expected.getMessage());
+    }
+  }
+
+  @Test
+  public void testTraverseTree() throws IOException {
+    createTestDirectoryTree();
+
+    Collection<Path> paths = traverseTree(topDir, new Predicate<Path>() {
+      @Override
+      public boolean apply(Path p) {
+        return !p.getPathString().contains("a-dir");
+      }
+    });
+    assertThat(paths).containsExactly(file1, file2);
+  }
+
+  @Test
+  public void testTraverseTreeDeep() throws IOException {
+    createTestDirectoryTree();
+
+    Collection<Path> paths = traverseTree(topDir,
+        Predicates.alwaysTrue());
+    assertThat(paths).containsExactly(aDir,
+        file3,
+        innerDir,
+        link1,
+        file1,
+        file2,
+        dirLink);
+  }
+
+  @Test
+  public void testTraverseTreeLinkDir() throws IOException {
+    // Use a new little tree for this test:
+    //  top-dir/
+    //    dir-link2 => linked-dir
+    //  linked-dir/
+    //    file
+    topDir = fileSystem.getPath("/top-dir");
+    Path dirLink2 = fileSystem.getPath("/top-dir/dir-link2");
+    Path linkedDir = fileSystem.getPath("/linked-dir");
+    Path linkedDirFile = fileSystem.getPath("/top-dir/dir-link2/file");
+
+    topDir.createDirectory();
+    linkedDir.createDirectory();
+    dirLink2.createSymbolicLink(linkedDir);  // simple symlink
+    FileSystemUtils.createEmptyFile(linkedDirFile);  // created through the link
+
+    // traverseTree doesn't follow links:
+    Collection<Path> paths = traverseTree(topDir, Predicates.alwaysTrue());
+    assertThat(paths).containsExactly(dirLink2);
+
+    paths = traverseTree(linkedDir, Predicates.alwaysTrue());
+    assertThat(paths).containsExactly(fileSystem.getPath("/linked-dir/file"));
+  }
+
+  @Test
+  public void testDeleteTreeCommandDeletesTree() throws IOException {
+    createTestDirectoryTree();
+    Path toDelete = topDir;
+    deleteTree(toDelete);
+
+    assertTrue(file4.exists());
+    assertFalse(topDir.exists());
+    assertFalse(file1.exists());
+    assertFalse(file2.exists());
+    assertFalse(aDir.exists());
+    assertFalse(file3.exists());
+  }
+
+  @Test
+  public void testDeleteTreeCommandsDeletesUnreadableDirectories() throws IOException {
+    createTestDirectoryTree();
+    Path toDelete = topDir;
+
+    try {
+      aDir.setReadable(false);
+    } catch (UnsupportedOperationException e) {
+      // For file systems that do not support setting readable attribute to
+      // false, this test is simply skipped.
+
+      return;
+    }
+
+    deleteTree(toDelete);
+    assertFalse(topDir.exists());
+    assertFalse(aDir.exists());
+
+  }
+
+  @Test
+  public void testDeleteTreeCommandDoesNotFollowLinksOut() throws IOException {
+    createTestDirectoryTree();
+    Path toDelete = topDir;
+    Path outboundLink = fileSystem.getPath("/top-dir/outbound-link");
+    outboundLink.createSymbolicLink(file4);
+
+    deleteTree(toDelete);
+
+    assertTrue(file4.exists());
+    assertFalse(topDir.exists());
+    assertFalse(file1.exists());
+    assertFalse(file2.exists());
+    assertFalse(aDir.exists());
+    assertFalse(file3.exists());
+  }
+
+  @Test
+  public void testDeleteTreesBelowNotPrefixed() throws IOException {
+    createTestDirectoryTree();
+    deleteTreesBelowNotPrefixed(topDir, new String[] { "file-"});
+    assertTrue(file1.exists());
+    assertTrue(file2.exists());
+    assertFalse(aDir.exists());
+  }
+
+  @Test
+  public void testCreateDirectories() throws IOException {
+    Path mainPath = fileSystem.getPath("/some/where/deep/in/the/hierarchy");
+    assertTrue(createDirectoryAndParents(mainPath));
+    assertTrue(mainPath.exists());
+    assertFalse(createDirectoryAndParents(mainPath));
+  }
+
+  @Test
+  public void testCreateDirectoriesWhenAncestorIsFile() throws IOException {
+    Path somewhereDeepIn = fileSystem.getPath("/somewhere/deep/in");
+    assertTrue(createDirectoryAndParents(somewhereDeepIn.getParentDirectory()));
+    FileSystemUtils.createEmptyFile(somewhereDeepIn);
+    Path theHierarchy = somewhereDeepIn.getChild("the-hierarchy");
+    try {
+      createDirectoryAndParents(theHierarchy);
+      fail();
+    } catch (IOException e) {
+      assertEquals("/somewhere/deep/in (Not a directory)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCreateDirectoriesWhenSymlinkToDir() throws IOException {
+    Path somewhereDeepIn = fileSystem.getPath("/somewhere/deep/in");
+    assertTrue(createDirectoryAndParents(somewhereDeepIn));
+    Path realDir = fileSystem.getPath("/real/dir");
+    assertTrue(createDirectoryAndParents(realDir));
+
+    Path theHierarchy = somewhereDeepIn.getChild("the-hierarchy");
+    theHierarchy.createSymbolicLink(realDir);
+
+    assertFalse(createDirectoryAndParents(theHierarchy));
+  }
+
+  @Test
+  public void testCreateDirectoriesWhenSymlinkEmbedded() throws IOException {
+    Path somewhereDeepIn = fileSystem.getPath("/somewhere/deep/in");
+    assertTrue(createDirectoryAndParents(somewhereDeepIn));
+    Path realDir = fileSystem.getPath("/real/dir");
+    assertTrue(createDirectoryAndParents(realDir));
+
+    Path the = somewhereDeepIn.getChild("the");
+    the.createSymbolicLink(realDir);
+
+    Path theHierarchy = somewhereDeepIn.getChild("hierarchy");
+    assertTrue(createDirectoryAndParents(theHierarchy));
+  }
+
+  PathFragment createPkg(Path rootA, Path rootB, String pkg) throws IOException {
+    if (rootA != null) {
+      createDirectoryAndParents(rootA.getRelative(pkg));
+      FileSystemUtils.createEmptyFile(rootA.getRelative(pkg).getChild("file"));
+    }
+    if (rootB != null) {
+      createDirectoryAndParents(rootB.getRelative(pkg));
+      FileSystemUtils.createEmptyFile(rootB.getRelative(pkg).getChild("file"));
+    }
+    return new PathFragment(pkg);
+  }
+
+  void assertLinksTo(Path fromRoot, Path toRoot, String relpart) throws IOException {
+    assertTrue(fromRoot.getRelative(relpart).isSymbolicLink());
+    assertEquals(toRoot.getRelative(relpart).asFragment(),
+                 fromRoot.getRelative(relpart).readSymbolicLink());
+  }
+
+  void assertIsDir(Path root, String relpart) {
+    assertTrue(root.getRelative(relpart).isDirectory(Symlinks.NOFOLLOW));
+  }
+
+  void dumpTree(Path root, PrintStream out) throws IOException {
+    out.println("\n" + root);
+    for (Path p : FileSystemUtils.traverseTree(root, Predicates.alwaysTrue())) {
+      if (p.isDirectory(Symlinks.NOFOLLOW)) {
+        out.println("  " + p + "/");
+      } else if (p.isSymbolicLink()) {
+        out.println("  " + p + " => " + p.readSymbolicLink());
+      } else {
+        out.println("  " + p + " [" + p.resolveSymbolicLinks() + "]");
+      }
+    }
+  }
+
+  @Test
+  public void testPlantLinkForest() throws IOException {
+    Path rootA = fileSystem.getPath("/A");
+    Path rootB = fileSystem.getPath("/B");
+
+    ImmutableMap<PathFragment, Path> packageRootMap = ImmutableMap.<PathFragment, Path>builder()
+        .put(createPkg(rootA, rootB, "pkgA"), rootA)
+        .put(createPkg(rootA, rootB, "dir1/pkgA"), rootA)
+        .put(createPkg(rootA, rootB, "dir1/pkgB"), rootB)
+        .put(createPkg(rootA, rootB, "dir2/pkg"), rootA)
+        .put(createPkg(rootA, rootB, "dir2/pkg/pkg"), rootB)
+        .put(createPkg(rootA, rootB, "pkgB"), rootB)
+        .put(createPkg(rootA, rootB, "pkgB/dir/pkg"), rootA)
+        .put(createPkg(rootA, rootB, "pkgB/pkg"), rootA)
+        .put(createPkg(rootA, rootB, "pkgB/pkg/pkg"), rootA)
+        .build();
+    createPkg(rootA, rootB, "pkgB/dir");  // create a file in there
+
+    //dumpTree(rootA, System.err);
+    //dumpTree(rootB, System.err);
+
+    Path linkRoot = fileSystem.getPath("/linkRoot");
+    createDirectoryAndParents(linkRoot);
+    plantLinkForest(packageRootMap, linkRoot);
+
+    //dumpTree(linkRoot, System.err);
+
+    assertLinksTo(linkRoot, rootA, "pkgA");
+    assertIsDir(linkRoot, "dir1");
+    assertLinksTo(linkRoot, rootA, "dir1/pkgA");
+    assertLinksTo(linkRoot, rootB, "dir1/pkgB");
+    assertIsDir(linkRoot, "dir2");
+    assertIsDir(linkRoot, "dir2/pkg");
+    assertLinksTo(linkRoot, rootA, "dir2/pkg/file");
+    assertLinksTo(linkRoot, rootB, "dir2/pkg/pkg");
+    assertIsDir(linkRoot, "pkgB");
+    assertIsDir(linkRoot, "pkgB/dir");
+    assertLinksTo(linkRoot, rootB, "pkgB/dir/file");
+    assertLinksTo(linkRoot, rootA, "pkgB/dir/pkg");
+    assertLinksTo(linkRoot, rootA, "pkgB/pkg");
+  }
+
+  @Test
+  public void testWriteIsoLatin1() throws Exception {
+    Path file = fileSystem.getPath("/does/not/exist/yet.txt");
+    FileSystemUtils.writeIsoLatin1(file, "Line 1", "Line 2", "Line 3");
+    String expected = "Line 1\nLine 2\nLine 3\n";
+    String actual = new String(FileSystemUtils.readContentAsLatin1(file));
+    assertEquals(expected, actual);
+  }
+
+  @Test
+  public void testWriteLinesAs() throws Exception {
+    Path file = fileSystem.getPath("/does/not/exist/yet.txt");
+    FileSystemUtils.writeLinesAs(file, UTF_8, "\u00F6"); // an oe umlaut
+    byte[] expected = new byte[] {(byte) 0xC3, (byte) 0xB6, 0x0A};//"\u00F6\n";
+    byte[] actual = FileSystemUtils.readContent(file);
+    assertArrayEquals(expected, actual);
+  }
+
+  @Test
+  public void testGetFileSystem() throws Exception {
+    Path mountTable = fileSystem.getPath("/proc/mounts");
+    FileSystemUtils.writeIsoLatin1(mountTable,
+        "/dev/sda1 / ext2 blah 0 0",
+        "/dev/mapper/_dev_sda6 /usr/local/google ext3 blah 0 0",
+        "devshm /dev/shm tmpfs blah 0 0",
+        "/dev/fuse /fuse/mnt fuse blah 0 0",
+        "mtvhome22.nfs:/vol/mtvhome22/johndoe /home/johndoe nfs blah 0 0",
+        "/dev/foo /foo dummy_foo blah 0 0",
+        "/dev/foobar /foobar dummy_foobar blah 0 0",
+        "proc proc proc rw,noexec,nosuid,nodev 0 0");
+    Path path = fileSystem.getPath("/usr/local/google/_blaze");
+    FileSystemUtils.createDirectoryAndParents(path);
+    assertEquals("ext3", FileSystemUtils.getFileSystem(path));
+
+    // Should match the root "/"
+    path = fileSystem.getPath("/usr/local/tmp");
+    FileSystemUtils.createDirectoryAndParents(path);
+    assertEquals("ext2", FileSystemUtils.getFileSystem(path));
+
+    // Make sure we don't consider /foobar matches /foo
+    path = fileSystem.getPath("/foo");
+    FileSystemUtils.createDirectoryAndParents(path);
+    assertEquals("dummy_foo", FileSystemUtils.getFileSystem(path));
+    path = fileSystem.getPath("/foobar");
+    FileSystemUtils.createDirectoryAndParents(path);
+    assertEquals("dummy_foobar", FileSystemUtils.getFileSystem(path));
+
+    path = fileSystem.getPath("/dev/shm/blaze");
+    FileSystemUtils.createDirectoryAndParents(path);
+    assertEquals("tmpfs", FileSystemUtils.getFileSystem(path));
+
+    Path fusePath = fileSystem.getPath("/fuse/mnt/tmp");
+    FileSystemUtils.createDirectoryAndParents(fusePath);
+    assertEquals("fuse", FileSystemUtils.getFileSystem(fusePath));
+
+    // Create a symlink and make sure it gives the file system of the symlink target.
+    path = fileSystem.getPath("/usr/local/google/_blaze/out");
+    path.createSymbolicLink(fusePath);
+    assertEquals("fuse", FileSystemUtils.getFileSystem(path));
+
+    // Non existent path should return "unknown"
+    path = fileSystem.getPath("/does/not/exist");
+    assertEquals("unknown", FileSystemUtils.getFileSystem(path));
+  }
+
+  @Test
+  public void testStartsWithAnySuccess() throws Exception {
+    PathFragment a = new PathFragment("a");
+    assertTrue(FileSystemUtils.startsWithAny(a,
+        Arrays.asList(new PathFragment("b"), new PathFragment("a"))));
+  }
+
+  @Test
+  public void testStartsWithAnyNotFound() throws Exception {
+    PathFragment a = new PathFragment("a");
+    assertFalse(FileSystemUtils.startsWithAny(a,
+        Arrays.asList(new PathFragment("b"), new PathFragment("c"))));
+  }
+
+  @Test
+  public void testIterateLines() throws Exception {
+    Path file = fileSystem.getPath("/test.txt");
+    FileSystemUtils.writeContent(file, ISO_8859_1, "a\nb");
+    assertEquals(Arrays.asList("a", "b"),
+        Lists.newArrayList(FileSystemUtils.iterateLinesAsLatin1(file)));
+
+    FileSystemUtils.writeContent(file, ISO_8859_1, "a\rb");
+    assertEquals(Arrays.asList("a", "b"),
+        Lists.newArrayList(FileSystemUtils.iterateLinesAsLatin1(file)));
+
+    FileSystemUtils.writeContent(file, ISO_8859_1, "a\r\nb");
+    assertEquals(Arrays.asList("a", "b"),
+        Lists.newArrayList(FileSystemUtils.iterateLinesAsLatin1(file)));
+  }
+
+  @Test
+  public void testEnsureSymbolicLinkDoesNotMakeUnnecessaryChanges() throws Exception {
+    PathFragment target = new PathFragment("/b");
+    Path file = fileSystem.getPath("/a");
+    file.createSymbolicLink(target);
+    long prevTimeMillis = clock.currentTimeMillis();
+    clock.advanceMillis(1000);
+    FileSystemUtils.ensureSymbolicLink(file, target);
+    long timestamp = file.getLastModifiedTime(Symlinks.NOFOLLOW);
+    assertTrue(timestamp == prevTimeMillis);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemsTest.java
new file mode 100644
index 0000000..88a000f
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemsTest.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+
+import com.google.devtools.build.lib.vfs.util.FileSystems;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * This class handles the tests for the FileSystems class.
+ */
+@RunWith(JUnit4.class)
+public class FileSystemsTest {
+
+  @Test
+  public void testFileSystemsCreatesOnlyOneDefaultNative() {
+    assertSame(FileSystems.initDefaultAsNative(),
+               FileSystems.initDefaultAsNative());
+  }
+
+  @Test
+  public void testFileSystemsCreatesOnlyOneDefaultJavaIo() {
+    assertSame(FileSystems.initDefaultAsJavaIo(),
+               FileSystems.initDefaultAsJavaIo());
+  }
+
+  @Test
+  public void testFileSystemsCanSwitchDefaults() {
+    assertNotSame(FileSystems.initDefaultAsNative(),
+                  FileSystems.initDefaultAsJavaIo());
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/GlobTest.java b/src/test/java/com/google/devtools/build/lib/vfs/GlobTest.java
new file mode 100644
index 0000000..37b7dc4
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/GlobTest.java
@@ -0,0 +1,417 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Tests {@link UnixGlob}
+ */
+@RunWith(JUnit4.class)
+public class GlobTest {
+
+  private Path tmpPath;
+  private FileSystem fs;
+  @Before
+  public void setUp() throws Exception {
+    fs = new InMemoryFileSystem();
+    tmpPath = fs.getPath("/globtmp");
+    for (String dir : ImmutableList.of("foo/bar/wiz",
+                         "foo/barnacle/wiz",
+                         "food/barnacle/wiz",
+                         "fool/barnacle/wiz")) {
+      FileSystemUtils.createDirectoryAndParents(tmpPath.getRelative(dir));
+    }
+    FileSystemUtils.createEmptyFile(tmpPath.getRelative("foo/bar/wiz/file"));
+  }
+
+  @Test
+  public void testQuestionMarkMatch() throws Exception {
+    assertGlobMatches("foo?", /* => */"food", "fool");
+  }
+
+  @Test
+  public void testQuestionMarkNoMatch() throws Exception {
+    assertGlobMatches("food/bar?" /* => nothing */);
+  }
+
+  @Test
+  public void testStartsWithStar() throws Exception {
+    assertGlobMatches("*oo", /* => */"foo");
+  }
+
+  @Test
+  public void testStartsWithStarWithMiddleStar() throws Exception {
+    assertGlobMatches("*f*o", /* => */"foo");
+  }
+
+  @Test
+  public void testEndsWithStar() throws Exception {
+    assertGlobMatches("foo*", /* => */"foo", "food", "fool");
+  }
+
+  @Test
+  public void testEndsWithStarWithMiddleStar() throws Exception {
+    assertGlobMatches("f*oo*", /* => */"foo", "food", "fool");
+  }
+
+  @Test
+  public void testMiddleStar() throws Exception {
+    assertGlobMatches("f*o", /* => */"foo");
+  }
+
+  @Test
+  public void testTwoMiddleStars() throws Exception {
+    assertGlobMatches("f*o*o", /* => */"foo");
+  }
+
+  @Test
+  public void testSingleStarPatternWithNamedChild() throws Exception {
+    assertGlobMatches("*/bar", /* => */"foo/bar");
+  }
+
+  @Test
+  public void testSingleStarPatternWithChildGlob() throws Exception {
+    assertGlobMatches("*/bar*", /* => */
+        "foo/bar", "foo/barnacle", "food/barnacle", "fool/barnacle");
+  }
+
+  @Test
+  public void testSingleStarAsChildGlob() throws Exception {
+    assertGlobMatches("foo/*/wiz", /* => */"foo/bar/wiz", "foo/barnacle/wiz");
+  }
+
+  @Test
+  public void testNoAsteriskAndFilesDontExist() throws Exception {
+    // Note un-UNIX like semantics:
+    assertGlobMatches("ceci/n'est/pas/une/globbe" /* => nothing */);
+  }
+
+  @Test
+  public void testSingleAsteriskUnderNonexistentDirectory() throws Exception {
+    // Note un-UNIX like semantics:
+    assertGlobMatches("not-there/*" /* => nothing */);
+  }
+
+  @Test
+  public void testGlobWithNonExistentBase() throws Exception {
+    Collection<Path> globResult = UnixGlob.forPath(fs.getPath("/does/not/exist"))
+        .addPattern("*.txt")
+        .globInterruptible();
+    assertEquals(0, globResult.size());
+  }
+
+  @Test
+  public void testGlobUnderFile() throws Exception {
+    assertGlobMatches("foo/bar/wiz/file/*" /* => nothing */);
+  }
+
+  @Test
+  public void testSingleFileExclude() throws Exception {
+    assertGlobWithExcludeMatches("*", "food", "foo", "fool");
+  }
+
+  @Test
+  public void testExcludeAll() throws Exception {
+    assertGlobWithExcludeMatches("*", "*");
+  }
+
+  @Test
+  public void testExcludeAllButNoMatches() throws Exception {
+    assertGlobWithExcludeMatches("not-there", "*");
+  }
+
+  @Test
+  public void testSingleFileExcludeDoesntMatch() throws Exception {
+    assertGlobWithExcludeMatches("food", "foo", "food");
+  }
+
+  @Test
+  public void testSingleFileExcludeForDirectoryWithChildGlob()
+      throws Exception {
+    assertGlobWithExcludeMatches("foo/*", "foo", "foo/bar", "foo/barnacle");
+  }
+
+  @Test
+  public void testChildGlobWithChildExclude()
+      throws Exception {
+    assertGlobWithExcludeMatches("foo/*", "foo/*");
+    assertGlobWithExcludeMatches("foo/bar", "foo/*");
+    assertGlobWithExcludeMatches("foo/bar", "foo/bar");
+    assertGlobWithExcludeMatches("foo/bar", "*/bar");
+    assertGlobWithExcludeMatches("foo/bar", "*/*");
+    assertGlobWithExcludeMatches("foo/bar/wiz", "*/*/*");
+    assertGlobWithExcludeMatches("foo/bar/wiz", "foo/*/*");
+    assertGlobWithExcludeMatches("foo/bar/wiz", "foo/bar/*");
+    assertGlobWithExcludeMatches("foo/bar/wiz", "foo/bar/wiz");
+    assertGlobWithExcludeMatches("foo/bar/wiz", "*/bar/wiz");
+    assertGlobWithExcludeMatches("foo/bar/wiz", "*/*/wiz");
+    assertGlobWithExcludeMatches("foo/bar/wiz", "foo/*/wiz");
+  }
+
+  private void assertGlobMatches(String pattern, String... expecteds)
+      throws Exception {
+    assertGlobWithExcludesMatches(
+        Collections.singleton(pattern), Collections.<String>emptyList(),
+        expecteds);
+  }
+
+  private void assertGlobMatches(Collection<String> pattern,
+                                 String... expecteds)
+      throws Exception {
+    assertGlobWithExcludesMatches(pattern, Collections.<String>emptyList(),
+        expecteds);
+  }
+
+  private void assertGlobWithExcludeMatches(String pattern, String exclude,
+                                            String... expecteds)
+      throws Exception {
+    assertGlobWithExcludesMatches(
+        Collections.singleton(pattern), Collections.singleton(exclude),
+        expecteds);
+  }
+
+  private void assertGlobWithExcludesMatches(Collection<String> pattern,
+                                             Collection<String> excludes,
+                                             String... expecteds)
+      throws Exception {
+    MoreAsserts.assertSameContents(resolvePaths(expecteds),
+        new UnixGlob.Builder(tmpPath)
+            .addPatterns(pattern)
+            .addExcludes(excludes)
+            .globInterruptible());
+  }
+
+  private Set<Path> resolvePaths(String... relativePaths) {
+    Set<Path> expectedFiles = new HashSet<>();
+    for (String expected : relativePaths) {
+      Path file = expected.equals(".")
+          ? tmpPath
+          : tmpPath.getRelative(expected);
+      expectedFiles.add(file);
+    }
+    return expectedFiles;
+  }
+
+  @Test
+  public void testGlobWithoutWildcardsDoesNotCallReaddir() throws Exception {
+    UnixGlob.FilesystemCalls syscalls = new UnixGlob.FilesystemCalls() {
+      @Override
+      public FileStatus statNullable(Path path, Symlinks symlinks) {
+        return UnixGlob.DEFAULT_SYSCALLS.statNullable(path, symlinks);
+      }
+
+      @Override
+      public Collection<Dirent> readdir(Path path, Symlinks symlinks) {
+        throw new IllegalStateException();
+      }
+    };
+
+    MoreAsserts.assertSameContents(ImmutableList.of(tmpPath.getRelative("foo/bar/wiz/file")),
+        new UnixGlob.Builder(tmpPath)
+            .addPattern("foo/bar/wiz/file")
+            .setFilesystemCalls(new AtomicReference<>(syscalls))
+            .glob());
+  }
+
+  @Test
+  public void testIllegalPatterns() throws Exception {
+    assertIllegalPattern("(illegal) pattern");
+    assertIllegalPattern("[illegal pattern");
+    assertIllegalPattern("}illegal pattern");
+    assertIllegalPattern("foo**bar");
+    assertIllegalPattern("");
+    assertIllegalPattern(".");
+    assertIllegalPattern("/foo");
+    assertIllegalPattern("./foo");
+    assertIllegalPattern("foo/");
+    assertIllegalPattern("foo/./bar");
+    assertIllegalPattern("../foo/bar");
+    assertIllegalPattern("foo//bar");
+  }
+
+  /**
+   * Tests that globs can contain Java regular expression special characters
+   */
+  @Test
+  public void testSpecialRegexCharacter() throws Exception {
+    Path tmpPath2 = fs.getPath("/globtmp2");
+    FileSystemUtils.createDirectoryAndParents(tmpPath2);
+    Path aDotB = tmpPath2.getChild("a.b");
+    FileSystemUtils.createEmptyFile(aDotB);
+    FileSystemUtils.createEmptyFile(tmpPath2.getChild("aab"));
+    // Note: this contains two asterisks because otherwise a RE is not built,
+    // as an optimization.
+    assertThat(UnixGlob.forPath(tmpPath2).addPattern("*a.b*").globInterruptible()).containsExactly(
+        aDotB);
+  }
+
+  @Test
+  public void testMatchesCallWithNoCache() {
+    assertTrue(UnixGlob.matches("*a*b", "CaCb", null));
+  }
+
+  @Test
+  public void testMultiplePatterns() throws Exception {
+    assertGlobMatches(Lists.newArrayList("foo", "fool"), "foo", "fool");
+  }
+
+  @Test
+  public void testMultiplePatternsWithExcludes() throws Exception {
+    assertGlobWithExcludesMatches(Lists.newArrayList("foo", "foo?"),
+        Lists.newArrayList("fool"), "foo", "food");
+  }
+
+  @Test
+  public void testMultiplePatternsWithOverlap() throws Exception {
+    assertGlobMatchesAnyOrder(Lists.newArrayList("food", "foo?"),
+                              "food", "fool");
+    assertGlobMatchesAnyOrder(Lists.newArrayList("food", "?ood", "f??d"),
+                              "food");
+    assertThat(resolvePaths("food", "fool", "foo")).containsExactlyElementsIn(
+        new UnixGlob.Builder(tmpPath).addPatterns("food", "xxx", "*").glob());
+
+  }
+
+  private void assertGlobMatchesAnyOrder(ArrayList<String> patterns,
+                                         String... paths) throws Exception {
+    assertThat(resolvePaths(paths)).containsExactlyElementsIn(
+        new UnixGlob.Builder(tmpPath).addPatterns(patterns).globInterruptible());
+  }
+
+  /**
+   * Tests that a glob returns files in sorted order.
+   */
+  @Test
+  public void testGlobEntriesAreSorted() throws Exception {
+    Collection<Path> directoryEntries = tmpPath.getDirectoryEntries();
+    List<Path> globResult = new UnixGlob.Builder(tmpPath)
+        .addPattern("*")
+        .setExcludeDirectories(false)
+        .globInterruptible();
+    assertThat(Ordering.natural().sortedCopy(directoryEntries)).containsExactlyElementsIn(
+        globResult).inOrder();
+  }
+
+  private void assertIllegalPattern(String pattern) throws Exception {
+    try {
+      new UnixGlob.Builder(tmpPath)
+          .addPattern(pattern)
+          .globInterruptible();
+      fail();
+    } catch (IllegalArgumentException e) {
+      MoreAsserts.assertContainsRegex("in glob pattern", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testHiddenFiles() throws Exception {
+    for (String dir : ImmutableList.of(".hidden", "..also.hidden", "not.hidden")) {
+      FileSystemUtils.createDirectoryAndParents(tmpPath.getRelative(dir));
+    }
+    // Note that these are not in the result: ".", ".."
+    assertGlobMatches("*", "not.hidden", "foo", "fool", "food", ".hidden", "..also.hidden");
+    assertGlobMatches("*.hidden", "not.hidden");
+  }
+
+  @Test
+  public void testCheckCanBeInterrupted() throws Exception {
+    final Thread mainThread = Thread.currentThread();
+    final ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
+
+    Predicate<Path> interrupterPredicate = new Predicate<Path>() {
+      @Override
+      public boolean apply(Path input) {
+        mainThread.interrupt();
+        return true;
+      }
+    };
+
+    try {
+      new UnixGlob.Builder(tmpPath)
+          .addPattern("**")
+          .setDirectoryFilter(interrupterPredicate)
+          .setThreadPool(executor)
+          .globInterruptible();
+      fail();  // Should have received InterruptedException
+    } catch (InterruptedException e) {
+      // good
+    }
+
+    assertFalse(executor.isShutdown());
+    executor.shutdown();
+    assertTrue(executor.awaitTermination(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS));
+  }
+
+  @Test
+  public void testCheckCannotBeInterrupted() throws Exception {
+    final Thread mainThread = Thread.currentThread();
+    final ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
+    final AtomicBoolean sentInterrupt = new AtomicBoolean(false);
+
+    Predicate<Path> interrupterPredicate = new Predicate<Path>() {
+      @Override
+      public boolean apply(Path input) {
+        if (!sentInterrupt.getAndSet(true)) {
+          mainThread.interrupt();
+        }
+        return true;
+      }
+    };
+
+    List<Path> result = new UnixGlob.Builder(tmpPath)
+        .addPatterns("**", "*")
+        .setDirectoryFilter(interrupterPredicate).setThreadPool(executor).glob();
+
+    // In the non-interruptible case, the interrupt bit should be set, but the
+    // glob should return the correct set of full results.
+    assertTrue(Thread.interrupted());
+    MoreAsserts.assertSameContents(resolvePaths(".", "foo", "foo/bar", "foo/bar/wiz",
+        "foo/bar/wiz/file", "foo/barnacle", "foo/barnacle/wiz", "food", "food/barnacle",
+        "food/barnacle/wiz", "fool", "fool/barnacle", "fool/barnacle/wiz"), result);
+
+    assertFalse(executor.isShutdown());
+    executor.shutdown();
+    assertTrue(executor.awaitTermination(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/JavaIoFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/JavaIoFileSystemTest.java
new file mode 100644
index 0000000..fdb6283
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/JavaIoFileSystemTest.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for the {@link JavaIoFileSystem}. That file system by itself is not
+ * capable of creating symlinks; use the unix one to create them, so that the
+ * test can check that the file system handles their existence correctly.
+ */
+@RunWith(JUnit4.class)
+public class JavaIoFileSystemTest extends SymlinkAwareFileSystemTest {
+
+  @Override
+  public FileSystem getFreshFileSystem() {
+    return new JavaIoFileSystem();
+  }
+
+  // The tests are just inherited from the FileSystemTest
+
+  // JavaIoFileSystem incorrectly throws a FileNotFoundException for all IO errors. This means that
+  // statIfFound incorrectly suppresses those errors.
+  @Override
+  @Test
+  public void testBadPermissionsThrowsExceptionOnStatIfFound() {}
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/ModifiedFileSetTest.java b/src/test/java/com/google/devtools/build/lib/vfs/ModifiedFileSetTest.java
new file mode 100644
index 0000000..96001df
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/ModifiedFileSetTest.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link ModifiedFileSet}.
+ */
+@RunWith(JUnit4.class)
+public class ModifiedFileSetTest {
+
+  @Test
+  public void testHashCodeAndEqualsContract() throws Exception {
+    PathFragment fragA = new PathFragment("a");
+    PathFragment fragB = new PathFragment("b");
+
+    ModifiedFileSet empty1 = ModifiedFileSet.NOTHING_MODIFIED;
+    ModifiedFileSet empty2 = ModifiedFileSet.builder().build();
+    ModifiedFileSet empty3 = ModifiedFileSet.builder().modifyAll(
+        ImmutableList.<PathFragment>of()).build();
+
+    ModifiedFileSet nonEmpty1 = ModifiedFileSet.builder().modifyAll(
+        ImmutableList.of(fragA, fragB)).build();
+    ModifiedFileSet nonEmpty2 = ModifiedFileSet.builder().modifyAll(
+        ImmutableList.of(fragB, fragA)).build();
+    ModifiedFileSet nonEmpty3 = ModifiedFileSet.builder().modify(fragA).modify(fragB).build();
+    ModifiedFileSet nonEmpty4 = ModifiedFileSet.builder().modify(fragB).modify(fragA).build();
+
+    ModifiedFileSet everythingModified = ModifiedFileSet.EVERYTHING_MODIFIED;
+
+    new EqualsTester()
+        .addEqualityGroup(empty1, empty2, empty3)
+        .addEqualityGroup(nonEmpty1, nonEmpty2, nonEmpty3, nonEmpty4)
+        .addEqualityGroup(everythingModified)
+        .testEquals();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java
new file mode 100644
index 0000000..9ab9bfa
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java
@@ -0,0 +1,481 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.testing.EqualsTester;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This class tests the functionality of the PathFragment.
+ */
+@RunWith(JUnit4.class)
+public class PathFragmentTest {
+  @Test
+  public void testMergeFourPathsWithAbsolute() {
+    assertEquals(new PathFragment("x/y/z/a/b/c/d/e"),
+        new PathFragment(new PathFragment("x/y"), new PathFragment("z/a"),
+            new PathFragment("/b/c"), // absolute!
+            new PathFragment("d/e")));
+  }
+
+  @Test
+  public void testEqualsAndHashCode() {
+    InMemoryFileSystem filesystem = new InMemoryFileSystem();
+
+    new EqualsTester()
+        .addEqualityGroup(new PathFragment("../relative/path"),
+                          new PathFragment("../relative/path"),
+                          new PathFragment(new File("../relative/path")))
+        .addEqualityGroup(new PathFragment("something/else"))
+        .addEqualityGroup(new PathFragment("/something/else"))
+        .addEqualityGroup(new PathFragment("/"),
+                          new PathFragment("//////"))
+        .addEqualityGroup(new PathFragment(""))
+        .addEqualityGroup(filesystem.getRootDirectory())  // A Path object.
+        .testEquals();
+  }
+
+  @Test
+  public void testHashCodeCache() {
+    PathFragment relativePath = new PathFragment("../relative/path");
+    PathFragment rootPath = new PathFragment("/");
+
+    int oldResult = relativePath.hashCode();
+    int rootResult = rootPath.hashCode();
+    assertEquals(oldResult, relativePath.hashCode());
+    assertEquals(rootResult, rootPath.hashCode());
+  }
+
+  private void checkRelativeTo(String path, String base) {
+    PathFragment relative = new PathFragment(path).relativeTo(base);
+    assertEquals(new PathFragment(path), new PathFragment(base).getRelative(relative).normalize());
+  }
+
+  @Test
+  public void testRelativeTo() {
+    assertPath("bar/baz", new PathFragment("foo/bar/baz").relativeTo("foo"));
+    assertPath("bar/baz", new PathFragment("/foo/bar/baz").relativeTo("/foo"));
+    assertPath("baz", new PathFragment("foo/bar/baz").relativeTo("foo/bar"));
+    assertPath("baz", new PathFragment("/foo/bar/baz").relativeTo("/foo/bar"));
+    assertPath("foo", new PathFragment("/foo").relativeTo("/"));
+    assertPath("foo", new PathFragment("foo").relativeTo(""));
+    assertPath("foo/bar", new PathFragment("foo/bar").relativeTo(""));
+
+    checkRelativeTo("foo/bar/baz", "foo");
+    checkRelativeTo("/foo/bar/baz", "/foo");
+    checkRelativeTo("foo/bar/baz", "foo/bar");
+    checkRelativeTo("/foo/bar/baz", "/foo/bar");
+    checkRelativeTo("/foo", "/");
+    checkRelativeTo("foo", "");
+    checkRelativeTo("foo/bar", "");
+  }
+
+  @Test
+  public void testIsAbsolute() {
+    assertTrue(new PathFragment("/absolute/test").isAbsolute());
+    assertFalse(new PathFragment("relative/test").isAbsolute());
+    assertTrue(new PathFragment(new File("/absolute/test")).isAbsolute());
+    assertFalse(new PathFragment(new File("relative/test")).isAbsolute());
+  }
+
+  @Test
+  public void testIsNormalized() {
+    assertTrue(new PathFragment("/absolute/path").isNormalized());
+    assertTrue(new PathFragment("some//path").isNormalized());
+    assertFalse(new PathFragment("some/./path").isNormalized());
+    assertFalse(new PathFragment("../some/path").isNormalized());
+    assertFalse(new PathFragment("some/other/../path").isNormalized());
+    assertTrue(new PathFragment("some/other//tricky..path..").isNormalized());
+    assertTrue(new PathFragment("/some/other//tricky..path..").isNormalized());
+  }
+
+  @Test
+  public void testRootNodeReturnsRootString() {
+    PathFragment rootFragment = new PathFragment("/");
+    assertEquals("/", rootFragment.getPathString());
+  }
+
+  @Test
+  public void testGetPathFragmentDoesNotNormalize() {
+    String nonCanonicalPath = "/a/weird/noncanonical/../path/.";
+    assertEquals(nonCanonicalPath,
+        new PathFragment(nonCanonicalPath).getPathString());
+  }
+
+  @Test
+  public void testGetRelative() {
+    assertEquals("a/b", new PathFragment("a").getRelative("b").getPathString());
+    assertEquals("a/b/c/d", new PathFragment("a/b").getRelative("c/d").getPathString());
+    assertEquals("/a/b", new PathFragment("c/d").getRelative("/a/b").getPathString());
+    assertEquals("a", new PathFragment("a").getRelative("").getPathString());
+    assertEquals("/", new PathFragment("/").getRelative("").getPathString());
+  }
+
+  @Test
+  public void testGetChildWorks() {
+    PathFragment pf = new PathFragment("../some/path");
+    assertEquals(new PathFragment("../some/path/hi"), pf.getChild("hi"));
+  }
+
+  @Test
+  public void testGetChildRejectsInvalidBaseNames() {
+    PathFragment pf = new PathFragment("../some/path");
+    assertGetChildFails(pf, ".");
+    assertGetChildFails(pf, "..");
+    assertGetChildFails(pf, "x/y");
+    assertGetChildFails(pf, "/y");
+    assertGetChildFails(pf, "y/");
+    assertGetChildFails(pf, "");
+  }
+
+  private void assertGetChildFails(PathFragment pf, String baseName) {
+    try {
+      pf.getChild(baseName);
+      fail();
+    } catch (Exception e) { /* Expected. */ }
+  }
+
+  // Tests after here test the canonicalization
+  private void assertRegular(String expected, String actual) {
+    assertEquals(expected, new PathFragment(actual).getPathString()); // compare string forms
+    assertEquals(new PathFragment(expected), new PathFragment(actual)); // compare fragment forms
+  }
+
+  @Test
+  public void testEmptyPathToEmptyPath() {
+    assertRegular("/", "/");
+    assertRegular("", "");
+  }
+
+  @Test
+  public void testRedundantSlashes() {
+    assertRegular("/", "///");
+    assertRegular("/foo/bar", "/foo///bar");
+    assertRegular("/foo/bar", "////foo//bar");
+  }
+
+  @Test
+  public void testSimpleNameToSimpleName() {
+    assertRegular("/foo", "/foo");
+    assertRegular("foo", "foo");
+  }
+
+  @Test
+  public void testSimplePathToSimplePath() {
+    assertRegular("/foo/bar", "/foo/bar");
+    assertRegular("foo/bar", "foo/bar");
+  }
+
+  @Test
+  public void testStripsTrailingSlash() {
+    assertRegular("/foo/bar", "/foo/bar/");
+  }
+
+  @Test
+  public void testGetParentDirectory() {
+    PathFragment fooBarWiz = new PathFragment("foo/bar/wiz");
+    PathFragment fooBar = new PathFragment("foo/bar");
+    PathFragment foo = new PathFragment("foo");
+    PathFragment empty = new PathFragment("");
+    assertEquals(fooBar, fooBarWiz.getParentDirectory());
+    assertEquals(foo, fooBar.getParentDirectory());
+    assertEquals(empty, foo.getParentDirectory());
+    assertNull(empty.getParentDirectory());
+
+    PathFragment fooBarWizAbs = new PathFragment("/foo/bar/wiz");
+    PathFragment fooBarAbs = new PathFragment("/foo/bar");
+    PathFragment fooAbs = new PathFragment("/foo");
+    PathFragment rootAbs = new PathFragment("/");
+    assertEquals(fooBarAbs, fooBarWizAbs.getParentDirectory());
+    assertEquals(fooAbs, fooBarAbs.getParentDirectory());
+    assertEquals(rootAbs, fooAbs.getParentDirectory());
+    assertNull(rootAbs.getParentDirectory());
+
+    // Note, this is surprising but correct behavior:
+    assertEquals(fooBarAbs,
+                 new PathFragment("/foo/bar/..").getParentDirectory());
+  }
+  
+  @Test
+  public void testSegmentsCount() {
+    assertEquals(2, new PathFragment("foo/bar").segmentCount());
+    assertEquals(2, new PathFragment("/foo/bar").segmentCount());
+    assertEquals(2, new PathFragment("foo//bar").segmentCount());
+    assertEquals(2, new PathFragment("/foo//bar").segmentCount());
+    assertEquals(1, new PathFragment("foo/").segmentCount());
+    assertEquals(1, new PathFragment("/foo/").segmentCount());
+    assertEquals(1, new PathFragment("foo").segmentCount());
+    assertEquals(1, new PathFragment("/foo").segmentCount());
+    assertEquals(0, new PathFragment("/").segmentCount());
+    assertEquals(0, new PathFragment("").segmentCount());
+  }
+
+
+  @Test
+  public void testGetSegment() {
+    assertEquals("foo", new PathFragment("foo/bar").getSegment(0));
+    assertEquals("bar", new PathFragment("foo/bar").getSegment(1));
+    assertEquals("foo", new PathFragment("/foo/bar").getSegment(0));
+    assertEquals("bar", new PathFragment("/foo/bar").getSegment(1));
+    assertEquals("foo", new PathFragment("foo/").getSegment(0));
+    assertEquals("foo", new PathFragment("/foo/").getSegment(0));
+    assertEquals("foo", new PathFragment("foo").getSegment(0));
+    assertEquals("foo", new PathFragment("/foo").getSegment(0));
+  }
+
+  @Test
+  public void testBasename() throws Exception {
+    assertEquals("bar", new PathFragment("foo/bar").getBaseName());
+    assertEquals("bar", new PathFragment("/foo/bar").getBaseName());
+    assertEquals("foo", new PathFragment("foo/").getBaseName());
+    assertEquals("foo", new PathFragment("/foo/").getBaseName());
+    assertEquals("foo", new PathFragment("foo").getBaseName());
+    assertEquals("foo", new PathFragment("/foo").getBaseName());
+    assertEquals("", new PathFragment("/").getBaseName());
+    assertEquals("", new PathFragment("").getBaseName());
+  }
+
+  private static void assertPath(String expected, PathFragment actual) {
+    assertEquals(expected, actual.getPathString());
+  }
+
+  @Test
+  public void testReplaceName() throws Exception {
+    assertPath("foo/baz", new PathFragment("foo/bar").replaceName("baz"));
+    assertPath("/foo/baz", new PathFragment("/foo/bar").replaceName("baz"));
+    assertPath("foo", new PathFragment("foo/bar").replaceName(""));
+    assertPath("baz", new PathFragment("foo/").replaceName("baz"));
+    assertPath("/baz", new PathFragment("/foo/").replaceName("baz"));
+    assertPath("baz", new PathFragment("foo").replaceName("baz"));
+    assertPath("/baz", new PathFragment("/foo").replaceName("baz"));
+    assertEquals(null, new PathFragment("/").replaceName("baz"));
+    assertEquals(null, new PathFragment("/").replaceName(""));
+    assertEquals(null, new PathFragment("").replaceName("baz"));
+    assertEquals(null, new PathFragment("").replaceName(""));
+
+    assertPath("foo/bar/baz", new PathFragment("foo/bar").replaceName("bar/baz"));
+    assertPath("foo/bar/baz", new PathFragment("foo/bar").replaceName("bar/baz/"));
+
+    // Absolute path arguments will clobber the original path.
+    assertPath("/absolute", new PathFragment("foo/bar").replaceName("/absolute"));
+    assertPath("/", new PathFragment("foo/bar").replaceName("/"));
+  }
+  @Test
+  public void testSubFragment() throws Exception {
+    assertPath("/foo/bar/baz",
+               new PathFragment("/foo/bar/baz").subFragment(0, 3));
+    assertPath("foo/bar/baz",
+               new PathFragment("foo/bar/baz").subFragment(0, 3));
+    assertPath("/foo/bar",
+               new PathFragment("/foo/bar/baz").subFragment(0, 2));
+    assertPath("bar/baz",
+               new PathFragment("/foo/bar/baz").subFragment(1, 3));
+    assertPath("/foo",
+               new PathFragment("/foo/bar/baz").subFragment(0, 1));
+    assertPath("bar",
+               new PathFragment("/foo/bar/baz").subFragment(1, 2));
+    assertPath("baz", new PathFragment("/foo/bar/baz").subFragment(2, 3));
+    assertPath("/", new PathFragment("/foo/bar/baz").subFragment(0, 0));
+    assertPath("", new PathFragment("foo/bar/baz").subFragment(0, 0));
+    assertPath("", new PathFragment("foo/bar/baz").subFragment(1, 1));
+    try {
+      fail("unexpectedly succeeded: " + new PathFragment("foo/bar/baz").subFragment(3, 2));
+    } catch (IndexOutOfBoundsException e) { /* Expected. */ }
+    try {
+      fail("unexpectedly succeeded: " + new PathFragment("foo/bar/baz").subFragment(4, 4));
+    } catch (IndexOutOfBoundsException e) { /* Expected. */ }
+  }
+
+  @Test
+  public void testStartsWith() {
+    PathFragment foobar = new PathFragment("/foo/bar");
+    PathFragment foobarRelative = new PathFragment("foo/bar");
+
+    // (path, prefix) => true
+    assertTrue(foobar.startsWith(foobar));
+    assertTrue(foobar.startsWith(new PathFragment("/")));
+    assertTrue(foobar.startsWith(new PathFragment("/foo")));
+    assertTrue(foobar.startsWith(new PathFragment("/foo/")));
+    assertTrue(foobar.startsWith(new PathFragment("/foo/bar/")));  // Includes trailing slash.
+
+    // (prefix, path) => false
+    assertFalse(new PathFragment("/foo").startsWith(foobar));
+    assertFalse(new PathFragment("/").startsWith(foobar));
+
+    // (absolute, relative) => false
+    assertFalse(foobar.startsWith(foobarRelative));
+    assertFalse(foobarRelative.startsWith(foobar));
+
+    // (relative path, relative prefix) => true
+    assertTrue(foobarRelative.startsWith(foobarRelative));
+    assertTrue(foobarRelative.startsWith(new PathFragment("foo")));
+    assertTrue(foobarRelative.startsWith(new PathFragment("")));
+
+    // (path, sibling) => false
+    assertFalse(new PathFragment("/foo/wiz").startsWith(foobar));
+    assertFalse(foobar.startsWith(new PathFragment("/foo/wiz")));
+
+    // Does not normalize.
+    PathFragment foodotbar = new PathFragment("foo/./bar");
+    assertTrue(foodotbar.startsWith(foodotbar));
+    assertTrue(foodotbar.startsWith(new PathFragment("foo/.")));
+    assertTrue(foodotbar.startsWith(new PathFragment("foo/./")));
+    assertTrue(foodotbar.startsWith(new PathFragment("foo/./bar")));
+    assertFalse(foodotbar.startsWith(new PathFragment("foo/bar")));
+  }
+
+  @Test
+  public void testEndsWith() {
+    PathFragment foobar = new PathFragment("/foo/bar");
+    PathFragment foobarRelative = new PathFragment("foo/bar");
+
+    // (path, suffix) => true
+    assertTrue(foobar.endsWith(foobar));
+    assertTrue(foobar.endsWith(new PathFragment("bar")));
+    assertTrue(foobar.endsWith(new PathFragment("foo/bar")));
+    assertTrue(foobar.endsWith(new PathFragment("/foo/bar")));
+    assertFalse(foobar.endsWith(new PathFragment("/bar")));
+
+    // (prefix, path) => false
+    assertFalse(new PathFragment("/foo").endsWith(foobar));
+    assertFalse(new PathFragment("/").endsWith(foobar));
+
+    // (suffix, path) => false
+    assertFalse(new PathFragment("/bar").endsWith(foobar));
+    assertFalse(new PathFragment("bar").endsWith(foobar));
+    assertFalse(new PathFragment("").endsWith(foobar));
+
+    // (absolute, relative) => true
+    assertTrue(foobar.endsWith(foobarRelative));
+
+    // (relative, absolute) => false
+    assertFalse(foobarRelative.endsWith(foobar));
+
+    // (relative path, relative prefix) => true
+    assertTrue(foobarRelative.endsWith(foobarRelative));
+    assertTrue(foobarRelative.endsWith(new PathFragment("bar")));
+    assertTrue(foobarRelative.endsWith(new PathFragment("")));
+
+    // (path, sibling) => false
+    assertFalse(new PathFragment("/foo/wiz").endsWith(foobar));
+    assertFalse(foobar.endsWith(new PathFragment("/foo/wiz")));
+  }
+
+  static List<PathFragment> toPaths(List<String> strs) {
+    List<PathFragment> paths = Lists.newArrayList();
+    for (String s : strs) {
+      paths.add(new PathFragment(s));
+    }
+    return paths;
+  }
+
+  @Test
+  public void testCompareTo() throws Exception {
+    List<String> pathStrs = ImmutableList.of(
+        "", "/", "//", ".", "/./", "foo/.//bar", "foo", "/foo", "foo/bar", "foo/Bar", "Foo/bar");
+    List<PathFragment> paths = toPaths(pathStrs);
+    // First test that compareTo is self-consistent.
+    for (PathFragment x : paths) {
+      for (PathFragment y : paths) {
+        for (PathFragment z : paths) {
+          // Anti-symmetry
+          assertEquals(Integer.signum(x.compareTo(y)),
+                       -1 * Integer.signum(y.compareTo(x)));
+          // Transitivity
+          if (x.compareTo(y) > 0 && y.compareTo(z) > 0) {
+            MoreAsserts.assertGreaterThan(0, x.compareTo(z));
+          }
+          // "Substitutability"
+          if (x.compareTo(y) == 0) {
+            assertEquals(Integer.signum(x.compareTo(z)), Integer.signum(y.compareTo(z)));
+          }
+          // Consistency with equals
+          assertEquals((x.compareTo(y) == 0), x.equals(y));
+        }
+      }
+    }
+    // Now test that compareTo does what we expect.  The exact ordering here doesn't matter much,
+    // but there are three things to notice: 1. absolute < relative, 2. comparison is lexicographic
+    // 3. repeated slashes are ignored. (PathFragment("//") prints as "/").
+    Collections.shuffle(paths);
+    Collections.sort(paths);
+    List<PathFragment> expectedOrder = toPaths(ImmutableList.of(
+        "/", "//", "/./", "/foo", "", ".", "Foo/bar", "foo", "foo/.//bar", "foo/Bar", "foo/bar"));
+    assertEquals(expectedOrder, paths);
+  }
+
+  @Test
+  public void testGetSafePathString() {
+    assertEquals("/", new PathFragment("/").getSafePathString());
+    assertEquals("/abc", new PathFragment("/abc").getSafePathString());
+    assertEquals(".", new PathFragment("").getSafePathString());
+    assertEquals(".", PathFragment.EMPTY_FRAGMENT.getSafePathString());
+    assertEquals("abc/def", new PathFragment("abc/def").getSafePathString());
+  }
+  
+  @Test
+  public void testNormalize() {
+    assertEquals(new PathFragment("/a/b"), new PathFragment("/a/b").normalize());
+    assertEquals(new PathFragment("/a/b"), new PathFragment("/a/./b").normalize());
+    assertEquals(new PathFragment("/b"), new PathFragment("/a/../b").normalize());
+    assertEquals(new PathFragment("a/b"), new PathFragment("a/b").normalize());
+    assertEquals(new PathFragment("../b"), new PathFragment("a/../../b").normalize());
+    assertEquals(new PathFragment(".."), new PathFragment("a/../..").normalize());
+    assertEquals(new PathFragment("b"), new PathFragment("a/../b").normalize());
+    assertEquals(new PathFragment("a/b"), new PathFragment("a/b/../b").normalize());
+    assertEquals(new PathFragment("/.."), new PathFragment("/..").normalize());
+  }
+
+  @Test
+  public void testSerializationSimple() throws Exception {
+   checkSerialization("a", 91);
+  }
+
+  @Test
+  public void testSerializationAbsolute() throws Exception {
+    checkSerialization("/foo", 94);
+   }
+
+  @Test
+  public void testSerializationNested() throws Exception {
+    checkSerialization("foo/bar/baz", 101);
+  }
+
+  private void checkSerialization(String pathFragmentString, int expectedSize) throws Exception {
+    PathFragment a = new PathFragment(pathFragmentString);
+    byte[] sa = TestUtils.serializeObject(a);
+    assertEquals(expectedSize, sa.length);
+
+    PathFragment a2 = (PathFragment) TestUtils.deserializeObject(sa);
+    assertEquals(a, a2);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java
new file mode 100644
index 0000000..43c94d4
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java
@@ -0,0 +1,218 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+/**
+ * This class tests the functionality of the PathFragment.
+ */
+@RunWith(JUnit4.class)
+public class PathFragmentWindowsTest {
+  
+  @Test
+  public void testWindowsSeparator() {
+    assertEquals("bar/baz", new PathFragment("bar\\baz").toString());
+    assertEquals("C:/bar/baz", new PathFragment("c:\\bar\\baz").toString());
+  }
+
+  @Test
+  public void testIsAbsoluteWindows() {
+    assertTrue(new PathFragment("C:/").isAbsolute());
+    assertTrue(new PathFragment("C:/").isAbsolute());
+    assertTrue(new PathFragment("C:/foo").isAbsolute());
+    assertTrue(new PathFragment("d:/foo/bar").isAbsolute());
+
+    assertFalse(new PathFragment("*:/").isAbsolute());
+
+    // C: is not an absolute path, it points to the current active directory on drive C:.
+    assertFalse(new PathFragment("C:").isAbsolute());
+    assertFalse(new PathFragment("C:foo").isAbsolute());
+  }
+
+  @Test
+  public void testIsAbsoluteWindowsBackslash() {
+    assertTrue(new PathFragment(new File("C:\\blah")).isAbsolute());
+    assertTrue(new PathFragment(new File("C:\\")).isAbsolute());
+    assertTrue(new PathFragment(new File("\\blah")).isAbsolute());
+    assertTrue(new PathFragment(new File("\\")).isAbsolute());
+  }
+
+  @Test
+  public void testIsNormalizedWindows() {
+    assertTrue(new PathFragment("C:/").isNormalized());
+    assertTrue(new PathFragment("C:/absolute/path").isNormalized());
+    assertFalse(new PathFragment("C:/absolute/./path").isNormalized());
+    assertFalse(new PathFragment("C:/absolute/../path").isNormalized());
+  }
+
+  @Test
+  public void testRootNodeReturnsRootStringWindows() {
+    PathFragment rootFragment = new PathFragment("C:/");
+    assertEquals("C:/", rootFragment.getPathString());
+  }
+
+  @Test
+  public void testGetRelativeWindows() {
+    assertEquals("C:/a/b", new PathFragment("C:/a").getRelative("b").getPathString());
+    assertEquals("C:/a/b/c/d", new PathFragment("C:/a/b").getRelative("c/d").getPathString());
+    assertEquals("C:/b", new PathFragment("C:/a").getRelative("C:/b").getPathString());
+    assertEquals("C:/c/d", new PathFragment("C:/a/b").getRelative("C:/c/d").getPathString());
+    assertEquals("C:/b", new PathFragment("a").getRelative("C:/b").getPathString());
+    assertEquals("C:/c/d", new PathFragment("a/b").getRelative("C:/c/d").getPathString());
+  }
+
+  @Test
+  public void testGetRelativeMixed() {
+    assertEquals("/b", new PathFragment("C:/a").getRelative("/b").getPathString());
+    assertEquals("C:/b", new PathFragment("/a").getRelative("C:/b").getPathString());
+  }
+
+  @Test
+  public void testGetChildWorks() {
+    PathFragment pf = new PathFragment("../some/path");
+    assertEquals(new PathFragment("../some/path/hi"), pf.getChild("hi"));
+  }
+
+  // Tests after here test the canonicalization
+  private void assertRegular(String expected, String actual) {
+    assertEquals(expected, new PathFragment(actual).getPathString()); // compare string forms
+    assertEquals(new PathFragment(expected), new PathFragment(actual)); // compare fragment forms
+  }
+
+  @Test
+  public void testEmptyPathToEmptyPathWindows() {
+    assertRegular("C:/", "C:/");
+  }
+
+  @Test
+  public void testEmptyRelativePathToEmptyPathWindows() {
+    assertRegular("C:", "C:");
+  }
+
+  @Test
+  public void testWindowsVolumeUppercase() {
+    assertRegular("C:/", "c:/");
+  }
+
+  @Test
+  public void testRedundantSlashesWindows() {
+    assertRegular("C:/", "C:///");
+    assertRegular("C:/foo/bar", "C:/foo///bar");
+    assertRegular("C:/foo/bar", "C:////foo//bar");
+  }
+
+  @Test
+  public void testSimpleNameToSimpleNameWindows() {
+    assertRegular("C:/foo", "C:/foo");
+  }
+
+  @Test
+  public void testStripsTrailingSlashWindows() {
+    assertRegular("C:/foo/bar", "C:/foo/bar/");
+  }
+
+  @Test
+  public void testGetParentDirectoryWindows() {
+    PathFragment fooBarWizAbs = new PathFragment("C:/foo/bar/wiz");
+    PathFragment fooBarAbs = new PathFragment("C:/foo/bar");
+    PathFragment fooAbs = new PathFragment("C:/foo");
+    PathFragment rootAbs = new PathFragment("C:/");
+    assertEquals(fooBarAbs, fooBarWizAbs.getParentDirectory());
+    assertEquals(fooAbs, fooBarAbs.getParentDirectory());
+    assertEquals(rootAbs, fooAbs.getParentDirectory());
+    assertNull(rootAbs.getParentDirectory());
+
+    // Note, this is suprising but correct behaviour:
+    assertEquals(fooBarAbs,
+                 new PathFragment("C:/foo/bar/..").getParentDirectory());
+  }
+
+  @Test
+  public void testSegmentsCountWindows() {
+    assertEquals(1, new PathFragment("C:/foo").segmentCount());
+    assertEquals(0, new PathFragment("C:/").segmentCount());
+  }
+
+  @Test
+  public void testGetSegmentWindows() {
+    assertEquals("foo", new PathFragment("C:/foo/bar").getSegment(0));
+    assertEquals("bar", new PathFragment("C:/foo/bar").getSegment(1));
+    assertEquals("foo", new PathFragment("C:/foo/").getSegment(0));
+    assertEquals("foo", new PathFragment("C:/foo").getSegment(0));
+  }
+
+  @Test
+  public void testBasenameWindows() throws Exception {
+    assertEquals("bar", new PathFragment("C:/foo/bar").getBaseName());
+    assertEquals("foo", new PathFragment("C:/foo").getBaseName());
+    // Never return the drive name as a basename.
+    assertEquals("", new PathFragment("C:/").getBaseName());
+  }
+
+  private static void assertPath(String expected, PathFragment actual) {
+    assertEquals(expected, actual.getPathString());
+  }
+
+  @Test
+  public void testReplaceNameWindows() throws Exception {
+    assertPath("C:/foo/baz", new PathFragment("C:/foo/bar").replaceName("baz"));
+    assertEquals(null, new PathFragment("C:/").replaceName("baz"));
+  }
+
+  @Test
+  public void testStartsWithWindows() {
+    assertTrue(new PathFragment("C:/foo/bar").startsWith(new PathFragment("C:/foo")));
+    assertTrue(new PathFragment("C:/foo/bar").startsWith(new PathFragment("C:/")));
+    assertTrue(new PathFragment("C:foo/bar").startsWith(new PathFragment("C:")));
+    assertTrue(new PathFragment("C:/").startsWith(new PathFragment("C:/")));
+    assertTrue(new PathFragment("C:").startsWith(new PathFragment("C:")));
+
+    // The first path is absolute, the second is not.
+    assertFalse(new PathFragment("C:/foo/bar").startsWith(new PathFragment("C:")));
+    assertFalse(new PathFragment("C:/").startsWith(new PathFragment("C:")));
+  }
+
+  @Test
+  public void testEndsWithWindows() {
+    assertTrue(new PathFragment("C:/foo/bar").endsWith(new PathFragment("bar")));
+    assertTrue(new PathFragment("C:/foo/bar").endsWith(new PathFragment("foo/bar")));
+    assertTrue(new PathFragment("C:/foo/bar").endsWith(new PathFragment("C:/foo/bar")));
+    assertTrue(new PathFragment("C:/").endsWith(new PathFragment("C:/")));
+  }
+
+  @Test
+  public void testGetSafePathStringWindows() {
+    assertEquals("C:/", new PathFragment("C:/").getSafePathString());
+    assertEquals("C:/abc", new PathFragment("C:/abc").getSafePathString());
+    assertEquals("C:/abc/def", new PathFragment("C:/abc/def").getSafePathString());
+  }
+
+  @Test
+  public void testNormalizeWindows() {
+    assertEquals(new PathFragment("C:/a/b"), new PathFragment("C:/a/b").normalize());
+    assertEquals(new PathFragment("C:/a/b"), new PathFragment("C:/a/./b").normalize());
+    assertEquals(new PathFragment("C:/b"), new PathFragment("C:/a/../b").normalize());
+    assertEquals(new PathFragment("C:/../b"), new PathFragment("C:/../b").normalize());
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathTest.java
new file mode 100644
index 0000000..738e454
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/PathTest.java
@@ -0,0 +1,312 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Lists;
+import com.google.common.testing.EqualsTester;
+import com.google.common.testing.GcFinalization;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.ref.WeakReference;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A test for {@link Path}.
+ */
+@RunWith(JUnit4.class)
+public class PathTest {
+  private FileSystem filesystem;
+  private Path root;
+
+  @Before
+  public void setUp() throws Exception {
+    filesystem = new InMemoryFileSystem(BlazeClock.instance());
+    root = filesystem.getRootDirectory();
+    Path first = root.getChild("first");
+    first.createDirectory();
+  }
+
+  @Test
+  public void testStartsWithWorksForSelf() {
+    assertStartsWithReturns(true, "/first/child", "/first/child");
+  }
+
+  @Test
+  public void testStartsWithWorksForChild() {
+    assertStartsWithReturns(true,
+        "/first/child", "/first/child/grandchild");
+  }
+
+  @Test
+  public void testStartsWithWorksForDeepDescendant() {
+    assertStartsWithReturns(true,
+        "/first/child", "/first/child/grandchild/x/y/z");
+  }
+
+  @Test
+  public void testStartsWithFailsForParent() {
+    assertStartsWithReturns(false, "/first/child", "/first");
+  }
+
+  @Test
+  public void testStartsWithFailsForSibling() {
+    assertStartsWithReturns(false, "/first/child", "/first/child2");
+  }
+
+  @Test
+  public void testStartsWithFailsForLinkToDescendant()
+      throws Exception {
+    Path linkTarget = filesystem.getPath("/first/linked_to");
+    FileSystemUtils.createEmptyFile(linkTarget);
+    Path second = filesystem.getPath("/second/");
+    second.createDirectory();
+    second.getChild("child_link").createSymbolicLink(linkTarget);
+    assertStartsWithReturns(false, "/first", "/second/child_link");
+  }
+
+  @Test
+  public void testStartsWithFailsForNullPrefix() {
+    try {
+      filesystem.getPath("/first").startsWith(null);
+      fail();
+    } catch (Exception e) {
+    }
+  }
+
+  private void assertStartsWithReturns(boolean expected,
+                                       String ancestor,
+                                       String descendant) {
+    Path parent = filesystem.getPath(ancestor);
+    Path child = filesystem.getPath(descendant);
+    assertEquals(expected, child.startsWith(parent));
+  }
+
+  @Test
+  public void testGetChildWorks() {
+    assertGetChildWorks("second");
+    assertGetChildWorks("...");
+    assertGetChildWorks("....");
+  }
+
+  private void assertGetChildWorks(String childName) {
+    assertEquals(filesystem.getPath("/first/" + childName),
+        filesystem.getPath("/first").getChild(childName));
+  }
+
+  @Test
+  public void testGetChildFailsForChildWithSlashes() {
+    assertGetChildFails("second/third");
+    assertGetChildFails("./third");
+    assertGetChildFails("../third");
+    assertGetChildFails("second/..");
+    assertGetChildFails("second/.");
+    assertGetChildFails("/third");
+    assertGetChildFails("third/");
+  }
+
+  private void assertGetChildFails(String childName) {
+    try {
+      filesystem.getPath("/first").getChild(childName);
+      fail();
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testGetChildFailsForDotAndDotDot() {
+    assertGetChildFails(".");
+    assertGetChildFails("..");
+  }
+
+  @Test
+  public void testGetChildFailsForEmptyString() {
+    assertGetChildFails("");
+  }
+
+  @Test
+  public void testRelativeToWorks() {
+    assertRelativeToWorks("apple", "/fruit/apple", "/fruit");
+    assertRelativeToWorks("apple/jonagold", "/fruit/apple/jonagold", "/fruit");
+  }
+
+  @Test
+  public void testGetRelativeWithStringWorks() {
+    assertGetRelativeWorks("/first/x/y", "y");
+    assertGetRelativeWorks("/y", "/y");
+    assertGetRelativeWorks("/first/x/x", "./x");
+    assertGetRelativeWorks("/first/y", "../y");
+    assertGetRelativeWorks("/", "../../../../..");
+  }
+
+  @Test
+  public void testAsFragmentWorks() {
+    assertAsFragmentWorks("/");
+    assertAsFragmentWorks("//");
+    assertAsFragmentWorks("/first");
+    assertAsFragmentWorks("/first/x/y");
+    assertAsFragmentWorks("/first/x/y.foo");
+  }
+
+  @Test
+  public void testGetRelativeWithFragmentWorks() {
+    Path dir = filesystem.getPath("/first/x");
+    assertEquals("/first/x/y",
+                 dir.getRelative(new PathFragment("y")).toString());
+    assertEquals("/first/x/x",
+                 dir.getRelative(new PathFragment("./x")).toString());
+    assertEquals("/first/y",
+                 dir.getRelative(new PathFragment("../y")).toString());
+
+  }
+
+  @Test
+  public void testGetRelativeWithAbsoluteFragmentWorks() {
+    Path root = filesystem.getPath("/first/x");
+    assertEquals("/x/y",
+                 root.getRelative(new PathFragment("/x/y")).toString());
+  }
+
+  @Test
+  public void testGetRelativeWithAbsoluteStringWorks() {
+    Path root = filesystem.getPath("/first/x");
+    assertEquals("/x/y", root.getRelative("/x/y").toString());
+  }
+
+  @Test
+  public void testComparableSortOrder() {
+    Path zzz = filesystem.getPath("/zzz");
+    Path ZZZ = filesystem.getPath("/ZZZ");
+    Path abc = filesystem.getPath("/abc");
+    Path aBc = filesystem.getPath("/aBc");
+    Path AbC = filesystem.getPath("/AbC");
+    Path ABC = filesystem.getPath("/ABC");
+    List<Path> list = Lists.newArrayList(zzz, ZZZ, ABC, aBc, AbC, abc);
+    Collections.sort(list);
+    assertThat(list).containsExactly(ABC, AbC, ZZZ, aBc, abc, zzz).inOrder();
+  }
+
+  @Test
+  public void testParentOfRootIsRoot() {
+    assertSame(root, root.getRelative(".."));
+
+    assertSame(root.getRelative("dots"),
+               root.getRelative("broken/../../dots"));
+  }
+
+  @Test
+  public void testSingleSegmentEquivalence() {
+    assertSame(
+        root.getRelative("aSingleSegment"),
+        root.getRelative("aSingleSegment"));
+  }
+
+  @Test
+  public void testSiblingNonEquivalenceString() {
+    assertNotSame(
+        root.getRelative("aSingleSegment"),
+        root.getRelative("aDifferentSegment"));
+  }
+
+  @Test
+  public void testSiblingNonEquivalenceFragment() {
+    assertNotSame(
+        root.getRelative(new PathFragment("aSingleSegment")),
+        root.getRelative(new PathFragment("aDifferentSegment")));
+  }
+
+  @Test
+  public void testHashCodeStableAcrossGarbageCollections() {
+    Path parent = filesystem.getPath("/a");
+    PathFragment childFragment = new PathFragment("b");
+    Path child = parent.getRelative(childFragment);
+    WeakReference<Path> childRef = new WeakReference<>(child);
+    int childHashCode1 = childRef.get().hashCode();
+    assertEquals(childHashCode1, parent.getRelative(childFragment).hashCode());
+    child = null;
+    GcFinalization.awaitClear(childRef);
+    int childHashCode2 = parent.getRelative(childFragment).hashCode();
+    assertEquals(childHashCode1, childHashCode2);
+  }
+
+  @Test
+  public void testSerialization() throws Exception {
+    FileSystem oldFileSystem = Path.getFileSystemForSerialization();
+    try {
+      Path.setFileSystemForSerialization(filesystem);
+      Path root = filesystem.getPath("/");
+      Path p1 = filesystem.getPath("/foo");
+      Path p2 = filesystem.getPath("/foo/bar");
+
+      ByteArrayOutputStream bos = new ByteArrayOutputStream();
+      ObjectOutputStream oos = new ObjectOutputStream(bos);
+
+      oos.writeObject(root);
+      oos.writeObject(p1);
+      oos.writeObject(p2);
+
+      ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+      ObjectInputStream ois = new ObjectInputStream(bis);
+
+      Path dsRoot = (Path) ois.readObject();
+      Path dsP1 = (Path) ois.readObject();
+      Path dsP2 = (Path) ois.readObject();
+
+      new EqualsTester()
+          .addEqualityGroup(root, dsRoot)
+          .addEqualityGroup(p1, dsP1)
+          .addEqualityGroup(p2, dsP2)
+          .testEquals();
+
+      assertTrue(p2.startsWith(p1));
+      assertTrue(p2.startsWith(dsP1));
+      assertTrue(dsP2.startsWith(p1));
+      assertTrue(dsP2.startsWith(dsP1));
+    } finally {
+      Path.setFileSystemForSerialization(oldFileSystem);
+    }
+  }
+
+  private void assertAsFragmentWorks(String expected) {
+    assertEquals(new PathFragment(expected), filesystem.getPath(expected).asFragment());
+  }
+
+  private void assertGetRelativeWorks(String expected, String relative) {
+    assertEquals(filesystem.getPath(expected),
+        filesystem.getPath("/first/x").getRelative(relative));
+  }
+
+  private void assertRelativeToWorks(String expected, String relative, String original) {
+    assertEquals(new PathFragment(expected),
+                 filesystem.getPath(relative).relativeTo(filesystem.getPath(original)));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathWindowsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathWindowsTest.java
new file mode 100644
index 0000000..c92fc2b
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/PathWindowsTest.java
@@ -0,0 +1,98 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * A test for windows aspects of {@link Path}.
+ */
+@RunWith(JUnit4.class)
+public class PathWindowsTest {
+  private FileSystem filesystem;
+  private Path root;
+
+  @Before
+  public void setUp() throws Exception {
+    filesystem = new InMemoryFileSystem(BlazeClock.instance());
+    root = filesystem.getRootDirectory();
+    Path first = root.getChild("first");
+    first.createDirectory();
+  }
+
+  private void assertAsFragmentWorks(String expected) {
+    assertEquals(new PathFragment(expected), filesystem.getPath(expected).asFragment());
+  }
+
+  @Test
+  public void testWindowsPath() {
+    Path p = filesystem.getPath("C:/foo/bar");
+    assertEquals("C:/foo/bar", p.getPathString());
+    assertEquals("C:/foo/bar", p.toString());
+  }
+
+  @Test
+  public void testAsFragmentWindows() {
+    assertAsFragmentWorks("C:/");
+    assertAsFragmentWorks("C://");
+    assertAsFragmentWorks("C:/first");
+    assertAsFragmentWorks("C:/first/x/y");
+    assertAsFragmentWorks("C:/first/x/y.foo");
+  }
+
+  @Test
+  public void testGetRelativeWithFragmentWindows() {
+    Path dir = filesystem.getPath("C:/first/x");
+    assertEquals("C:/first/x/y",
+                 dir.getRelative(new PathFragment("y")).toString());
+    assertEquals("C:/first/x/x",
+                 dir.getRelative(new PathFragment("./x")).toString());
+    assertEquals("C:/first/y",
+                 dir.getRelative(new PathFragment("../y")).toString());
+    assertEquals("C:/first/y",
+        dir.getRelative(new PathFragment("../y")).toString());
+    assertEquals("C:/y",
+        dir.getRelative(new PathFragment("../../../y")).toString());
+  }
+
+  @Test
+  public void testGetRelativeWithAbsoluteFragmentWindows() {
+    Path root = filesystem.getPath("C:/first/x");
+    assertEquals("C:/x/y",
+                 root.getRelative(new PathFragment("C:/x/y")).toString());
+  }
+
+  @Test
+  public void testGetRelativeWithAbsoluteStringWorksWindows() {
+    Path root = filesystem.getPath("C:/first/x");
+    assertEquals("C:/x/y", root.getRelative("C:/x/y").toString());
+  }
+
+  @Test
+  public void testParentOfRootIsRootWindows() {
+    assertSame(root, root.getRelative(".."));
+
+    assertSame(root.getRelative("dots"),
+               root.getRelative("broken/../../dots"));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/RecursiveGlobTest.java b/src/test/java/com/google/devtools/build/lib/vfs/RecursiveGlobTest.java
new file mode 100644
index 0000000..5e0012a
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/RecursiveGlobTest.java
@@ -0,0 +1,227 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertSameContents;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Tests {@link UnixGlob} recursive globs.
+ */
+@RunWith(JUnit4.class)
+public class RecursiveGlobTest {
+
+  private Path tmpPath;
+  private FileSystem fileSystem;
+  
+  @Before
+  public void setUp() throws Exception {
+    fileSystem = new InMemoryFileSystem(BlazeClock.instance());
+    tmpPath = fileSystem.getPath("/rglobtmp");
+    for (String dir : ImmutableList.of("foo/bar/wiz",
+                         "foo/baz/wiz",
+                         "foo/baz/quip/wiz",
+                         "food/baz/wiz",
+                         "fool/baz/wiz")) {
+      FileSystemUtils.createDirectoryAndParents(tmpPath.getRelative(dir));
+    }
+    FileSystemUtils.createEmptyFile(tmpPath.getRelative("foo/bar/wiz/file"));
+  }
+
+  @Test
+  public void testDoubleStar() throws Exception {
+    assertGlobMatches("**", ".", "foo", "foo/bar", "foo/bar/wiz", "foo/baz", "foo/baz/quip",
+                      "foo/baz/quip/wiz", "foo/baz/wiz", "foo/bar/wiz/file", "food", "food/baz",
+                      "food/baz/wiz", "fool", "fool/baz", "fool/baz/wiz");
+  }
+
+  @Test
+  public void testDoubleDoubleStar() throws Exception {
+    assertGlobMatches("**/**", ".", "foo", "foo/bar", "foo/bar/wiz", "foo/baz", "foo/baz/quip",
+                      "foo/baz/quip/wiz", "foo/baz/wiz", "foo/bar/wiz/file", "food", "food/baz",
+                      "food/baz/wiz", "fool", "fool/baz", "fool/baz/wiz");
+  }
+
+  @Test
+  public void testDirectoryWithDoubleStar() throws Exception {
+    assertGlobMatches("foo/**", "foo", "foo/bar", "foo/bar/wiz", "foo/baz", "foo/baz/quip",
+                      "foo/baz/quip/wiz", "foo/baz/wiz", "foo/bar/wiz/file");
+  }
+
+  @Test
+  public void testIllegalPatterns() throws Exception {
+    for (String prefix : Lists.newArrayList("", "*/", "**/", "ba/")) {
+      String suffix = ("/" + prefix).substring(0, prefix.length());
+      for (String pattern : Lists.newArrayList("**fo", "fo**", "**fo**", "fo**fo", "fo**fo**fo")) {
+        assertIllegalWildcard(prefix + pattern);
+        assertIllegalWildcard(pattern + suffix);
+        assertIllegalWildcard("foo", pattern + suffix);
+      }
+    }
+  }
+
+  @Test
+  public void testDoubleStarPatternWithNamedChild() throws Exception {
+    assertGlobMatches("**/bar", "foo/bar");
+  }
+
+  @Test
+  public void testDoubleStarPatternWithChildGlob() throws Exception {
+    assertGlobMatches("**/ba*",
+        "foo/bar", "foo/baz", "food/baz", "fool/baz");
+  }
+
+  @Test
+  public void testDoubleStarAsChildGlob() throws Exception {
+    assertGlobMatches("foo/**/wiz", "foo/bar/wiz", "foo/baz/quip/wiz", "foo/baz/wiz");
+  }
+
+  @Test
+  public void testDoubleStarUnderNonexistentDirectory() throws Exception {
+    assertGlobMatches("not-there/**" /* => nothing */);
+  }
+
+  @Test
+  public void testDoubleStarGlobWithNonExistentBase() throws Exception {
+    Collection<Path> globResult = UnixGlob.forPath(fileSystem.getPath("/does/not/exist"))
+        .addPattern("**")
+        .globInterruptible();
+    assertEquals(0, globResult.size());
+  }
+
+  @Test
+  public void testDoubleStarUnderFile() throws Exception {
+    assertGlobMatches("foo/bar/wiz/file/**" /* => nothing */);
+  }
+
+  @Test
+  public void testSingleFileExclude() throws Exception {
+    assertGlobWithExcludeMatches("**", "food", ".", "foo", "foo/bar", "foo/bar/wiz", "foo/baz",
+                                 "foo/baz/quip", "foo/baz/quip/wiz", "foo/baz/wiz",
+                                 "foo/bar/wiz/file", "food/baz", "food/baz/wiz", "fool", "fool/baz",
+                                 "fool/baz/wiz");
+  }
+
+  @Test
+  public void testSingleFileExcludeForDirectoryWithChildGlob()
+      throws Exception {
+    assertGlobWithExcludeMatches("foo/**", "foo", "foo/bar", "foo/bar/wiz", "foo/baz",
+                                 "foo/baz/quip", "foo/baz/quip/wiz", "foo/baz/wiz",
+                                 "foo/bar/wiz/file");
+  }
+
+  @Test
+  public void testGlobExcludeForDirectoryWithChildGlob()
+      throws Exception {
+    assertGlobWithExcludeMatches("foo/**", "foo/*", "foo", "foo/bar/wiz", "foo/baz/quip",
+                                 "foo/baz/quip/wiz", "foo/baz/wiz", "foo/bar/wiz/file");
+  }
+
+  @Test
+  public void testExcludeAll() throws Exception {
+    assertGlobWithExcludesMatches(Lists.newArrayList("**"),
+                                  Lists.newArrayList("*", "*/*", "*/*/*", "*/*/*/*"), ".");
+  }
+
+  @Test
+  public void testManualGlobExcludeForDirectoryWithChildGlob()
+      throws Exception {
+    assertGlobWithExcludesMatches(Lists.newArrayList("foo/**"),
+                                  Lists.newArrayList("foo", "foo/*", "foo/*/*", "foo/*/*/*"));
+  }
+
+  private void assertGlobMatches(String pattern, String... expecteds)
+      throws Exception {
+    assertGlobWithExcludesMatches(
+        Collections.singleton(pattern), Collections.<String>emptyList(),
+        expecteds);
+  }
+
+  private void assertGlobWithExcludeMatches(String pattern, String exclude,
+                                            String... expecteds)
+      throws Exception {
+    assertGlobWithExcludesMatches(
+        Collections.singleton(pattern), Collections.singleton(exclude),
+        expecteds);
+  }
+
+  private void assertGlobWithExcludesMatches(Collection<String> pattern,
+                                             Collection<String> excludes,
+                                             String... expecteds) throws Exception {
+    assertSameContents(resolvePaths(expecteds),
+        new UnixGlob.Builder(tmpPath)
+            .addPatterns(pattern)
+            .addExcludes(excludes)
+            .globInterruptible());
+  }
+
+  private Set<Path> resolvePaths(String... relativePaths) {
+    Set<Path> expectedFiles = new HashSet<>();
+    for (String expected : relativePaths) {
+      Path file = expected.equals(".")
+          ? tmpPath
+          : tmpPath.getRelative(expected);
+      expectedFiles.add(file);
+    }
+    return expectedFiles;
+  }
+
+  /**
+   * Tests that a recursive glob returns files in sorted order.
+   */
+  @Test
+  public void testGlobEntriesAreSorted() throws Exception {
+    List<Path> globResult = new UnixGlob.Builder(tmpPath)
+        .addPattern("**")
+        .setExcludeDirectories(false)
+        .globInterruptible();
+
+    assertThat(Ordering.natural().sortedCopy(globResult)).containsExactlyElementsIn(globResult)
+        .inOrder();
+  }
+
+  private void assertIllegalWildcard(String pattern, String... excludePatterns)
+      throws Exception {
+    try {
+      new UnixGlob.Builder(tmpPath)
+          .addPattern(pattern)
+          .addExcludes(excludePatterns)
+          .globInterruptible();
+      fail();
+    } catch (IllegalArgumentException e) {
+      MoreAsserts.assertContainsRegex("recursive wildcard must be its own segment", e.getMessage());
+    }
+  }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/RootedPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/RootedPathTest.java
new file mode 100644
index 0000000..46d286d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/RootedPathTest.java
@@ -0,0 +1,56 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.testing.EqualsTester;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link RootedPath}.
+ */
+@RunWith(JUnit4.class)
+public class RootedPathTest {
+  private FileSystem filesystem;
+  private Path root;
+
+  @Before
+  public void setUp() throws Exception {
+    filesystem = new InMemoryFileSystem(BlazeClock.instance());
+    root = filesystem.getRootDirectory();
+  }
+
+  @Test
+  public void testEqualsAndHashCodeContract() throws Exception {
+    Path pkgRoot1 = root.getRelative("pkgroot1");
+    Path pkgRoot2 = root.getRelative("pkgroot2");
+    RootedPath rootedPathA1 = RootedPath.toRootedPath(pkgRoot1, new PathFragment("foo/bar"));
+    RootedPath rootedPathA2 = RootedPath.toRootedPath(pkgRoot1, new PathFragment("foo/bar"));
+    RootedPath absolutePath1 = RootedPath.toRootedPath(root, new PathFragment("pkgroot1/foo/bar"));
+    RootedPath rootedPathB1 = RootedPath.toRootedPath(pkgRoot2, new PathFragment("foo/bar"));
+    RootedPath rootedPathB2 = RootedPath.toRootedPath(pkgRoot2, new PathFragment("foo/bar"));
+    RootedPath absolutePath2 = RootedPath.toRootedPath(root, new PathFragment("pkgroot2/foo/bar"));
+    new EqualsTester()
+      .addEqualityGroup(rootedPathA1, rootedPathA2)
+      .addEqualityGroup(rootedPathB1, rootedPathB2)
+      .addEqualityGroup(absolutePath1)
+      .addEqualityGroup(absolutePath2)
+      .testEquals();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystemTest.java
new file mode 100644
index 0000000..6c8071f
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystemTest.java
@@ -0,0 +1,806 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+
+/**
+ * Generic tests for any file system that implements {@link ScopeEscapableFileSystem},
+ * i.e. any file system that supports symlinks that escape its scope.
+ *
+ * Each suitable file system test should inherit from this class, thereby obtaining
+ * all the tests.
+ */
+public abstract class ScopeEscapableFileSystemTest extends SymlinkAwareFileSystemTest {
+
+  /**
+   * Trivial FileSystem implementation that can record the last path passed to each method
+   * and read/write to a unified "state" variable (which can then be checked by tests) for
+   * each data type this class manipulates.
+   *
+   * The default implementation of each method throws an exception. Each test case should
+   * selectively override the methods it expects to be invoked.
+   */
+  private static class TestDelegator extends FileSystem {
+    protected Path lastPath;
+    protected boolean booleanState;
+    protected long longState;
+    protected Object objectState;
+
+    public void setState(boolean state) { booleanState = state; }
+    public void setState(long state) { longState = state; }
+    public void setState(Object state) { objectState = state; }
+
+    public boolean booleanState() { return booleanState; }
+    public long longState() { return longState; }
+    public Object objectState() { return objectState; }
+
+    public PathFragment lastPath() {
+      Path ans = lastPath;
+      // Clear this out to protect against accidental matches when testing the same path multiple
+      // consecutive times.
+      lastPath = null;
+      return ans != null ? ans.asFragment() : null;
+    }
+
+    @Override public boolean supportsModifications() { return true; }
+    @Override public boolean supportsSymbolicLinks() { return true; }
+
+    private static RuntimeException re() {
+      return new RuntimeException("This method should not be called in this context");
+    }
+
+    @Override protected boolean isReadable(Path path) { throw re(); }
+    @Override protected boolean isWritable(Path path) { throw re(); }
+    @Override protected boolean isDirectory(Path path, boolean followSymlinks) { throw re(); }
+    @Override protected boolean isFile(Path path, boolean followSymlinks) { throw re(); }
+    @Override protected boolean isExecutable(Path path) { throw re(); }
+    @Override protected boolean exists(Path path, boolean followSymlinks) {throw re(); }
+    @Override protected boolean isSymbolicLink(Path path) { throw re(); }
+    @Override protected boolean createDirectory(Path path) { throw re(); }
+    @Override protected boolean delete(Path path) { throw re(); }
+
+    @Override protected long getFileSize(Path path, boolean followSymlinks) { throw re(); }
+    @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { throw re(); }
+
+    @Override protected void setWritable(Path path, boolean writable) { throw re(); }
+    @Override protected void setExecutable(Path path, boolean executable) { throw re(); }
+    @Override protected void setReadable(Path path, boolean readable) { throw re(); }
+    @Override protected void setLastModifiedTime(Path path, long newTime) { throw re(); }
+    @Override protected void renameTo(Path sourcePath, Path targetPath) { throw re(); }
+    @Override protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) {
+      throw re();
+    }
+
+    @Override protected PathFragment readSymbolicLink(Path path) { throw re(); }
+    @Override protected InputStream getInputStream(Path path) { throw re(); }
+    @Override protected Collection<Path> getDirectoryEntries(Path path) { throw re(); }
+    @Override protected OutputStream getOutputStream(Path path, boolean append)  { throw re(); }
+    @Override
+    protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
+      throw re();
+    }
+  }
+
+  protected static final PathFragment SCOPE_ROOT = new PathFragment("/fs/root");
+
+  private Path fileLink;
+  private PathFragment fileLinkTarget;
+  private Path dirLink;
+  private PathFragment dirLinkTarget;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+
+    Preconditions.checkState(testFS instanceof ScopeEscapableFileSystem,
+        "Not ScopeEscapable: " + testFS);
+    ((ScopeEscapableFileSystem) testFS).enableScopeChecking(false);
+    for (int i = 1; i <= SCOPE_ROOT.segmentCount(); i++) {
+      testFS.getPath(SCOPE_ROOT.subFragment(0, i)).createDirectory();
+    }
+
+    fileLink = testFS.getPath(SCOPE_ROOT.getRelative("link"));
+    fileLinkTarget = new PathFragment("/should/be/delegated/fileLinkTarget");
+    testFS.createSymbolicLink(fileLink, fileLinkTarget);
+
+    dirLink = testFS.getPath(SCOPE_ROOT.getRelative("dirlink"));
+    dirLinkTarget = new PathFragment("/should/be/delegated/dirLinkTarget");
+    testFS.createSymbolicLink(dirLink, dirLinkTarget);
+  }
+
+  /**
+   * Returns the file system supplied by {@link #getFreshFileSystem}, cast to
+   * a {@link ScopeEscapableFileSystem}. Also enables scope checking within
+   * the file system (which we keep disabled for inherited tests that aren't
+   * intended to test scope boundaries).
+   */
+  private ScopeEscapableFileSystem scopedFS() {
+    ScopeEscapableFileSystem fs = (ScopeEscapableFileSystem) testFS;
+    fs.enableScopeChecking(true);
+    return fs;
+  }
+
+  // Checks that the semi-resolved path passed to the delegator matches the expected value.
+  private void checkPath(TestDelegator delegator, PathFragment expectedDelegatedPath) {
+    assertTrue(expectedDelegatedPath.equals(delegator.lastPath()));
+  }
+
+  // Asserts that the condition is false and checks that the expected path was delegated.
+  private void assertFalseWithPathCheck(boolean result, TestDelegator delegator,
+      PathFragment expectedDelegatedPath) {
+    assertFalse(result);
+    checkPath(delegator, expectedDelegatedPath);
+  }
+
+  // Asserts that the condition is true and checks that the expected path was delegated.
+  private void assertTrueWithPathCheck(boolean result, TestDelegator delegator,
+      PathFragment expectedDelegatedPath) {
+    assertTrue(result);
+    checkPath(delegator, expectedDelegatedPath);
+  }
+
+  /////////////////////////////////////////////////////////////////////////////
+  // Tests:
+  /////////////////////////////////////////////////////////////////////////////
+
+  @Test
+  public void testIsReadableCallOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected boolean isReadable(Path path) {
+        lastPath = path;
+        return booleanState();
+      }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(false);
+    assertFalseWithPathCheck(fileLink.isReadable(), delegator, fileLinkTarget);
+    assertFalseWithPathCheck(dirLink.getRelative("a").isReadable(), delegator,
+        dirLinkTarget.getRelative("a"));
+
+    delegator.setState(true);
+    assertTrueWithPathCheck(fileLink.isReadable(), delegator, fileLinkTarget);
+    assertTrueWithPathCheck(dirLink.getRelative("a").isReadable(), delegator,
+        dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testIsWritableCallOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected boolean isWritable(Path path) {
+        lastPath = path;
+        return booleanState();
+      }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(false);
+    assertFalseWithPathCheck(fileLink.isWritable(), delegator, fileLinkTarget);
+    assertFalseWithPathCheck(dirLink.getRelative("a").isWritable(), delegator,
+        dirLinkTarget.getRelative("a"));
+
+    delegator.setState(true);
+    assertTrueWithPathCheck(fileLink.isWritable(), delegator, fileLinkTarget);
+    assertTrueWithPathCheck(dirLink.getRelative("a").isWritable(), delegator,
+        dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testisExecutableCallOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected boolean isExecutable(Path path) {
+        lastPath = path;
+        return booleanState();
+      }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(false);
+    assertFalseWithPathCheck(fileLink.isExecutable(), delegator, fileLinkTarget);
+    assertFalseWithPathCheck(dirLink.getRelative("a").isExecutable(), delegator,
+        dirLinkTarget.getRelative("a"));
+
+    delegator.setState(true);
+    assertTrueWithPathCheck(fileLink.isExecutable(), delegator, fileLinkTarget);
+    assertTrueWithPathCheck(dirLink.getRelative("a").isExecutable(), delegator,
+        dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testIsDirectoryCallOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected boolean isDirectory(Path path, boolean followSymlinks) {
+        lastPath = path;
+        return booleanState();
+      }
+      @Override protected boolean exists(Path path, boolean followSymlinks) { return true; }
+      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(false);
+    assertFalseWithPathCheck(fileLink.isDirectory(), delegator, fileLinkTarget);
+    assertFalseWithPathCheck(dirLink.getRelative("a").isDirectory(), delegator,
+        dirLinkTarget.getRelative("a"));
+
+    delegator.setState(true);
+    assertTrueWithPathCheck(fileLink.isDirectory(), delegator, fileLinkTarget);
+    assertTrueWithPathCheck(dirLink.getRelative("a").isDirectory(), delegator,
+        dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testIsFileCallOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected boolean isFile(Path path, boolean followSymlinks) {
+        lastPath = path;
+        return booleanState();
+      }
+      @Override protected boolean exists(Path path, boolean followSymlinks) { return true; }
+      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(false);
+    assertFalseWithPathCheck(fileLink.isFile(), delegator, fileLinkTarget);
+    assertFalseWithPathCheck(dirLink.getRelative("a").isFile(), delegator,
+        dirLinkTarget.getRelative("a"));
+
+    delegator.setState(true);
+    assertTrueWithPathCheck(fileLink.isFile(), delegator, fileLinkTarget);
+    assertTrueWithPathCheck(dirLink.getRelative("a").isFile(), delegator,
+        dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testIsSymbolicLinkCallOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected boolean isSymbolicLink(Path path) {
+        lastPath = path;
+        return booleanState();
+      }
+      @Override protected boolean exists(Path path, boolean followSymlinks) { return true; }
+      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; }
+      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
+    };
+    scopedFS().setDelegator(delegator);
+
+    // We shouldn't follow final-segment links, so they should never invoke the delegator.
+    delegator.setState(false);
+    assertTrue(fileLink.isSymbolicLink());
+    assertTrue(delegator.lastPath() == null);
+
+    assertFalseWithPathCheck(dirLink.getRelative("a").isSymbolicLink(), delegator,
+        dirLinkTarget.getRelative("a"));
+
+    delegator.setState(true);
+    assertTrueWithPathCheck(dirLink.getRelative("a").isSymbolicLink(), delegator,
+        dirLinkTarget.getRelative("a"));
+  }
+
+  /**
+   * Returns a test delegator that reflects info passed to Path.exists() calls.
+   */
+  private TestDelegator newExistsDelegator() {
+    return new TestDelegator() {
+      @Override protected boolean exists(Path path, boolean followSymlinks) {
+        lastPath = path;
+        return booleanState();
+      }
+      @Override protected FileStatus stat(Path path, boolean followSymlinks) throws IOException {
+        if (!exists(path, followSymlinks)) {
+          throw new IOException("Expected exception on stat of non-existent file");
+        }
+        return super.stat(path, followSymlinks);
+      }
+      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; }
+    };
+  }
+
+  @Test
+  public void testExistsCallOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = newExistsDelegator();
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(false);
+    assertFalseWithPathCheck(fileLink.exists(), delegator, fileLinkTarget);
+    assertFalseWithPathCheck(dirLink.getRelative("a").exists(), delegator,
+        dirLinkTarget.getRelative("a"));
+
+    delegator.setState(true);
+    assertTrueWithPathCheck(fileLink.exists(), delegator, fileLinkTarget);
+    assertTrueWithPathCheck(dirLink.getRelative("a").exists(), delegator,
+        dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testCreateDirectoryCallOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected boolean createDirectory(Path path) {
+        lastPath = path;
+        return booleanState();
+      }
+      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(false);
+    assertFalseWithPathCheck(dirLink.getRelative("a").createDirectory(), delegator,
+        dirLinkTarget.getRelative("a"));
+
+    delegator.setState(true);
+    assertTrueWithPathCheck(dirLink.getRelative("a").createDirectory(), delegator,
+        dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testDeleteCallOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected boolean delete(Path path) {
+        lastPath = path;
+        return booleanState();
+      }
+      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
+      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(false);
+    assertTrue(fileLink.delete());
+    assertTrue(delegator.lastPath() == null);  // Deleting a link shouldn't require delegation.
+    assertFalseWithPathCheck(dirLink.getRelative("a").delete(), delegator,
+        dirLinkTarget.getRelative("a"));
+
+    delegator.setState(true);
+    assertTrueWithPathCheck(dirLink.getRelative("a").delete(), delegator,
+        dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testCallGetFileSizeOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected long getFileSize(Path path, boolean followSymlinks) {
+        lastPath = path;
+        return longState();
+      }
+      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; }
+    };
+    scopedFS().setDelegator(delegator);
+
+    final int state1 = 10;
+    delegator.setState(state1);
+    assertEquals(state1, fileLink.getFileSize());
+    checkPath(delegator, fileLinkTarget);
+    assertEquals(state1, dirLink.getRelative("a").getFileSize());
+    checkPath(delegator, dirLinkTarget.getRelative("a"));
+
+    final int state2 = 10;
+    delegator.setState(state2);
+    assertEquals(state2, fileLink.getFileSize());
+    checkPath(delegator, fileLinkTarget);
+    assertEquals(state2, dirLink.getRelative("a").getFileSize());
+    checkPath(delegator, dirLinkTarget.getRelative("a"));
+   }
+
+  @Test
+  public void testCallGetLastModifiedTimeOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) {
+        lastPath = path;
+        return longState();
+      }
+    };
+    scopedFS().setDelegator(delegator);
+
+    final int state1 = 10;
+    delegator.setState(state1);
+    assertEquals(state1, fileLink.getLastModifiedTime());
+    checkPath(delegator, fileLinkTarget);
+    assertEquals(state1, dirLink.getRelative("a").getLastModifiedTime());
+    checkPath(delegator, dirLinkTarget.getRelative("a"));
+
+    final int state2 = 10;
+    delegator.setState(state2);
+    assertEquals(state2, fileLink.getLastModifiedTime());
+    checkPath(delegator, fileLinkTarget);
+    assertEquals(state2, dirLink.getRelative("a").getLastModifiedTime());
+    checkPath(delegator, dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testCallSetReadableOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected void setReadable(Path path, boolean readable) {
+        lastPath = path;
+        setState(readable);
+      }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(false);
+    fileLink.setReadable(true);
+    assertTrue(delegator.booleanState());
+    checkPath(delegator, fileLinkTarget);
+    fileLink.setReadable(false);
+    assertFalse(delegator.booleanState());
+    checkPath(delegator, fileLinkTarget);
+
+    delegator.setState(false);
+    dirLink.getRelative("a").setReadable(true);
+    assertTrue(delegator.booleanState());
+    checkPath(delegator, dirLinkTarget.getRelative("a"));
+    dirLink.getRelative("a").setReadable(false);
+    assertFalse(delegator.booleanState());
+    checkPath(delegator, dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testCallSetWritableOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected void setWritable(Path path, boolean writable) {
+        lastPath = path;
+        setState(writable);
+      }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(false);
+    fileLink.setWritable(true);
+    assertTrue(delegator.booleanState());
+    checkPath(delegator, fileLinkTarget);
+    fileLink.setWritable(false);
+    assertFalse(delegator.booleanState());
+    checkPath(delegator, fileLinkTarget);
+
+    delegator.setState(false);
+    dirLink.getRelative("a").setWritable(true);
+    assertTrue(delegator.booleanState());
+    checkPath(delegator, dirLinkTarget.getRelative("a"));
+    dirLink.getRelative("a").setWritable(false);
+    assertFalse(delegator.booleanState());
+    checkPath(delegator, dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testCallSetExecutableOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected void setReadable(Path path, boolean readable) {
+        lastPath = path;
+        setState(readable);
+      }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(false);
+    fileLink.setReadable(true);
+    assertTrue(delegator.booleanState());
+    checkPath(delegator, fileLinkTarget);
+    fileLink.setReadable(false);
+    assertFalse(delegator.booleanState());
+    checkPath(delegator, fileLinkTarget);
+
+    delegator.setState(false);
+    dirLink.getRelative("a").setReadable(true);
+    assertTrue(delegator.booleanState());
+    checkPath(delegator, dirLinkTarget.getRelative("a"));
+    dirLink.getRelative("a").setReadable(false);
+    assertFalse(delegator.booleanState());
+    checkPath(delegator, dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testCallSetLastModifiedTimeOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected void setLastModifiedTime(Path path, long newTime) {
+        lastPath = path;
+        setState(newTime);
+      }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(0);
+    fileLink.setLastModifiedTime(10);
+    assertEquals(10, delegator.longState());
+    checkPath(delegator, fileLinkTarget);
+    fileLink.setLastModifiedTime(15);
+    assertEquals(15, delegator.longState());
+    checkPath(delegator, fileLinkTarget);
+
+    dirLink.getRelative("a").setLastModifiedTime(20);
+    assertEquals(20, delegator.longState());
+    checkPath(delegator, dirLinkTarget.getRelative("a"));
+    dirLink.getRelative("a").setLastModifiedTime(25);
+    assertEquals(25, delegator.longState());
+    checkPath(delegator, dirLinkTarget.getRelative("a"));
+  }
+
+  @Test
+  public void testCallRenameToOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected void renameTo(Path sourcePath, Path targetPath) {
+        lastPath = sourcePath;
+        setState(targetPath);
+      }
+      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
+    };
+    scopedFS().setDelegator(delegator);
+
+    // Renaming a link should work fine.
+    delegator.setState(null);
+    fileLink.renameTo(testFS.getPath(SCOPE_ROOT).getRelative("newname"));
+    assertEquals(null, delegator.lastPath());  // Renaming a link shouldn't require delegation.
+    assertEquals(null, delegator.objectState());
+
+    // Renaming an out-of-scope path to an in-scope path should fail due to filesystem mismatch
+    // errors.
+    Path newPath = testFS.getPath(SCOPE_ROOT.getRelative("blah"));
+    try {
+      dirLink.getRelative("a").renameTo(newPath);
+      fail("This is an attempt at a cross-filesystem renaming, which should fail");
+    } catch (IOException e) {
+      // Expected.
+    }
+
+    // Renaming an out-of-scope path to another out-of-scope path can be valid.
+    newPath = dirLink.getRelative("b");
+    dirLink.getRelative("a").renameTo(newPath);
+    assertEquals(dirLinkTarget.getRelative("a"), delegator.lastPath());
+    assertEquals(dirLinkTarget.getRelative("b"), ((Path) delegator.objectState()).asFragment());
+  }
+
+  @Test
+  public void testCallCreateSymbolicLinkOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) {
+        lastPath = linkPath;
+        setState(targetFragment);
+      }
+      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
+    };
+    scopedFS().setDelegator(delegator);
+
+    PathFragment newLinkTarget = new PathFragment("/something/else");
+    dirLink.getRelative("a").createSymbolicLink(newLinkTarget);
+    assertEquals(dirLinkTarget.getRelative("a"), delegator.lastPath());
+    assertSame(newLinkTarget, delegator.objectState());
+  }
+
+  @Test
+  public void testCallReadSymbolicLinkOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected PathFragment readSymbolicLink(Path path) {
+        lastPath = path;
+        return (PathFragment) objectState;
+      }
+      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
+    };
+    scopedFS().setDelegator(delegator);
+
+    // Since we're not following the link, this shouldn't invoke delegation.
+    delegator.setState(new PathFragment("whatever"));
+    PathFragment p = fileLink.readSymbolicLink();
+    assertEquals(null, delegator.lastPath());
+    assertNotSame(delegator.objectState(), p);
+
+    // This should.
+    p = dirLink.getRelative("a").readSymbolicLink();
+    assertEquals(dirLinkTarget.getRelative("a"), delegator.lastPath());
+    assertSame(delegator.objectState(), p);
+  }
+
+  @Test
+  public void testCallGetInputStreamOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected InputStream getInputStream(Path path) {
+        lastPath = path;
+        return (InputStream) objectState;
+      }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(new ByteArrayInputStream("blah".getBytes()));
+    InputStream is = fileLink.getInputStream();
+    assertEquals(fileLinkTarget, delegator.lastPath());
+    assertSame(delegator.objectState(), is);
+
+    delegator.setState(new ByteArrayInputStream("blah2".getBytes()));
+    is = dirLink.getInputStream();
+    assertEquals(dirLinkTarget, delegator.lastPath());
+    assertSame(delegator.objectState(), is);
+  }
+
+  @Test
+  public void testCallGetOutputStreamOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected OutputStream getOutputStream(Path path, boolean append)  {
+        lastPath = path;
+        return (OutputStream) objectState;
+      }
+      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(new ByteArrayOutputStream());
+    OutputStream os = fileLink.getOutputStream();
+    assertEquals(fileLinkTarget, delegator.lastPath());
+    assertSame(delegator.objectState(), os);
+
+    delegator.setState(new ByteArrayOutputStream());
+    os = dirLink.getOutputStream();
+    assertEquals(dirLinkTarget, delegator.lastPath());
+    assertSame(delegator.objectState(), os);
+  }
+
+  @Test
+  public void testCallGetDirectoryEntriesOnEscapingSymlink() throws Exception {
+    TestDelegator delegator = new TestDelegator() {
+      @Override protected Collection<Path> getDirectoryEntries(Path path) {
+        lastPath = path;
+        return ImmutableList.of((Path) objectState);
+      }
+      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
+    };
+    scopedFS().setDelegator(delegator);
+
+    delegator.setState(testFS.getPath("/anything"));
+    Collection<Path> entries = dirLink.getDirectoryEntries();
+    assertEquals(dirLinkTarget, delegator.lastPath());
+    assertEquals(1, entries.size());
+    assertSame(delegator.objectState(), entries.iterator().next());
+  }
+
+  /**
+   * Asserts that "link" is an in-scope link that doesn't result in an out-of-FS
+   * delegation. If link is relative, its path is relative to SCOPE_ROOT.
+   *
+   * Note that we don't actually check that the canonicalized target path matches
+   * the link's target value. Such testing should be covered by
+   * SymlinkAwareFileSystemTest.
+   */
+  private void assertInScopeLink(String link, String target, TestDelegator d) throws IOException {
+    Path l = testFS.getPath(SCOPE_ROOT.getRelative(link));
+    testFS.createSymbolicLink(l, new PathFragment(target));
+    l.exists();
+    assertNull(d.lastPath());
+  }
+
+  /**
+   * Asserts that "link" is an out-of-scope link and that the re-delegated path
+   * matches expectedPath. If link is relative, its path is relative to SCOPE_ROOT.
+   */
+  private void assertOutOfScopeLink(String link, String target, String expectedPath,
+      TestDelegator d) throws IOException {
+    Path l = testFS.getPath(SCOPE_ROOT.getRelative(link));
+    testFS.createSymbolicLink(l, new PathFragment(target));
+    l.exists();
+    assertEquals(expectedPath, d.lastPath().getPathString());
+  }
+
+  /**
+   * Returns the scope root with the final n segments chopped off (or a 0-segment path
+   * if n > SCOPE_ROOT.segmentCount()).
+   */
+  private String chopScopeRoot(int n) {
+    return SCOPE_ROOT
+        .subFragment(0, n > SCOPE_ROOT.segmentCount() ? 0 : SCOPE_ROOT.segmentCount() - n)
+        .getPathString();
+  }
+
+  /**
+   * Tests that absolute symlinks with ".." and "." segments are delegated to
+   * the expected paths.
+   */
+  @Test
+  public void testAbsoluteSymlinksWithParentReferences() throws Exception {
+    TestDelegator d = newExistsDelegator();
+    scopedFS().setDelegator(d);
+    testFS.createDirectory(testFS.getPath(SCOPE_ROOT.getRelative("dir")));
+    String scopeRoot = SCOPE_ROOT.getPathString();
+    String scopeBase = SCOPE_ROOT.getBaseName();
+
+    // Symlinks that should never escape our scope.
+    assertInScopeLink("ilink1", scopeRoot, d);
+    assertInScopeLink("ilink2", scopeRoot + "/target", d);
+    assertInScopeLink("ilink3", scopeRoot + "/dir/../target", d);
+    assertInScopeLink("ilink4", scopeRoot + "/dir/../dir/dir2/../target", d);
+    assertInScopeLink("ilink5", scopeRoot + "/./dir/.././target", d);
+    assertInScopeLink("ilink6", scopeRoot + "/../" + scopeBase + "/target", d);
+    assertInScopeLink("ilink7", "/some/path/../.." + scopeRoot + "/target", d);
+
+    // Symlinks that should escape our scope.
+    assertOutOfScopeLink("olink1", scopeRoot + "/../target", chopScopeRoot(1) + "/target", d);
+    assertOutOfScopeLink("olink2", "/some/other/path", "/some/other/path", d);
+    assertOutOfScopeLink("olink3", scopeRoot + "/../target", chopScopeRoot(1) + "/target", d);
+    assertOutOfScopeLink("olink4", chopScopeRoot(1) + "/target", chopScopeRoot(1) + "/target", d);
+    assertOutOfScopeLink("olink5", scopeRoot + "/../../../../target", "/target", d);
+
+    // In-scope symlink that's not the final segment in a query.
+    Path iDirLink = testFS.getPath(SCOPE_ROOT.getRelative("ilinkdir"));
+    testFS.createSymbolicLink(iDirLink, SCOPE_ROOT.getRelative("dir"));
+    iDirLink.getRelative("file").exists();
+    assertNull(d.lastPath());
+
+    // Out-of-scope symlink that's not the final segment in a query.
+    Path oDirLink = testFS.getPath(SCOPE_ROOT.getRelative("olinkdir"));
+    testFS.createSymbolicLink(oDirLink, new PathFragment("/some/other/dir"));
+    oDirLink.getRelative("file").exists();
+    assertEquals("/some/other/dir/file", d.lastPath().getPathString());
+  }
+
+  /**
+   * Tests that relative symlinks with ".." and "." segments are delegated to
+   * the expected paths.
+   */
+  @Test
+  public void testRelativeSymlinksWithParentReferences() throws Exception {
+    TestDelegator d = newExistsDelegator();
+    scopedFS().setDelegator(d);
+    testFS.createDirectory(testFS.getPath(SCOPE_ROOT.getRelative("dir")));
+    testFS.createDirectory(testFS.getPath(SCOPE_ROOT.getRelative("dir/dir2")));
+    testFS.createDirectory(testFS.getPath(SCOPE_ROOT.getRelative("dir/dir2/dir3")));
+    String scopeRoot = SCOPE_ROOT.getPathString();
+    String scopeBase = SCOPE_ROOT.getBaseName();
+
+    // Symlinks that should never escape our scope.
+    assertInScopeLink("ilink1", "target", d);
+    assertInScopeLink("ilink2", "dir/../otherdir/target", d);
+    assertInScopeLink("dir/ilink3", "../target", d);
+    assertInScopeLink("dir/dir2/ilink4", "../../target", d);
+    assertInScopeLink("dir/dir2/ilink5", ".././../dir/./target", d);
+    assertInScopeLink("dir/dir2/ilink6", "../dir2/../../dir/dir2/dir3/../../../target", d);
+
+    // Symlinks that should escape our scope.
+    assertOutOfScopeLink("olink1", "../target", chopScopeRoot(1) + "/target", d);
+    assertOutOfScopeLink("dir/olink2", "../../target", chopScopeRoot(1) + "/target", d);
+    assertOutOfScopeLink("olink3", "../" + scopeBase + "/target", scopeRoot + "/target", d);
+    assertOutOfScopeLink("dir/dir2/olink5", "../../../target", chopScopeRoot(1) + "/target", d);
+    assertOutOfScopeLink("dir/dir2/olink6", "../dir2/../../dir/dir2/../../../target",
+        chopScopeRoot(1) + "/target", d);
+    assertOutOfScopeLink("dir/olink7", "../../../target", chopScopeRoot(2) + "target", d);
+    assertOutOfScopeLink("olink8", "../../../../../target", "/target", d);
+
+    // In-scope symlink that's not the final segment in a query.
+    Path iDirLink = testFS.getPath(SCOPE_ROOT.getRelative("dir/dir2/ilinkdir"));
+    testFS.createSymbolicLink(iDirLink, new PathFragment("../../dir"));
+    iDirLink.getRelative("file").exists();
+    assertNull(d.lastPath());
+
+    // Out-of-scope symlink that's not the final segment in a query.
+    Path oDirLink = testFS.getPath(SCOPE_ROOT.getRelative("dir/dir2/olinkdir"));
+    testFS.createSymbolicLink(oDirLink, new PathFragment("../../../other/dir"));
+    oDirLink.getRelative("file").exists();
+    assertEquals(chopScopeRoot(1) + "/other/dir/file", d.lastPath().getPathString());
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java
new file mode 100644
index 0000000..a728c88
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java
@@ -0,0 +1,717 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.devtools.build.lib.testutil.MoreAsserts;
+import com.google.devtools.build.lib.vfs.FileSystem.NotASymlinkException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * This class handles the generic tests that any filesystem must pass.
+ *
+ * <p>Each filesystem-test should inherit from this class, thereby obtaining
+ * all the tests.
+ */
+public abstract class SymlinkAwareFileSystemTest extends FileSystemTest {
+
+  protected Path xLinkToFile;
+  protected Path xLinkToLinkToFile;
+  protected Path xLinkToDirectory;
+  protected Path xDanglingLink;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+
+    // % ls -lR
+    // -rw-rw-r-- xFile
+    // drwxrwxr-x xNonEmptyDirectory
+    // -rw-rw-r-- xNonEmptyDirectory/foo
+    // drwxrwxr-x xEmptyDirectory
+    // lrwxrwxr-x xLinkToFile -> xFile
+    // lrwxrwxr-x xLinkToDirectory -> xEmptyDirectory
+    // lrwxrwxr-x xLinkToLinkToFile -> xLinkToFile
+    // lrwxrwxr-x xDanglingLink -> xNothing
+
+    xLinkToFile = absolutize("xLinkToFile");
+    xLinkToLinkToFile = absolutize("xLinkToLinkToFile");
+    xLinkToDirectory = absolutize("xLinkToDirectory");
+    xDanglingLink = absolutize("xDanglingLink");
+
+    createSymbolicLink(xLinkToFile, xFile);
+    createSymbolicLink(xLinkToLinkToFile, xLinkToFile);
+    createSymbolicLink(xLinkToDirectory, xEmptyDirectory);
+    createSymbolicLink(xDanglingLink, xNothing);
+  }
+
+  @Test
+  public void testCreateLinkToFile() throws IOException {
+    Path newPath = xEmptyDirectory.getChild("new-file");
+    FileSystemUtils.createEmptyFile(newPath);
+
+    Path linkPath = xEmptyDirectory.getChild("some-link");
+
+    createSymbolicLink(linkPath, newPath);
+
+    assertTrue(linkPath.isSymbolicLink());
+
+    assertTrue(linkPath.isFile());
+    assertFalse(linkPath.isFile(Symlinks.NOFOLLOW));
+    assertTrue(linkPath.isFile(Symlinks.FOLLOW));
+
+    assertFalse(linkPath.isDirectory());
+    assertFalse(linkPath.isDirectory(Symlinks.NOFOLLOW));
+    assertFalse(linkPath.isDirectory(Symlinks.FOLLOW));
+
+    if (supportsSymlinks) {
+      assertEquals(newPath.toString().length(), linkPath.getFileSize(Symlinks.NOFOLLOW));
+      assertEquals(newPath.getFileSize(Symlinks.NOFOLLOW), linkPath.getFileSize());
+    }
+    assertEquals(2,
+                 linkPath.getParentDirectory().getDirectoryEntries().size());
+    assertThat(linkPath.getParentDirectory().getDirectoryEntries()).containsExactly(newPath,
+        linkPath);
+  }
+
+  @Test
+  public void testCreateLinkToDirectory() throws IOException {
+    Path newPath = xEmptyDirectory.getChild("new-file");
+    newPath.createDirectory();
+
+    Path linkPath = xEmptyDirectory.getChild("some-link");
+
+    createSymbolicLink(linkPath, newPath);
+
+    assertTrue(linkPath.isSymbolicLink());
+    assertFalse(linkPath.isFile());
+    assertTrue(linkPath.isDirectory());
+    assertEquals(2,
+                 linkPath.getParentDirectory().getDirectoryEntries().size());
+    assertThat(linkPath.getParentDirectory().
+      getDirectoryEntries()).containsExactly(newPath, linkPath);
+  }
+
+  @Test
+  public void testFileCanonicalPath() throws IOException {
+    Path newPath = absolutize("new-file");
+    FileSystemUtils.createEmptyFile(newPath);
+    newPath = newPath.resolveSymbolicLinks();
+
+    Path link1 = absolutize("some-link");
+    Path link2 = absolutize("some-link2");
+
+    createSymbolicLink(link1, newPath);
+    createSymbolicLink(link2, link1);
+
+    assertCanonicalPathsMatch(newPath, link1, link2);
+  }
+
+  @Test
+  public void testDirectoryCanonicalPath() throws IOException {
+    Path newPath = absolutize("new-folder");
+    newPath.createDirectory();
+    newPath = newPath.resolveSymbolicLinks();
+
+    Path newFile = newPath.getChild("file");
+    FileSystemUtils.createEmptyFile(newFile);
+
+    Path link1 = absolutize("some-link");
+    Path link2 = absolutize("some-link2");
+
+    createSymbolicLink(link1, newPath);
+    createSymbolicLink(link2, link1);
+
+    Path linkFile1 = link1.getChild("file");
+    Path linkFile2 = link2.getChild("file");
+
+    assertCanonicalPathsMatch(newFile, linkFile1, linkFile2);
+  }
+
+  private void assertCanonicalPathsMatch(Path newPath, Path link1, Path link2)
+      throws IOException {
+    assertEquals(newPath, link1.resolveSymbolicLinks());
+    assertEquals(newPath, link2.resolveSymbolicLinks());
+  }
+
+  //
+  //  createDirectory
+  //
+
+  @Test
+  public void testCreateDirectoryWhereDanglingSymlinkAlreadyExists() {
+    try {
+      xDanglingLink.createDirectory();
+      fail();
+    } catch (IOException e) {
+      assertEquals(xDanglingLink + " (File exists)", e.getMessage());
+    }
+    assertTrue(xDanglingLink.isSymbolicLink()); // still a symbolic link
+    assertFalse(xDanglingLink.isDirectory(Symlinks.FOLLOW)); // link still dangles
+  }
+
+  @Test
+  public void testCreateDirectoryWhereSymlinkAlreadyExists() {
+    try {
+      xLinkToDirectory.createDirectory();
+      fail();
+    } catch (IOException e) {
+      assertEquals(xLinkToDirectory + " (File exists)", e.getMessage());
+    }
+    assertTrue(xLinkToDirectory.isSymbolicLink()); // still a symbolic link
+    assertTrue(xLinkToDirectory.isDirectory(Symlinks.FOLLOW)); // link still points to dir
+  }
+
+  //  createSymbolicLink(PathFragment)
+
+  @Test
+  public void testCreateSymbolicLinkFromFragment() throws IOException {
+    String[] linkTargets = {
+      "foo",
+      "foo/bar",
+      ".",
+      "..",
+      "../foo",
+      "../../foo",
+      "../../../../../../../../../../../../../../../../../../../../../foo",
+      "/foo",
+      "/foo/bar",
+      "/..",
+      "/foo/../bar",
+    };
+    Path linkPath = absolutize("link");
+    for (String linkTarget : linkTargets) {
+      PathFragment relative = new PathFragment(linkTarget);
+      linkPath.delete();
+      createSymbolicLink(linkPath, relative);
+      if (supportsSymlinks) {
+        assertEquals(linkTarget.length(), linkPath.getFileSize(Symlinks.NOFOLLOW));
+        assertEquals(relative, linkPath.readSymbolicLink());
+      }
+    }
+  }
+
+  @Test
+  public void testLinkToRootResolvesCorrectly() throws IOException {
+    Path rootPath = testFS.getPath("/");
+    Path linkPath = absolutize("link");
+    createSymbolicLink(linkPath, rootPath);
+
+    // resolveSymbolicLinks requires an existing path:
+    try {
+      linkPath.getRelative("test").resolveSymbolicLinks();
+      fail();
+    } catch (FileNotFoundException e) { /* ok */ }
+
+    // The path may not be a symlink, neither on Darwin nor on Linux.
+    Path rootChild = testFS.getPath("/sbin");
+    if (!rootChild.isDirectory()) {
+      rootChild.createDirectory();
+    }
+    assertEquals(rootChild, linkPath.getRelative("sbin").resolveSymbolicLinks());
+  }
+
+  @Test
+  public void testLinkToFragmentContainingLinkResolvesCorrectly() throws IOException {
+    Path link1 = absolutize("link1");
+    PathFragment link1target = new PathFragment("link2/foo");
+    Path link2 = absolutize("link2");
+    Path link2target = xNonEmptyDirectory;
+
+    createSymbolicLink(link1, link1target); // ln -s link2/foo link1
+    createSymbolicLink(link2, link2target); // ln -s xNonEmptyDirectory link2
+    // link1 --> xNonEmptyDirectory/foo
+    assertEquals(link1.resolveSymbolicLinks(), link2target.getRelative("foo"));
+  }
+
+  //
+  //  readSymbolicLink / resolveSymbolicLinks
+  //
+
+  @Test
+  public void testRecursiveSymbolicLink() throws IOException {
+    Path link = absolutize("recursive-link");
+    createSymbolicLink(link, link);
+
+    if (supportsSymlinks) {
+      try {
+        link.resolveSymbolicLinks();
+        fail();
+      } catch (IOException e) {
+        assertEquals(link + " (Too many levels of symbolic links)",
+                     e.getMessage());
+      }
+    }
+  }
+
+  @Test
+  public void testMutuallyRecursiveSymbolicLinks() throws IOException {
+    Path link1 = absolutize("link1");
+    Path link2 = absolutize("link2");
+    createSymbolicLink(link2, link1);
+    createSymbolicLink(link1, link2);
+
+    if (supportsSymlinks) {
+      try {
+        link1.resolveSymbolicLinks();
+        fail();
+      } catch (IOException e) {
+        assertEquals(link1 + " (Too many levels of symbolic links)", e.getMessage());
+      }
+    }
+  }
+
+  @Test
+  public void testResolveSymbolicLinksENOENT() {
+    if (supportsSymlinks) {
+      try {
+        xDanglingLink.resolveSymbolicLinks();
+        fail();
+      } catch (IOException e) {
+        assertEquals(xNothing + " (No such file or directory)", e.getMessage());
+      }
+    }
+  }
+
+  @Test
+  public void testResolveSymbolicLinksENOTDIR() throws IOException {
+    if (supportsSymlinks) {
+      Path badLinkTarget = xFile.getChild("bad"); // parent is not a directory!
+      Path badLink = absolutize("badLink");
+      createSymbolicLink(badLink, badLinkTarget);
+      try {
+        badLink.resolveSymbolicLinks();
+        fail();
+      } catch (IOException e) {
+        // ok.  Ideally we would assert "(Not a directory)" in the error
+        // message, but that would require yet another stat in the
+        // implementation.
+      }
+    }
+  }
+
+  @Test
+  public void testResolveSymbolicLinksWithUplevelRefs() throws IOException {
+    if (supportsSymlinks) {
+      // Create a series of links that refer to xFile as ./xFile,
+      // ./../foo/xFile, ./../../bar/foo/xFile, etc.  They should all resolve
+      // to xFile.
+      Path ancestor = xFile;
+      String prefix = "./";
+      while ((ancestor = ancestor.getParentDirectory()) != null) {
+        xLinkToFile.delete();
+        createSymbolicLink(xLinkToFile, new PathFragment(prefix + xFile.relativeTo(ancestor)));
+        assertEquals(xFile, xLinkToFile.resolveSymbolicLinks());
+
+        prefix += "../";
+      }
+    }
+  }
+
+  @Test
+  public void testReadSymbolicLink() throws IOException {
+    if (supportsSymlinks) {
+      assertEquals(xNothing.toString(),
+                   xDanglingLink.readSymbolicLink().toString());
+    }
+
+    assertEquals(xFile.toString(),
+                 xLinkToFile.readSymbolicLink().toString());
+
+    assertEquals(xEmptyDirectory.toString(),
+                 xLinkToDirectory.readSymbolicLink().toString());
+
+    try {
+      xFile.readSymbolicLink(); // not a link
+      fail();
+    } catch (NotASymlinkException e) {
+      assertEquals(xFile.toString(), e.getMessage());
+    }
+
+    try {
+      xNothing.readSymbolicLink(); // nothing there
+      fail();
+    } catch (IOException e) {
+      assertEquals(xNothing + " (No such file or directory)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCannotCreateSymbolicLinkWithReadOnlyParent()
+      throws IOException {
+    xEmptyDirectory.setWritable(false);
+    Path xChildOfReadonlyDir = xEmptyDirectory.getChild("x");
+    if (supportsSymlinks) {
+      try {
+        xChildOfReadonlyDir.createSymbolicLink(xNothing);
+        fail();
+      } catch (IOException e) {
+        assertEquals(xChildOfReadonlyDir + " (Permission denied)", e.getMessage());
+      }
+    }
+  }
+
+  //
+  // createSymbolicLink
+  //
+
+  @Test
+  public void testCanCreateDanglingLink() throws IOException {
+    Path newPath = absolutize("non-existing-dir/new-file");
+    Path someLink = absolutize("dangling-link");
+    createSymbolicLink(someLink, newPath);
+    assertTrue(someLink.isSymbolicLink());
+    assertTrue(someLink.exists(Symlinks.NOFOLLOW)); // the link itself exists
+    assertFalse(someLink.exists()); // ...but the referent doesn't
+    if (supportsSymlinks) {
+      try {
+        someLink.resolveSymbolicLinks();
+      } catch (FileNotFoundException e) {
+        assertEquals(newPath.getParentDirectory()
+                     + " (No such file or directory)", e.getMessage());
+      }
+    }
+  }
+
+  @Test
+  public void testCannotCreateSymbolicLinkWithoutParent() throws IOException {
+    Path xChildOfMissingDir = xNothing.getChild("x");
+    if (supportsSymlinks) {
+      try {
+        xChildOfMissingDir.createSymbolicLink(xFile);
+        fail();
+      } catch (FileNotFoundException e) {
+        MoreAsserts.assertEndsWith(" (No such file or directory)", e.getMessage());
+      }
+    }
+  }
+
+  @Test
+  public void testCreateSymbolicLinkWhereNothingExists() throws IOException {
+    createSymbolicLink(xNothing, xFile);
+    assertTrue(xNothing.isSymbolicLink());
+  }
+
+  @Test
+  public void testCreateSymbolicLinkWhereDirectoryAlreadyExists() {
+    try {
+      createSymbolicLink(xEmptyDirectory, xFile);
+      fail();
+    } catch (IOException e) { // => couldn't be created
+      assertEquals(xEmptyDirectory + " (File exists)", e.getMessage());
+    }
+    assertTrue(xEmptyDirectory.isDirectory(Symlinks.NOFOLLOW));
+  }
+
+  @Test
+  public void testCreateSymbolicLinkWhereFileAlreadyExists() {
+    try {
+      createSymbolicLink(xFile, xEmptyDirectory);
+      fail();
+    } catch (IOException e) { // => couldn't be created
+      assertEquals(xFile + " (File exists)", e.getMessage());
+    }
+    assertTrue(xFile.isFile(Symlinks.NOFOLLOW));
+  }
+
+  @Test
+  public void testCreateSymbolicLinkWhereDanglingSymlinkAlreadyExists() {
+    try {
+      createSymbolicLink(xDanglingLink, xFile);
+      fail();
+    } catch (IOException e) {
+      assertEquals(xDanglingLink + " (File exists)", e.getMessage());
+    }
+    assertTrue(xDanglingLink.isSymbolicLink()); // still a symbolic link
+    assertFalse(xDanglingLink.isDirectory()); // link still dangles
+  }
+
+  @Test
+  public void testCreateSymbolicLinkWhereSymlinkAlreadyExists() {
+    try {
+      createSymbolicLink(xLinkToDirectory, xNothing);
+      fail();
+    } catch (IOException e) {
+      assertEquals(xLinkToDirectory + " (File exists)", e.getMessage());
+    }
+    assertTrue(xLinkToDirectory.isSymbolicLink()); // still a symbolic link
+    assertTrue(xLinkToDirectory.isDirectory()); // link still points to dir
+  }
+
+  @Test
+  public void testDeleteLink() throws IOException {
+    Path newPath = xEmptyDirectory.getChild("new-file");
+    Path someLink = xEmptyDirectory.getChild("a-link");
+    FileSystemUtils.createEmptyFile(newPath);
+    createSymbolicLink(someLink, newPath);
+
+    assertEquals(xEmptyDirectory.getDirectoryEntries().size(), 2);
+
+    assertTrue(someLink.delete());
+    assertEquals(xEmptyDirectory.getDirectoryEntries().size(), 1);
+
+    assertThat(xEmptyDirectory.getDirectoryEntries()).containsExactly(newPath);
+  }
+
+  // Testing the links
+  @Test
+  public void testLinkFollowedToDirectory() throws IOException {
+    Path theDirectory = absolutize("foo/");
+    assertTrue(theDirectory.createDirectory());
+    Path newPath1 = absolutize("foo/new-file-1");
+    Path newPath2 = absolutize("foo/new-file-2");
+    Path newPath3 = absolutize("foo/new-file-3");
+
+    FileSystemUtils.createEmptyFile(newPath1);
+    FileSystemUtils.createEmptyFile(newPath2);
+    FileSystemUtils.createEmptyFile(newPath3);
+
+    Path linkPath = absolutize("link");
+    createSymbolicLink(linkPath, theDirectory);
+
+    Path resultPath1 = absolutize("link/new-file-1");
+    Path resultPath2 = absolutize("link/new-file-2");
+    Path resultPath3 = absolutize("link/new-file-3");
+    assertThat(linkPath.getDirectoryEntries()).containsExactly(resultPath1, resultPath2,
+        resultPath3);
+  }
+
+  @Test
+  public void testDanglingLinkIsNoFile() throws IOException {
+    Path newPath1 = absolutize("new-file-1");
+    Path newPath2 = absolutize("new-file-2");
+    FileSystemUtils.createEmptyFile(newPath1);
+    assertTrue(newPath2.createDirectory());
+
+    Path linkPath1 = absolutize("link1");
+    Path linkPath2 = absolutize("link2");
+    createSymbolicLink(linkPath1, newPath1);
+    createSymbolicLink(linkPath2, newPath2);
+
+    newPath1.delete();
+    newPath2.delete();
+
+    assertFalse(linkPath1.isFile());
+    assertFalse(linkPath2.isDirectory());
+  }
+
+  @Test
+  public void testWriteOnLinkChangesFile() throws IOException {
+    Path testFile = absolutize("test-file");
+    FileSystemUtils.createEmptyFile(testFile);
+    String testData = "abc19";
+
+    Path testLink = absolutize("a-link");
+    createSymbolicLink(testLink, testFile);
+
+    FileSystemUtils.writeContentAsLatin1(testLink, testData);
+    String resultData =
+      new String(FileSystemUtils.readContentAsLatin1(testFile));
+
+    assertEquals(testData,resultData);
+  }
+
+  //
+  // Symlink tests:
+  //
+
+  @Test
+  public void testExistsWithSymlinks() throws IOException {
+    Path a = absolutize("a");
+    Path b = absolutize("b");
+    FileSystemUtils.createEmptyFile(b);
+    createSymbolicLink(a, b);  // ln -sf "b" "a"
+    assertTrue(a.exists()); // = exists(FOLLOW)
+    assertTrue(b.exists()); // = exists(FOLLOW)
+    assertTrue(a.exists(Symlinks.FOLLOW));
+    assertTrue(b.exists(Symlinks.FOLLOW));
+    assertTrue(a.exists(Symlinks.NOFOLLOW));
+    assertTrue(b.exists(Symlinks.NOFOLLOW));
+    b.delete(); // "a" is now a dangling link
+    assertFalse(a.exists()); // = exists(FOLLOW)
+    assertFalse(b.exists()); // = exists(FOLLOW)
+    assertFalse(a.exists(Symlinks.FOLLOW));
+    assertFalse(b.exists(Symlinks.FOLLOW));
+
+    assertTrue(a.exists(Symlinks.NOFOLLOW)); // symlink still exists
+    assertFalse(b.exists(Symlinks.NOFOLLOW));
+  }
+
+  @Test
+  public void testIsDirectoryWithSymlinks() throws IOException {
+    Path a = absolutize("a");
+    Path b = absolutize("b");
+    b.createDirectory();
+    createSymbolicLink(a, b);  // ln -sf "b" "a"
+    assertTrue(a.isDirectory()); // = isDirectory(FOLLOW)
+    assertTrue(b.isDirectory()); // = isDirectory(FOLLOW)
+    assertTrue(a.isDirectory(Symlinks.FOLLOW));
+    assertTrue(b.isDirectory(Symlinks.FOLLOW));
+    assertFalse(a.isDirectory(Symlinks.NOFOLLOW)); // it's a link!
+    assertTrue(b.isDirectory(Symlinks.NOFOLLOW));
+    b.delete(); // "a" is now a dangling link
+    assertFalse(a.isDirectory()); // = isDirectory(FOLLOW)
+    assertFalse(b.isDirectory()); // = isDirectory(FOLLOW)
+    assertFalse(a.isDirectory(Symlinks.FOLLOW));
+    assertFalse(b.isDirectory(Symlinks.FOLLOW));
+    assertFalse(a.isDirectory(Symlinks.NOFOLLOW));
+    assertFalse(b.isDirectory(Symlinks.NOFOLLOW));
+  }
+
+  @Test
+  public void testIsFileWithSymlinks() throws IOException {
+    Path a = absolutize("a");
+    Path b = absolutize("b");
+    FileSystemUtils.createEmptyFile(b);
+    createSymbolicLink(a, b);  // ln -sf "b" "a"
+    assertTrue(a.isFile()); // = isFile(FOLLOW)
+    assertTrue(b.isFile()); // = isFile(FOLLOW)
+    assertTrue(a.isFile(Symlinks.FOLLOW));
+    assertTrue(b.isFile(Symlinks.FOLLOW));
+    assertFalse(a.isFile(Symlinks.NOFOLLOW)); // it's a link!
+    assertTrue(b.isFile(Symlinks.NOFOLLOW));
+    b.delete(); // "a" is now a dangling link
+    assertFalse(a.isFile()); // = isFile()
+    assertFalse(b.isFile()); // = isFile()
+    assertFalse(a.isFile());
+    assertFalse(b.isFile());
+    assertFalse(a.isFile(Symlinks.NOFOLLOW));
+    assertFalse(b.isFile(Symlinks.NOFOLLOW));
+  }
+
+  @Test
+  public void testGetDirectoryEntriesOnLinkToDirectory() throws Exception {
+    Path fooAlias = xNothing.getChild("foo");
+    createSymbolicLink(xNothing, xNonEmptyDirectory);
+    Collection<Path> dirents = xNothing.getDirectoryEntries();
+    assertThat(dirents).containsExactly(fooAlias);
+  }
+
+  @Test
+  public void testFilesOfLinkedDirectories() throws Exception {
+    Path child = xEmptyDirectory.getChild("child");
+    Path aliasToChild = xLinkToDirectory.getChild("child");
+
+    assertFalse(aliasToChild.exists());
+    FileSystemUtils.createEmptyFile(child);
+    assertTrue(aliasToChild.exists());
+    assertTrue(aliasToChild.isFile());
+    assertFalse(aliasToChild.isDirectory());
+
+    validateLinkedReferenceObeysReadOnly(child, aliasToChild);
+    validateLinkedReferenceObeysExecutable(child, aliasToChild);
+  }
+
+  @Test
+  public void testDirectoriesOfLinkedDirectories() throws Exception {
+    Path childDir = xEmptyDirectory.getChild("childDir");
+    Path linkToChildDir = xLinkToDirectory.getChild("childDir");
+
+    assertFalse(linkToChildDir.exists());
+    childDir.createDirectory();
+    assertTrue(linkToChildDir.exists());
+    assertTrue(linkToChildDir.isDirectory());
+    assertFalse(linkToChildDir.isFile());
+
+    validateLinkedReferenceObeysReadOnly(childDir, linkToChildDir);
+    validateLinkedReferenceObeysExecutable(childDir, linkToChildDir);
+  }
+
+  @Test
+  public void testDirectoriesOfLinkedDirectoriesOfLinkedDirectories() throws Exception {
+    Path childDir = xEmptyDirectory.getChild("childDir");
+    Path linkToLinkToDirectory = absolutize("xLinkToLinkToDirectory");
+    createSymbolicLink(linkToLinkToDirectory, xLinkToDirectory);
+    Path linkToChildDir = linkToLinkToDirectory.getChild("childDir");
+
+    assertFalse(linkToChildDir.exists());
+    childDir.createDirectory();
+    assertTrue(linkToChildDir.exists());
+    assertTrue(linkToChildDir.isDirectory());
+    assertFalse(linkToChildDir.isFile());
+
+    validateLinkedReferenceObeysReadOnly(childDir, linkToChildDir);
+    validateLinkedReferenceObeysExecutable(childDir, linkToChildDir);
+  }
+
+  private void validateLinkedReferenceObeysReadOnly(Path path, Path link) throws IOException {
+    path.setWritable(false);
+    assertFalse(path.isWritable());
+    assertFalse(link.isWritable());
+    path.setWritable(true);
+    assertTrue(path.isWritable());
+    assertTrue(link.isWritable());
+    path.setWritable(false);
+    assertFalse(path.isWritable());
+    assertFalse(link.isWritable());
+  }
+
+  private void validateLinkedReferenceObeysExecutable(Path path, Path link) throws IOException {
+    path.setExecutable(true);
+    assertTrue(path.isExecutable());
+    assertTrue(link.isExecutable());
+    path.setExecutable(false);
+    assertFalse(path.isExecutable());
+    assertFalse(link.isExecutable());
+    path.setExecutable(true);
+    assertTrue(path.isExecutable());
+    assertTrue(link.isExecutable());
+  }
+
+  @Test
+  public void testReadingFileFromLinkedDirectory() throws Exception {
+    Path linkedTo = absolutize("linkedTo");
+    linkedTo.createDirectory();
+    Path child = linkedTo.getChild("child");
+    FileSystemUtils.createEmptyFile(child);
+
+    byte[] outputData = "This is a test".getBytes();
+    FileSystemUtils.writeContent(child, outputData);
+
+    Path link = absolutize("link");
+    createSymbolicLink(link, linkedTo);
+    Path linkedChild = link.getChild("child");
+    byte[] inputData = FileSystemUtils.readContent(linkedChild);
+    assertArrayEquals(outputData, inputData);
+  }
+
+  @Test
+  public void testCreatingFileInLinkedDirectory() throws Exception {
+    Path linkedTo = absolutize("linkedTo");
+    linkedTo.createDirectory();
+    Path child = linkedTo.getChild("child");
+
+    Path link = absolutize("link");
+    createSymbolicLink(link, linkedTo);
+    Path linkedChild = link.getChild("child");
+    byte[] outputData = "This is a test".getBytes();
+    FileSystemUtils.writeContent(linkedChild, outputData);
+
+    byte[] inputData = FileSystemUtils.readContent(child);
+    assertArrayEquals(outputData, inputData);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java
new file mode 100644
index 0000000..396a9f8
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java
@@ -0,0 +1,330 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Tests for the UnionFileSystem, both of generic FileSystem functionality
+ * (inherited) and tests of UnionFileSystem-specific behavior.
+ */
+@RunWith(JUnit4.class)
+public class UnionFileSystemTest extends SymlinkAwareFileSystemTest {
+  private XAttrInMemoryFs inDelegate;
+  private XAttrInMemoryFs outDelegate;
+  private XAttrInMemoryFs defaultDelegate;
+  private UnionFileSystem unionfs;
+
+  private static final String XATTR_VAL = "SOME_XATTR_VAL";
+  private static final String XATTR_KEY = "SOME_XATTR_KEY";
+
+  private void setupDelegateFileSystems() {
+    inDelegate = new XAttrInMemoryFs(BlazeClock.instance());
+    outDelegate = new XAttrInMemoryFs(BlazeClock.instance());
+    defaultDelegate = new XAttrInMemoryFs(BlazeClock.instance());
+
+    unionfs = createDefaultUnionFileSystem();
+  }
+
+  private UnionFileSystem createDefaultUnionFileSystem() {
+    return createDefaultUnionFileSystem(false);
+  }
+
+  private UnionFileSystem createDefaultUnionFileSystem(boolean readOnly) {
+    return new UnionFileSystem(ImmutableMap.<PathFragment, FileSystem>of(
+        new PathFragment("/in"), inDelegate,
+        new PathFragment("/out"), outDelegate),
+        defaultDelegate, readOnly);
+  }
+
+  @Override
+  protected FileSystem getFreshFileSystem() {
+    // Executed with each new test because it is called by super.setUp().
+    setupDelegateFileSystems();
+    return unionfs;
+  }
+
+  @Override
+  public void destroyFileSystem(FileSystem fileSystem) {
+    // Nothing.
+  }
+
+  // Tests of UnionFileSystem-specific behavior below.
+
+  @Test
+  public void testBasicDelegation() throws Exception {
+    unionfs = createDefaultUnionFileSystem();
+    Path fooPath = unionfs.getPath("/foo");
+    Path inPath = unionfs.getPath("/in");
+    Path outPath = unionfs.getPath("/out/in.txt");
+    assertSame(inDelegate, unionfs.getDelegate(inPath));
+    assertSame(outDelegate, unionfs.getDelegate(outPath));
+    assertSame(defaultDelegate, unionfs.getDelegate(fooPath));
+  }
+
+  @Test
+  public void testBasicXattr() throws Exception {
+    Path fooPath = unionfs.getPath("/foo");
+    Path inPath = unionfs.getPath("/in");
+    Path outPath = unionfs.getPath("/out/in.txt");
+
+    assertArrayEquals(XATTR_VAL.getBytes(UTF_8), inPath.getxattr(XATTR_KEY));
+    assertArrayEquals(XATTR_VAL.getBytes(UTF_8), outPath.getxattr(XATTR_KEY));
+    assertArrayEquals(XATTR_VAL.getBytes(UTF_8), fooPath.getxattr(XATTR_KEY));
+    assertNull(inPath.getxattr("not_key"));
+    assertNull(outPath.getxattr("not_key"));
+    assertNull(fooPath.getxattr("not_key"));
+  }
+
+  @Test
+  public void testDefaultFileSystemRequired() throws Exception {
+    try {
+      new UnionFileSystem(ImmutableMap.<PathFragment, FileSystem>of(), null);
+      fail("Able to create a UnionFileSystem with no default!");
+    } catch (NullPointerException expected) {
+      // OK - should fail in this case.
+    }
+  }
+
+  // Check for appropriate registration and lookup of delegate filesystems based
+  // on path prefixes, including non-canonical paths.
+  @Test
+  public void testPrefixDelegation() throws Exception {
+    unionfs = new UnionFileSystem(ImmutableMap.<PathFragment, FileSystem>of(
+              new PathFragment("/foo"), inDelegate,
+              new PathFragment("/foo/bar"), outDelegate), defaultDelegate);
+
+    assertSame(inDelegate, unionfs.getDelegate(unionfs.getPath("/foo/foo.txt")));
+    assertSame(outDelegate, unionfs.getDelegate(unionfs.getPath("/foo/bar/foo.txt")));
+    assertSame(inDelegate, unionfs.getDelegate(unionfs.getPath("/foo/bar/../foo.txt")));
+    assertSame(defaultDelegate, unionfs.getDelegate(unionfs.getPath("/bar/foo.txt")));
+    assertSame(defaultDelegate, unionfs.getDelegate(unionfs.getPath("/foo/bar/../..")));
+  }
+
+  // Checks that files cannot be modified when the filesystem is created
+  // read-only, even if the delegate filesystems are read/write.
+  @Test
+  public void testModificationFlag() throws Exception {
+    assertTrue(unionfs.supportsModifications());
+    Path outPath = unionfs.getPath("/out/foo.txt");
+    assertTrue(unionfs.createDirectory(outPath.getParentDirectory()));
+    OutputStream outFile = unionfs.getOutputStream(outPath);
+    outFile.write('b');
+    outFile.close();
+
+    unionfs.setExecutable(outPath, true);
+
+    // Note that this does not destroy the underlying filesystems;
+    // UnionFileSystem is just a view.
+    unionfs = createDefaultUnionFileSystem(true);
+    assertFalse(unionfs.supportsModifications());
+
+    InputStream outFileInput = unionfs.getInputStream(outPath);
+    int outFileByte = outFileInput.read();
+    outFileInput.close();
+    assertEquals('b', outFileByte);
+
+    assertTrue(unionfs.isExecutable(outPath));
+
+    // Modifying files through the unionfs isn't permitted, even if the
+    // delegates are read/write.
+    try {
+      unionfs.setExecutable(outPath, false);
+      fail("Modification to a read-only UnionFileSystem succeeded.");
+    } catch (UnsupportedOperationException expected) {
+      // OK - should fail.
+    }
+  }
+
+  // Checks that roots of delegate filesystems are created outside of the
+  // delegate filesystems; i.e. they can be seen from the filesystem of the parent.
+  @Test
+  public void testDelegateRootDirectoryCreation() throws Exception {
+    Path foo = unionfs.getPath("/foo");
+    Path bar = unionfs.getPath("/bar");
+    Path out = unionfs.getPath("/out");
+    assertTrue(unionfs.createDirectory(foo));
+    assertTrue(unionfs.createDirectory(bar));
+    assertTrue(unionfs.createDirectory(out));
+    Path outFile = unionfs.getPath("/out/in");
+    FileSystemUtils.writeContentAsLatin1(outFile, "Out");
+
+    // FileSystemTest.setUp() silently creates the test root on the filesystem...
+    Path testDirUnderRoot = unionfs.getPath(workingDir.asFragment().subFragment(0, 1));
+    assertThat(unionfs.getDirectoryEntries(unionfs.getRootDirectory())).containsExactly(foo, bar,
+        out, testDirUnderRoot);
+    assertThat(unionfs.getDirectoryEntries(out)).containsExactly(outFile);
+
+    assertSame(unionfs.getDelegate(foo), defaultDelegate);
+    assertEquals(foo.asFragment(), unionfs.adjustPath(foo, defaultDelegate).asFragment());
+    assertSame(unionfs.getDelegate(bar), defaultDelegate);
+    assertSame(unionfs.getDelegate(outFile), outDelegate);
+    assertSame(unionfs.getDelegate(out), outDelegate);
+
+    // As a fragment (i.e. without filesystem or root info), the path name should be preserved.
+    assertEquals(outFile.asFragment(), unionfs.adjustPath(outFile, outDelegate).asFragment());
+  }
+
+  // Ensure that the right filesystem is still chosen when paths contain "..".
+  @Test
+  public void testDelegationOfUpLevelReferences() throws Exception {
+    assertSame(defaultDelegate, unionfs.getDelegate(unionfs.getPath("/in/../foo.txt")));
+    assertSame(inDelegate, unionfs.getDelegate(unionfs.getPath("/out/../in")));
+    assertSame(outDelegate, unionfs.getDelegate(unionfs.getPath("/out/../in/../out/foo.txt")));
+    assertSame(inDelegate, unionfs.getDelegate(unionfs.getPath("/in/./foo.txt")));
+  }
+
+  // Basic *explicit* cross-filesystem symlink check.
+  // Note: This does not work implicitly yet, as the next test illustrates.
+  @Test
+  public void testCrossDeviceSymlinks() throws Exception {
+    assertTrue(unionfs.createDirectory(unionfs.getPath("/out")));
+
+    // Create an "/in" directory directly on the output delegate to bypass the
+    // UnionFileSystem's mapping.
+    assertTrue(inDelegate.getPath("/in").createDirectory());
+    OutputStream outStream = inDelegate.getPath("/in/bar.txt").getOutputStream();
+    outStream.write('i');
+    outStream.close();
+
+    Path outFoo = unionfs.getPath("/out/foo");
+    unionfs.createSymbolicLink(outFoo, new PathFragment("../in/bar.txt"));
+    assertTrue(unionfs.stat(outFoo, false).isSymbolicLink());
+
+    try {
+      unionfs.stat(outFoo, true).isFile();
+      fail("Stat on cross-device symlink succeeded!");
+    } catch (FileNotFoundException expected) {
+      // OK
+    }
+
+    Path resolved = unionfs.resolveSymbolicLinks(outFoo);
+    assertSame(unionfs, resolved.getFileSystem());
+    InputStream barInput = resolved.getInputStream();
+    int barChar = barInput.read();
+    barInput.close();
+    assertEquals('i', barChar);
+  }
+
+  @Test
+  public void testNoDelegateLeakage() throws Exception {
+    assertSame(unionfs, unionfs.getPath("/in/foo.txt").getFileSystem());
+    assertSame(unionfs, unionfs.getPath("/in/foo/bar").getParentDirectory().getFileSystem());
+    unionfs.createDirectory(unionfs.getPath("/out"));
+    unionfs.createDirectory(unionfs.getPath("/out/foo"));
+    unionfs.createDirectory(unionfs.getPath("/out/foo/bar"));
+    assertSame(unionfs, Iterables.getOnlyElement(unionfs.getDirectoryEntries(
+        unionfs.getPath("/out/foo"))).getParentDirectory().getFileSystem());
+  }
+
+  // Prefix mappings can apply to files starting with a prefix within a directory.
+  @Test
+  public void testWithinDirectoryMapping() throws Exception {
+    unionfs = new UnionFileSystem(ImmutableMap.<PathFragment, FileSystem>of(
+        new PathFragment("/fruit/a"), inDelegate,
+        new PathFragment("/fruit/b"), outDelegate), defaultDelegate);
+    assertTrue(unionfs.createDirectory(unionfs.getPath("/fruit")));
+    assertTrue(defaultDelegate.getPath("/fruit").isDirectory());
+    assertTrue(inDelegate.getPath("/fruit").createDirectory());
+    assertTrue(outDelegate.getPath("/fruit").createDirectory());
+
+    Path apple = unionfs.getPath("/fruit/apple");
+    Path banana = unionfs.getPath("/fruit/banana");
+    Path cherry = unionfs.getPath("/fruit/cherry");
+    unionfs.createDirectory(apple);
+    unionfs.createDirectory(banana);
+    assertSame(inDelegate, unionfs.getDelegate(apple));
+    assertSame(outDelegate, unionfs.getDelegate(banana));
+    assertSame(defaultDelegate, unionfs.getDelegate(cherry));
+
+    FileSystemUtils.writeContentAsLatin1(apple.getRelative("table"), "penny");
+    FileSystemUtils.writeContentAsLatin1(banana.getRelative("nana"), "nanana");
+    FileSystemUtils.writeContentAsLatin1(cherry, "garcia");
+
+    assertEquals("penny", new String(
+        FileSystemUtils.readContentAsLatin1(inDelegate.getPath("/fruit/apple/table"))));
+    assertEquals("nanana", new String(
+        FileSystemUtils.readContentAsLatin1(outDelegate.getPath("/fruit/banana/nana"))));
+    assertEquals("garcia", new String(
+        FileSystemUtils.readContentAsLatin1(defaultDelegate.getPath("/fruit/cherry"))));
+  }
+
+  // Write using the VFS through a UnionFileSystem and check that the file can
+  // be read back in the same location using standard Java IO.
+  // There is a similar test in UnixFileSystem, but this is essential to ensure
+  // that paths aren't being remapped in some nasty way on the underlying FS.
+  @Test
+  public void testDelegateOperationsReflectOnLocalFilesystem() throws Exception {
+    unionfs = new UnionFileSystem(ImmutableMap.<PathFragment, FileSystem>of(
+        workingDir.getParentDirectory().asFragment(), new UnixFileSystem()),
+        defaultDelegate, false);
+    // This is a child of the current tmpdir, and doesn't exist on its own.
+    // It would be created in setup(), but of course, that didn't use a UnixFileSystem.
+    unionfs.createDirectory(workingDir);
+    Path testFile = unionfs.getPath(workingDir.getRelative("test_file").asFragment());
+    assertTrue(testFile.asFragment().startsWith(workingDir.asFragment()));
+    String testString = "This is a test file";
+    FileSystemUtils.writeContentAsLatin1(testFile, testString);
+    try {
+      assertEquals(testString, new String(FileSystemUtils.readContentAsLatin1(testFile)));
+    } finally {
+      testFile.delete();
+      assertTrue(unionfs.delete(workingDir));
+    }
+  }
+
+  // Regression test for [UnionFS: Directory creation across mapping fails.]
+  @Test
+  public void testCreateParentsAcrossMapping() throws Exception {
+    unionfs = new UnionFileSystem(ImmutableMap.<PathFragment, FileSystem>of(
+        new PathFragment("/out/dir"), outDelegate), defaultDelegate, false);
+    Path outDir = unionfs.getPath("/out/dir/biz/bang");
+    FileSystemUtils.createDirectoryAndParents(outDir);
+    assertTrue(outDir.isDirectory());
+  }
+
+  private static class XAttrInMemoryFs extends InMemoryFileSystem {
+    public XAttrInMemoryFs(Clock clock) {
+      super(clock);
+    }
+
+    @Override
+    protected byte[] getxattr(Path path, String name, boolean followSymlinks) {
+      assertSame(this, path.getFileSystem());
+      return (name.equals(XATTR_KEY)) ? XATTR_VAL.getBytes(UTF_8) : null;
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnixFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnixFileSystemTest.java
new file mode 100644
index 0000000..0ded404
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/UnixFileSystemTest.java
@@ -0,0 +1,63 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+
+/**
+ * Tests for the {@link UnixFileSystem} class.
+ */
+@RunWith(JUnit4.class)
+public class UnixFileSystemTest extends SymlinkAwareFileSystemTest {
+
+  @Override
+  protected FileSystem getFreshFileSystem() {
+    return new UnixFileSystem();
+  }
+
+  @Override
+  public void destroyFileSystem(FileSystem fileSystem) {
+    // Nothing.
+  }
+
+  @Override
+  protected void expectNotFound(Path path) throws IOException {
+    assertNull(path.statIfFound());
+  }
+
+  // Most tests are just inherited from FileSystemTest.
+
+  @Test
+  public void testCircularSymlinkFound() throws Exception {
+    Path linkA = absolutize("link-a");
+    Path linkB = absolutize("link-b");
+    linkA.createSymbolicLink(linkB);
+    linkB.createSymbolicLink(linkA);
+    assertFalse(linkA.exists(Symlinks.FOLLOW));
+    try {
+      linkA.statIfFound(Symlinks.FOLLOW);
+      fail();
+    } catch (IOException expected) {
+      // Expected.
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnixPathEqualityTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathEqualityTest.java
new file mode 100644
index 0000000..f5f58e2
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathEqualityTest.java
@@ -0,0 +1,118 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * This tests how canonical paths and non-canonical paths are equal with each
+ * other, and also how paths from different filesystems behave with each other.
+ */
+@RunWith(JUnit4.class)
+public class UnixPathEqualityTest {
+
+  private FileSystem otherUnixFs;
+  private FileSystem unixFs;
+
+  @Before
+  public void setUp() throws Exception {
+    unixFs = new UnixFileSystem();
+    otherUnixFs = new UnixFileSystem();
+    assertTrue(unixFs != otherUnixFs);
+  }
+
+  private void assertTwoWayEquals(Object obj1, Object obj2) {
+    assertTrue(obj1.equals(obj2));
+    assertTrue(obj2.equals(obj1));
+    assertEquals(obj1.hashCode(), obj2.hashCode());
+  }
+
+  private void assertTwoWayNotEquals(Object obj1, Object obj2) {
+    assertFalse(obj1.equals(obj2));
+    assertFalse(obj2.equals(obj1));
+  }
+
+  @Test
+  public void testPathsAreEqualEvenIfNotCanonical() {
+    // This path is already canonical, so there's no difference between
+    // the canonical / nonCanonical path, as far as equals is concerned
+    Path nonCanonical = unixFs.getPath("/a/canonical/unix/path");
+    Path canonical = unixFs.getPath("/a/canonical/unix/path");
+    assertTwoWayEquals(nonCanonical, canonical);
+  }
+
+  @Test
+  public void testPathsAreNeverEqualWithStrings() {
+    // Make sure that paths aren't equal to plain old strings
+    Path nonCanonical = unixFs.getPath("/a/non/../canonical/unix/path");
+    Path canonical = unixFs.getPath("/a/non/../canonical/unix/path");
+    assertTwoWayNotEquals(nonCanonical, "/a/non/../canonical/unix/path");
+    assertTwoWayNotEquals(canonical, "/a/non/../canonical/unix/path");
+  }
+
+  @Test
+  public void testCanonicalPathsFromDifferentFileSystemsAreNeverEqual() {
+    Path canonical = unixFs.getPath("/canonical/path");
+    Path otherCanonical = otherUnixFs.getPath("/canonical/path");
+    assertTwoWayNotEquals(canonical, otherCanonical);
+  }
+
+  @Test
+  public void testNonCanonicalPathsFromDifferentFileSystemsAreNeverEqual() {
+    Path nonCanonical = unixFs.getPath("/non/canonical/path");
+    Path otherNonCanonical = otherUnixFs.getPath("/non/canonical/path");
+    assertTwoWayNotEquals(nonCanonical, otherNonCanonical);
+  }
+
+  @Test
+  public void testCrossFilesystemStartsWithReturnsFalse() {
+    assertFalse(unixFs.getPath("/a").startsWith(otherUnixFs.getPath("/b")));
+  }
+
+  @Test
+  public void testCrossFilesystemOperationsForbidden() throws Exception {
+    Path a = unixFs.getPath("/a");
+    Path b = otherUnixFs.getPath("/b");
+
+    try {
+      a.renameTo(b);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage()).contains("different filesystems");
+    }
+
+    try {
+      a.relativeTo(b);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage()).contains("different filesystems");
+    }
+
+    try {
+      a.createSymbolicLink(b);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage()).contains("different filesystems");
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnixPathGetParentTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathGetParentTest.java
new file mode 100644
index 0000000..0f679c3
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathGetParentTest.java
@@ -0,0 +1,87 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.vfs.util.FileSystems;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+
+/**
+ * A test for {@link Path} in the context of {@link UnixFileSystem}.
+ */
+@RunWith(JUnit4.class)
+public class UnixPathGetParentTest {
+
+  private FileSystem unixFs;
+  private Path testRoot;
+
+  @Before
+  public void setUp() throws Exception {
+    unixFs = FileSystems.initDefaultAsNative();
+    testRoot = unixFs.getPath(TestUtils.tmpDir()).getRelative("UnixPathGetParentTest");
+    FileSystemUtils.createDirectoryAndParents(testRoot);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    FileSystemUtils.deleteTree(testRoot); // (comment out during debugging)
+  }
+
+  private Path getParent(String path) {
+    return unixFs.getPath(path).getParentDirectory();
+  }
+
+  @Test
+  public void testAbsoluteRootHasNoParent() {
+    assertEquals(null, getParent("/"));
+  }
+
+  @Test
+  public void testParentOfSimpleDirectory() {
+    assertEquals("/foo", getParent("/foo/bar").getPathString());
+  }
+
+  @Test
+  public void testParentOfDotDotInMiddleOfPathname() {
+    assertEquals("/", getParent("/foo/../bar").getPathString());
+  }
+
+  @Test
+  public void testGetPathDoesNormalizationWithoutIO() throws IOException {
+    Path tmp = testRoot.getChild("tmp");
+    Path tmpWiz = tmp.getChild("wiz");
+
+    tmp.createDirectory();
+
+    // ln -sf /tmp /tmp/wiz
+    tmpWiz.createSymbolicLink(tmp);
+
+    assertEquals(testRoot, tmp.getParentDirectory());
+
+    assertEquals(tmp, tmpWiz.getParentDirectory());
+
+    // Under UNIX, inode(/tmp/wiz/..) == inode(/).  However getPath() does not
+    // perform I/O, only string operations, so it disagrees:
+    assertEquals(tmp, tmp.getRelative(new PathFragment("wiz/..")));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java
new file mode 100644
index 0000000..b593367
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java
@@ -0,0 +1,279 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.vfs.util.FileSystems;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Tests for {@link Path}.
+ */
+@RunWith(JUnit4.class)
+public class UnixPathTest {
+
+  private FileSystem unixFs;
+  private File aDirectory;
+  private File aFile;
+  private File anotherFile;
+  private File tmpDir;
+
+  protected FileSystem getUnixFileSystem() {
+    return FileSystems.initDefaultAsNative();
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    unixFs = getUnixFileSystem();
+    tmpDir = new File(TestUtils.tmpDir(), "tmpDir");
+    tmpDir.mkdirs();
+    aDirectory = new File(tmpDir, "a_directory");
+    aDirectory.mkdirs();
+    aFile = new File(tmpDir, "a_file");
+    new FileWriter(aFile).close();
+    anotherFile = new File(aDirectory, "another_file.txt");
+    new FileWriter(anotherFile).close();
+  }
+
+  @Test
+  public void testExists() {
+    assertTrue(unixFs.getPath(aDirectory.getPath()).exists());
+    assertTrue(unixFs.getPath(aFile.getPath()).exists());
+    assertFalse(unixFs.getPath("/does/not/exist").exists());
+  }
+
+  @Test
+  public void testDirectoryEntriesForDirectory() throws IOException {
+    Collection<Path> entries =
+        unixFs.getPath(tmpDir.getPath()).getDirectoryEntries();
+    List<Path> expectedEntries = Arrays.asList(
+      unixFs.getPath(tmpDir.getPath() + "/a_file"),
+      unixFs.getPath(tmpDir.getPath() + "/a_directory"));
+
+    assertEquals(new HashSet<Object>(expectedEntries),
+        new HashSet<Object>(entries));
+  }
+
+  @Test
+  public void testDirectoryEntriesForFileThrowsException() {
+    try {
+      unixFs.getPath(aFile.getPath()).getDirectoryEntries();
+      fail("No exception thrown.");
+    } catch (IOException x) {
+      // The expected result.
+    }
+  }
+
+  @Test
+  public void testIsFileIsTrueForFile() {
+    assertTrue(unixFs.getPath(aFile.getPath()).isFile());
+  }
+
+  @Test
+  public void testIsFileIsFalseForDirectory() {
+    assertFalse(unixFs.getPath(aDirectory.getPath()).isFile());
+  }
+
+  @Test
+  public void testBaseName() {
+    assertEquals("base", unixFs.getPath("/foo/base").getBaseName());
+  }
+
+  @Test
+  public void testBaseNameRunsAfterDotDotInterpretation() {
+    assertEquals("base", unixFs.getPath("/base/foo/..").getBaseName());
+  }
+
+  @Test
+  public void testParentOfRootIsRoot() {
+    assertEquals(unixFs.getPath("/"), unixFs.getPath("/.."));
+    assertEquals(unixFs.getPath("/"), unixFs.getPath("/../../../../../.."));
+    assertEquals(unixFs.getPath("/foo"), unixFs.getPath("/../../../foo"));
+  }
+
+  @Test
+  public void testIsDirectory() {
+    assertTrue(unixFs.getPath(aDirectory.getPath()).isDirectory());
+    assertFalse(unixFs.getPath(aFile.getPath()).isDirectory());
+    assertFalse(unixFs.getPath("/does/not/exist").isDirectory());
+  }
+
+  @Test
+  public void testListNonExistingDirectoryThrowsException() {
+    try {
+      unixFs.getPath("/does/not/exist").getDirectoryEntries();
+      fail("No exception thrown.");
+    } catch (IOException ex) {
+      // success!
+    }
+  }
+
+  private void assertPathSet(Collection<Path> actual, String... expected) {
+    List<String> actualStrings = Lists.newArrayListWithCapacity(actual.size());
+
+    for (Path path : actual) {
+      actualStrings.add(path.getPathString());
+    }
+
+    assertThat(actualStrings).containsExactlyElementsIn(Arrays.asList(expected));
+  }
+
+  @Test
+  public void testGlob() throws Exception {
+    Collection<Path> textFiles = UnixGlob.forPath(unixFs.getPath(tmpDir.getPath()))
+        .addPattern("*/*.txt")
+        .globInterruptible();
+    assertEquals(1, textFiles.size());
+    Path onlyFile = textFiles.iterator().next();
+    assertEquals(unixFs.getPath(anotherFile.getPath()), onlyFile);
+
+    Collection<Path> onlyFiles =
+        UnixGlob.forPath(unixFs.getPath(tmpDir.getPath()))
+        .addPattern("*")
+        .setExcludeDirectories(true)
+        .globInterruptible();
+    assertPathSet(onlyFiles, aFile.getPath());
+
+    Collection<Path> directoriesToo =
+        UnixGlob.forPath(unixFs.getPath(tmpDir.getPath()))
+        .addPattern("*")
+        .setExcludeDirectories(false)
+        .globInterruptible();
+    assertPathSet(directoriesToo, aFile.getPath(), aDirectory.getPath());
+  }
+
+  @Test
+  public void testGetRelative() {
+    Path relative = unixFs.getPath("/foo").getChild("bar");
+    Path expected = unixFs.getPath("/foo/bar");
+    assertEquals(expected, relative);
+  }
+
+  @Test
+  public void testEqualsAndHash() {
+    Path path = unixFs.getPath("/foo/bar");
+    Path equalPath = unixFs.getPath("/foo/bar");
+    Path differentPath = unixFs.getPath("/foo/bar/baz");
+    Object differentType = new Object();
+
+    assertEquals(path.hashCode(), equalPath.hashCode());
+    assertEquals(path, equalPath);
+    assertFalse(path.equals(differentPath));
+    assertFalse(path.equals(differentType));
+  }
+
+  @Test
+  public void testLatin1ReadAndWrite() throws IOException {
+    char[] allLatin1Chars = new char[256];
+    for (int i = 0; i < 256; i++) {
+      allLatin1Chars[i] = (char) i;
+    }
+    Path path = unixFs.getPath(aFile.getPath());
+    String latin1String = new String(allLatin1Chars);
+    FileSystemUtils.writeContentAsLatin1(path, latin1String);
+    String fileContent = new String(FileSystemUtils.readContentAsLatin1(path));
+    assertEquals(fileContent, latin1String);
+  }
+
+  /**
+   * Verify that the encoding implemented by
+   * {@link FileSystemUtils#writeContentAsLatin1(Path, String)}
+   * really is 8859-1 (latin1).
+   */
+  @Test
+  public void testVerifyLatin1() throws IOException {
+    char[] allLatin1Chars = new char[256];
+    for( int i = 0; i < 256; i++) {
+      allLatin1Chars[i] = (char)i;
+    }
+    Path path = unixFs.getPath(aFile.getPath());
+    String latin1String = new String(allLatin1Chars);
+    FileSystemUtils.writeContentAsLatin1(path, latin1String);
+    byte[] bytes = FileSystemUtils.readContent(path);
+    assertEquals(new String(bytes, "ISO-8859-1"), latin1String);
+  }
+
+  @Test
+  public void testBytesReadAndWrite() throws IOException {
+    byte[] bytes = new byte[] { (byte) 0xdeadbeef, (byte) 0xdeadbeef>>8,
+                                (byte) 0xdeadbeef>>16, (byte) 0xdeadbeef>>24 };
+    Path path = unixFs.getPath(aFile.getPath());
+    FileSystemUtils.writeContent(path, bytes);
+    byte[] content = FileSystemUtils.readContent(path);
+    assertEquals(bytes.length, content.length);
+    for (int i = 0; i < bytes.length; i++) {
+      assertEquals(bytes[i], content[i]);
+    }
+  }
+
+  @Test
+  public void testInputOutputStreams() throws IOException {
+    Path path = unixFs.getPath(aFile.getPath());
+    OutputStream out = path.getOutputStream();
+    for (int i = 0; i < 256; i++) {
+      out.write(i);
+    }
+    out.close();
+    InputStream in = path.getInputStream();
+    for (int i = 0; i < 256; i++) {
+      assertEquals(i, in.read());
+    }
+    assertEquals(-1, in.read());
+    in.close();
+  }
+
+  @Test
+  public void testAbsolutePathRoot() {
+    assertEquals("/", new Path(null).toString());
+  }
+
+  @Test
+  public void testAbsolutePath() {
+    Path segment = new Path(null, "bar.txt",
+      new Path(null, "foo", new Path(null)));
+    assertEquals("/foo/bar.txt", segment.toString());
+  }
+
+  @Test
+  public void testDerivedSegmentEquality() {
+    Path absoluteSegment = new Path(null);
+
+    Path derivedNode = absoluteSegment.getChild("derivedSegment");
+    Path otherDerivedNode = absoluteSegment.getChild("derivedSegment");
+
+    assertSame(derivedNode, otherDerivedNode);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/ZipFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/ZipFileSystemTest.java
new file mode 100644
index 0000000..9dc1276
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/ZipFileSystemTest.java
@@ -0,0 +1,233 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Lists;
+import com.google.common.io.CharStreams;
+import com.google.devtools.build.lib.testutil.BlazeTestUtils;
+import com.google.devtools.build.lib.testutil.TestConstants;
+import com.google.devtools.build.lib.vfs.util.FileSystems;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class ZipFileSystemTest {
+
+  /**
+   * Expected listing of sample zip files, in alpha sorted order
+   */
+  private static final String[] LISTING = {
+    "/dir1",
+    "/dir1/file1a",
+    "/dir1/file1b",
+    "/dir2",
+    "/dir2/dir3",
+    "/dir2/dir3/dir4",
+    "/dir2/dir3/dir4/file4",
+    "/dir2/file2",
+    "/file0",
+  };
+
+  private FileSystem zipFS1;
+  private FileSystem zipFS2;
+
+  @Before
+  public void setUp() throws Exception {
+    FileSystem unixFs = FileSystems.initDefaultAsNative();
+    Path testdataDir = unixFs.getPath(BlazeTestUtils.runfilesDir()).getRelative(
+        TestConstants.JAVATESTS_ROOT + "/com/google/devtools/build/lib/vfs");
+    Path zPath1 = testdataDir.getChild("sample_with_dirs.zip");
+    Path zPath2 = testdataDir.getChild("sample_without_dirs.zip");
+    zipFS1 = new ZipFileSystem(zPath1);
+    zipFS2 = new ZipFileSystem(zPath2);
+  }
+
+  private void checkExists(FileSystem fs) {
+    assertTrue(fs.getPath("/dir2/dir3/dir4").exists());
+    assertTrue(fs.getPath("/dir2/dir3/dir4/file4").exists());
+    assertFalse(fs.getPath("/dir2/dir3/dir4/bogus").exists());
+  }
+
+  @Test
+  public void testExists() {
+    checkExists(zipFS1);
+    checkExists(zipFS2);
+  }
+
+  private void checkIsFile(FileSystem fs) {
+    assertFalse(fs.getPath("/dir2/dir3/dir4").isFile());
+    assertTrue(fs.getPath("/dir2/dir3/dir4/file4").isFile());
+    assertFalse(fs.getPath("/dir2/dir3/dir4/bogus").isFile());
+  }
+
+  @Test
+  public void testIsFile() {
+    checkIsFile(zipFS1);
+    checkIsFile(zipFS2);
+  }
+
+  private void checkIsDir(FileSystem fs) {
+    assertTrue(fs.getPath("/dir2/dir3/dir4").isDirectory());
+    assertFalse(fs.getPath("/dir2/dir3/dir4/file4").isDirectory());
+    assertFalse(fs.getPath("/bogus/mobogus").isDirectory());
+    assertFalse(fs.getPath("/bogus").isDirectory());
+  }
+
+  @Test
+  public void testIsDir() {
+    checkIsDir(zipFS1);
+    checkIsDir(zipFS2);
+  }
+
+  /**
+   * Recursively add the contents of a given path, rendered as strings, into a
+   * given list.
+   */
+  private static void listChildren(Path p, List<String> list)
+      throws IOException {
+    for (Path c : p.getDirectoryEntries()) {
+      list.add(c.getPathString());
+      if (c.isDirectory()) {
+        listChildren(c, list);
+      }
+    }
+  }
+
+  private void checkListing(FileSystem fs) throws Exception {
+    List<String> list = new ArrayList<>();
+    listChildren(fs.getRootDirectory(), list);
+    Collections.sort(list);
+    assertEquals(Lists.newArrayList(LISTING), list);
+  }
+
+  @Test
+  public void testListing() throws Exception {
+    checkListing(zipFS1);
+    checkListing(zipFS2);
+
+    // Regression test for: creation of a path (i.e. a file *name*)
+    // must not affect the result of getDirectoryEntries().
+    zipFS1.getPath("/dir1/notthere");
+    checkListing(zipFS1);
+  }
+
+  private void checkFileSize(FileSystem fs, String name, long expectedSize)
+      throws IOException {
+    assertEquals(expectedSize, fs.getPath(name).getFileSize());
+  }
+
+  @Test
+  public void testCanReadRoot() {
+    Path rootDirectory = zipFS1.getRootDirectory();
+    assertTrue(rootDirectory.isDirectory());
+  }
+
+  @Test
+  public void testFileSize() throws IOException {
+    checkFileSize(zipFS1, "/dir1/file1a", 5);
+    checkFileSize(zipFS2, "/dir1/file1a", 5);
+    checkFileSize(zipFS1, "/dir2/dir3/dir4/file4", 5000);
+    checkFileSize(zipFS2, "/dir2/dir3/dir4/file4", 5000);
+  }
+
+  private void checkCantGetFileSize(FileSystem fs, String name) {
+    try {
+      fs.getPath(name).getFileSize();
+      fail();
+    } catch (IOException expected) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testCantGetFileSize() {
+    checkCantGetFileSize(zipFS1, "/dir2/dir3/dir4/bogus");
+    checkCantGetFileSize(zipFS2, "/dir2/dir3/dir4/bogus");
+  }
+
+  private void checkOpenFile(FileSystem fs, String name, int expectedSize)
+      throws Exception {
+    InputStream is = fs.getPath(name).getInputStream();
+    List<String> lines = CharStreams.readLines(new InputStreamReader(is, "ISO-8859-1"));
+    assertEquals(expectedSize, lines.size());
+    for (int i = 0; i < expectedSize; i++) {
+      assertEquals("body", lines.get(i));
+    }
+  }
+
+  @Test
+  public void testOpenSmallFile() throws Exception {
+    checkOpenFile(zipFS1, "/dir1/file1a", 1);
+    checkOpenFile(zipFS2, "/dir1/file1a", 1);
+  }
+
+  @Test
+  public void testOpenBigFile() throws Exception {
+    checkOpenFile(zipFS1, "/dir2/dir3/dir4/file4", 1000);
+    checkOpenFile(zipFS2, "/dir2/dir3/dir4/file4", 1000);
+  }
+
+  private void checkCantOpenFile(FileSystem fs, String name) {
+    try {
+      fs.getPath(name).getInputStream();
+      fail();
+    } catch (IOException expected) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testCantOpenFile() throws Exception {
+    checkCantOpenFile(zipFS1, "/dir2/dir3/dir4/bogus");
+    checkCantOpenFile(zipFS2, "/dir2/dir3/dir4/bogus");
+  }
+
+  private void checkCantCreateAnything(FileSystem fs, String name)  {
+    Path p = fs.getPath(name);
+    try {
+      p.createDirectory();
+      fail();
+    } catch (Exception expected) {}
+    try {
+      FileSystemUtils.createEmptyFile(p);
+      fail();
+    } catch (Exception expected) {}
+    try {
+      p.createSymbolicLink(p);
+      fail();
+    } catch (Exception expected) {}
+  }
+
+  @Test
+  public void testCantCreateAnything() throws Exception {
+    checkCantCreateAnything(zipFS1, "/dir2/dir3/dir4/new");
+    checkCantCreateAnything(zipFS2, "/dir2/dir3/dir4/new");
+  }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfoTest.java b/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfoTest.java
new file mode 100644
index 0000000..dbdd64a
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfoTest.java
@@ -0,0 +1,73 @@
+// Copyright 2014 Google Inc. 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.vfs.inmemoryfs;
+
+import static org.junit.Assert.fail;
+
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.Clock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class InMemoryContentInfoTest {
+
+  private Clock clock;
+
+  @Before
+  public void setUp() throws Exception {
+    clock = BlazeClock.instance();
+  }
+
+  @Test
+  public void testDirectoryCannotAddNullChild() {
+    InMemoryDirectoryInfo directory = new InMemoryDirectoryInfo(clock);
+
+    try {
+      directory.addChild("bar", null);
+      fail("NullPointerException not thrown.");
+    } catch (NullPointerException e) {
+      // success.
+    }
+  }
+
+  @Test
+  public void testDirectoryCannotAddChildTwice() {
+    InMemoryDirectoryInfo directory = new InMemoryDirectoryInfo(clock);
+    InMemoryFileInfo otherFile = new InMemoryFileInfo(clock);
+    directory.addChild("bar", otherFile);
+
+    try {
+      directory.addChild("bar", otherFile);
+      fail("IllegalArgumentException not thrown.");
+    } catch (IllegalArgumentException e) {
+      // success.
+    }
+  }
+
+  @Test
+  public void testDirectoryRemoveNonExistingChild() {
+    InMemoryDirectoryInfo directory = new InMemoryDirectoryInfo(clock);
+    try {
+      directory.removeChild("bar");
+      fail();
+    } catch (IllegalArgumentException e) {
+      // success
+    }
+  }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystemTest.java
new file mode 100644
index 0000000..65ea6f6
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystemTest.java
@@ -0,0 +1,414 @@
+// Copyright 2014 Google Inc. 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.vfs.inmemoryfs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.testutil.TestThread;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.ScopeEscapableFileSystemTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Tests for {@link InMemoryFileSystem}. Note that most tests are inherited
+ * from {@link ScopeEscapableFileSystemTest} and ancestors. This specific
+ * file focuses only on concurrency tests.
+ *
+ */
+@RunWith(JUnit4.class)
+public class InMemoryFileSystemTest extends ScopeEscapableFileSystemTest {
+
+  @Override
+  public FileSystem getFreshFileSystem() {
+    return new InMemoryFileSystem(BlazeClock.instance(), SCOPE_ROOT);
+  }
+
+  @Override
+  public void destroyFileSystem(FileSystem fileSystem) {
+    // Nothing.
+  }
+
+  private static final int NUM_THREADS_FOR_CONCURRENCY_TESTS = 10;
+  private static final String TEST_FILE_DATA = "data";
+
+  /**
+   * Writes the given data to the given file.
+   */
+  private static void writeToFile(Path path, String data) throws IOException {
+    OutputStream out = path.getOutputStream();
+    out.write(data.getBytes(Charset.defaultCharset()));
+    out.close();
+  }
+
+  /**
+   * Tests concurrent creation of a substantial tree hierarchy including
+   * files, directories, symlinks, file contents, and permissions.
+   */
+  @Test
+  public void testConcurrentTreeConstruction() throws Exception {
+    final int NUM_TO_WRITE = 10000;
+    final AtomicInteger baseSelector = new AtomicInteger();
+
+    // 1) Define the intended path structure.
+    class PathCreator extends TestThread {
+      @Override
+      public void runTest() throws Exception {
+        Path base = testFS.getPath("/base" + baseSelector.getAndIncrement());
+        base.createDirectory();
+
+        for (int i = 0; i < NUM_TO_WRITE; i++) {
+          Path subdir1 = base.getRelative("subdir1_" + i);
+          subdir1.createDirectory();
+          Path subdir2 = base.getRelative("subdir2_" + i);
+          subdir2.createDirectory();
+
+          Path file = base.getRelative("somefile" + i);
+          writeToFile(file, TEST_FILE_DATA);
+
+          subdir1.setReadable(true);
+          subdir2.setReadable(false);
+          file.setReadable(true);
+
+          subdir1.setWritable(false);
+          subdir2.setWritable(true);
+          file.setWritable(false);
+
+          subdir1.setExecutable(false);
+          subdir2.setExecutable(true);
+          file.setExecutable(false);
+
+          subdir1.setLastModifiedTime(100);
+          subdir2.setLastModifiedTime(200);
+          file.setLastModifiedTime(300);
+
+          Path symlink = base.getRelative("symlink" + i);
+          symlink.createSymbolicLink(file);
+        }
+      }
+    }
+
+    // 2) Construct the tree.
+    Collection<TestThread> threads =
+        Lists.newArrayListWithCapacity(NUM_THREADS_FOR_CONCURRENCY_TESTS);
+    for (int i = 0; i < NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) {
+      TestThread thread = new PathCreator();
+      thread.start();
+      threads.add(thread);
+    }
+    for (TestThread thread : threads) {
+      thread.joinAndAssertState(0);
+    }
+
+    // 3) Define the validation logic.
+    class PathValidator extends TestThread {
+      @Override
+      public void runTest() throws Exception {
+        Path base = testFS.getPath("/base" + baseSelector.getAndIncrement());
+        assertTrue(base.exists());
+        assertFalse(base.getRelative("notreal").exists());
+
+        for (int i = 0; i < NUM_TO_WRITE; i++) {
+          Path subdir1 = base.getRelative("subdir1_" + i);
+          assertTrue(subdir1.exists());
+          assertTrue(subdir1.isDirectory());
+          assertTrue(subdir1.isReadable());
+          assertFalse(subdir1.isWritable());
+          assertFalse(subdir1.isExecutable());
+          assertEquals(100, subdir1.getLastModifiedTime());
+
+          Path subdir2 = base.getRelative("subdir2_" + i);
+          assertTrue(subdir2.exists());
+          assertTrue(subdir2.isDirectory());
+          assertFalse(subdir2.isReadable());
+          assertTrue(subdir2.isWritable());
+          assertTrue(subdir2.isExecutable());
+          assertEquals(200, subdir2.getLastModifiedTime());
+
+          Path file = base.getRelative("somefile" + i);
+          assertTrue(file.exists());
+          assertTrue(file.isFile());
+          assertTrue(file.isReadable());
+          assertFalse(file.isWritable());
+          assertFalse(file.isExecutable());
+          assertEquals(300, file.getLastModifiedTime());
+          BufferedReader reader = new BufferedReader(
+              new InputStreamReader(file.getInputStream(), Charset.defaultCharset()));
+          assertEquals(TEST_FILE_DATA, reader.readLine());
+          assertEquals(null, reader.readLine());
+
+          Path symlink = base.getRelative("symlink" + i);
+          assertTrue(symlink.exists());
+          assertTrue(symlink.isSymbolicLink());
+          assertEquals(file.asFragment(), symlink.readSymbolicLink());
+        }
+      }
+    }
+
+    // 4) Validate the results.
+    baseSelector.set(0);
+    threads = Lists.newArrayListWithCapacity(NUM_THREADS_FOR_CONCURRENCY_TESTS);
+    for (int i = 0; i < NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) {
+      TestThread thread = new PathValidator();
+      thread.start();
+      threads.add(thread);
+    }
+    for (TestThread thread : threads) {
+      thread.joinAndAssertState(0);
+    }
+  }
+
+  /**
+   * Tests concurrent creation of many files, all within the same directory.
+   */
+  @Test
+  public void testConcurrentDirectoryConstruction() throws Exception {
+   final int NUM_TO_WRITE = 10000;
+    final AtomicInteger baseSelector = new AtomicInteger();
+
+    // 1) Define the intended path structure.
+    class PathCreator extends TestThread {
+      @Override
+      public void runTest() throws Exception {
+        final int threadId = baseSelector.getAndIncrement();
+        Path base = testFS.getPath("/common_dir");
+        base.createDirectory();
+
+        for (int i = 0; i < NUM_TO_WRITE; i++) {
+          Path file = base.getRelative("somefile_" + threadId + "_" + i);
+          writeToFile(file, TEST_FILE_DATA);
+          file.setReadable(i % 2 == 0);
+          file.setWritable(i % 3 == 0);
+          file.setExecutable(i % 4 == 0);
+          file.setLastModifiedTime(i);
+          Path symlink = base.getRelative("symlink_" + threadId + "_" + i);
+          symlink.createSymbolicLink(file);
+        }
+      }
+    }
+
+    // 2) Create the files.
+    Collection<TestThread> threads =
+        Lists.newArrayListWithCapacity(NUM_THREADS_FOR_CONCURRENCY_TESTS);
+    for (int i = 0; i < NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) {
+      TestThread thread = new PathCreator();
+      thread.start();
+      threads.add(thread);
+    }
+    for (TestThread thread : threads) {
+      thread.joinAndAssertState(0);
+    }
+
+    // 3) Define the validation logic.
+    class PathValidator extends TestThread {
+      @Override
+      public void runTest() throws Exception {
+        final int threadId = baseSelector.getAndIncrement();
+        Path base = testFS.getPath("/common_dir");
+        assertTrue(base.exists());
+
+        for (int i = 0; i < NUM_TO_WRITE; i++) {
+          Path file = base.getRelative("somefile_" + threadId + "_" + i);
+          assertTrue(file.exists());
+          assertTrue(file.isFile());
+          assertEquals(i % 2 == 0, file.isReadable());
+          assertEquals(i % 3 == 0, file.isWritable());
+          assertEquals(i % 4 == 0, file.isExecutable());
+          assertEquals(i, file.getLastModifiedTime());
+          if (file.isReadable()) {
+            BufferedReader reader = new BufferedReader(
+                new InputStreamReader(file.getInputStream(), Charset.defaultCharset()));
+            assertEquals(TEST_FILE_DATA, reader.readLine());
+            assertEquals(null, reader.readLine());
+          }
+
+          Path symlink = base.getRelative("symlink_" + threadId + "_" + i);
+          assertTrue(symlink.exists());
+          assertTrue(symlink.isSymbolicLink());
+          assertEquals(file.asFragment(), symlink.readSymbolicLink());
+        }
+      }
+    }
+
+    // 4) Validate the results.
+    baseSelector.set(0);
+    threads = Lists.newArrayListWithCapacity(NUM_THREADS_FOR_CONCURRENCY_TESTS);
+    for (int i = 0; i < NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) {
+      TestThread thread = new PathValidator();
+      thread.start();
+      threads.add(thread);
+    }
+    for (TestThread thread : threads) {
+      thread.joinAndAssertState(0);
+    }
+  }
+
+  /**
+   * Tests concurrent file deletion.
+   */
+  @Test
+  public void testConcurrentDeletion() throws Exception {
+    final int NUM_TO_WRITE = 10000;
+    final AtomicInteger baseSelector = new AtomicInteger();
+
+    final Path base = testFS.getPath("/base");
+    base.createDirectory();
+
+    // 1) Create a bunch of files.
+    for (int i = 0; i < NUM_TO_WRITE; i++) {
+      writeToFile(base.getRelative("file" + i), TEST_FILE_DATA);
+    }
+
+    // 2) Define our deletion strategy.
+    class FileDeleter extends TestThread {
+      @Override
+      public void runTest() throws Exception {
+        for (int i = 0; i < NUM_TO_WRITE / NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) {
+          int whichFile = baseSelector.getAndIncrement();
+          Path file = base.getRelative("file" + whichFile);
+          if (whichFile % 25 != 0) {
+            assertTrue(file.delete());
+          } else {
+            // Throw another concurrent access point into the mix.
+            file.setExecutable(whichFile % 2 == 0);
+          }
+          assertFalse(base.getRelative("doesnotexist" + whichFile).delete());
+        }
+      }
+    }
+
+    // 3) Delete some files.
+    Collection<TestThread> threads =
+        Lists.newArrayListWithCapacity(NUM_THREADS_FOR_CONCURRENCY_TESTS);
+    for (int i = 0; i < NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) {
+      TestThread thread = new FileDeleter();
+      thread.start();
+      threads.add(thread);
+    }
+    for (TestThread thread : threads) {
+      thread.joinAndAssertState(0);
+    }
+
+    // 4) Check the results.
+    for (int i = 0; i < NUM_TO_WRITE; i++) {
+      Path file = base.getRelative("file" + i);
+      if (i % 25 != 0) {
+        assertFalse(file.exists());
+      } else {
+        assertTrue(file.exists());
+        assertEquals(i % 2 == 0, file.isExecutable());
+      }
+    }
+  }
+
+  /**
+   * Tests concurrent file renaming.
+   */
+  @Test
+  public void testConcurrentRenaming() throws Exception {
+    final int NUM_TO_WRITE = 10000;
+    final AtomicInteger baseSelector = new AtomicInteger();
+
+    final Path base = testFS.getPath("/base");
+    base.createDirectory();
+
+    // 1) Create a bunch of files.
+    for (int i = 0; i < NUM_TO_WRITE; i++) {
+      writeToFile(base.getRelative("file" + i), TEST_FILE_DATA);
+    }
+
+    // 2) Define our renaming strategy.
+    class FileDeleter extends TestThread {
+      @Override
+      public void runTest() throws Exception {
+        for (int i = 0; i < NUM_TO_WRITE / NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) {
+          int whichFile = baseSelector.getAndIncrement();
+          Path file = base.getRelative("file" + whichFile);
+          if (whichFile % 25 != 0) {
+            Path newName = base.getRelative("newname" + whichFile);
+            file.renameTo(newName);
+          } else {
+            // Throw another concurrent access point into the mix.
+            file.setExecutable(whichFile % 2 == 0);
+          }
+          assertFalse(base.getRelative("doesnotexist" + whichFile).delete());
+        }
+      }
+    }
+
+    // 3) Rename some files.
+    Collection<TestThread> threads =
+        Lists.newArrayListWithCapacity(NUM_THREADS_FOR_CONCURRENCY_TESTS);
+    for (int i = 0; i < NUM_THREADS_FOR_CONCURRENCY_TESTS; i++) {
+      TestThread thread = new FileDeleter();
+      thread.start();
+      threads.add(thread);
+    }
+    for (TestThread thread : threads) {
+      thread.joinAndAssertState(0);
+    }
+
+    // 4) Check the results.
+    for (int i = 0; i < NUM_TO_WRITE; i++) {
+      Path file = base.getRelative("file" + i);
+      if (i % 25 != 0) {
+        assertFalse(file.exists());
+        assertTrue(base.getRelative("newname" + i).exists());
+      } else {
+        assertTrue(file.exists());
+        assertEquals(i % 2 == 0, file.isExecutable());
+      }
+    }
+  }
+
+  @Test
+  public void testEloop() throws Exception {
+    Path a = testFS.getPath("/a");
+    Path b = testFS.getPath("/b");
+    a.createSymbolicLink(new PathFragment("b"));
+    b.createSymbolicLink(new PathFragment("a"));
+    try {
+      a.stat();
+    } catch (IOException e) {
+      assertEquals("/a (Too many levels of symbolic links)", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testEloopSelf() throws Exception {
+    Path a = testFS.getPath("/a");
+    a.createSymbolicLink(new PathFragment("a"));
+    try {
+      a.stat();
+    } catch (IOException e) {
+      assertEquals("/a (Too many levels of symbolic links)", e.getMessage());
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/sample_with_dirs.zip b/src/test/java/com/google/devtools/build/lib/vfs/sample_with_dirs.zip
new file mode 100644
index 0000000..22ff63c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/sample_with_dirs.zip
Binary files differ
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/sample_without_dirs.zip b/src/test/java/com/google/devtools/build/lib/vfs/sample_without_dirs.zip
new file mode 100644
index 0000000..f3ec5ab
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/sample_without_dirs.zip
Binary files differ
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/util/FileSystems.java b/src/test/java/com/google/devtools/build/lib/vfs/util/FileSystems.java
new file mode 100644
index 0000000..6a79a2b
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/util/FileSystems.java
@@ -0,0 +1,93 @@
+// Copyright 2014 Google Inc. 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.vfs.util;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.JavaIoFileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.UnionFileSystem;
+import com.google.devtools.build.lib.vfs.UnixFileSystem;
+import com.google.devtools.build.lib.vfs.ZipFileSystem;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * This static file system singleton manages access to a single default
+ * {@link FileSystem} instance created within the methods of this class.
+ */
+@ThreadSafe
+public final class FileSystems {
+
+  private FileSystems() {}
+
+  private static FileSystem defaultFileSystem;
+
+  /**
+   * Initializes the default {@link FileSystem} instance as a platform native
+   * (Unix) file system, creating one iff needed, and returns the instance.
+   *
+   * <p>This method is idempotent as long as the initialization is of the same
+   * type (Native/JavaIo/Union).
+   */
+  public static synchronized FileSystem initDefaultAsNative() {
+    if (!(defaultFileSystem instanceof UnixFileSystem)) {
+      defaultFileSystem = new UnixFileSystem();
+    }
+    return defaultFileSystem;
+  }
+
+  /**
+   * Initializes the default {@link FileSystem} instance as a java.io.File
+   * file system, creating one iff needed, and returns the instance.
+   *
+   * <p>This method is idempotent as long as the initialization is of the same
+   * type (Native/JavaIo/Union).
+   */
+  public static synchronized FileSystem initDefaultAsJavaIo() {
+    if (!(defaultFileSystem instanceof JavaIoFileSystem)) {
+      defaultFileSystem = new JavaIoFileSystem();
+    }
+    return defaultFileSystem;
+  }
+
+  /**
+   * Initializes the default {@link FileSystem} instance as a
+   * {@link UnionFileSystem}, creating one iff needed,
+   * and returns the instance.
+   *
+   * <p>This method is idempotent as long as the initialization is of the same
+   * type (Native/JavaIo/Union).
+   *
+   * @param prefixMapping the desired mapping of path prefixes to delegate file systems
+   * @param rootFileSystem the default file system for paths that don't match any prefix map
+   */
+  public static synchronized FileSystem initDefaultAsUnion(
+      Map<PathFragment, FileSystem> prefixMapping, FileSystem rootFileSystem) {
+    if (!(defaultFileSystem instanceof UnionFileSystem)) {
+      defaultFileSystem = new UnionFileSystem(prefixMapping, rootFileSystem);
+    }
+    return defaultFileSystem;
+  }
+
+  /**
+   * Returns a new instance of a simple {@link FileSystem} implementation that
+   * presents the contents of a zip file as a read-only file system view.
+   */
+  public static FileSystem newZipFileSystem(Path zipFile) throws IOException {
+    return new ZipFileSystem(zipFile);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/util/FsApparatus.java b/src/test/java/com/google/devtools/build/lib/vfs/util/FsApparatus.java
new file mode 100644
index 0000000..5d93351
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/util/FsApparatus.java
@@ -0,0 +1,158 @@
+// Copyright 2014 Google Inc. 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.vfs.util;
+
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.StringUtilities;
+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.inmemoryfs.InMemoryFileSystem;
+
+import junit.framework.AssertionFailedError;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Base class for a testing apparatus for a scratch filesystem.
+ */
+public class FsApparatus {
+
+  /* ---------- State that the apparatus initializes / operates on --------- */
+  protected FileSystem fileSystem = null;
+  protected Path workingDir = null;
+
+  public static FsApparatus newInMemory() {
+    return new FsApparatus();
+  }
+
+  // TestUtil.getTmpDir is slow, so cache the result here
+  private static final String TMP_DIR =
+      new File(TestUtils.tmpDir(), "bs").toString();
+
+
+  /**
+   * When using a Native file system, absolute paths will be treated as absolute paths on the unix
+   * file system, as opposed to paths relative to the backing temp directory. So for simplicity,
+   * you ought to only use relative paths for FsApparatus#file, FsApparatus#dir, and
+   * FsApparatus#path. Otherwise, be aware of the following issue
+   *
+   *   Path p1 = scratch.path(...);
+   *   Path p2 = scratch.path(p1.getPathString());
+   *
+   * We'd like the invariant that p1.equals(p2) regardless if scratch is in-memory or not, but this
+   * does not hold with our usage of Unix filesystems.
+   */
+  public static FsApparatus newNative() {
+    FileSystem fs = FileSystems.initDefaultAsNative();
+    Path wd = fs.getPath(TMP_DIR);
+
+    try {
+      FileSystemUtils.deleteTree(wd);
+    } catch (IOException e) {
+      throw new AssertionFailedError(e.getMessage());
+    }
+
+    return new FsApparatus(fs, wd);
+  }
+
+  private FsApparatus() {
+    fileSystem = new InMemoryFileSystem(BlazeClock.instance());
+    workingDir = fileSystem.getPath("/");
+  }
+
+  public FsApparatus(FileSystem fs, Path cwd) {
+    fileSystem = fs;
+    workingDir = cwd;
+  }
+
+  public FsApparatus(FileSystem fs) {
+    fileSystem = fs;
+    workingDir = fs.getPath("/");
+  }
+
+  public FileSystem fs() {
+    return fileSystem;
+  }
+
+  /**
+   * Initializes this apparatus (if it hasn't been initialized yet), and creates
+   * a scratch file in the scratch filesystem with the given {@code pathName}
+   * with {@code lines} being its content. The method returns a Path instance
+   * for the scratch file.
+   */
+  public Path file(String pathName, String... lines) throws IOException {
+    Path file = path(pathName);
+    Path parentDir = file.getParentDirectory();
+    if (!parentDir.exists()) {
+      FileSystemUtils.createDirectoryAndParents(parentDir);
+    }
+    if (file.exists()) {
+      throw new IOException("Could not create scratch file (file exists) "
+          + file);
+    }
+    String fileContent = StringUtilities.joinLines(lines);
+    FileSystemUtils.writeContentAsLatin1(file, fileContent);
+    return file;
+  }
+
+  /**
+   * Initializes this apparatus (if it hasn't been initialized yet), and creates
+   * a directory in the scratch filesystem, with the given {@code pathName}.
+   * Creates parent directories as necessary.
+   */
+  public Path dir(String pathName) throws IOException {
+    Path dir = path(pathName);
+    if (!dir.exists()) {
+      FileSystemUtils.createDirectoryAndParents(dir);
+    }
+    if (!dir.isDirectory()) {
+      throw new IOException("Exists, but is not a directory: " + dir);
+    }
+    return dir;
+  }
+
+  /**
+   * Initializes this apparatus (if it hasn't been initialized yet), and returns
+   * a path object describing a file, directory, or symlink pointed at by
+   * {@code pathName}. Note that this will not create any entity in the
+   * filesystem; i.e., the file that the object is describing may not exist in
+   * the filesystem.
+   */
+  public Path path(String pathName) {
+    return workingDir.getRelative(pathName);
+  }
+
+  /**
+   * Create a fresh directory in the system temporary directory, instead of the
+   * testing directory provided by the testing framework. This path is usually
+   * shorter than a path starting with TestUtil.getTmpDir(). We care about the
+   * length because of the path length restriction for Unix local socket files.
+   *
+   * Clients are responsible for deleting the directory after tests.
+   */
+  public Path createUnixTempDir() throws IOException {
+    if (fileSystem instanceof InMemoryFileSystem) {
+      throw new IOException("Can not create Unix temporary directories in "
+                            + "an in-memory file system");
+    }
+    File file = File.createTempFile("scratch", "tmp");
+    final Path path = fileSystem.getPath(file.getAbsolutePath());
+    path.delete();
+    path.createDirectory();
+    return path;
+  }
+}