// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.vfs;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;

import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.vfs.FileSystem.NotASymlinkException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collection;
import org.junit.Before;
import org.junit.Test;

/**
 * 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;

  @Before
  public final void createSymbolicLinks() throws Exception  {
    // % 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);

    assertThat(linkPath.isSymbolicLink()).isTrue();

    assertThat(linkPath.isFile()).isTrue();
    assertThat(linkPath.isFile(Symlinks.NOFOLLOW)).isFalse();
    assertThat(linkPath.isFile(Symlinks.FOLLOW)).isTrue();

    assertThat(linkPath.isDirectory()).isFalse();
    assertThat(linkPath.isDirectory(Symlinks.NOFOLLOW)).isFalse();
    assertThat(linkPath.isDirectory(Symlinks.FOLLOW)).isFalse();

    if (testFS.supportsSymbolicLinksNatively(linkPath)) {
      assertThat(linkPath.getFileSize(Symlinks.NOFOLLOW)).isEqualTo(newPath.toString().length());
      assertThat(linkPath.getFileSize()).isEqualTo(newPath.getFileSize(Symlinks.NOFOLLOW));
    }
    assertThat(linkPath.getParentDirectory().getDirectoryEntries()).hasSize(2);
    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);

    assertThat(linkPath.isSymbolicLink()).isTrue();
    assertThat(linkPath.isFile()).isFalse();
    assertThat(linkPath.isDirectory()).isTrue();
    assertThat(linkPath.getParentDirectory().getDirectoryEntries()).hasSize(2);
    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 {
    assertThat(link1.resolveSymbolicLinks()).isEqualTo(newPath);
    assertThat(link2.resolveSymbolicLinks()).isEqualTo(newPath);
  }

  //
  //  createDirectory
  //

  @Test
  public void testCreateDirectoryWhereDanglingSymlinkAlreadyExists() {
    try {
      xDanglingLink.createDirectory();
      fail();
    } catch (IOException e) {
      assertThat(e).hasMessage(xDanglingLink + " (File exists)");
    }
    assertThat(xDanglingLink.isSymbolicLink()).isTrue(); // still a symbolic link
    assertThat(xDanglingLink.isDirectory(Symlinks.FOLLOW)).isFalse(); // link still dangles
  }

  @Test
  public void testCreateDirectoryWhereSymlinkAlreadyExists() {
    try {
      xLinkToDirectory.createDirectory();
      fail();
    } catch (IOException e) {
      assertThat(e).hasMessage(xLinkToDirectory + " (File exists)");
    }
    assertThat(xLinkToDirectory.isSymbolicLink()).isTrue(); // still a symbolic link
    assertThat(xLinkToDirectory.isDirectory(Symlinks.FOLLOW)).isTrue(); // 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 = PathFragment.create(linkTarget);
      linkPath.delete();
      createSymbolicLink(linkPath, relative);
      if (testFS.supportsSymbolicLinksNatively(linkPath)) {
        assertThat(linkPath.getFileSize(Symlinks.NOFOLLOW))
            .isEqualTo(relative.getSafePathString().length());
        assertThat(linkPath.readSymbolicLink()).isEqualTo(relative);
      }
    }
  }

  @Test
  public void testLinkToRootResolvesCorrectly() throws IOException {
    if (OS.getCurrent() == OS.WINDOWS) {
      // This test cannot be run on Windows, it mixes "/" paths with "C:/" paths
      return;
    }
    Path rootPath = testFS.getPath("/");

    try {
      rootPath.getChild("testDir").createDirectory();
    } catch (IOException e) {
      // Do nothing. This is a real FS, and we don't have permission.
    }

    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.
    String nonLinkEntry = null;
    for (String child : testFS.getDirectoryEntries(rootPath)) {
      Path p = rootPath.getChild(child);
      if (!p.isSymbolicLink() && p.isDirectory()) {
        nonLinkEntry = p.getBaseName();
        break;
      }
    }

    assertThat(nonLinkEntry).isNotNull();
    Path rootChild = testFS.getPath("/" + nonLinkEntry);
    assertThat(linkPath.getRelative(nonLinkEntry).resolveSymbolicLinks()).isEqualTo(rootChild);
  }

  @Test
  public void testLinkToFragmentContainingLinkResolvesCorrectly() throws IOException {
    Path link1 = absolutize("link1");
    PathFragment link1target = PathFragment.create("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
    assertThat(link2target.getRelative("foo")).isEqualTo(link1.resolveSymbolicLinks());
  }

  //
  //  readSymbolicLink / resolveSymbolicLinks
  //

  @Test
  public void testRecursiveSymbolicLink() throws IOException {
    Path link = absolutize("recursive-link");
    createSymbolicLink(link, link);

    if (testFS.supportsSymbolicLinksNatively(link)) {
      try {
        link.resolveSymbolicLinks();
        fail();
      } catch (IOException e) {
        assertThat(e).hasMessage(link + " (Too many levels of symbolic links)");
      }
    }
  }

  @Test
  public void testMutuallyRecursiveSymbolicLinks() throws IOException {
    Path link1 = absolutize("link1");
    Path link2 = absolutize("link2");
    createSymbolicLink(link2, link1);
    createSymbolicLink(link1, link2);

    if (testFS.supportsSymbolicLinksNatively(link1)) {
      try {
        link1.resolveSymbolicLinks();
        fail();
      } catch (IOException e) {
        assertThat(e).hasMessage(link1 + " (Too many levels of symbolic links)");
      }
    }
  }

  @Test
  public void testResolveSymbolicLinksENOENT() {
    if (testFS.supportsSymbolicLinksNatively(xDanglingLink)) {
      try {
        xDanglingLink.resolveSymbolicLinks();
        fail();
      } catch (IOException e) {
        assertThat(e).hasMessage(xNothing + " (No such file or directory)");
      }
    }
  }

  @Test
  public void testResolveSymbolicLinksENOTDIR() throws IOException {
    Path badLinkTarget = xFile.getChild("bad"); // parent is not a directory!
    Path badLink = absolutize("badLink");
    if (testFS.supportsSymbolicLinksNatively(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 (testFS.supportsSymbolicLinksNatively(xLinkToFile)) {
      // 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, PathFragment.create(prefix + xFile.relativeTo(ancestor)));
        assertThat(xLinkToFile.resolveSymbolicLinks()).isEqualTo(xFile);

        prefix += "../";
      }
    }
  }

  @Test
  public void testReadSymbolicLink() throws IOException {
    if (testFS.supportsSymbolicLinksNatively(xDanglingLink)) {
      assertThat(xDanglingLink.readSymbolicLink().toString()).isEqualTo(xNothing.toString());
    }

    assertThat(xLinkToFile.readSymbolicLink().toString()).isEqualTo(xFile.toString());

    assertThat(xLinkToDirectory.readSymbolicLink().toString())
        .isEqualTo(xEmptyDirectory.toString());

    try {
      xFile.readSymbolicLink(); // not a link
      fail();
    } catch (NotASymlinkException e) {
      assertThat(e).hasMessage(xFile.toString());
    }

    try {
      xNothing.readSymbolicLink(); // nothing there
      fail();
    } catch (IOException e) {
      assertThat(e).hasMessage(xNothing + " (No such file or directory)");
    }
  }

  @Test
  public void testCannotCreateSymbolicLinkWithReadOnlyParent()
      throws IOException {
    xEmptyDirectory.setWritable(false);
    Path xChildOfReadonlyDir = xEmptyDirectory.getChild("x");
    if (testFS.supportsSymbolicLinksNatively(xChildOfReadonlyDir)) {
      try {
        xChildOfReadonlyDir.createSymbolicLink(xNothing);
        fail();
      } catch (IOException e) {
        assertThat(e).hasMessage(xChildOfReadonlyDir + " (Permission denied)");
      }
    }
  }

  //
  // createSymbolicLink
  //

  @Test
  public void testCanCreateDanglingLink() throws IOException {
    Path newPath = absolutize("non-existing-dir/new-file");
    Path someLink = absolutize("dangling-link");
    createSymbolicLink(someLink, newPath);
    assertThat(someLink.isSymbolicLink()).isTrue();
    assertThat(someLink.exists(Symlinks.NOFOLLOW)).isTrue(); // the link itself exists
    assertThat(someLink.exists()).isFalse(); // ...but the referent doesn't
    if (testFS.supportsSymbolicLinksNatively(someLink)) {
      try {
        someLink.resolveSymbolicLinks();
      } catch (FileNotFoundException e) {
        assertThat(e).hasMessage(newPath.getParentDirectory() + " (No such file or directory)");
      }
    }
  }

  @Test
  public void testCannotCreateSymbolicLinkWithoutParent() throws IOException {
    Path xChildOfMissingDir = xNothing.getChild("x");
    if (testFS.supportsSymbolicLinksNatively(xChildOfMissingDir)) {
      try {
        xChildOfMissingDir.createSymbolicLink(xFile);
        fail();
      } catch (FileNotFoundException e) {
        assertThat(e).hasMessageThat().endsWith(" (No such file or directory)");
      }
    }
  }

  @Test
  public void testCreateSymbolicLinkWhereNothingExists() throws IOException {
    createSymbolicLink(xNothing, xFile);
    assertThat(xNothing.isSymbolicLink()).isTrue();
  }

  @Test
  public void testCreateSymbolicLinkWhereDirectoryAlreadyExists() {
    try {
      createSymbolicLink(xEmptyDirectory, xFile);
      fail();
    } catch (IOException e) { // => couldn't be created
      assertThat(e).hasMessage(xEmptyDirectory + " (File exists)");
    }
    assertThat(xEmptyDirectory.isDirectory(Symlinks.NOFOLLOW)).isTrue();
  }

  @Test
  public void testCreateSymbolicLinkWhereFileAlreadyExists() {
    try {
      createSymbolicLink(xFile, xEmptyDirectory);
      fail();
    } catch (IOException e) { // => couldn't be created
      assertThat(e).hasMessage(xFile + " (File exists)");
    }
    assertThat(xFile.isFile(Symlinks.NOFOLLOW)).isTrue();
  }

  @Test
  public void testCreateSymbolicLinkWhereDanglingSymlinkAlreadyExists() {
    try {
      createSymbolicLink(xDanglingLink, xFile);
      fail();
    } catch (IOException e) {
      assertThat(e).hasMessage(xDanglingLink + " (File exists)");
    }
    assertThat(xDanglingLink.isSymbolicLink()).isTrue(); // still a symbolic link
    assertThat(xDanglingLink.isDirectory()).isFalse(); // link still dangles
  }

  @Test
  public void testCreateSymbolicLinkWhereSymlinkAlreadyExists() {
    try {
      createSymbolicLink(xLinkToDirectory, xNothing);
      fail();
    } catch (IOException e) {
      assertThat(e).hasMessage(xLinkToDirectory + " (File exists)");
    }
    assertThat(xLinkToDirectory.isSymbolicLink()).isTrue(); // still a symbolic link
    assertThat(xLinkToDirectory.isDirectory()).isTrue(); // 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);

    assertThat(xEmptyDirectory.getDirectoryEntries()).hasSize(2);

    assertThat(someLink.delete()).isTrue();
    assertThat(xEmptyDirectory.getDirectoryEntries()).hasSize(1);

    assertThat(xEmptyDirectory.getDirectoryEntries()).containsExactly(newPath);
  }

  // Testing the links
  @Test
  public void testLinkFollowedToDirectory() throws IOException {
    Path theDirectory = absolutize("foo/");
    assertThat(theDirectory.createDirectory()).isTrue();
    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);
    assertThat(newPath2.createDirectory()).isTrue();

    Path linkPath1 = absolutize("link1");
    Path linkPath2 = absolutize("link2");
    createSymbolicLink(linkPath1, newPath1);
    createSymbolicLink(linkPath2, newPath2);

    newPath1.delete();
    newPath2.delete();

    assertThat(linkPath1.isFile()).isFalse();
    assertThat(linkPath2.isDirectory()).isFalse();
  }

  @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));

    assertThat(resultData).isEqualTo(testData);
  }

  //
  // 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"
    assertThat(a.exists()).isTrue(); // = exists(FOLLOW)
    assertThat(b.exists()).isTrue(); // = exists(FOLLOW)
    assertThat(a.exists(Symlinks.FOLLOW)).isTrue();
    assertThat(b.exists(Symlinks.FOLLOW)).isTrue();
    assertThat(a.exists(Symlinks.NOFOLLOW)).isTrue();
    assertThat(b.exists(Symlinks.NOFOLLOW)).isTrue();
    b.delete(); // "a" is now a dangling link
    assertThat(a.exists()).isFalse(); // = exists(FOLLOW)
    assertThat(b.exists()).isFalse(); // = exists(FOLLOW)
    assertThat(a.exists(Symlinks.FOLLOW)).isFalse();
    assertThat(b.exists(Symlinks.FOLLOW)).isFalse();

    assertThat(a.exists(Symlinks.NOFOLLOW)).isTrue(); // symlink still exists
    assertThat(b.exists(Symlinks.NOFOLLOW)).isFalse();
  }

  @Test
  public void testIsDirectoryWithSymlinks() throws IOException {
    Path a = absolutize("a");
    Path b = absolutize("b");
    b.createDirectory();
    createSymbolicLink(a, b);  // ln -sf "b" "a"
    assertThat(a.isDirectory()).isTrue(); // = isDirectory(FOLLOW)
    assertThat(b.isDirectory()).isTrue(); // = isDirectory(FOLLOW)
    assertThat(a.isDirectory(Symlinks.FOLLOW)).isTrue();
    assertThat(b.isDirectory(Symlinks.FOLLOW)).isTrue();
    assertThat(a.isDirectory(Symlinks.NOFOLLOW)).isFalse(); // it's a link!
    assertThat(b.isDirectory(Symlinks.NOFOLLOW)).isTrue();
    b.delete(); // "a" is now a dangling link
    assertThat(a.isDirectory()).isFalse(); // = isDirectory(FOLLOW)
    assertThat(b.isDirectory()).isFalse(); // = isDirectory(FOLLOW)
    assertThat(a.isDirectory(Symlinks.FOLLOW)).isFalse();
    assertThat(b.isDirectory(Symlinks.FOLLOW)).isFalse();
    assertThat(a.isDirectory(Symlinks.NOFOLLOW)).isFalse();
    assertThat(b.isDirectory(Symlinks.NOFOLLOW)).isFalse();
  }

  @Test
  public void testIsFileWithSymlinks() throws IOException {
    Path a = absolutize("a");
    Path b = absolutize("b");
    FileSystemUtils.createEmptyFile(b);
    createSymbolicLink(a, b);  // ln -sf "b" "a"
    assertThat(a.isFile()).isTrue(); // = isFile(FOLLOW)
    assertThat(b.isFile()).isTrue(); // = isFile(FOLLOW)
    assertThat(a.isFile(Symlinks.FOLLOW)).isTrue();
    assertThat(b.isFile(Symlinks.FOLLOW)).isTrue();
    assertThat(a.isFile(Symlinks.NOFOLLOW)).isFalse(); // it's a link!
    assertThat(b.isFile(Symlinks.NOFOLLOW)).isTrue();
    b.delete(); // "a" is now a dangling link
    assertThat(a.isFile()).isFalse(); // = isFile()
    assertThat(b.isFile()).isFalse(); // = isFile()
    assertThat(a.isFile()).isFalse();
    assertThat(b.isFile()).isFalse();
    assertThat(a.isFile(Symlinks.NOFOLLOW)).isFalse();
    assertThat(b.isFile(Symlinks.NOFOLLOW)).isFalse();
  }

  @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");

    assertThat(aliasToChild.exists()).isFalse();
    FileSystemUtils.createEmptyFile(child);
    assertThat(aliasToChild.exists()).isTrue();
    assertThat(aliasToChild.isFile()).isTrue();
    assertThat(aliasToChild.isDirectory()).isFalse();

    validateLinkedReferenceObeysReadOnly(child, aliasToChild);
    validateLinkedReferenceObeysExecutable(child, aliasToChild);
  }

  @Test
  public void testDirectoriesOfLinkedDirectories() throws Exception {
    Path childDir = xEmptyDirectory.getChild("childDir");
    Path linkToChildDir = xLinkToDirectory.getChild("childDir");

    assertThat(linkToChildDir.exists()).isFalse();
    childDir.createDirectory();
    assertThat(linkToChildDir.exists()).isTrue();
    assertThat(linkToChildDir.isDirectory()).isTrue();
    assertThat(linkToChildDir.isFile()).isFalse();

    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");

    assertThat(linkToChildDir.exists()).isFalse();
    childDir.createDirectory();
    assertThat(linkToChildDir.exists()).isTrue();
    assertThat(linkToChildDir.isDirectory()).isTrue();
    assertThat(linkToChildDir.isFile()).isFalse();

    validateLinkedReferenceObeysReadOnly(childDir, linkToChildDir);
    validateLinkedReferenceObeysExecutable(childDir, linkToChildDir);
  }

  private void validateLinkedReferenceObeysReadOnly(Path path, Path link) throws IOException {
    path.setWritable(false);
    assertThat(path.isWritable()).isFalse();
    assertThat(link.isWritable()).isFalse();
    path.setWritable(true);
    assertThat(path.isWritable()).isTrue();
    assertThat(link.isWritable()).isTrue();
    path.setWritable(false);
    assertThat(path.isWritable()).isFalse();
    assertThat(link.isWritable()).isFalse();
  }

  private void validateLinkedReferenceObeysExecutable(Path path, Path link) throws IOException {
    path.setExecutable(true);
    assertThat(path.isExecutable()).isTrue();
    assertThat(link.isExecutable()).isTrue();
    path.setExecutable(false);
    assertThat(path.isExecutable()).isFalse();
    assertThat(link.isExecutable()).isFalse();
    path.setExecutable(true);
    assertThat(path.isExecutable()).isTrue();
    assertThat(link.isExecutable()).isTrue();
  }

  @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);
    assertThat(inputData).isEqualTo(outputData);
  }

  @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);
    assertThat(inputData).isEqualTo(outputData);
  }
}
