Stale file removal fixes for Swift 4.0 (#176)

* Use `set` instead of `unordered_set`

Turns out that `std::set_difference` does not work with unordered sets,
but this didn't become apparent with the smaller sets we are using in
the unit tests.

* Fix `BuildSystemTaskTests`

This one needed to be updated after changes to
`MockBuildSystemDelegate`.

* Allow stale file removal build values (#167)

* Add delegate support for command reporting (#158)

Commands can now report additional errors, notes and warnings, not tied
to a command process as before. This is used by the `StaleFileRemoval`
command to report deleted files and demotes all its errors to warnings.

* Extract the `isLocatedUnderRootPath` check (#169)

Previously, we were incrementing the pointer with the first `res.second++`, so the next part of the if statement would see the character after that.

This extracts that code, fixes the issue and adds unit tests.
diff --git a/include/llbuild/BuildSystem/BuildSystem.h b/include/llbuild/BuildSystem/BuildSystem.h
index f40f5ce..2d91337 100644
--- a/include/llbuild/BuildSystem/BuildSystem.h
+++ b/include/llbuild/BuildSystem/BuildSystem.h
@@ -39,6 +39,8 @@
 class BuildValue;
 class Command;
 class Tool;
+
+bool pathIsPrefixedByPath(std::string path, std::string prefixPath);
   
 class BuildSystemDelegate {
   // DO NOT COPY
@@ -176,6 +178,21 @@
   /// commands).
   virtual void commandStarted(Command*) = 0;
 
+  /// Called to report an error during the execution of a command.
+  ///
+  /// \param data - The error message.
+  virtual void commandHadError(Command*, StringRef data) = 0;
+
+  /// Called to report a note during the execution of a command.
+  ///
+  /// \param data - The note message.
+  virtual void commandHadNote(Command*, StringRef data) = 0;
+
+  /// Called to report a warning during the execution of a command.
+  ///
+  /// \param data - The warning message.
+  virtual void commandHadWarning(Command*, StringRef data) = 0;
+
   /// Called by the build system to report a command has completed.
   ///
   /// \param result - The result of command (e.g. success, failure, etc).
diff --git a/include/llbuild/BuildSystem/BuildSystemFrontend.h b/include/llbuild/BuildSystem/BuildSystemFrontend.h
index 767683e..4c7c09e 100644
--- a/include/llbuild/BuildSystem/BuildSystemFrontend.h
+++ b/include/llbuild/BuildSystem/BuildSystemFrontend.h
@@ -199,6 +199,21 @@
   /// corresponding \see commandFinished() call.
   virtual void commandStarted(Command*) override;
 
+  /// Called to report an error during the execution of a command.
+  ///
+  /// \param data - The error message.
+  virtual void commandHadError(Command*, StringRef data) override;
+
+  /// Called to report a note during the execution of a command.
+  ///
+  /// \param data - The note message.
+  virtual void commandHadNote(Command*, StringRef data) override;
+
+  /// Called to report a warning during the execution of a command.
+  ///
+  /// \param data - The warning message.
+  virtual void commandHadWarning(Command*, StringRef data) override;
+
   /// Called by the build system to report a command has completed.
   ///
   /// \param result - The result of command (e.g. success, failure, etc).
diff --git a/lib/BuildSystem/BuildSystem.cpp b/lib/BuildSystem/BuildSystem.cpp
index ecefa41..2965c94 100644
--- a/lib/BuildSystem/BuildSystem.cpp
+++ b/lib/BuildSystem/BuildSystem.cpp
@@ -46,7 +46,7 @@
 
 #include <memory>
 #include <mutex>
-#include <unordered_set>
+#include <set>
 
 #include <unistd.h>
 
@@ -2421,8 +2421,8 @@
     }
 
     std::vector<StringRef> priorValueList = priorValue.getStaleFileList();
-    std::unordered_set<std::string> priorNodes(priorValueList.begin(), priorValueList.end());
-    std::unordered_set<std::string> expectedNodes(expectedOutputs.begin(), expectedOutputs.end());
+    std::set<std::string> priorNodes(priorValueList.begin(), priorValueList.end());
+    std::set<std::string> expectedNodes(expectedOutputs.begin(), expectedOutputs.end());
 
     std::set_difference(priorNodes.begin(), priorNodes.end(),
                         expectedNodes.begin(), expectedNodes.end(),
@@ -2444,7 +2444,6 @@
     computeFilesToDelete();
 
     bsci.getDelegate().commandStarted(this);
-    auto success = true;
 
     for (auto fileToDelete : filesToDelete) {
       // If no root paths are specified, any path is valid.
@@ -2452,32 +2451,30 @@
 
       // If root paths are defined, stale file paths should be absolute.
       if (roots.size() > 0 && fileToDelete[0] != path_separator) {
-        bsci.getDelegate().error("", {}, (Twine("Stale file '") + fileToDelete + "' has a relative path. This is invalid in combination with the root path attribute."));
-        success = false;
+        bsci.getDelegate().commandHadWarning(this, "Stale file '" + fileToDelete + "' has a relative path. This is invalid in combination with the root path attribute.\n");
         continue;
       }
 
       // Check if the file is located under one of the allowed root paths.
       for (auto root : roots) {
-        auto res = std::mismatch(root.begin(), root.end(), fileToDelete.begin());
-        if (res.first == root.end() && ((*(res.first++) == '\0') || (*(res.first++) == path_separator))) {
+        if (pathIsPrefixedByPath(fileToDelete, root)) {
           isLocatedUnderRootPath = true;
         }
       }
 
       if (!isLocatedUnderRootPath) {
-        bsci.getDelegate().error("", {}, (Twine("Stale file '") + fileToDelete + "' is located outside of the allowed root paths."));
-        success = false;
+        bsci.getDelegate().commandHadWarning(this, "Stale file '" + fileToDelete + "' is located outside of the allowed root paths.\n");
         continue;
       }
 
-      if (!getBuildSystem(bsci.getBuildEngine()).getDelegate().getFileSystem().remove(fileToDelete)) {
-        bsci.getDelegate().error("", {}, (Twine("cannot remove stale file '") + fileToDelete + "': " + strerror(errno)));
-        success = false;
+      if (getBuildSystem(bsci.getBuildEngine()).getDelegate().getFileSystem().remove(fileToDelete)) {
+        bsci.getDelegate().commandHadNote(this, "Removed stale file '" + fileToDelete + "'\n");
+      } else {
+        bsci.getDelegate().commandHadWarning(this, "cannot remove stale file '" + fileToDelete + "': " + strerror(errno) + "\n");
       }
     }
 
-    bsci.getDelegate().commandFinished(this, success ? CommandResult::Succeeded : CommandResult::Failed);
+    bsci.getDelegate().commandFinished(this, CommandResult::Succeeded);
 
     // Complete with a successful result.
     return BuildValue::makeStaleFileRemoval(expectedOutputs);
@@ -2652,3 +2649,13 @@
 void BuildSystem::resetForBuild() {
   static_cast<BuildSystemImpl*>(impl)->resetForBuild();
 }
+
+// This function checks if the given path is prefixed by another path.
+bool llbuild::buildsystem::pathIsPrefixedByPath(std::string path, std::string prefixPath) {
+  static char path_separator = llvm::sys::path::get_separator()[0];
+  auto res = std::mismatch(prefixPath.begin(), prefixPath.end(), path.begin());
+  // Check if `prefixPath` has been exhausted or just a separator remains.
+  bool isPrefix = res.first == prefixPath.end() || (*(res.first++) == path_separator);
+  // Check if `path` has been exhausted or just a separator remains.
+  return isPrefix && (res.second == path.end() || (*(res.second++) == path_separator));
+}
diff --git a/lib/BuildSystem/BuildSystemFrontend.cpp b/lib/BuildSystem/BuildSystemFrontend.cpp
index e30554e..4a9948a 100644
--- a/lib/BuildSystem/BuildSystemFrontend.cpp
+++ b/lib/BuildSystem/BuildSystemFrontend.cpp
@@ -480,6 +480,21 @@
   fflush(stdout);
 }
 
+void BuildSystemFrontendDelegate::commandHadError(Command* command, StringRef data) {
+  fwrite(data.data(), data.size(), 1, stderr);
+  fflush(stderr);
+}
+
+void BuildSystemFrontendDelegate::commandHadNote(Command* command, StringRef data) {
+  fwrite(data.data(), data.size(), 1, stdout);
+  fflush(stdout);
+}
+
+void BuildSystemFrontendDelegate::commandHadWarning(Command* command, StringRef data) {
+  fwrite(data.data(), data.size(), 1, stdout);
+  fflush(stdout);
+}
+
 void BuildSystemFrontendDelegate::commandFinished(Command*, CommandResult) {
 }
 
diff --git a/lib/BuildSystem/ExternalCommand.cpp b/lib/BuildSystem/ExternalCommand.cpp
index 6d33f4e..a26644d 100644
--- a/lib/BuildSystem/ExternalCommand.cpp
+++ b/lib/BuildSystem/ExternalCommand.cpp
@@ -242,14 +242,14 @@
   assert(value.isExistingInput() || value.isMissingInput() ||
          value.isMissingOutput() || value.isFailedInput() ||
          value.isVirtualInput()  || value.isSkippedCommand() ||
-         value.isDirectoryTreeSignature());
+         value.isDirectoryTreeSignature() || value.isStaleFileRemoval());
 
   // If the input should cause this command to skip, how should it skip?
   auto getSkipValueForInput = [&]() -> llvm::Optional<BuildValue> {
     // If the value is an signature, existing, or virtual input, we are always
     // good.
     if (value.isDirectoryTreeSignature() | value.isExistingInput() ||
-        value.isVirtualInput())
+        value.isVirtualInput() || value.isStaleFileRemoval())
       return llvm::None;
 
     // We explicitly allow running the command against a missing output, under
diff --git a/unittests/BuildSystem/BuildSystemTaskTests.cpp b/unittests/BuildSystem/BuildSystemTaskTests.cpp
index 55ae57d..7397774 100644
--- a/unittests/BuildSystem/BuildSystemTaskTests.cpp
+++ b/unittests/BuildSystem/BuildSystemTaskTests.cpp
@@ -408,8 +408,8 @@
     "commandPreparing(C.1)",
     "commandStarted(C.1)",
     // FIXME: Maybe it's worth creating a virtual FileSystem implementation and checking if `remove` has been called
-    "cannot remove stale file 'a.out': No such file or directory",
-    "commandFinished(C.1: 1)",
+    "commandWarning(C.1) cannot remove stale file 'a.out': No such file or directory\n",
+    "commandFinished(C.1: 0)",
   }), delegate.getMessages());
 }
 
@@ -431,7 +431,7 @@
     C.1:
       tool: stale-file-removal
       description: STALE-FILE-REMOVAL
-      expectedOutputs: ["/bar/a.out"]
+      expectedOutputs: ["/bar/a.out", "/foo", "/foobar.txt"]
 )END";
   }
 
