]> granicus.if.org Git - clang/commitdiff
Adding HardLink Support to VirtualFileSystem.
authorIlya Biryukov <ibiryukov@google.com>
Tue, 4 Sep 2018 14:15:53 +0000 (14:15 +0000)
committerIlya Biryukov <ibiryukov@google.com>
Tue, 4 Sep 2018 14:15:53 +0000 (14:15 +0000)
Summary:
Added support of creating a hardlink from one file to another file.
After a hardlink is added between two files, both file will have the same:
  1. UniqueID (inode)
  2. Size
  3. Buffer

This will bring replay of compilation closer to the actual compilation. There are instances where clang checks for the UniqueID of the file/header to be loaded which leads to a different behavior during replay as all files have different UniqueIDs.

Patch by Utkarsh Saxena!

Reviewers: ilya-biryukov

Reviewed By: ilya-biryukov

Subscribers: cfe-commits

Differential Revision: https://reviews.llvm.org/D51359

git-svn-id: https://llvm.org/svn/llvm-project/cfe/trunk@341366 91177308-0d34-0410-b5e6-96231b3b80d8

include/clang/Basic/VirtualFileSystem.h
lib/Basic/VirtualFileSystem.cpp
unittests/Basic/VirtualFileSystemTest.cpp

index 2480b91123c155427e150f696b05482dec79c23e..c808e9026b8be4cc1e90c89a002ec25202143837 100644 (file)
@@ -323,6 +323,7 @@ public:
 namespace detail {
 
 class InMemoryDirectory;
+class InMemoryFile;
 
 } // namespace detail
 
@@ -332,6 +333,15 @@ class InMemoryFileSystem : public FileSystem {
   std::string WorkingDirectory;
   bool UseNormalizedPaths = true;
 
+  /// If HardLinkTarget is non-null, a hardlink is created to the To path which
+  /// must be a file. If it is null then it adds the file as the public addFile.
+  bool addFile(const Twine &Path, time_t ModificationTime,
+               std::unique_ptr<llvm::MemoryBuffer> Buffer,
+               Optional<uint32_t> User, Optional<uint32_t> Group,
+               Optional<llvm::sys::fs::file_type> Type,
+               Optional<llvm::sys::fs::perms> Perms,
+               const detail::InMemoryFile *HardLinkTarget);
+
 public:
   explicit InMemoryFileSystem(bool UseNormalizedPaths = true);
   ~InMemoryFileSystem() override;
@@ -348,6 +358,20 @@ public:
                Optional<llvm::sys::fs::file_type> Type = None,
                Optional<llvm::sys::fs::perms> Perms = None);
 
+  /// Add a hard link to a file.
+  /// Here hard links are not intended to be fully equivalent to the classical
+  /// filesystem. Both the hard link and the file share the same buffer and
+  /// status (and thus have the same UniqueID). Because of this there is no way
+  /// to distinguish between the link and the file after the link has been
+  /// added.
+  ///
+  /// The To path must be an existing file or a hardlink. The From file must not
+  /// have been added before. The To Path must not be a directory. The From Node
+  /// is added as a hard link which points to the resolved file of To Node.
+  /// \return true if the above condition is satisfied and hardlink was
+  /// successfully created, false otherwise.
+  bool addHardLink(const Twine &From, const Twine &To);
+
   /// Add a buffer to the VFS with a path. The VFS does not own the buffer.
   /// If present, User, Group, Type and Perms apply to the newly-created file
   /// or directory.
index 1af4a113029f2559cf3bec72655946b1bbb517ea..35716fd7b8567e7a4f929bfb59e9b58da61f3500 100644 (file)
@@ -16,6 +16,7 @@
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/IntrusiveRefCntPtr.h"
+#include "llvm/ADT/None.h"
 #include "llvm/ADT/Optional.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/SmallString.h"
@@ -25,9 +26,9 @@
 #include "llvm/ADT/Twine.h"
 #include "llvm/ADT/iterator_range.h"
 #include "llvm/Config/llvm-config.h"
