#include "Annotations.h"
#include "ClangdServer.h"
#include "Compiler.h"
#include "Config.h"
#include "Diagnostics.h"
#include "GlobalCompilationDatabase.h"
#include "Matchers.h"
#include "ParsedAST.h"
#include "Preamble.h"
#include "TUScheduler.h"
#include "TestFS.h"
#include "TestIndex.h"
#include "clang-include-cleaner/Record.h"
#include "support/Cancellation.h"
#include "support/Context.h"
#include "support/Path.h"
#include "support/TestTracer.h"
#include "support/Threading.h"
#include "clang/Basic/DiagnosticDriver.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/FunctionExtras.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace clang {
namespace clangd {
namespace {
using ::testing::AllOf;
using ::testing::AnyOf;
using ::testing::Contains;
using ::testing::Each;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::Not;
using ::testing::Pair;
using ::testing::Pointee;
using ::testing::SizeIs;
using ::testing::UnorderedElementsAre;
MATCHER_P2(TUState, PreambleActivity, ASTActivity, "") {
if (arg.PreambleActivity != PreambleActivity) {
*result_listener << "preamblestate is "
<< static_cast<uint8_t>(arg.PreambleActivity);
return false;
}
if (arg.ASTActivity.K != ASTActivity) {
*result_listener << "aststate is " << arg.ASTActivity.K;
return false;
}
return true;
}
static Key<std::string> BoundPath;
Context bindPath(PathRef F) {
return Context::current().derive(BoundPath, F.str());
}
llvm::StringRef boundPath() {
const std::string *V = Context::current().get(BoundPath);
return V ? *V : llvm::StringRef("");
}
TUScheduler::Options optsForTest() {
TUScheduler::Options Opts(ClangdServer::optsForTest());
Opts.ContextProvider = bindPath;
return Opts;
}
class TUSchedulerTests : public ::testing::Test {
protected:
ParseInputs getInputs(PathRef File, std::string Contents) {
ParseInputs Inputs;
Inputs.CompileCommand = *CDB.getCompileCommand(File);
Inputs.TFS = &FS;
Inputs.Contents = std::move(Contents);
Inputs.Opts = ParseOptions();
return Inputs;
}
void updateWithCallback(TUScheduler &S, PathRef File,
llvm::StringRef Contents, WantDiagnostics WD,
llvm::unique_function<void()> CB) {
updateWithCallback(S, File, getInputs(File, std::string(Contents)), WD,
std::move(CB));
}
void updateWithCallback(TUScheduler &S, PathRef File, ParseInputs Inputs,
WantDiagnostics WD,
llvm::unique_function<void()> CB) {
WithContextValue Ctx(llvm::make_scope_exit(std::move(CB)));
S.update(File, Inputs, WD);
}
static Key<llvm::unique_function<void(PathRef File, std::vector<Diag>)>>
DiagsCallbackKey;
static std::unique_ptr<ParsingCallbacks> captureDiags() {
class CaptureDiags : public ParsingCallbacks {
public:
void onMainAST(PathRef File, ParsedAST &AST, PublishFn Publish) override {
reportDiagnostics(File, AST.getDiagnostics(), Publish);
}
void onFailedAST(PathRef File, llvm::StringRef Version,
std::vector<Diag> Diags, PublishFn Publish) override {
reportDiagnostics(File, Diags, Publish);
}
private:
void reportDiagnostics(PathRef File, llvm::ArrayRef<Diag> Diags,
PublishFn Publish) {
auto *D = Context::current().get(DiagsCallbackKey);
if (!D)
return;
Publish([&]() {
const_cast<llvm::unique_function<void(PathRef, std::vector<Diag>)> &>(
*D)(File, Diags);
});
}
};
return std::make_unique<CaptureDiags>();
}
void updateWithDiags(TUScheduler &S, PathRef File, ParseInputs Inputs,
WantDiagnostics WD,
llvm::unique_function<void(std::vector<Diag>)> CB) {
Path OrigFile = File.str();
WithContextValue Ctx(DiagsCallbackKey,
[OrigFile, CB = std::move(CB)](
PathRef File, std::vector<Diag> Diags) mutable {
assert(File == OrigFile);
CB(std::move(Diags));
});
S.update(File, std::move(Inputs), WD);
}
void updateWithDiags(TUScheduler &S, PathRef File, llvm::StringRef Contents,
WantDiagnostics WD,
llvm::unique_function<void(std::vector<Diag>)> CB) {
return updateWithDiags(S, File, getInputs(File, std::string(Contents)), WD,
std::move(CB));
}
MockFS FS;
MockCompilationDatabase CDB;
};
Key<llvm::unique_function<void(PathRef File, std::vector<Diag>)>>
TUSchedulerTests::DiagsCallbackKey;
TEST_F(TUSchedulerTests, MissingFiles) {
TUScheduler S(CDB, optsForTest());
auto Added = testPath("added.cpp");
FS.Files[Added] = "x";
auto Missing = testPath("missing.cpp");
FS.Files[Missing] = "";
S.update(Added, getInputs(Added, "x"), WantDiagnostics::No);
S.runWithAST("", Missing,
[&](Expected<InputsAndAST> AST) { EXPECT_ERROR(AST); });
S.runWithPreamble(
"", Missing, TUScheduler::Stale,
[&](Expected<InputsAndPreamble> Preamble) { EXPECT_ERROR(Preamble); });
S.remove(Missing);
S.runWithAST("", Added,
[&](Expected<InputsAndAST> AST) { EXPECT_TRUE(bool(AST)); });
S.runWithPreamble("", Added, TUScheduler::Stale,
[&](Expected<InputsAndPreamble> Preamble) {
EXPECT_TRUE(bool(Preamble));
});
S.remove(Added);
S.runWithAST("", Added,
[&](Expected<InputsAndAST> AST) { EXPECT_ERROR(AST); });
S.runWithPreamble("", Added, TUScheduler::Stale,
[&](Expected<InputsAndPreamble> Preamble) {
ASSERT_FALSE(bool(Preamble));
llvm::consumeError(Preamble.takeError());
});
S.remove(Added);
}
TEST_F(TUSchedulerTests, WantDiagnostics) {
std::atomic<int> CallbackCount(0);
{
Notification Ready;
TUScheduler S(CDB, optsForTest(), captureDiags());
auto Path = testPath("foo.cpp");
updateWithDiags(S, Path, ";", WantDiagnostics::Yes,
[&](std::vector<Diag>) { Ready.wait(); });
updateWithDiags(S, Path, ";request diags", WantDiagnostics::Yes,
[&](std::vector<Diag>) { ++CallbackCount; });
updateWithDiags(S, Path, ";auto (clobbered)", WantDiagnostics::Auto,
[&](std::vector<Diag>) {
ADD_FAILURE()
<< "auto should have been cancelled by auto";
});
updateWithDiags(S, Path, ";request no diags", WantDiagnostics::No,
[&](std::vector<Diag>) {
ADD_FAILURE() << "no diags should not be called back";
});
updateWithDiags(S, Path, ";auto (produces)", WantDiagnostics::Auto,
[&](std::vector<Diag>) { ++CallbackCount; });
Ready.notify();
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
}
EXPECT_EQ(2, CallbackCount);
}
TEST_F(TUSchedulerTests, Debounce) {
auto Opts = optsForTest();
Opts.UpdateDebounce = DebouncePolicy::fixed(std::chrono::milliseconds(500));
TUScheduler S(CDB, Opts, captureDiags());
auto Path = testPath("foo.cpp");
updateWithDiags(S, Path, "auto (debounced)", WantDiagnostics::Auto,
[&](std::vector<Diag>) {
ADD_FAILURE()
<< "auto should have been debounced and canceled";
});
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Notification N;
updateWithDiags(S, Path, "auto (timed out)", WantDiagnostics::Auto,
[&](std::vector<Diag>) { N.notify(); });
EXPECT_TRUE(N.wait(timeoutSeconds(60)));
updateWithDiags(S, Path, "auto (discarded)", WantDiagnostics::Auto,
[&](std::vector<Diag>) {
ADD_FAILURE()
<< "auto should have been discarded (dead write)";
});
}
TEST_F(TUSchedulerTests, Cancellation) {
std::vector<StringRef> DiagsSeen, ReadsSeen, ReadsCanceled;
{
Notification Proceed;
TUScheduler S(CDB, optsForTest(), captureDiags());
auto Path = testPath("foo.cpp");
auto Update = [&](StringRef ID) -> Canceler {
auto T = cancelableTask();
WithContext C(std::move(T.first));
updateWithDiags(
S, Path, ("//" + ID).str(), WantDiagnostics::Yes,
[&, ID](std::vector<Diag> Diags) { DiagsSeen.push_back(ID); });
return std::move(T.second);
};
auto Read = [&](StringRef ID) -> Canceler {
auto T = cancelableTask();
WithContext C(std::move(T.first));
S.runWithAST(ID, Path, [&, ID](llvm::Expected<InputsAndAST> E) {
if (auto Err = E.takeError()) {
if (Err.isA<CancelledError>()) {
ReadsCanceled.push_back(ID);
consumeError(std::move(Err));
} else {
ADD_FAILURE() << "Non-cancelled error for " << ID << ": "
<< llvm::toString(std::move(Err));
}
} else {
ReadsSeen.push_back(ID);
}
});
return std::move(T.second);
};
updateWithCallback(S, Path, "", WantDiagnostics::Yes,
[&]() { Proceed.wait(); });
Update("U1")();
Read("R1")();
Update("U2")();
Read("R2A")();
Read("R2B");
Update("U3");
Read("R3")();
Proceed.notify();
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
}
EXPECT_THAT(DiagsSeen, ElementsAre("U2", "U3"))
<< "U1 and all dependent reads were cancelled. "
"U2 has a dependent read R2A. "
"U3 was not cancelled.";
EXPECT_THAT(ReadsSeen, ElementsAre("R2B"))
<< "All reads other than R2B were cancelled";
EXPECT_THAT(ReadsCanceled, ElementsAre("R1", "R2A", "R3"))
<< "All reads other than R2B were cancelled";
}
TEST_F(TUSchedulerTests, InvalidationNoCrash) {
auto Path = testPath("foo.cpp");
TUScheduler S(CDB, optsForTest(), captureDiags());
Notification StartedRunning;
Notification ScheduledChange;
S.update(Path, getInputs(Path, ""), WantDiagnostics::Auto);
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
S.runWithAST(
"invalidatable-but-running", Path,
[&](llvm::Expected<InputsAndAST> AST) {
StartedRunning.notify();
ScheduledChange.wait();
ASSERT_TRUE(bool(AST));
},
TUScheduler::InvalidateOnUpdate);
StartedRunning.wait();
S.update(Path, getInputs(Path, ""), WantDiagnostics::Auto);
ScheduledChange.notify();
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
}
TEST_F(TUSchedulerTests, Invalidation) {
auto Path = testPath("foo.cpp");
TUScheduler S(CDB, optsForTest(), captureDiags());
std::atomic<int> Builds(0), Actions(0);
Notification Start;
updateWithDiags(S, Path, "a", WantDiagnostics::Yes, [&](std::vector<Diag>) {
++Builds;
Start.wait();
});
S.runWithAST(
"invalidatable", Path,
[&](llvm::Expected<InputsAndAST> AST) {
++Actions;
EXPECT_FALSE(bool(AST));
llvm::Error E = AST.takeError();
EXPECT_TRUE(E.isA<CancelledError>());
handleAllErrors(std::move(E), [&](const CancelledError &E) {
EXPECT_EQ(E.Reason, static_cast<int>(ErrorCode::ContentModified));
});
},
TUScheduler::InvalidateOnUpdate);
S.runWithAST(
"not-invalidatable", Path,
[&](llvm::Expected<InputsAndAST> AST) {
++Actions;
EXPECT_TRUE(bool(AST));
},
TUScheduler::NoInvalidation);
updateWithDiags(S, Path, "b", WantDiagnostics::Auto, [&](std::vector<Diag>) {
++Builds;
ADD_FAILURE() << "Shouldn't build, all dependents invalidated";
});
S.runWithAST(
"invalidatable", Path,
[&](llvm::Expected<InputsAndAST> AST) {
++Actions;
EXPECT_FALSE(bool(AST));
llvm::Error E = AST.takeError();
EXPECT_TRUE(E.isA<CancelledError>());
consumeError(std::move(E));
},
TUScheduler::InvalidateOnUpdate);
updateWithDiags(S, Path, "c", WantDiagnostics::Auto,
[&](std::vector<Diag>) { ++Builds; });
S.runWithAST(
"invalidatable", Path,
[&](llvm::Expected<InputsAndAST> AST) {
++Actions;
EXPECT_TRUE(bool(AST)) << "Shouldn't be invalidated, no update follows";
},
TUScheduler::InvalidateOnUpdate);
Start.notify();
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_EQ(2, Builds.load()) << "Middle build should be skipped";
EXPECT_EQ(4, Actions.load()) << "All actions should run (some with error)";
}
TEST_F(TUSchedulerTests, InvalidationUnchanged) {
auto Path = testPath("foo.cpp");
TUScheduler S(CDB, optsForTest(), captureDiags());
std::atomic<int> Actions(0);
Notification Start;
updateWithDiags(S, Path, "a", WantDiagnostics::Yes, [&](std::vector<Diag>) {
Start.wait();
});
S.runWithAST(
"invalidatable", Path,
[&](llvm::Expected<InputsAndAST> AST) {
++Actions;
EXPECT_TRUE(bool(AST))
<< "Should not invalidate based on an update with same content: "
<< llvm::toString(AST.takeError());
},
TUScheduler::InvalidateOnUpdate);
updateWithDiags(S, Path, "a", WantDiagnostics::Yes, [&](std::vector<Diag>) {
ADD_FAILURE() << "Shouldn't build, identical to previous";
});
Start.notify();
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_EQ(1, Actions.load()) << "All actions should run";
}
TEST_F(TUSchedulerTests, ManyUpdates) {
const int FilesCount = 3;
const int UpdatesPerFile = 10;
std::mutex Mut;
int TotalASTReads = 0;
int TotalPreambleReads = 0;
int TotalUpdates = 0;
llvm::StringMap<int> LatestDiagVersion;
{
auto Opts = optsForTest();
Opts.UpdateDebounce = DebouncePolicy::fixed(std::chrono::milliseconds(50));
TUScheduler S(CDB, Opts, captureDiags());
std::vector<std::string> Files;
for (int I = 0; I < FilesCount; ++I) {
std::string Name = "foo" + std::to_string(I) + ".cpp";
Files.push_back(testPath(Name));
this->FS.Files[Files.back()] = "";
}
StringRef Contents1 = R"cpp(int a;)cpp";
StringRef Contents2 = R"cpp(int main() { return 1; })cpp";
StringRef Contents3 = R"cpp(int a; int b; int sum() { return a + b; })cpp";
StringRef AllContents[] = {Contents1, Contents2, Contents3};
const int AllContentsSize = 3;
static Key<int> NonceKey;
int Nonce = 0;
for (int FileI = 0; FileI < FilesCount; ++FileI) {
for (int UpdateI = 0; UpdateI < UpdatesPerFile; ++UpdateI) {
auto Contents = AllContents[(FileI + UpdateI) % AllContentsSize];
auto File = Files[FileI];
auto Inputs = getInputs(File, Contents.str());
{
WithContextValue WithNonce(NonceKey, ++Nonce);
Inputs.Version = std::to_string(UpdateI);
updateWithDiags(
S, File, Inputs, WantDiagnostics::Auto,
[File, Nonce, Version(Inputs.Version), &Mut, &TotalUpdates,
&LatestDiagVersion](std::vector<Diag>) {
EXPECT_THAT(Context::current().get(NonceKey), Pointee(Nonce));
EXPECT_EQ(File, boundPath());
std::lock_guard<std::mutex> Lock(Mut);
++TotalUpdates;
EXPECT_EQ(File, *TUScheduler::getFileBeingProcessedInContext());
auto It = LatestDiagVersion.try_emplace(File, -1);
const int PrevVersion = It.first->second;
int CurVersion;
ASSERT_TRUE(llvm::to_integer(Version, CurVersion, 10));
EXPECT_LT(PrevVersion, CurVersion);
It.first->getValue() = CurVersion;
});
}
{
WithContextValue WithNonce(NonceKey, ++Nonce);
S.runWithAST(
"CheckAST", File,
[File, Inputs, Nonce, &Mut,
&TotalASTReads](Expected<InputsAndAST> AST) {
EXPECT_THAT(Context::current().get(NonceKey), Pointee(Nonce));
EXPECT_EQ(File, boundPath());
ASSERT_TRUE((bool)AST);
EXPECT_EQ(AST->Inputs.Contents, Inputs.Contents);
EXPECT_EQ(AST->Inputs.Version, Inputs.Version);
EXPECT_EQ(AST->AST.version(), Inputs.Version);
std::lock_guard<std::mutex> Lock(Mut);
++TotalASTReads;
EXPECT_EQ(File, *TUScheduler::getFileBeingProcessedInContext());
});
}
{
WithContextValue WithNonce(NonceKey, ++Nonce);
S.runWithPreamble(
"CheckPreamble", File, TUScheduler::Stale,
[File, Inputs, Nonce, &Mut,
&TotalPreambleReads](Expected<InputsAndPreamble> Preamble) {
EXPECT_THAT(Context::current().get(NonceKey), Pointee(Nonce));
EXPECT_EQ(File, boundPath());
ASSERT_TRUE((bool)Preamble);
EXPECT_EQ(Preamble->Contents, Inputs.Contents);
std::lock_guard<std::mutex> Lock(Mut);
++TotalPreambleReads;
EXPECT_EQ(File, *TUScheduler::getFileBeingProcessedInContext());
});
}
}
}
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
}
std::lock_guard<std::mutex> Lock(Mut);
EXPECT_GE(TotalUpdates, FilesCount);
EXPECT_LE(TotalUpdates, FilesCount * UpdatesPerFile);
for (const auto &Entry : LatestDiagVersion)
EXPECT_EQ(Entry.second, UpdatesPerFile - 1);
EXPECT_EQ(TotalASTReads, FilesCount * UpdatesPerFile);
EXPECT_EQ(TotalPreambleReads, FilesCount * UpdatesPerFile);
}
TEST_F(TUSchedulerTests, EvictedAST) {
std::atomic<int> BuiltASTCounter(0);
auto Opts = optsForTest();
Opts.AsyncThreadsCount = 1;
Opts.RetentionPolicy.MaxRetainedASTs = 2;
trace::TestTracer Tracer;
TUScheduler S(CDB, Opts);
llvm::StringLiteral SourceContents = R"cpp(
int* a;
double* b = a;
)cpp";
llvm::StringLiteral OtherSourceContents = R"cpp(
int* a;
double* b = a + 0;
)cpp";
auto Foo = testPath("foo.cpp");
auto Bar = testPath("bar.cpp");
auto Baz = testPath("baz.cpp");
EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "hit"), SizeIs(0));
EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "miss"), SizeIs(0));
updateWithCallback(S, Foo, SourceContents, WantDiagnostics::Yes,
[&BuiltASTCounter]() { ++BuiltASTCounter; });
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
ASSERT_EQ(BuiltASTCounter.load(), 1);
EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "hit"), SizeIs(0));
EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "miss"), SizeIs(1));
updateWithCallback(S, Bar, SourceContents, WantDiagnostics::Yes,
[&BuiltASTCounter]() { ++BuiltASTCounter; });
updateWithCallback(S, Baz, SourceContents, WantDiagnostics::Yes,
[&BuiltASTCounter]() { ++BuiltASTCounter; });
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
ASSERT_EQ(BuiltASTCounter.load(), 3);
EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "hit"), SizeIs(0));
EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "miss"), SizeIs(2));
ASSERT_THAT(S.getFilesWithCachedAST(), UnorderedElementsAre(Bar, Baz));
updateWithCallback(S, Foo, OtherSourceContents, WantDiagnostics::Yes,
[&BuiltASTCounter]() { ++BuiltASTCounter; });
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
ASSERT_EQ(BuiltASTCounter.load(), 4);
EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "hit"), SizeIs(0));
EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "miss"), SizeIs(1));
EXPECT_THAT(S.getFilesWithCachedAST(),
UnorderedElementsAre(Foo, AnyOf(Bar, Baz)));
}
TEST_F(TUSchedulerTests, NoopChangesDontThrashCache) {
auto Opts = optsForTest();
Opts.RetentionPolicy.MaxRetainedASTs = 1;
TUScheduler S(CDB, Opts);
auto Foo = testPath("foo.cpp");
auto FooInputs = getInputs(Foo, "int x=1;");
auto Bar = testPath("bar.cpp");
auto BarInputs = getInputs(Bar, "int x=2;");
S.update(Foo, FooInputs, WantDiagnostics::Auto);
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
S.update(Bar, BarInputs, WantDiagnostics::Auto);
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
ASSERT_THAT(S.getFilesWithCachedAST(), ElementsAre(Bar));
S.update(Foo, FooInputs, WantDiagnostics::Auto);
S.update(Foo, FooInputs, WantDiagnostics::Auto);
S.update(Foo, FooInputs, WantDiagnostics::Auto);
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
ASSERT_THAT(S.getFilesWithCachedAST(), ElementsAre(Bar));
ASSERT_EQ(S.fileStats().lookup(Foo).ASTBuilds, 1u);
ASSERT_EQ(S.fileStats().lookup(Bar).ASTBuilds, 1u);
}
TEST_F(TUSchedulerTests, EmptyPreamble) {
TUScheduler S(CDB, optsForTest());
auto Foo = testPath("foo.cpp");
auto Header = testPath("foo.h");
FS.Files[Header] = "void foo()";
FS.Timestamps[Header] = time_t(0);
auto *WithPreamble = R"cpp(
#include "foo.h"
int main() {}
)cpp";
auto *WithEmptyPreamble = R"cpp(int main() {})cpp";
S.update(Foo, getInputs(Foo, WithPreamble), WantDiagnostics::Auto);
S.runWithPreamble(
"getNonEmptyPreamble", Foo, TUScheduler::Stale,
[&](Expected<InputsAndPreamble> Preamble) {
EXPECT_GT(
cantFail(std::move(Preamble)).Preamble->Preamble.getBounds().Size,
0u);
});
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
S.update(Foo, getInputs(Foo, WithEmptyPreamble), WantDiagnostics::Auto);
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
S.runWithPreamble(
"getEmptyPreamble", Foo, TUScheduler::Stale,
[&](Expected<InputsAndPreamble> Preamble) {
EXPECT_EQ(
cantFail(std::move(Preamble)).Preamble->Preamble.getBounds().Size,
0u);
});
}
TEST_F(TUSchedulerTests, ASTSignalsSmokeTests) {
TUScheduler S(CDB, optsForTest());
auto Foo = testPath("foo.cpp");
auto Header = testPath("foo.h");
FS.Files[Header] = "namespace tar { int foo(); }";
const char *Contents = R"cpp(
#include "foo.h"
namespace ns {
int func() {
return tar::foo());
}
} // namespace ns
)cpp";
S.update(Foo, getInputs(Foo, Contents), WantDiagnostics::Yes);
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
Notification TaskRun;
S.runWithPreamble(
"ASTSignals", Foo, TUScheduler::Stale,
[&](Expected<InputsAndPreamble> IP) {
ASSERT_FALSE(!IP);
std::vector<std::pair<StringRef, int>> NS;
for (const auto &P : IP->Signals->RelatedNamespaces)
NS.emplace_back(P.getKey(), P.getValue());
EXPECT_THAT(NS,
UnorderedElementsAre(Pair("ns::", 1), Pair("tar::", 1)));
std::vector<std::pair<SymbolID, int>> Sym;
for (const auto &P : IP->Signals->ReferencedSymbols)
Sym.emplace_back(P.getFirst(), P.getSecond());
EXPECT_THAT(Sym, UnorderedElementsAre(Pair(ns("tar").ID, 1),
Pair(ns("ns").ID, 1),
Pair(func("tar::foo").ID, 1),
Pair(func("ns::func").ID, 1)));
TaskRun.notify();
});
TaskRun.wait();
}
TEST_F(TUSchedulerTests, RunWaitsForPreamble) {
TUScheduler S(CDB, optsForTest());
auto Foo = testPath("foo.cpp");
auto *NonEmptyPreamble = R"cpp(
#define FOO 1
#define BAR 2
int main() {}
)cpp";
constexpr int ReadsToSchedule = 10;
std::mutex PreamblesMut;
std::vector<const void *> Preambles(ReadsToSchedule, nullptr);
S.update(Foo, getInputs(Foo, NonEmptyPreamble), WantDiagnostics::Auto);
for (int I = 0; I < ReadsToSchedule; ++I) {
S.runWithPreamble(
"test", Foo, TUScheduler::Stale,
[I, &PreamblesMut, &Preambles](Expected<InputsAndPreamble> IP) {
std::lock_guard<std::mutex> Lock(PreamblesMut);
Preambles[I] = cantFail(std::move(IP)).Preamble;
});
}
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
std::lock_guard<std::mutex> Lock(PreamblesMut);
ASSERT_NE(Preambles[0], nullptr);
ASSERT_THAT(Preambles, Each(Preambles[0]));
}
TEST_F(TUSchedulerTests, NoopOnEmptyChanges) {
TUScheduler S(CDB, optsForTest(), captureDiags());
auto Source = testPath("foo.cpp");
auto Header = testPath("foo.h");
FS.Files[Header] = "int a;";
FS.Timestamps[Header] = time_t(0);
std::string SourceContents = R"cpp(
#include "foo.h"
int b = a;
)cpp";
auto DoUpdate = [&](std::string Contents) -> bool {
std::atomic<bool> Updated(false);
Updated = false;
updateWithDiags(S, Source, Contents, WantDiagnostics::Yes,
[&Updated](std::vector<Diag>) { Updated = true; });
bool UpdateFinished = S.blockUntilIdle(timeoutSeconds(60));
if (!UpdateFinished)
ADD_FAILURE() << "Updated has not finished in one second. Threading bug?";
return Updated;
};
ASSERT_TRUE(DoUpdate(SourceContents));
ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 1u);
ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 1u);
ASSERT_FALSE(DoUpdate(SourceContents));
ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 1u);
ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 1u);
FS.Timestamps[Header] = time_t(1);
ASSERT_TRUE(DoUpdate(SourceContents));
ASSERT_FALSE(DoUpdate(SourceContents));
ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 2u);
ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 2u);
SourceContents += "\nint c = b;";
ASSERT_TRUE(DoUpdate(SourceContents));
ASSERT_FALSE(DoUpdate(SourceContents));
ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 3u);
ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 2u);
CDB.ExtraClangFlags.push_back("-DSOMETHING");
ASSERT_TRUE(DoUpdate(SourceContents));
ASSERT_FALSE(DoUpdate(SourceContents));
ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 5u);
ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 3u);
}
TEST_F(TUSchedulerTests, MissingHeader) {
CDB.ExtraClangFlags.push_back("-I" + testPath("a"));
CDB.ExtraClangFlags.push_back("-I" + testPath("b"));
FS.Files.try_emplace("a/__unused__");
FS.Files.try_emplace("b/__unused__");
TUScheduler S(CDB, optsForTest(), captureDiags());
auto Source = testPath("foo.cpp");
auto HeaderA = testPath("a/foo.h");
auto HeaderB = testPath("b/foo.h");
auto *SourceContents = R"cpp(
#include "foo.h"
int c = b;
)cpp";
ParseInputs Inputs = getInputs(Source, SourceContents);
std::atomic<size_t> DiagCount(0);
updateWithDiags(
S, Source, Inputs, WantDiagnostics::Yes,
[&DiagCount](std::vector<Diag> Diags) {
++DiagCount;
EXPECT_THAT(Diags,
ElementsAre(Field(&Diag::Message, "'foo.h' file not found"),
Field(&Diag::Message,
"use of undeclared identifier 'b'")));
});
S.blockUntilIdle(timeoutSeconds(60));
FS.Files[HeaderB] = "int b;";
FS.Timestamps[HeaderB] = time_t(1);
updateWithDiags(S, Source, Inputs, WantDiagnostics::Yes,
[&DiagCount](std::vector<Diag> Diags) {
++DiagCount;
EXPECT_THAT(Diags, IsEmpty());
});
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
FS.Files[HeaderA] = "int a;";
FS.Timestamps[HeaderA] = time_t(1);
updateWithDiags(S, Source, Inputs, WantDiagnostics::Yes,
[&DiagCount](std::vector<Diag> Diags) {
++DiagCount;
ADD_FAILURE()
<< "Didn't expect new diagnostics when adding a/foo.h";
});
Inputs.ForceRebuild = true;
updateWithDiags(
S, Source, Inputs, WantDiagnostics::Yes,
[&DiagCount](std::vector<Diag> Diags) {
++DiagCount;
ElementsAre(Field(&Diag::Message, "use of undeclared identifier 'b'"));
});
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_EQ(DiagCount, 3U);
}
TEST_F(TUSchedulerTests, NoChangeDiags) {
trace::TestTracer Tracer;
TUScheduler S(CDB, optsForTest(), captureDiags());
auto FooCpp = testPath("foo.cpp");
const auto *Contents = "int a; int b;";
EXPECT_THAT(Tracer.takeMetric("ast_access_read", "hit"), SizeIs(0));
EXPECT_THAT(Tracer.takeMetric("ast_access_read", "miss"), SizeIs(0));
EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "hit"), SizeIs(0));
EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "miss"), SizeIs(0));
updateWithDiags(
S, FooCpp, Contents, WantDiagnostics::No,
[](std::vector<Diag>) { ADD_FAILURE() << "Should not be called."; });
S.runWithAST("touchAST", FooCpp, [](Expected<InputsAndAST> IA) {
cantFail(std::move(IA));
});
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_THAT(Tracer.takeMetric("ast_access_read", "hit"), SizeIs(0));
EXPECT_THAT(Tracer.takeMetric("ast_access_read", "miss"), SizeIs(1));
std::atomic<bool> SeenDiags(false);
updateWithDiags(S, FooCpp, Contents, WantDiagnostics::Auto,
[&](std::vector<Diag>) { SeenDiags = true; });
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
ASSERT_TRUE(SeenDiags);
EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "hit"), SizeIs(1));
EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "miss"), SizeIs(0));
updateWithDiags(
S, FooCpp, Contents, WantDiagnostics::Auto,
[&](std::vector<Diag>) { ADD_FAILURE() << "Should not be called."; });
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
}
TEST_F(TUSchedulerTests, Run) {
for (bool Sync : {false, true}) {
auto Opts = optsForTest();
if (Sync)
Opts.AsyncThreadsCount = 0;
TUScheduler S(CDB, Opts);
std::atomic<int> Counter(0);
S.run("add 1", "", [&] { ++Counter; });
S.run("add 2", "", [&] { Counter += 2; });
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_EQ(Counter.load(), 3);
Notification TaskRun;
Key<int> TestKey;
WithContextValue CtxWithKey(TestKey, 10);
const char *Path = "somepath";
S.run("props context", Path, [&] {
EXPECT_EQ(Context::current().getExisting(TestKey), 10);
EXPECT_EQ(Path, boundPath());
TaskRun.notify();
});
TaskRun.wait();
}
}
TEST_F(TUSchedulerTests, TUStatus) {
class CaptureTUStatus : public ClangdServer::Callbacks {
public:
void onFileUpdated(PathRef File, const TUStatus &Status) override {
auto ASTAction = Status.ASTActivity.K;
auto PreambleAction = Status.PreambleActivity;
std::lock_guard<std::mutex> Lock(Mutex);
if (ASTActions.empty() || ASTActions.back() != ASTAction)
ASTActions.push_back(ASTAction);
if (PreambleActions.empty() || PreambleActions.back() != PreambleAction)
PreambleActions.push_back(PreambleAction);
}
std::vector<PreambleAction> preambleStatuses() {
std::lock_guard<std::mutex> Lock(Mutex);
return PreambleActions;
}
std::vector<ASTAction::Kind> astStatuses() {
std::lock_guard<std::mutex> Lock(Mutex);
return ASTActions;
}
private:
std::mutex Mutex;
std::vector<ASTAction::Kind> ASTActions;
std::vector<PreambleAction> PreambleActions;
} CaptureTUStatus;
MockFS FS;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &CaptureTUStatus);
Annotations Code("int m^ain () {}");
Server.addDocument(testPath("foo.cpp"), Code.code(), "1",
WantDiagnostics::Auto);
ASSERT_TRUE(Server.blockUntilIdleForTest());
Server.locateSymbolAt(testPath("foo.cpp"), Code.point(),
[](Expected<std::vector<LocatedSymbol>> Result) {
ASSERT_TRUE((bool)Result);
});
ASSERT_TRUE(Server.blockUntilIdleForTest());
EXPECT_THAT(CaptureTUStatus.preambleStatuses(),
ElementsAre(
PreambleAction::Idle,
PreambleAction::Building,
PreambleAction::Idle));
EXPECT_THAT(CaptureTUStatus.astStatuses(),
ElementsAre(
ASTAction::RunningAction,
ASTAction::Building,
ASTAction::Idle,
ASTAction::RunningAction,
ASTAction::Idle));
}
TEST_F(TUSchedulerTests, CommandLineErrors) {
CDB.ExtraClangFlags = {"-fsome-unknown-flag"};
Notification Ready;
TUScheduler S(CDB, optsForTest(), captureDiags());
std::vector<Diag> Diagnostics;
updateWithDiags(S, testPath("foo.cpp"), "void test() {}",
WantDiagnostics::Yes, [&](std::vector<Diag> D) {
Diagnostics = std::move(D);
Ready.notify();
});
Ready.wait();
EXPECT_THAT(
Diagnostics,
ElementsAre(AllOf(
Field(&Diag::ID, Eq(diag::err_drv_unknown_argument)),
Field(&Diag::Name, Eq("drv_unknown_argument")),
Field(&Diag::Message, "unknown argument: '-fsome-unknown-flag'"))));
}
TEST_F(TUSchedulerTests, CommandLineWarnings) {
CDB.ExtraClangFlags = {"-Wsome-unknown-warning"};
Notification Ready;
TUScheduler S(CDB, optsForTest(), captureDiags());
std::vector<Diag> Diagnostics;
updateWithDiags(S, testPath("foo.cpp"), "void test() {}",
WantDiagnostics::Yes, [&](std::vector<Diag> D) {
Diagnostics = std::move(D);
Ready.notify();
});
Ready.wait();
EXPECT_THAT(Diagnostics, IsEmpty());
}
TEST(DebouncePolicy, Compute) {
namespace c = std::chrono;
DebouncePolicy::clock::duration History[] = {
c::seconds(0),
c::seconds(5),
c::seconds(10),
c::seconds(20),
};
DebouncePolicy Policy;
Policy.Min = c::seconds(3);
Policy.Max = c::seconds(25);
auto Compute = [&](llvm::ArrayRef<DebouncePolicy::clock::duration> History) {
return c::duration_cast<c::duration<float, c::seconds::period>>(
Policy.compute(History))
.count();
};
EXPECT_NEAR(10, Compute(History), 0.01) << "(upper) median = 10";
Policy.RebuildRatio = 1.5;
EXPECT_NEAR(15, Compute(History), 0.01) << "median = 10, ratio = 1.5";
Policy.RebuildRatio = 3;
EXPECT_NEAR(25, Compute(History), 0.01) << "constrained by max";
Policy.RebuildRatio = 0;
EXPECT_NEAR(3, Compute(History), 0.01) << "constrained by min";
EXPECT_NEAR(25, Compute({}), 0.01) << "no history -> max";
}
TEST_F(TUSchedulerTests, AsyncPreambleThread) {
class BlockPreambleThread : public ParsingCallbacks {
public:
BlockPreambleThread(llvm::StringRef BlockVersion, Notification &N)
: BlockVersion(BlockVersion), N(N) {}
void onPreambleAST(
PathRef Path, llvm::StringRef Version, CapturedASTCtx,
std::shared_ptr<const include_cleaner::PragmaIncludes>) override {
if (Version == BlockVersion)
N.wait();
}
private:
llvm::StringRef BlockVersion;
Notification &N;
};
static constexpr llvm::StringLiteral InputsV0 = "v0";
static constexpr llvm::StringLiteral InputsV1 = "v1";
Notification Ready;
TUScheduler S(CDB, optsForTest(),
std::make_unique<BlockPreambleThread>(InputsV1, Ready));
Path File = testPath("foo.cpp");
auto PI = getInputs(File, "");
PI.Version = InputsV0.str();
S.update(File, PI, WantDiagnostics::Auto);
S.blockUntilIdle(timeoutSeconds(60));
PI.Version = InputsV1.str();
S.update(File, PI, WantDiagnostics::Auto);
Notification RunASTAction;
S.runWithAST("test", File, [&](Expected<InputsAndAST> AST) {
ASSERT_TRUE(bool(AST));
EXPECT_THAT(AST->AST.preambleVersion(), InputsV0);
EXPECT_THAT(AST->Inputs.Version, InputsV1.str());
RunASTAction.notify();
});
RunASTAction.wait();
Ready.notify();
}
TEST_F(TUSchedulerTests, OnlyPublishWhenPreambleIsBuilt) {
struct PreamblePublishCounter : public ParsingCallbacks {
PreamblePublishCounter(int &PreamblePublishCount)
: PreamblePublishCount(PreamblePublishCount) {}
void onPreamblePublished(PathRef File) override { ++PreamblePublishCount; }
int &PreamblePublishCount;
};
int PreamblePublishCount = 0;
TUScheduler S(CDB, optsForTest(),
std::make_unique<PreamblePublishCounter>(PreamblePublishCount));
Path File = testPath("foo.cpp");
S.update(File, getInputs(File, ""), WantDiagnostics::Auto);
S.blockUntilIdle(timeoutSeconds(60));
EXPECT_EQ(PreamblePublishCount, 1);
S.update(File, getInputs(File, ""), WantDiagnostics::Auto);
S.blockUntilIdle(timeoutSeconds(60));
EXPECT_EQ(PreamblePublishCount, 1);
S.update(File, getInputs(File, "#define FOO"), WantDiagnostics::Auto);
S.blockUntilIdle(timeoutSeconds(60));
EXPECT_EQ(PreamblePublishCount, 2);
}
TEST_F(TUSchedulerTests, PublishWithStalePreamble) {
class BlockPreambleThread : public ParsingCallbacks {
public:
using DiagsCB = std::function<void(ParsedAST &)>;
BlockPreambleThread(Notification &UnblockPreamble, DiagsCB CB)
: UnblockPreamble(UnblockPreamble), CB(std::move(CB)) {}
void onPreambleAST(
PathRef Path, llvm::StringRef Version, CapturedASTCtx,
std::shared_ptr<const include_cleaner::PragmaIncludes>) override {
if (BuildBefore)
ASSERT_TRUE(UnblockPreamble.wait(timeoutSeconds(60)))
<< "Expected notification";
BuildBefore = true;
}
void onMainAST(PathRef File, ParsedAST &AST, PublishFn Publish) override {
CB(AST);
}
void onFailedAST(PathRef File, llvm::StringRef Version,
std::vector<Diag> Diags, PublishFn Publish) override {
ADD_FAILURE() << "Received failed ast for: " << File << " with version "
<< Version << '\n';
}
private:
bool BuildBefore = false;
Notification &UnblockPreamble;
std::function<void(ParsedAST &)> CB;
};
class DiagCollector {
public:
void onDiagnostics(ParsedAST &AST) {
std::scoped_lock<std::mutex> Lock(DiagMu);
DiagVersions.emplace_back(
std::make_pair(AST.preambleVersion()->str(), AST.version().str()));
DiagsReceived.notify_all();
}
std::pair<std::string, std::string>
waitForNewDiags(TUScheduler &S, PathRef File, ParseInputs PI) {
std::unique_lock<std::mutex> Lock(DiagMu);
S.update(File, std::move(PI), WantDiagnostics::Auto);
size_t OldSize = DiagVersions.size();
bool ReceivedDiags = DiagsReceived.wait_for(
Lock, std::chrono::seconds(5),
[this, OldSize] { return OldSize + 1 == DiagVersions.size(); });
if (!ReceivedDiags) {
ADD_FAILURE() << "Timed out waiting for diags";
return {"invalid", "version"};
}
return DiagVersions.back();
}
std::vector<std::pair<std::string, std::string>> diagVersions() {
std::scoped_lock<std::mutex> Lock(DiagMu);
return DiagVersions;
}
private:
std::condition_variable DiagsReceived;
std::mutex DiagMu;
std::vector<std::pair</*PreambleVersion*/ std::string,
/*MainFileVersion*/ std::string>>
DiagVersions;
};
DiagCollector Collector;
Notification UnblockPreamble;
auto DiagCallbacks = std::make_unique<BlockPreambleThread>(
UnblockPreamble,
[&Collector](ParsedAST &AST) { Collector.onDiagnostics(AST); });
TUScheduler S(CDB, optsForTest(), std::move(DiagCallbacks));
Path File = testPath("foo.cpp");
auto BlockForDiags = [&](ParseInputs PI) {
return Collector.waitForNewDiags(S, File, std::move(PI));
};
auto PI = getInputs(File, "");
PI.Version = PI.Contents = "1";
ASSERT_THAT(BlockForDiags(PI), testing::Pair("1", "1"));
PI.Version = "2";
PI.Contents = "#define BAR\n" + PI.Version;
ASSERT_THAT(BlockForDiags(PI), testing::Pair("1", "2"));
PI.Version = "3";
PI.Contents = "#define FOO\n" + PI.Version;
ASSERT_THAT(BlockForDiags(PI), testing::Pair("1", "3"));
UnblockPreamble.notify();
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_THAT(Collector.diagVersions().back(), Pair(PI.Version, PI.Version));
PI.Version = "4";
PI.Contents = "#define FOO\n" + PI.Version;
S.update(File, PI, WantDiagnostics::No);
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_THAT(Collector.diagVersions().back(), Pair("3", "3"));
}
TEST_F(TUSchedulerTests, IncluderCache) {
static std::string Main = testPath("main.cpp"), Main2 = testPath("main2.cpp"),
Main3 = testPath("main3.cpp"),
NoCmd = testPath("no_cmd.h"),
Unreliable = testPath("unreliable.h"),
OK = testPath("ok.h"),
NotIncluded = testPath("not_included.h");
struct NoHeadersCDB : public GlobalCompilationDatabase {
std::optional<tooling::CompileCommand>
getCompileCommand(PathRef File) const override {
if (File == NoCmd || File == NotIncluded || FailAll)
return std::nullopt;
auto Basic = getFallbackCommand(File);
Basic.Heuristic.clear();
if (File == Unreliable) {
Basic.Heuristic = "not reliable";
} else if (File == Main) {
Basic.CommandLine.push_back("-DMAIN");
} else if (File == Main2) {
Basic.CommandLine.push_back("-DMAIN2");
} else if (File == Main3) {
Basic.CommandLine.push_back("-DMAIN3");
}
return Basic;
}
std::atomic<bool> FailAll{false};
} CDB;
TUScheduler S(CDB, optsForTest());
auto GetFlags = [&](PathRef Header) {
S.update(Header, getInputs(Header, ";"), WantDiagnostics::Yes);
EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
Notification CmdDone;
tooling::CompileCommand Cmd;
S.runWithPreamble("GetFlags", Header, TUScheduler::StaleOrAbsent,
[&](llvm::Expected<InputsAndPreamble> Inputs) {
ASSERT_FALSE(!Inputs) << Inputs.takeError();
Cmd = std::move(Inputs->Command);
CmdDone.notify();
});
CmdDone.wait();
EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
return Cmd.CommandLine;
};
for (const auto &Path : {NoCmd, Unreliable, OK, NotIncluded})
FS.Files[Path] = ";";
EXPECT_THAT(GetFlags(Main), Contains("-DMAIN")) << "sanity check";
EXPECT_THAT(GetFlags(NoCmd), Not(Contains("-DMAIN"))) << "no includes yet";
const char *AllIncludes = R"cpp(
#include "no_cmd.h"
#include "ok.h"
#include "unreliable.h"
)cpp";
S.update(Main, getInputs(Main, AllIncludes), WantDiagnostics::Yes);
EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_THAT(GetFlags(NoCmd), Contains("-DMAIN"))
<< "Included from main file, has no own command";
EXPECT_THAT(GetFlags(Unreliable), Contains("-DMAIN"))
<< "Included from main file, own command is heuristic";
EXPECT_THAT(GetFlags(OK), Not(Contains("-DMAIN")))
<< "Included from main file, but own command is used";
EXPECT_THAT(GetFlags(NotIncluded), Not(Contains("-DMAIN")))
<< "Not included from main file";
std::string SomeIncludes = R"cpp(
#include "no_cmd.h"
#include "not_included.h"
)cpp";
S.update(Main2, getInputs(Main2, SomeIncludes), WantDiagnostics::Yes);
EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_THAT(GetFlags(NoCmd),
AllOf(Contains("-DMAIN"), Not(Contains("-DMAIN2"))))
<< "mainfile association is stable";
EXPECT_THAT(GetFlags(NotIncluded),
AllOf(Contains("-DMAIN2"), Not(Contains("-DMAIN"))))
<< "new headers are associated with new mainfile";
S.update(Main, getInputs(Main, ""), WantDiagnostics::Yes);
EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_THAT(GetFlags(NoCmd),
AllOf(Contains("-DMAIN"), Not(Contains("-DMAIN2"))))
<< "mainfile association not updated yet!";
S.update(Main3, getInputs(Main3, SomeIncludes), WantDiagnostics::Yes);
EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_THAT(GetFlags(NoCmd), Contains("-DMAIN3"))
<< "association invalidated and then claimed by main3";
EXPECT_THAT(GetFlags(Unreliable), Contains("-DMAIN"))
<< "association invalidated but not reclaimed";
EXPECT_THAT(GetFlags(NotIncluded), Contains("-DMAIN2"))
<< "association still valid";
CDB.FailAll = true;
EXPECT_THAT(GetFlags(NoCmd), Not(Contains("-DMAIN3")))
<< "association should've been invalidated.";
S.update(Main3, getInputs(Main3, SomeIncludes), WantDiagnostics::Yes);
EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
CDB.FailAll = false;
S.update(Main3, getInputs(Main3, SomeIncludes), WantDiagnostics::Yes);
EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_THAT(GetFlags(NoCmd), Contains("-DMAIN3"))
<< "association invalidated and then claimed by main3";
}
TEST_F(TUSchedulerTests, PreservesLastActiveFile) {
for (bool Sync : {false, true}) {
auto Opts = optsForTest();
if (Sync)
Opts.AsyncThreadsCount = 0;
TUScheduler S(CDB, Opts);
auto CheckNoFileActionsSeesLastActiveFile =
[&](llvm::StringRef LastActiveFile) {
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
std::atomic<int> Counter(0);
S.run("run-UsesLastActiveFile", "", [&] {
++Counter;
EXPECT_EQ(LastActiveFile, boundPath());
});
S.runQuick("runQuick-UsesLastActiveFile", "", [&] {
++Counter;
EXPECT_EQ(LastActiveFile, boundPath());
});
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_EQ(2, Counter.load());
};
CheckNoFileActionsSeesLastActiveFile("");
auto Path = testPath("run.cc");
S.run(Path, Path, [] {});
CheckNoFileActionsSeesLastActiveFile(Path);
Path = testPath("runQuick.cc");
S.runQuick(Path, Path, [] {});
CheckNoFileActionsSeesLastActiveFile(Path);
Path = testPath("runWithAST.cc");
S.update(Path, getInputs(Path, ""), WantDiagnostics::No);
S.runWithAST(Path, Path, [](llvm::Expected<InputsAndAST> Inp) {
EXPECT_TRUE(bool(Inp));
});
CheckNoFileActionsSeesLastActiveFile(Path);
Path = testPath("runWithPreamble.cc");
S.update(Path, getInputs(Path, ""), WantDiagnostics::No);
S.runWithPreamble(
Path, Path, TUScheduler::Stale,
[](llvm::Expected<InputsAndPreamble> Inp) { EXPECT_TRUE(bool(Inp)); });
CheckNoFileActionsSeesLastActiveFile(Path);
Path = testPath("update.cc");
S.update(Path, getInputs(Path, ""), WantDiagnostics::No);
CheckNoFileActionsSeesLastActiveFile(Path);
auto LastActive = Path;
Path = testPath("runWithAST.cc");
S.update(Path, getInputs(Path, ""), WantDiagnostics::No);
CheckNoFileActionsSeesLastActiveFile(LastActive);
}
}
TEST_F(TUSchedulerTests, PreambleThrottle) {
const int NumRequests = 4;
struct : public PreambleThrottler {
std::mutex Mu;
std::vector<std::string> Acquires;
std::vector<RequestID> Releases;
llvm::DenseMap<RequestID, Callback> Callbacks;
std::optional<std::pair<RequestID, Notification *>> Notify;
RequestID acquire(llvm::StringRef Filename, Callback CB) override {
RequestID ID;
Callback Invoke;
{
std::lock_guard<std::mutex> Lock(Mu);
ID = Acquires.size();
Acquires.emplace_back(Filename);
if (Acquires.size() == NumRequests) {
Invoke = std::move(CB);
} else {
Callbacks.try_emplace(ID, std::move(CB));
}
}
if (Invoke)
Invoke();
{
std::lock_guard<std::mutex> Lock(Mu);
if (Notify && ID == Notify->first) {
Notify->second->notify();
Notify.reset();
}
}
return ID;
}
void release(RequestID ID) override {
Callback SatisfyNext;
{
std::lock_guard<std::mutex> Lock(Mu);
Releases.push_back(ID);
if (ID > 0 && Acquires.size() == NumRequests)
SatisfyNext = std::move(Callbacks[ID - 1]);
}
if (SatisfyNext)
SatisfyNext();
}
void reset() {
Acquires.clear();
Releases.clear();
Callbacks.clear();
}
} Throttler;
struct CaptureBuiltFilenames : public ParsingCallbacks {
std::vector<std::string> &Filenames;
CaptureBuiltFilenames(std::vector<std::string> &Filenames)
: Filenames(Filenames) {}
void onPreambleAST(
PathRef Path, llvm::StringRef Version, CapturedASTCtx,
std::shared_ptr<const include_cleaner::PragmaIncludes> PI) override {
Filenames.emplace_back(Path);
}
};
auto Opts = optsForTest();
Opts.AsyncThreadsCount = 2 * NumRequests;
Opts.PreambleThrottler = &Throttler;
std::vector<std::string> Filenames;
{
std::vector<std::string> BuiltFilenames;
TUScheduler S(CDB, Opts,
std::make_unique<CaptureBuiltFilenames>(BuiltFilenames));
for (unsigned I = 0; I < NumRequests; ++I) {
auto Path = testPath(std::to_string(I) + ".cc");
Filenames.push_back(Path);
S.update(Path, getInputs(Path, ""), WantDiagnostics::Yes);
}
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60)));
EXPECT_THAT(Throttler.Acquires,
testing::UnorderedElementsAreArray(Filenames));
EXPECT_THAT(BuiltFilenames,
testing::UnorderedElementsAreArray(Filenames));
EXPECT_THAT(BuiltFilenames,
testing::ElementsAreArray(Throttler.Acquires.rbegin(),
Throttler.Acquires.rend()));
EXPECT_THAT(Throttler.Releases, ElementsAre(3, 2, 1, 0));
}
Throttler.reset();
Notification AfterAcquire2;
Notification AfterFinishA;
Throttler.Notify = {1, &AfterAcquire2};
std::vector<std::string> BuiltFilenames;
auto A = testPath("a.cc");
auto B = testPath("b.cc");
Filenames = {A, B};
{
TUScheduler S(CDB, Opts,
std::make_unique<CaptureBuiltFilenames>(BuiltFilenames));
updateWithCallback(S, A, getInputs(A, ""), WantDiagnostics::Yes,
[&] { AfterFinishA.notify(); });
S.update(B, getInputs(B, ""), WantDiagnostics::Yes);
AfterAcquire2.wait();
EXPECT_THAT(Throttler.Acquires,
testing::UnorderedElementsAreArray(Filenames));
EXPECT_THAT(BuiltFilenames, testing::IsEmpty());
EXPECT_THAT(Throttler.Releases, testing::IsEmpty());
#if 0
S.remove(A);
AfterFinishA.wait();
EXPECT_THAT(BuiltFilenames, testing::IsEmpty());
EXPECT_THAT(Throttler.Releases, ElementsAre(AnyOf(1, 0)));
#endif
}
EXPECT_THAT(Throttler.Acquires,
testing::UnorderedElementsAreArray(Filenames));
EXPECT_THAT(BuiltFilenames, testing::IsEmpty());
EXPECT_THAT(Throttler.Releases, UnorderedElementsAre(1, 0));
}
}
}
}