@@ -451,8 +451,10 @@
     auto result = system.build(keyToBuild);
 
     ASSERT_TRUE(result.getValue().isStaleFileRemoval());
-    ASSERT_EQ(result.getValue().getStaleFileList().size(), 1UL);
+    ASSERT_EQ(result.getValue().getStaleFileList().size(), 3UL);
     ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[0].str().c_str(), "/bar/a.out") == 0);
+    ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[1].str().c_str(), "/foo") == 0);
+    ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[2].str().c_str(), "/foobar.txt") == 0);
 
     ASSERT_EQ(std::vector<std::string>({
       "commandPreparing(C.1)",
@@ -474,7 +476,7 @@
     C.1:
       tool: stale-file-removal
       description: STALE-FILE-REMOVAL
-      expectedOutputs: ["/bar/b.out", "/foo", "/foobar.txt"]
+      expectedOutputs: ["/bar/b.out"]
       roots: ["/foo"]
 )END";
   }
@@ -487,19 +489,20 @@
   auto result = system.build(keyToBuild);
 
   ASSERT_TRUE(result.getValue().isStaleFileRemoval());
-  ASSERT_EQ(result.getValue().getStaleFileList().size(), 3UL);
+  ASSERT_EQ(result.getValue().getStaleFileList().size(), 1UL);
   ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[0].str().c_str(), "/bar/b.out") == 0);