-#include "llvm/Support/Compiler.h"
 #include "llvm/Support/Casting.h"
 #include "llvm/Support/Chrono.h"
+#include "llvm/Support/Compiler.h"
 #include "llvm/Support/Debug.h"
 #include "llvm/Support/Errc.h"
 #include "llvm/Support/ErrorHandling.h"
@@ -466,57 +467,68 @@ namespace vfs {
 
 namespace detail {
 
-enum InMemoryNodeKind { IME_File, IME_Directory };
+enum InMemoryNodeKind { IME_File, IME_Directory, IME_HardLink };
 
 /// The in memory file system is a tree of Nodes. Every node can either be a
-/// file or a directory.
+/// file , hardlink or a directory.
 class InMemoryNode {
-  Status Stat;
   InMemoryNodeKind Kind;
-
-protected:
-  /// Return Stat.  This should only be used for internal/debugging use.  When
-  /// clients wants the Status of this node, they should use
-  /// \p getStatus(StringRef).
-  const Status &getStatus() const { return Stat; }
+  std::string FileName;
 
 public:
-  InMemoryNode(Status Stat, InMemoryNodeKind Kind)
-      : Stat(std::move(Stat)), Kind(Kind) {}
+  InMemoryNode(llvm::StringRef FileName, InMemoryNodeKind Kind)
+      : Kind(Kind), FileName(llvm::sys::path::filename(FileName)) {}
   virtual ~InMemoryNode() = default;
 
+  /// Get the filename of this node (the name without the directory part).
+  StringRef getFileName() const { return FileName; }
+  InMemoryNodeKind getKind() const { return Kind; }
+  virtual std::string toString(unsigned Indent) const = 0;
+};
+
+class InMemoryFile : public InMemoryNode {
+  Status Stat;
+  std::unique_ptr<llvm::MemoryBuffer> Buffer;
+
+public:
+  InMemoryFile(Status Stat, std::unique_ptr<llvm::MemoryBuffer> Buffer)
+      : InMemoryNode(Stat.getName(), IME_File), Stat(std::move(Stat)),
+        Buffer(std::move(Buffer)) {}
+
   /// Return the \p Status for this node. \p RequestedName should be the name
   /// through which the caller referred to this node. It will override
   /// \p Status::Name in the return value, to mimic the behavior of \p RealFile.
   Status getStatus(StringRef RequestedName) const {
     return Status::copyWithNewName(Stat, RequestedName);
   }
+  llvm::MemoryBuffer *getBuffer() const { return Buffer.get(); }
 
-  /// Get the filename of this node (the name without the directory part).
-  StringRef getFileName() const {
-    return llvm::sys::path::filename(Stat.getName());
+  std::string toString(unsigned Indent) const override {
+    return (std::string(Indent, ' ') + Stat.getName() + "\n").str();
+  }
+
+  static bool classof(const InMemoryNode *N) {
+    return N->getKind() == IME_File;
   }
-  InMemoryNodeKind getKind() const { return Kind; }
-  virtual std::string toString(unsigned Indent) const = 0;
 };
 
 namespace {
 
-class InMemoryFile : public InMemoryNode {
-  std::unique_ptr<llvm::MemoryBuffer> Buffer;
+class InMemoryHardLink : public InMemoryNode {
+  const InMemoryFile &ResolvedFile;
 
 public:
-  InMemoryFile(Status Stat, std::unique_ptr<llvm::MemoryBuffer> Buffer)
-      : InMemoryNode(std::move(Stat), IME_File), Buffer(std::move(Buffer)) {}
-
-  llvm::MemoryBuffer *getBuffer() { return Buffer.get(); }
+  InMemoryHardLink(StringRef Path, const InMemoryFile &ResolvedFile)
+      : InMemoryNode(Path, IME_HardLink), ResolvedFile(ResolvedFile) {}
+  const InMemoryFile &getResolvedFile() const { return ResolvedFile; }
 
   std::string toString(unsigned Indent) const override {
-    return (std::string(Indent, ' ') + getStatus().getName() + "\n").str();
+    return std::string(Indent, ' ') + "HardLink to -> " +
+           ResolvedFile.toString(0);
   }
 
   static bool classof(const InMemoryNode *N) {
-    return N->getKind() == IME_File;
+    return N->getKind() == IME_HardLink;
   }
 };
 
@@ -524,12 +536,13 @@ public:
 /// \p InMemoryFileAdaptor mimic as much as possible the behavior of
 /// \p RealFile.
 class InMemoryFileAdaptor : public File {
-  InMemoryFile &Node;
+  const InMemoryFile &Node;
   /// The name to use when returning a Status for this file.
   std::string RequestedName;
 
 public:
-  explicit InMemoryFileAdaptor(InMemoryFile &Node, std::string RequestedName)
+  explicit InMemoryFileAdaptor(const InMemoryFile &Node,
+                               std::string RequestedName)
       : Node(Node), RequestedName(std::move(RequestedName)) {}
 
   llvm::ErrorOr<Status> status() override {
@@ -546,16 +559,22 @@ public:
 
   std::error_code close() override { return {}; }
 };
-
 } // namespace
 
 class InMemoryDirectory : public InMemoryNode {
+  Status Stat;
   std::map<std::string, std::unique_ptr<InMemoryNode>> Entries;
 
 public:
   InMemoryDirectory(Status Stat)
-      : InMemoryNode(std::move(Stat), IME_Directory) {}
+      : InMemoryNode(Stat.getName(), IME_Directory), Stat(std::move(Stat)) {}
 
+  /// Return the \p Status for this node. \p RequestedName should be the name
+  /// through which the caller referred to this node. It will override
+  /// \p Status::Name in the return value, to mimic the behavior of \p RealFile.
+  Status getStatus(StringRef RequestedName) const {
+    return Status::copyWithNewName(Stat, RequestedName);
+  }
   InMemoryNode *getChild(StringRef Name) {
     auto I = Entries.find(Name);
     if (I != Entries.end())
@@ -575,7 +594,7 @@ public:
 
   std::string toString(unsigned Indent) const override {
     std::string Result =
-        (std::string(Indent, ' ') + getStatus().getName() + "\n").str();
+        (std::string(Indent, ' ') + Stat.getName() + "\n").str();
     for (const auto &Entry : Entries)
       Result += Entry.second->toString(Indent + 2);
     return Result;
@@ -586,6 +605,17 @@ public:
   }
 };
 
+namespace {
+Status getNodeStatus(const InMemoryNode *Node, StringRef RequestedName) {
+  if (auto Dir = dyn_cast<detail::InMemoryDirectory>(Node))
+    return Dir->getStatus(RequestedName);
+  if (auto File = dyn_cast<detail::InMemoryFile>(Node))
+    return File->getStatus(RequestedName);
+  if (auto Link = dyn_cast<detail::InMemoryHardLink>(Node))
+    return Link->getResolvedFile().getStatus(RequestedName);
+  llvm_unreachable("Unknown node type");
+}
+} // namespace
 } // namespace detail
 
 InMemoryFileSystem::InMemoryFileSystem(bool UseNormalizedPaths)
@@ -606,7 +636,8 @@ bool InMemoryFileSystem::addFile(const Twine &P, time_t ModificationTime,
                                  Optional<uint32_t> User,
                                  Optional<uint32_t> Group,
                                  Optional<llvm::sys::fs::file_type> Type,
-                                 Optional<llvm::sys::fs::perms> Perms) {
+                                 Optional<llvm::sys::fs::perms> Perms,
+                                 const detail::InMemoryFile *HardLinkTarget) {
   SmallString<128> Path;
   P.toVector(Path);
 
@@ -627,6 +658,7 @@ bool InMemoryFileSystem::addFile(const Twine &P, time_t ModificationTime,
   const auto ResolvedGroup = Group.getValueOr(0);
   const auto ResolvedType = Type.getValueOr(sys::fs::file_type::regular_file);
   const auto ResolvedPerms = Perms.getValueOr(sys::fs::all_all);
+  assert(!(HardLinkTarget && Buffer) && "HardLink cannot have a buffer");
   // Any intermediate directories we create should be accessible by
   // the owner, even if Perms says otherwise for the final path.
   const auto NewDirectoryPerms = ResolvedPerms | sys::fs::owner_all;
@@ -636,17 +668,22 @@ bool InMemoryFileSystem::addFile(const Twine &P, time_t ModificationTime,
     ++I;
     if (!Node) {
       if (I == E) {
-        // End of the path, create a new file or directory.
-        Status Stat(P.str(), getNextVirtualUniqueID(),
-                    llvm::sys::toTimePoint(ModificationTime), ResolvedUser,
-                    ResolvedGroup, Buffer->getBufferSize(), ResolvedType,
-                    ResolvedPerms);
+        // End of the path.
         std::unique_ptr<detail::InMemoryNode> Child;
-        if (ResolvedType == sys::fs::file_type::directory_file) {
-          Child.reset(new detail::InMemoryDirectory(std::move(Stat)));
-        } else {
-          Child.reset(new detail::InMemoryFile(std::move(Stat),
-                                               std::move(Buffer)));
+        if (HardLinkTarget)
+          Child.reset(new detail::InMemoryHardLink(P.str(), *HardLinkTarget));
+        else {
+          // Create a new file or directory.
+          Status Stat(P.str(), getNextVirtualUniqueID(),
+                      llvm::sys::toTimePoint(ModificationTime), ResolvedUser,
+                      ResolvedGroup, Buffer->getBufferSize(), ResolvedType,
+                      ResolvedPerms);
+          if (ResolvedType == sys::fs::file_type::directory_file) {
+            Child.reset(new detail::InMemoryDirectory(std::move(Stat)));
+          } else {
+            Child.reset(
+                new detail::InMemoryFile(std::move(Stat), std::move(Buffer)));
+          }
         }
         Dir->addChild(Name, std::move(Child));
         return true;
@@ -656,8 +693,8 @@ bool InMemoryFileSystem::addFile(const Twine &P, time_t ModificationTime,
       Status Stat(
           StringRef(Path.str().begin(), Name.end() - Path.str().begin()),
           getNextVirtualUniqueID(), llvm::sys::toTimePoint(ModificationTime),
-          ResolvedUser, ResolvedGroup, Buffer->getBufferSize(),
-          sys::fs::file_type::directory_file, NewDirectoryPerms);
+          ResolvedUser, ResolvedGroup, 0, sys::fs::file_type::directory_file,
+          NewDirectoryPerms);
       Dir = cast<detail::InMemoryDirectory>(Dir->addChild(
           Name, llvm::make_unique<detail::InMemoryDirectory>(std::move(Stat))));
       continue;
@@ -666,20 +703,35 @@ bool InMemoryFileSystem::addFile(const Twine &P, time_t ModificationTime,
     if (auto *NewDir = dyn_cast<detail::InMemoryDirectory>(Node)) {
       Dir = NewDir;
     } else {
-      assert(isa<detail::InMemoryFile>(Node) &&
-             "Must be either file or directory!");
+      assert((isa<detail::InMemoryFile>(Node) ||
+              isa<detail::InMemoryHardLink>(Node)) &&
+             "Must be either file, hardlink or directory!");
 
       // Trying to insert a directory in place of a file.
       if (I != E)
         return false;
 
       // Return false only if the new file is different from the existing one.
+      if (auto Link = dyn_cast<detail::InMemoryHardLink>(Node)) {
+        return Link->getResolvedFile().getBuffer()->getBuffer() ==
+               Buffer->getBuffer();
+      }
       return cast<detail::InMemoryFile>(Node)->getBuffer()->getBuffer() ==
              Buffer->getBuffer();
     }
   }
 }
 
+bool InMemoryFileSystem::addFile(const Twine &P, time_t ModificationTime,
+                                 std::unique_ptr<llvm::MemoryBuffer> Buffer,
+                                 Optional<uint32_t> User,
+                                 Optional<uint32_t> Group,
+                                 Optional<llvm::sys::fs::file_type> Type,
+                                 Optional<llvm::sys::fs::perms> Perms) {
+  return addFile(P, ModificationTime, std::move(Buffer), User, Group, Type,
+                 Perms, /*HardLinkTarget=*/nullptr);
+}
+
 bool InMemoryFileSystem::addFileNoOwn(const Twine &P, time_t ModificationTime,
                                       llvm::MemoryBuffer *Buffer,
                                       Optional<uint32_t> User,
@@ -693,7 +745,7 @@ bool InMemoryFileSystem::addFileNoOwn(const Twine &P, time_t ModificationTime,
                  std::move(Perms));
 }
 
-static ErrorOr<detail::InMemoryNode *>
+static ErrorOr<const detail::InMemoryNode *>
 lookupInMemoryNode(const InMemoryFileSystem &FS, detail::InMemoryDirectory *Dir,
                    const Twine &P) {
   SmallString<128> Path;
@@ -724,6 +776,12 @@ lookupInMemoryNode(const InMemoryFileSystem &FS, detail::InMemoryDirectory *Dir,
       return errc::no_such_file_or_directory;
     }
 
+    // If Node is HardLink then return the resolved file.
+    if (auto File = dyn_cast<detail::InMemoryHardLink>(Node)) {
+      if (I == E)
+        return &File->getResolvedFile();
+      return errc::no_such_file_or_directory;
+    }
     // Traverse directories.
     Dir = cast<detail::InMemoryDirectory>(Node);
     if (I == E)
@@ -731,10 +789,22 @@ lookupInMemoryNode(const InMemoryFileSystem &FS, detail::InMemoryDirectory *Dir,
   }
 }
 
+bool InMemoryFileSystem::addHardLink(const Twine &FromPath,
+                                     const Twine &ToPath) {
+  auto FromNode = lookupInMemoryNode(*this, Root.get(), FromPath);
+  auto ToNode = lookupInMemoryNode(*this, Root.get(), ToPath);
+  // FromPath must not have been added before. ToPath must have been added
+  // before. Resolved ToPath must be a File.
+  if (!ToNode || FromNode || !isa<detail::InMemoryFile>(*ToNode))
+    return false;
+  return this->addFile(FromPath, 0, nullptr, None, None, None, None,
+                       cast<detail::InMemoryFile>(*ToNode));
+}
+
 llvm::ErrorOr<Status> InMemoryFileSystem::status(const Twine &Path) {
   auto Node = lookupInMemoryNode(*this, Root.get(), Path);
   if (Node)
-    return (*Node)->getStatus(Path.str());
+    return detail::getNodeStatus(*Node, Path.str());
   return Node.getError();
 }
 
@@ -766,7 +836,7 @@ class InMemoryDirIterator : public clang::vfs::detail::DirIterImpl {
     if (I != E) {
       SmallString<256> Path(RequestedDirName);
       llvm::sys::path::append(Path, I->second->getFileName());
-      CurrentEntry = I->second->getStatus(Path);
+      CurrentEntry = detail::getNodeStatus(I->second.get(), Path);
     } else {
       // When we're at the end, make CurrentEntry invalid and DirIterImpl will
       // do the rest.
@@ -777,7 +847,7 @@ class InMemoryDirIterator : public clang::vfs::detail::DirIterImpl {
 public:
   InMemoryDirIterator() = default;
 
-  explicit InMemoryDirIterator(detail::InMemoryDirectory &Dir,
+  explicit InMemoryDirIterator(const detail::InMemoryDirectory &Dir,
                                std::string RequestedDirName)
       : I(Dir.begin()), E(Dir.end()),
         RequestedDirName(std::move(RequestedDirName)) {
index 23dbc578558bf40dcd0c4f0470f78127f803561e..3d41db7a05d8d85ea741b88c5908042a32a7cb72 100644 (file)
 #include "llvm/Support/MemoryBuffer.h"
 #include "llvm/Support/Path.h"
 #include "llvm/Support/SourceMgr.h"
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include <map>
+#include <string>
 
 using namespace clang;
 using namespace llvm;
@@ -697,6 +699,16 @@ protected:
         NormalizedFS(/*UseNormalizedPaths=*/true) {}
 };
 
+MATCHER_P2(IsHardLinkTo, FS, Target, "") {
+  StringRef From = arg;
+  StringRef To = Target;
+  auto OpenedFrom = FS->openFileForRead(From);
+  auto OpenedTo = FS->openFileForRead(To);
+  return !OpenedFrom.getError() && !OpenedTo.getError() &&
+         (*OpenedFrom)->status()->getUniqueID() ==
+             (*OpenedTo)->status()->getUniqueID();
+}
+
 TEST_F(InMemoryFileSystemTest, IsEmpty) {
   auto Stat = FS.status("/a");
   ASSERT_EQ(Stat.getError(),errc::no_such_file_or_directory) << FS.toString();
@@ -958,6 +970,108 @@ TEST_F(InMemoryFileSystemTest, StatusName) {
   ASSERT_EQ("../b/c", getPosixPath(It->getName()));
 }
 
+TEST_F(InMemoryFileSystemTest, AddHardLinkToFile) {
+  StringRef FromLink = "/path/to/FROM/link";
+  StringRef Target = "/path/to/TO/file";
+  FS.addFile(Target, 0, MemoryBuffer::getMemBuffer("content of target"));
+  EXPECT_TRUE(FS.addHardLink(FromLink, Target));
+  EXPECT_THAT(FromLink, IsHardLinkTo(&FS, Target));
+  EXPECT_TRUE(FS.status(FromLink)->getSize() == FS.status(Target)->getSize());
+  EXPECT_TRUE(FS.getBufferForFile(FromLink)->get()->getBuffer() ==
+              FS.getBufferForFile(Target)->get()->getBuffer());
+}
+
+TEST_F(InMemoryFileSystemTest, AddHardLinkInChainPattern) {
+  StringRef Link0 = "/path/to/0/link";
+  StringRef Link1 = "/path/to/1/link";
+  StringRef Link2 = "/path/to/2/link";
+  StringRef Target = "/path/to/target";
+  FS.addFile(Target, 0, MemoryBuffer::getMemBuffer("content of target file"));
+  EXPECT_TRUE(FS.addHardLink(Link2, Target));
+  EXPECT_TRUE(FS.addHardLink(Link1, Link2));
+  EXPECT_TRUE(FS.addHardLink(Link0, Link1));
+  EXPECT_THAT(Link0, IsHardLinkTo(&FS, Target));
+  EXPECT_THAT(Link1, IsHardLinkTo(&FS, Target));
+  EXPECT_THAT(Link2, IsHardLinkTo(&FS, Target));
+}
+
+TEST_F(InMemoryFileSystemTest, AddHardLinkToAFileThatWasNotAddedBefore) {
+  EXPECT_FALSE(FS.addHardLink("/path/to/link", "/path/to/target"));
+}
+
+TEST_F(InMemoryFileSystemTest, AddHardLinkFromAFileThatWasAddedBefore) {
+  StringRef Link = "/path/to/link";
+  StringRef Target = "/path/to/target";
+  FS.addFile(Target, 0, MemoryBuffer::getMemBuffer("content of target"));
+  FS.addFile(Link, 0, MemoryBuffer::getMemBuffer("content of link"));
+  EXPECT_FALSE(FS.addHardLink(Link, Target));
+}
+
+TEST_F(InMemoryFileSystemTest, AddSameHardLinkMoreThanOnce) {
+  StringRef Link = "/path/to/link";
+  StringRef Target = "/path/to/target";
+  FS.addFile(Target, 0, MemoryBuffer::getMemBuffer("content of target"));
+  EXPECT_TRUE(FS.addHardLink(Link, Target));
+  EXPECT_FALSE(FS.addHardLink(Link, Target));
+}
+
+TEST_F(InMemoryFileSystemTest, AddFileInPlaceOfAHardLinkWithSameContent) {
+  StringRef Link = "/path/to/link";
+  StringRef Target = "/path/to/target";
+  StringRef Content = "content of target";
+  EXPECT_TRUE(FS.addFile(Target, 0, MemoryBuffer::getMemBuffer(Content)));
+  EXPECT_TRUE(FS.addHardLink(Link, Target));
+  EXPECT_TRUE(FS.addFile(Link, 0, MemoryBuffer::getMemBuffer(Content)));
+}
+
+TEST_F(InMemoryFileSystemTest, AddFileInPlaceOfAHardLinkWithDifferentContent) {
+  StringRef Link = "/path/to/link";
+  StringRef Target = "/path/to/target";
+  StringRef Content = "content of target";
+  StringRef LinkContent = "different content of link";
+  EXPECT_TRUE(FS.addFile(Target, 0, MemoryBuffer::getMemBuffer(Content)));
+  EXPECT_TRUE(FS.addHardLink(Link, Target));
+  EXPECT_FALSE(FS.addFile(Link, 0, MemoryBuffer::getMemBuffer(LinkContent)));
+}
+
+TEST_F(InMemoryFileSystemTest, AddHardLinkToADirectory) {
+  StringRef Dir = "path/to/dummy/dir";
+  StringRef Link = "/path/to/link";
+  StringRef File = "path/to/dummy/dir/target";
+  StringRef Content = "content of target";
+  EXPECT_TRUE(FS.addFile(File, 0, MemoryBuffer::getMemBuffer(Content)));
+  EXPECT_FALSE(FS.addHardLink(Link, Dir));
+}
+
+TEST_F(InMemoryFileSystemTest, AddHardLinkFromADirectory) {
+  StringRef Dir = "path/to/dummy/dir";
+  StringRef Target = "path/to/dummy/dir/target";
+  StringRef Content = "content of target";
+  EXPECT_TRUE(FS.addFile(Target, 0, MemoryBuffer::getMemBuffer(Content)));
+  EXPECT_FALSE(FS.addHardLink(Dir, Target));
+}
+
+TEST_F(InMemoryFileSystemTest, AddHardLinkUnderAFile) {
+  StringRef CommonContent = "content string";
+  FS.addFile("/a/b", 0, MemoryBuffer::getMemBuffer(CommonContent));
+  FS.addFile("/c/d", 0, MemoryBuffer::getMemBuffer(CommonContent));
+  EXPECT_FALSE(FS.addHardLink("/c/d/e", "/a/b"));
+}
+
+TEST_F(InMemoryFileSystemTest, RecursiveIterationWithHardLink) {
+  std::error_code EC;
+  FS.addFile("/a/b", 0, MemoryBuffer::getMemBuffer("content string"));
+  EXPECT_TRUE(FS.addHardLink("/c/d", "/a/b"));
+  auto I = vfs::recursive_directory_iterator(FS, "/", EC);
+  ASSERT_FALSE(EC);
+  std::vector<std::string> Nodes;
+  for (auto E = vfs::recursive_directory_iterator(); !EC && I != E;
+       I.increment(EC)) {
+    Nodes.push_back(getPosixPath(I->getName()));
+  }
+  EXPECT_THAT(Nodes, testing::UnorderedElementsAre("/a", "/a/b", "/c", "/c/d"));
+}
+
 // NOTE: in the tests below, we use '//root/' as our root directory, since it is
 // a legal *absolute* path on Windows as well as *nix.
 class VFSFromYAMLTest : public ::testing::Test {