//===- unittests/DirectoryWatcher/DirectoryWatcherTest.cpp ----------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "clang/DirectoryWatcher/DirectoryWatcher.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/Path.h" #include "llvm/Support/raw_ostream.h" #include "llvm/Testing/Support/Error.h" #include "gtest/gtest.h" #include #include #include #include using namespace llvm; using namespace llvm::sys; using namespace llvm::sys::fs; using namespace clang; namespace clang { static bool operator==(const DirectoryWatcher::Event &lhs, const DirectoryWatcher::Event &rhs) { return lhs.Filename == rhs.Filename && static_cast(lhs.Kind) == static_cast(rhs.Kind); } } // namespace clang namespace { typedef DirectoryWatcher::Event::EventKind EventKind; struct DirectoryWatcherTestFixture { std::string TestRootDir; std::string TestWatchedDir; DirectoryWatcherTestFixture() { SmallString<128> pathBuf; #ifndef NDEBUG std::error_code UniqDirRes = #endif createUniqueDirectory("dirwatcher", pathBuf); assert(!UniqDirRes); TestRootDir = pathBuf.str(); path::append(pathBuf, "watch"); TestWatchedDir = pathBuf.str(); #ifndef NDEBUG std::error_code CreateDirRes = #endif create_directory(TestWatchedDir, false); assert(!CreateDirRes); } ~DirectoryWatcherTestFixture() { remove_directories(TestRootDir); } SmallString<128> getPathInWatched(const std::string &testFile) { SmallString<128> pathBuf; pathBuf = TestWatchedDir; path::append(pathBuf, testFile); return pathBuf; } void addFile(const std::string &testFile) { Expected ft = openNativeFileForWrite(getPathInWatched(testFile), CD_CreateNew, OF_None); if (ft) { closeFile(*ft); } else { llvm::errs() << llvm::toString(ft.takeError()) << "\n"; llvm::errs() << getPathInWatched(testFile) << "\n"; llvm_unreachable("Couldn't create test file."); } } void deleteFile(const std::string &testFile) { std::error_code EC = remove(getPathInWatched(testFile), /*IgnoreNonExisting=*/false); ASSERT_FALSE(EC); } }; std::string eventKindToString(const EventKind K) { switch (K) { case EventKind::Removed: return "Removed"; case EventKind::Modified: return "Modified"; case EventKind::WatchedDirRemoved: return "WatchedDirRemoved"; case EventKind::WatcherGotInvalidated: return "WatcherGotInvalidated"; } llvm_unreachable("unknown event kind"); } struct VerifyingConsumer { std::vector ExpectedInitial; const std::vector ExpectedInitialCopy; std::vector ExpectedNonInitial; const std::vector ExpectedNonInitialCopy; std::vector OptionalNonInitial; std::vector UnexpectedInitial; std::vector UnexpectedNonInitial; std::mutex Mtx; std::condition_variable ResultIsReady; VerifyingConsumer( const std::vector &ExpectedInitial, const std::vector &ExpectedNonInitial, const std::vector &OptionalNonInitial = {}) : ExpectedInitial(ExpectedInitial), ExpectedInitialCopy(ExpectedInitial), ExpectedNonInitial(ExpectedNonInitial), ExpectedNonInitialCopy(ExpectedNonInitial), OptionalNonInitial(OptionalNonInitial) {} // This method is used by DirectoryWatcher. void consume(DirectoryWatcher::Event E, bool IsInitial) { if (IsInitial) consumeInitial(E); else consumeNonInitial(E); } void consumeInitial(DirectoryWatcher::Event E) { std::unique_lock L(Mtx); auto It = std::find(ExpectedInitial.begin(), ExpectedInitial.end(), E); if (It == ExpectedInitial.end()) { UnexpectedInitial.push_back(E); } else { ExpectedInitial.erase(It); } if (result()) { L.unlock(); ResultIsReady.notify_one(); } } void consumeNonInitial(DirectoryWatcher::Event E) { std::unique_lock L(Mtx); auto It = std::find(ExpectedNonInitial.begin(), ExpectedNonInitial.end(), E); if (It == ExpectedNonInitial.end()) { auto OptIt = std::find(OptionalNonInitial.begin(), OptionalNonInitial.end(), E); if (OptIt != OptionalNonInitial.end()) { OptionalNonInitial.erase(OptIt); } else { UnexpectedNonInitial.push_back(E); } } else { ExpectedNonInitial.erase(It); } if (result()) { L.unlock(); ResultIsReady.notify_one(); } } // This method is used by DirectoryWatcher. void consume(llvm::ArrayRef Es, bool IsInitial) { for (const auto &E : Es) consume(E, IsInitial); } // Not locking - caller has to lock Mtx. llvm::Optional result() const { if (ExpectedInitial.empty() && ExpectedNonInitial.empty() && UnexpectedInitial.empty() && UnexpectedNonInitial.empty()) return true; if (!UnexpectedInitial.empty() || !UnexpectedNonInitial.empty()) return false; return llvm::None; } // This method is used by tests. // \returns true on success bool blockUntilResult() { std::unique_lock L(Mtx); while (true) { if (result()) return *result(); ResultIsReady.wait(L, [this]() { return result().hasValue(); }); } return false; // Just to make compiler happy. } void printUnmetExpectations(llvm::raw_ostream &OS) { // If there was any issue, print the expected state if ( !ExpectedInitial.empty() || !ExpectedNonInitial.empty() || !UnexpectedInitial.empty() || !UnexpectedNonInitial.empty() ) { OS << "Expected initial events: \n"; for (const auto &E : ExpectedInitialCopy) { OS << eventKindToString(E.Kind) << " " << E.Filename << "\n"; } OS << "Expected non-initial events: \n"; for (const auto &E : ExpectedNonInitialCopy) { OS << eventKindToString(E.Kind) << " " << E.Filename << "\n"; } } if (!ExpectedInitial.empty()) { OS << "Expected but not seen initial events: \n"; for (const auto &E : ExpectedInitial) { OS << eventKindToString(E.Kind) << " " << E.Filename << "\n"; } } if (!ExpectedNonInitial.empty()) { OS << "Expected but not seen non-initial events: \n"; for (const auto &E : ExpectedNonInitial) { OS << eventKindToString(E.Kind) << " " << E.Filename << "\n"; } } if (!UnexpectedInitial.empty()) { OS << "Unexpected initial events seen: \n"; for (const auto &E : UnexpectedInitial) { OS << eventKindToString(E.Kind) << " " << E.Filename << "\n"; } } if (!UnexpectedNonInitial.empty()) { OS << "Unexpected non-initial events seen: \n"; for (const auto &E : UnexpectedNonInitial) { OS << eventKindToString(E.Kind) << " " << E.Filename << "\n"; } } } }; void checkEventualResultWithTimeout(VerifyingConsumer &TestConsumer) { std::packaged_task task( [&TestConsumer]() { return TestConsumer.blockUntilResult(); }); std::future WaitForExpectedStateResult = task.get_future(); std::thread worker(std::move(task)); worker.detach(); EXPECT_TRUE(WaitForExpectedStateResult.wait_for(std::chrono::seconds(3)) == std::future_status::ready) << "The expected result state wasn't reached before the time-out."; std::unique_lock L(TestConsumer.Mtx); EXPECT_TRUE(TestConsumer.result().hasValue()); if (TestConsumer.result().hasValue()) { EXPECT_TRUE(*TestConsumer.result()); } if ((TestConsumer.result().hasValue() && !TestConsumer.result().getValue()) || !TestConsumer.result().hasValue()) TestConsumer.printUnmetExpectations(llvm::outs()); } } // namespace TEST(DirectoryWatcherTest, InitialScanSync) { DirectoryWatcherTestFixture fixture; fixture.addFile("a"); fixture.addFile("b"); fixture.addFile("c"); VerifyingConsumer TestConsumer{ {{EventKind::Modified, "a"}, {EventKind::Modified, "b"}, {EventKind::Modified, "c"}}, {}, // We have to ignore these as it's a race between the test process // which is scanning the directory and kernel which is sending // notification. {{EventKind::Modified, "a"}, {EventKind::Modified, "b"}, {EventKind::Modified, "c"}} }; llvm::Expected> DW = DirectoryWatcher::create( fixture.TestWatchedDir, [&TestConsumer](llvm::ArrayRef Events, bool IsInitial) { TestConsumer.consume(Events, IsInitial); }, /*waitForInitialSync=*/true); ASSERT_THAT_ERROR(DW.takeError(), Succeeded()); checkEventualResultWithTimeout(TestConsumer); } TEST(DirectoryWatcherTest, InitialScanAsync) { DirectoryWatcherTestFixture fixture; fixture.addFile("a"); fixture.addFile("b"); fixture.addFile("c"); VerifyingConsumer TestConsumer{ {{EventKind::Modified, "a"}, {EventKind::Modified, "b"}, {EventKind::Modified, "c"}}, {}, // We have to ignore these as it's a race between the test process // which is scanning the directory and kernel which is sending // notification. {{EventKind::Modified, "a"}, {EventKind::Modified, "b"}, {EventKind::Modified, "c"}} }; llvm::Expected> DW = DirectoryWatcher::create( fixture.TestWatchedDir, [&TestConsumer](llvm::ArrayRef Events, bool IsInitial) { TestConsumer.consume(Events, IsInitial); }, /*waitForInitialSync=*/false); ASSERT_THAT_ERROR(DW.takeError(), Succeeded()); checkEventualResultWithTimeout(TestConsumer); } TEST(DirectoryWatcherTest, AddFiles) { DirectoryWatcherTestFixture fixture; VerifyingConsumer TestConsumer{ {}, {{EventKind::Modified, "a"}, {EventKind::Modified, "b"}, {EventKind::Modified, "c"}}}; llvm::Expected> DW = DirectoryWatcher::create( fixture.TestWatchedDir, [&TestConsumer](llvm::ArrayRef Events, bool IsInitial) { TestConsumer.consume(Events, IsInitial); }, /*waitForInitialSync=*/true); ASSERT_THAT_ERROR(DW.takeError(), Succeeded()); fixture.addFile("a"); fixture.addFile("b"); fixture.addFile("c"); checkEventualResultWithTimeout(TestConsumer); } TEST(DirectoryWatcherTest, ModifyFile) { DirectoryWatcherTestFixture fixture; fixture.addFile("a"); VerifyingConsumer TestConsumer{ {{EventKind::Modified, "a"}}, {{EventKind::Modified, "a"}}, {{EventKind::Modified, "a"}}}; llvm::Expected> DW = DirectoryWatcher::create( fixture.TestWatchedDir, [&TestConsumer](llvm::ArrayRef Events, bool IsInitial) { TestConsumer.consume(Events, IsInitial); }, /*waitForInitialSync=*/true); ASSERT_THAT_ERROR(DW.takeError(), Succeeded()); // modify the file { std::error_code error; llvm::raw_fd_ostream bStream(fixture.getPathInWatched("a"), error, CD_OpenExisting); assert(!error); bStream << "foo"; } checkEventualResultWithTimeout(TestConsumer); } TEST(DirectoryWatcherTest, DeleteFile) { DirectoryWatcherTestFixture fixture; fixture.addFile("a"); VerifyingConsumer TestConsumer{ {{EventKind::Modified, "a"}}, {{EventKind::Removed, "a"}}, {{EventKind::Modified, "a"}, {EventKind::Removed, "a"}}}; llvm::Expected> DW = DirectoryWatcher::create( fixture.TestWatchedDir, [&TestConsumer](llvm::ArrayRef Events, bool IsInitial) { TestConsumer.consume(Events, IsInitial); }, /*waitForInitialSync=*/true); ASSERT_THAT_ERROR(DW.takeError(), Succeeded()); fixture.deleteFile("a"); checkEventualResultWithTimeout(TestConsumer); } TEST(DirectoryWatcherTest, DeleteWatchedDir) { DirectoryWatcherTestFixture fixture; VerifyingConsumer TestConsumer{ {}, {{EventKind::WatchedDirRemoved, ""}, {EventKind::WatcherGotInvalidated, ""}}}; llvm::Expected> DW = DirectoryWatcher::create( fixture.TestWatchedDir, [&TestConsumer](llvm::ArrayRef Events, bool IsInitial) { TestConsumer.consume(Events, IsInitial); }, /*waitForInitialSync=*/true); ASSERT_THAT_ERROR(DW.takeError(), Succeeded()); remove_directories(fixture.TestWatchedDir); checkEventualResultWithTimeout(TestConsumer); } TEST(DirectoryWatcherTest, InvalidatedWatcher) { DirectoryWatcherTestFixture fixture; VerifyingConsumer TestConsumer{ {}, {{EventKind::WatcherGotInvalidated, ""}}}; { llvm::Expected> DW = DirectoryWatcher::create( fixture.TestWatchedDir, [&TestConsumer](llvm::ArrayRef Events, bool IsInitial) { TestConsumer.consume(Events, IsInitial); }, /*waitForInitialSync=*/true); ASSERT_THAT_ERROR(DW.takeError(), Succeeded()); } // DW is destructed here. checkEventualResultWithTimeout(TestConsumer); }