-  ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[1].str().c_str(), "/foo") == 0);
-  ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[2].str().c_str(), "/foobar.txt") == 0);
+
+  auto messages = delegate.getMessages();
+  std::sort(messages.begin(), messages.end());
 
   ASSERT_EQ(std::vector<std::string>({
+    "commandFinished(C.1: 0)",
     "commandPreparing(C.1)",
     "commandStarted(C.1)",
-    "Stale file '/bar/a.out' is located outside of the allowed root paths.",
-    // FIXME: Enable once stale file removal issues are no longer errros.
-    //"Stale file '/foobar.txt' is located outside of the allowed root paths.",
-    "commandFinished(C.1: 1)",
-  }), delegate.getMessages());
+    "commandWarning(C.1) Stale file '/bar/a.out' is located outside of the allowed root paths.\n",
+    "commandWarning(C.1) Stale file '/foobar.txt' is located outside of the allowed root paths.\n",
+    "commandWarning(C.1) cannot remove stale file '/foo': No such file or directory\n",
+  }), messages);
 }
 
 TEST(BuildSystemTaskTests, staleFileRemovalWithRootsEnforcesAbsolutePaths) {
@@ -582,8 +585,8 @@
   ASSERT_EQ(std::vector<std::string>({
     "commandPreparing(C.1)",
     "commandStarted(C.1)",
-    "Stale file 'a.out' has a relative path. This is invalid in combination with the root path attribute.",
-    "commandFinished(C.1: 1)",
+    "commandWarning(C.1) Stale file 'a.out' has a relative path. This is invalid in combination with the root path attribute.\n",
+    "commandFinished(C.1: 0)",
   }), delegate.getMessages());
 }
 
