
We've observed this test being significantly flaky on our Mac CI machines when we're running the full check-clang suite. It fails because the wait_for condition isn't met within 3 seconds. We believe it's because our CI machines are somewhat underpowered and pretty heavily loaded when we're running the full check-clang suite. I ran some experiments on increasing the timeout. I ran the full check-clang suite 100 times with each timeout value and recorded how many flaky failures we encountered in these tests. The results are: 3 second timeout (baseline): 20 failures 10 second timeout: 14 failures 20 second timeout: 4 failures 30 second timeout: 2 failures 40 second timeout: 1 failure 50 second timeout: 0 failures 60 second timeout: 0 failures I ran another set of 100 tests for the 50 second timeout and observed one flaky failure. By contrast, I ended up running check-clang 500 times for the 60 second timeout and didn't observe a single flaky failure. That's how the 60 second timeout value used in this patch was derived. While a 60 second timeout might seem high, keep in mind that: - This is a timeout, not a sleep; the test should require much less time the vast majority of instances, especially on more powerful machines. - The long timeout is most likely to occur when other tests are also running at the same time, so the latency of the timeout will also be masked by the latency of the other tests. See https://reviews.llvm.org/D58418?id=200123#inline-554211 for where this timeout was originally introduced and the possibility of raising it if it wasn't enough was discussed. Reviewed By: plotfi Differential Revision: https://reviews.llvm.org/D97878
502 lines
16 KiB
C++
502 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;
|
|
|
|
// We've observed this test being significantly flaky when running on a heavily
|
|
// loaded machine (e.g. when it's being run as part of the full check-clang
|
|
// suite). Set a high timeout value to avoid this flakiness. The 60s timeout
|
|
// value was determined empirically. It's a timeout value, not a sleep value,
|
|
// and the test should require much less time in practice the vast majority of
|
|
// instances. The cases where we do come close to (or still end up hitting) the
|
|
// longer timeout are most likely to occur when other tests are also running at
|
|
// the same time (e.g. as part of the full check-clang suite), in which case the
|
|
// latency of the timeout will be masked by the latency of the other tests.
|
|
constexpr std::chrono::seconds EventualResultTimeout(60);
|
|
|
|
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(EventualResultTimeout) ==
|
|
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);
|
|
}
|