491 lines
16 KiB
C++
491 lines
16 KiB
C++
//===- 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 <condition_variable>
|
|
#include <future>
|
|
#include <mutex>
|
|
#include <thread>
|
|
|
|
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<int>(lhs.Kind) == static_cast<int>(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 = std::string(pathBuf.str());
|
|
path::append(pathBuf, "watch");
|
|
TestWatchedDir = std::string(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<file_t> 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<DirectoryWatcher::Event> ExpectedInitial;
|
|
const std::vector<DirectoryWatcher::Event> ExpectedInitialCopy;
|
|
std::vector<DirectoryWatcher::Event> ExpectedNonInitial;
|
|
const std::vector<DirectoryWatcher::Event> ExpectedNonInitialCopy;
|
|
std::vector<DirectoryWatcher::Event> OptionalNonInitial;
|
|
std::vector<DirectoryWatcher::Event> UnexpectedInitial;
|
|
std::vector<DirectoryWatcher::Event> UnexpectedNonInitial;
|
|
std::mutex Mtx;
|
|
std::condition_variable ResultIsReady;
|
|
|
|
VerifyingConsumer(
|
|
const std::vector<DirectoryWatcher::Event> &ExpectedInitial,
|
|
const std::vector<DirectoryWatcher::Event> &ExpectedNonInitial,
|
|
const std::vector<DirectoryWatcher::Event> &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<std::mutex> 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<std::mutex> 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<DirectoryWatcher::Event> Es, bool IsInitial) {
|
|
for (const auto &E : Es)
|
|
consume(E, IsInitial);
|
|
}
|
|
|
|
// Not locking - caller has to lock Mtx.
|
|
llvm::Optional<bool> 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<std::mutex> 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<int(void)> task(
|
|
[&TestConsumer]() { return TestConsumer.blockUntilResult(); });
|
|
std::future<int> 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<std::mutex> 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<std::unique_ptr<DirectoryWatcher>> DW =
|
|
DirectoryWatcher::create(
|
|
fixture.TestWatchedDir,
|
|
[&TestConsumer](llvm::ArrayRef<DirectoryWatcher::Event> 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<std::unique_ptr<DirectoryWatcher>> DW =
|
|
DirectoryWatcher::create(
|
|
fixture.TestWatchedDir,
|
|
[&TestConsumer](llvm::ArrayRef<DirectoryWatcher::Event> 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<std::unique_ptr<DirectoryWatcher>> DW =
|
|
DirectoryWatcher::create(
|
|
fixture.TestWatchedDir,
|
|
[&TestConsumer](llvm::ArrayRef<DirectoryWatcher::Event> 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<std::unique_ptr<DirectoryWatcher>> DW =
|
|
DirectoryWatcher::create(
|
|
fixture.TestWatchedDir,
|
|
[&TestConsumer](llvm::ArrayRef<DirectoryWatcher::Event> 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<std::unique_ptr<DirectoryWatcher>> DW =
|
|
DirectoryWatcher::create(
|
|
fixture.TestWatchedDir,
|
|
[&TestConsumer](llvm::ArrayRef<DirectoryWatcher::Event> 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<std::unique_ptr<DirectoryWatcher>> DW =
|
|
DirectoryWatcher::create(
|
|
fixture.TestWatchedDir,
|
|
[&TestConsumer](llvm::ArrayRef<DirectoryWatcher::Event> 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<std::unique_ptr<DirectoryWatcher>> DW =
|
|
DirectoryWatcher::create(
|
|
fixture.TestWatchedDir,
|
|
[&TestConsumer](llvm::ArrayRef<DirectoryWatcher::Event> Events,
|
|
bool IsInitial) {
|
|
TestConsumer.consume(Events, IsInitial);
|
|
},
|
|
/*waitForInitialSync=*/true);
|
|
ASSERT_THAT_ERROR(DW.takeError(), Succeeded());
|
|
} // DW is destructed here.
|
|
|
|
checkEventualResultWithTimeout(TestConsumer);
|
|
}
|
|
|
|
TEST(DirectoryWatcherTest, InvalidatedWatcherAsync) {
|
|
DirectoryWatcherTestFixture fixture;
|
|
fixture.addFile("a");
|
|
|
|
// This test is checking that we get the initial notification for 'a' before
|
|
// the final 'invalidated'. Strictly speaking, we do not care whether 'a' is
|
|
// processed or not, only that it is neither racing with, nor after
|
|
// 'invalidated'. In practice, it is always processed in our implementations.
|
|
VerifyingConsumer TestConsumer{
|
|
{{EventKind::Modified, "a"}},
|
|
{{EventKind::WatcherGotInvalidated, ""}},
|
|
// 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"}},
|
|
};
|
|
|
|
// A counter that can help detect data races on the event receiver,
|
|
// particularly if used with TSan. Expected count will be 2 or 3 depending on
|
|
// whether we get the kernel event or not (see comment above).
|
|
unsigned Count = 0;
|
|
{
|
|
llvm::Expected<std::unique_ptr<DirectoryWatcher>> DW =
|
|
DirectoryWatcher::create(
|
|
fixture.TestWatchedDir,
|
|
[&TestConsumer,
|
|
&Count](llvm::ArrayRef<DirectoryWatcher::Event> Events,
|
|
bool IsInitial) {
|
|
Count += 1;
|
|
TestConsumer.consume(Events, IsInitial);
|
|
},
|
|
/*waitForInitialSync=*/false);
|
|
ASSERT_THAT_ERROR(DW.takeError(), Succeeded());
|
|
} // DW is destructed here.
|
|
|
|
checkEventualResultWithTimeout(TestConsumer);
|
|
ASSERT_TRUE(Count == 2u || Count == 3u);
|
|
}
|