@@ -667,10 +670,120 @@
   ASSERT_EQ(std::vector<std::string>({
     "commandPreparing(C.1)",
     "commandStarted(C.1)",
-    "cannot remove stale file '/foo/': No such file or directory",
-    "Stale file 'a.out' has a relative path. This is invalid in combination with the root path attribute.",
-    "commandFinished(C.1: 1)",
+    "commandWarning(C.1) cannot remove stale file '/foo/': No such file or directory\n",
+    "commandWarning(C.1) Stale file 'a.out' has a relative path. This is invalid in combination with the root path attribute.\n",
+    "commandFinished(C.1: 0)",
   }), delegate.getMessages());
 }
 
+TEST(BuildSystemTaskTests, staleFileRemovalWithManyFiles) {
+  TmpDir tempDir{ __FUNCTION__ };
+
+  SmallString<256> manifest{ tempDir.str() };
+  sys::path::append(manifest, "manifest.llbuild");
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream os(manifest, ec, llvm::sys::fs::F_Text);
+    assert(!ec);
+
+    os << R"END(
+client:
+  name: mock
+
+commands:
+    C.1:
+      tool: stale-file-removal
+      expectedOutputs: ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/Objects-normal/x86_64/empty.o", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Headers/CoreBasic-extra-header.h", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Headers/CoreBasic.h", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Resources/CoreBasic-extra-resource.txt", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Resources/CoreBasic-resource.txt", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Modules/module.modulemap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/CoreBasic, /var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Headers", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Resources", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Resources/Info.plist", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/CoreBasic", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Headers", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Modules", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Resources", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/Current", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/CoreBasic-all-non-framework-target-headers.hmap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/CoreBasic-all-target-headers.hmap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/CoreBasic-generated-files.hmap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/CoreBasic-own-target-headers.hmap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/CoreBasic-project-headers.hmap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/CoreBasic.hmap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/Objects-normal/x86_64/CoreBasic.LinkFileList", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/all-product-headers.yaml", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/module.modulemap"]
+      roots: ["/tmp/basic.dst", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products"]
+)END";
+  }
+
+  auto keyToBuild = BuildKey::makeCommand("C.1");
+
+  SmallString<256> builddb{ tempDir.str() };
+  sys::path::append(builddb, "build.db");
+
+  // We will check that the same file is present in both file lists, so it should not show up in the difference.
+  std::string linkFileList = "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/Objects-normal/x86_64/CoreBasic.LinkFileList";
+
+  {
+    MockBuildSystemDelegate delegate(/*trackAllMessages=*/true);
+    BuildSystem system(delegate);
+    system.attachDB(builddb.c_str(), nullptr);
+
+    bool loadingResult = system.loadDescription(manifest);
+    ASSERT_TRUE(loadingResult);
+
+    auto result = system.build(keyToBuild);
+
+    ASSERT_TRUE(result.getValue().isStaleFileRemoval());
+    ASSERT_EQ(result.getValue().getStaleFileList().size(), 50UL);
+
+    // Check that `LinkFileList` is present in list of files of initial build
+    bool hasLinkFileList = false;
+    for (auto staleFile : result.getValue().getStaleFileList()) {
+      if (staleFile == linkFileList) {
+        hasLinkFileList = true;
+      }
+    }
+    ASSERT_TRUE(hasLinkFileList);
+
+    ASSERT_EQ(std::vector<std::string>({
+      "commandPreparing(C.1)",
+      "commandStarted(C.1)",
+      "commandFinished(C.1: 0)",
+    }), delegate.getMessages());
+  }
+
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream os(manifest, ec, llvm::sys::fs::F_Text);
+    assert(!ec);
+
+    os << R"END(
+client:
+  name: mock
+
+commands:
+    C.1:
+      tool: stale-file-removal
+      expectedOutputs: ["/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/Objects-normal/x86_64/empty.o", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Headers/CoreBasic.h", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Resources/CoreBasic-resource.txt", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/CoreBasic", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Headers", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Resources", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/A/Resources/Info.plist", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/CoreBasic", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Headers", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Resources", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products/Debug/CoreBasic.framework/Versions/Current", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/CoreBasic-all-non-framework-target-headers.hmap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/CoreBasic-all-target-headers.hmap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/CoreBasic-generated-files.hmap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/CoreBasic-own-target-headers.hmap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/CoreBasic-project-headers.hmap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/CoreBasic.hmap", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/Objects-normal/x86_64/CoreBasic.LinkFileList", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates/basic.build/Debug/CoreBasic.build/all-product-headers.yaml"]
+      roots: ["/tmp/basic.dst", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Intermediates", "/var/folders/1f/r6txd12925s9kytl1_pckt_w0000gp/T/Tests/Build/Products"]
+)END";
+  }
+
+  MockBuildSystemDelegate delegate(/*trackAllMessages=*/true);
+  BuildSystem system(delegate);
+  system.attachDB(builddb.c_str(), nullptr);
+  bool loadingResult = system.loadDescription(manifest);
+  ASSERT_TRUE(loadingResult);
+  auto result = system.build(keyToBuild);
+
+  ASSERT_TRUE(result.getValue().isStaleFileRemoval());
+  ASSERT_EQ(result.getValue().getStaleFileList().size(), 22UL);
+
+  // Check that `LinkFileList` is present in list of files of second build
+  bool hasLinkFileList = false;
+  for (auto staleFile : result.getValue().getStaleFileList()) {
+    if (staleFile == linkFileList) {
+      hasLinkFileList = true;
+    }
+  }
+  ASSERT_TRUE(hasLinkFileList);
+
+  // Check that `LinkFileList` is not present in the diff
+  std::vector<std::string> messages = delegate.getMessages();
+  ASSERT_FALSE(std::find(messages.begin(), messages.end(), "cannot remove stale file '" + linkFileList + "': No such file or directory") != messages.end());
+}
+
+TEST(BuildSystemTaskTests, staleFileRemovalPathIsPrefixedByPath) {
+  ASSERT_TRUE(pathIsPrefixedByPath("/foo/bar", "/foo"));
+  ASSERT_TRUE(pathIsPrefixedByPath("/foo", "/foo"));
+  ASSERT_TRUE(pathIsPrefixedByPath("/foo/", "/foo"));
+  ASSERT_TRUE(pathIsPrefixedByPath("/foo", "/foo/"));
+
+  ASSERT_FALSE(pathIsPrefixedByPath("/bar", "/foo"));
+  ASSERT_FALSE(pathIsPrefixedByPath("/foobar", "/foo"));
+}
+
 }
diff --git a/unittests/BuildSystem/MockBuildSystemDelegate.h b/unittests/BuildSystem/MockBuildSystemDelegate.h
index b9b4f98..0c02417 100644
--- a/unittests/BuildSystem/MockBuildSystemDelegate.h
+++ b/unittests/BuildSystem/MockBuildSystemDelegate.h
@@ -123,6 +123,28 @@
     }
   }
 
+  virtual void commandHadError(Command* command, StringRef data) {
+    llvm::errs() << "error: " << command->getName() << ": " << data.str() << "\n";
+    {
+      std::unique_lock<std::mutex> lock(messagesMutex);
+      messages.push_back(data.str());
+    }
+  }
+
+  virtual void commandHadNote(Command* command, StringRef data) {
+    if (trackAllMessages) {
+      std::unique_lock<std::mutex> lock(messagesMutex);
+      messages.push_back(("commandNote(" + command->getName() + ") " + data).str());
+    }
+  }
+
+  virtual void commandHadWarning(Command* command, StringRef data) {
+    if (trackAllMessages) {
+      std::unique_lock<std::mutex> lock(messagesMutex);
+      messages.push_back(("commandWarning(" + command->getName() + ") " + data).str());
+    }
+  }
+
   virtual void commandFinished(Command* command, CommandResult result) {
     if (trackAllMessages) {
       std::unique_lock<std::mutex> lock(messagesMutex);