forked from OSchip/llvm-project
715 lines
26 KiB
C++
715 lines
26 KiB
C++
//===-- TUSchedulerTests.cpp ------------------------------------*- C++ -*-===//
|
|
//
|
|
// 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 "Annotations.h"
|
|
#include "Context.h"
|
|
#include "Matchers.h"
|
|
#include "TUScheduler.h"
|
|
#include "TestFS.h"
|
|
#include "llvm/ADT/ScopeExit.h"
|
|
#include "gmock/gmock.h"
|
|
#include "gtest/gtest.h"
|
|
#include <algorithm>
|
|
#include <utility>
|
|
|
|
namespace clang {
|
|
namespace clangd {
|
|
namespace {
|
|
|
|
using ::testing::AnyOf;
|
|
using ::testing::Each;
|
|
using ::testing::ElementsAre;
|
|
using ::testing::Pointee;
|
|
using ::testing::UnorderedElementsAre;
|
|
|
|
MATCHER_P2(TUState, State, ActionName, "") {
|
|
return arg.Action.S == State && arg.Action.Name == ActionName;
|
|
}
|
|
|
|
class TUSchedulerTests : public ::testing::Test {
|
|
protected:
|
|
ParseInputs getInputs(PathRef File, std::string Contents) {
|
|
ParseInputs Inputs;
|
|
Inputs.CompileCommand = *CDB.getCompileCommand(File);
|
|
Inputs.FS = buildTestFS(Files, Timestamps);
|
|
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) {
|
|
WithContextValue Ctx(llvm::make_scope_exit(std::move(CB)));
|
|
S.update(File, getInputs(File, Contents), WD);
|
|
}
|
|
|
|
static Key<llvm::unique_function<void(PathRef File, std::vector<Diag>)>>
|
|
DiagsCallbackKey;
|
|
|
|
/// A diagnostics callback that should be passed to TUScheduler when it's used
|
|
/// in updateWithDiags.
|
|
static std::unique_ptr<ParsingCallbacks> captureDiags() {
|
|
class CaptureDiags : public ParsingCallbacks {
|
|
void onDiagnostics(PathRef File, std::vector<Diag> Diags) override {
|
|
auto D = Context::current().get(DiagsCallbackKey);
|
|
if (!D)
|
|
return;
|
|
const_cast<llvm::unique_function<void(PathRef, std::vector<Diag>)> &> (
|
|
*D)(File, Diags);
|
|
}
|
|
};
|
|
return llvm::make_unique<CaptureDiags>();
|
|
}
|
|
|
|
/// Schedule an update and call \p CB with the diagnostics it produces, if
|
|
/// any. The TUScheduler should be created with captureDiags as a
|
|
/// DiagsCallback for this to work.
|
|
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,
|
|
Bind(
|
|
[OrigFile](decltype(CB) CB, PathRef File, std::vector<Diag> Diags) {
|
|
assert(File == OrigFile);
|
|
CB(std::move(Diags));
|
|
},
|
|
std::move(CB)));
|
|
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, Contents), WD,
|
|
std::move(CB));
|
|
}
|
|
|
|
llvm::StringMap<std::string> Files;
|
|
llvm::StringMap<time_t> Timestamps;
|
|
MockCompilationDatabase CDB;
|
|
};
|
|
|
|
Key<llvm::unique_function<void(PathRef File, std::vector<Diag>)>>
|
|
TUSchedulerTests::DiagsCallbackKey;
|
|
|
|
TEST_F(TUSchedulerTests, MissingFiles) {
|
|
TUScheduler S(CDB, getDefaultAsyncThreadsCount(),
|
|
/*StorePreamblesInMemory=*/true, /*ASTCallbacks=*/nullptr,
|
|
/*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(),
|
|
ASTRetentionPolicy());
|
|
|
|
auto Added = testPath("added.cpp");
|
|
Files[Added] = "";
|
|
|
|
auto Missing = testPath("missing.cpp");
|
|
Files[Missing] = "";
|
|
|
|
S.update(Added, getInputs(Added, ""), WantDiagnostics::No);
|
|
|
|
// Assert each operation for missing file is an error (even if it's available
|
|
// in VFS).
|
|
S.runWithAST("", Missing,
|
|
[&](Expected<InputsAndAST> AST) { EXPECT_ERROR(AST); });
|
|
S.runWithPreamble(
|
|
"", Missing, TUScheduler::Stale,
|
|
[&](Expected<InputsAndPreamble> Preamble) { EXPECT_ERROR(Preamble); });
|
|
// remove() shouldn't crash on missing files.
|
|
S.remove(Missing);
|
|
|
|
// Assert there aren't any errors for added file.
|
|
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);
|
|
|
|
// Assert that all operations fail after removing the file.
|
|
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());
|
|
});
|
|
// remove() shouldn't crash on missing files.
|
|
S.remove(Added);
|
|
}
|
|
|
|
TEST_F(TUSchedulerTests, WantDiagnostics) {
|
|
std::atomic<int> CallbackCount(0);
|
|
{
|
|
// To avoid a racy test, don't allow tasks to actualy run on the worker
|
|
// thread until we've scheduled them all.
|
|
Notification Ready;
|
|
TUScheduler S(
|
|
CDB, getDefaultAsyncThreadsCount(),
|
|
/*StorePreamblesInMemory=*/true, captureDiags(),
|
|
/*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(),
|
|
ASTRetentionPolicy());
|
|
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(10)));
|
|
}
|
|
EXPECT_EQ(2, CallbackCount);
|
|
}
|
|
|
|
TEST_F(TUSchedulerTests, Debounce) {
|
|
std::atomic<int> CallbackCount(0);
|
|
{
|
|
TUScheduler S(CDB, getDefaultAsyncThreadsCount(),
|
|
/*StorePreamblesInMemory=*/true, captureDiags(),
|
|
/*UpdateDebounce=*/std::chrono::seconds(1),
|
|
ASTRetentionPolicy());
|
|
// FIXME: we could probably use timeouts lower than 1 second here.
|
|
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(200));
|
|
updateWithDiags(S, Path, "auto (timed out)", WantDiagnostics::Auto,
|
|
[&](std::vector<Diag>) { ++CallbackCount; });
|
|
std::this_thread::sleep_for(std::chrono::seconds(2));
|
|
updateWithDiags(S, Path, "auto (shut down)", WantDiagnostics::Auto,
|
|
[&](std::vector<Diag>) { ++CallbackCount; });
|
|
|
|
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
|
|
}
|
|
EXPECT_EQ(2, CallbackCount);
|
|
}
|
|
|
|
static std::vector<std::string> includes(const PreambleData *Preamble) {
|
|
std::vector<std::string> Result;
|
|
if (Preamble)
|
|
for (const auto &Inclusion : Preamble->Includes.MainFileIncludes)
|
|
Result.push_back(Inclusion.Written);
|
|
return Result;
|
|
}
|
|
|
|
TEST_F(TUSchedulerTests, PreambleConsistency) {
|
|
std::atomic<int> CallbackCount(0);
|
|
{
|
|
Notification InconsistentReadDone; // Must live longest.
|
|
TUScheduler S(
|
|
CDB, getDefaultAsyncThreadsCount(), /*StorePreamblesInMemory=*/true,
|
|
/*ASTCallbacks=*/nullptr,
|
|
/*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(),
|
|
ASTRetentionPolicy());
|
|
auto Path = testPath("foo.cpp");
|
|
// Schedule two updates (A, B) and two preamble reads (stale, consistent).
|
|
// The stale read should see A, and the consistent read should see B.
|
|
// (We recognize the preambles by their included files).
|
|
updateWithCallback(S, Path, "#include <A>", WantDiagnostics::Yes, [&]() {
|
|
// This callback runs in between the two preamble updates.
|
|
|
|
// This blocks update B, preventing it from winning the race
|
|
// against the stale read.
|
|
// If the first read was instead consistent, this would deadlock.
|
|
InconsistentReadDone.wait();
|
|
// This delays update B, preventing it from winning a race
|
|
// against the consistent read. The consistent read sees B
|
|
// only because it waits for it.
|
|
// If the second read was stale, it would usually see A.
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
});
|
|
S.update(Path, getInputs(Path, "#include <B>"), WantDiagnostics::Yes);
|
|
|
|
S.runWithPreamble("StaleRead", Path, TUScheduler::Stale,
|
|
[&](Expected<InputsAndPreamble> Pre) {
|
|
ASSERT_TRUE(bool(Pre));
|
|
assert(bool(Pre));
|
|
EXPECT_THAT(includes(Pre->Preamble),
|
|
ElementsAre("<A>"));
|
|
InconsistentReadDone.notify();
|
|
++CallbackCount;
|
|
});
|
|
S.runWithPreamble("ConsistentRead", Path, TUScheduler::Consistent,
|
|
[&](Expected<InputsAndPreamble> Pre) {
|
|
ASSERT_TRUE(bool(Pre));
|
|
EXPECT_THAT(includes(Pre->Preamble),
|
|
ElementsAre("<B>"));
|
|
++CallbackCount;
|
|
});
|
|
}
|
|
EXPECT_EQ(2, CallbackCount);
|
|
}
|
|
|
|
TEST_F(TUSchedulerTests, Cancellation) {
|
|
// We have the following update/read sequence
|
|
// U0
|
|
// U1(WantDiags=Yes) <-- cancelled
|
|
// R1 <-- cancelled
|
|
// U2(WantDiags=Yes) <-- cancelled
|
|
// R2A <-- cancelled
|
|
// R2B
|
|
// U3(WantDiags=Yes)
|
|
// R3 <-- cancelled
|
|
std::vector<std::string> DiagsSeen, ReadsSeen, ReadsCanceled;
|
|
{
|
|
Notification Proceed; // Ensure we schedule everything.
|
|
TUScheduler S(
|
|
CDB, getDefaultAsyncThreadsCount(), /*StorePreamblesInMemory=*/true,
|
|
/*ASTCallbacks=*/captureDiags(),
|
|
/*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(),
|
|
ASTRetentionPolicy());
|
|
auto Path = testPath("foo.cpp");
|
|
// Helper to schedule a named update and return a function to cancel it.
|
|
auto Update = [&](std::string ID) -> Canceler {
|
|
auto T = cancelableTask();
|
|
WithContext C(std::move(T.first));
|
|
updateWithDiags(
|
|
S, Path, "//" + ID, WantDiagnostics::Yes,
|
|
[&, ID](std::vector<Diag> Diags) { DiagsSeen.push_back(ID); });
|
|
return std::move(T.second);
|
|
};
|
|
// Helper to schedule a named read and return a function to cancel it.
|
|
auto Read = [&](std::string 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(); });
|
|
// The second parens indicate cancellation, where present.
|
|
Update("U1")();
|
|
Read("R1")();
|
|
Update("U2")();
|
|
Read("R2A")();
|
|
Read("R2B");
|
|
Update("U3");
|
|
Read("R3")();
|
|
Proceed.notify();
|
|
|
|
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
|
|
}
|
|
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, ManyUpdates) {
|
|
const int FilesCount = 3;
|
|
const int UpdatesPerFile = 10;
|
|
|
|
std::mutex Mut;
|
|
int TotalASTReads = 0;
|
|
int TotalPreambleReads = 0;
|
|
int TotalUpdates = 0;
|
|
|
|
// Run TUScheduler and collect some stats.
|
|
{
|
|
TUScheduler S(CDB, getDefaultAsyncThreadsCount(),
|
|
/*StorePreamblesInMemory=*/true, captureDiags(),
|
|
/*UpdateDebounce=*/std::chrono::milliseconds(50),
|
|
ASTRetentionPolicy());
|
|
|
|
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->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;
|
|
|
|
// Scheduler may run tasks asynchronously, but should propagate the context.
|
|
// We stash a nonce in the context, and verify it in the task.
|
|
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);
|
|
updateWithDiags(
|
|
S, File, Inputs, WantDiagnostics::Auto,
|
|
[File, Nonce, &Mut, &TotalUpdates](std::vector<Diag>) {
|
|
EXPECT_THAT(Context::current().get(NonceKey), Pointee(Nonce));
|
|
|
|
std::lock_guard<std::mutex> Lock(Mut);
|
|
++TotalUpdates;
|
|
EXPECT_EQ(File, *TUScheduler::getFileBeingProcessedInContext());
|
|
});
|
|
}
|
|
{
|
|
WithContextValue WithNonce(NonceKey, ++Nonce);
|
|
S.runWithAST(
|
|
"CheckAST", File,
|
|
[File, Inputs, Nonce, &Mut,
|
|
&TotalASTReads](Expected<InputsAndAST> AST) {
|
|
EXPECT_THAT(Context::current().get(NonceKey), Pointee(Nonce));
|
|
|
|
ASSERT_TRUE((bool)AST);
|
|
EXPECT_EQ(AST->Inputs.FS, Inputs.FS);
|
|
EXPECT_EQ(AST->Inputs.Contents, Inputs.Contents);
|
|
|
|
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));
|
|
|
|
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(10)));
|
|
} // TUScheduler destructor waits for all operations to finish.
|
|
|
|
std::lock_guard<std::mutex> Lock(Mut);
|
|
EXPECT_EQ(TotalUpdates, FilesCount * UpdatesPerFile);
|
|
EXPECT_EQ(TotalASTReads, FilesCount * UpdatesPerFile);
|
|
EXPECT_EQ(TotalPreambleReads, FilesCount * UpdatesPerFile);
|
|
}
|
|
|
|
TEST_F(TUSchedulerTests, EvictedAST) {
|
|
std::atomic<int> BuiltASTCounter(0);
|
|
ASTRetentionPolicy Policy;
|
|
Policy.MaxRetainedASTs = 2;
|
|
TUScheduler S(CDB,
|
|
/*AsyncThreadsCount=*/1, /*StorePreambleInMemory=*/true,
|
|
/*ASTCallbacks=*/nullptr,
|
|
/*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(),
|
|
Policy);
|
|
|
|
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");
|
|
|
|
// Build one file in advance. We will not access it later, so it will be the
|
|
// one that the cache will evict.
|
|
updateWithCallback(S, Foo, SourceContents, WantDiagnostics::Yes,
|
|
[&BuiltASTCounter]() { ++BuiltASTCounter; });
|
|
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
|
|
ASSERT_EQ(BuiltASTCounter.load(), 1);
|
|
|
|
// Build two more files. Since we can retain only 2 ASTs, these should be the
|
|
// ones we see in the cache later.
|
|
updateWithCallback(S, Bar, SourceContents, WantDiagnostics::Yes,
|
|
[&BuiltASTCounter]() { ++BuiltASTCounter; });
|
|
updateWithCallback(S, Baz, SourceContents, WantDiagnostics::Yes,
|
|
[&BuiltASTCounter]() { ++BuiltASTCounter; });
|
|
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
|
|
ASSERT_EQ(BuiltASTCounter.load(), 3);
|
|
|
|
// Check only the last two ASTs are retained.
|
|
ASSERT_THAT(S.getFilesWithCachedAST(), UnorderedElementsAre(Bar, Baz));
|
|
|
|
// Access the old file again.
|
|
updateWithCallback(S, Foo, OtherSourceContents, WantDiagnostics::Yes,
|
|
[&BuiltASTCounter]() { ++BuiltASTCounter; });
|
|
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
|
|
ASSERT_EQ(BuiltASTCounter.load(), 4);
|
|
|
|
// Check the AST for foo.cpp is retained now and one of the others got
|
|
// evicted.
|
|
EXPECT_THAT(S.getFilesWithCachedAST(),
|
|
UnorderedElementsAre(Foo, AnyOf(Bar, Baz)));
|
|
}
|
|
|
|
TEST_F(TUSchedulerTests, EmptyPreamble) {
|
|
TUScheduler S(CDB,
|
|
/*AsyncThreadsCount=*/4, /*StorePreambleInMemory=*/true,
|
|
/*ASTCallbacks=*/nullptr,
|
|
/*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(),
|
|
ASTRetentionPolicy());
|
|
|
|
auto Foo = testPath("foo.cpp");
|
|
auto Header = testPath("foo.h");
|
|
|
|
Files[Header] = "void foo()";
|
|
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) {
|
|
// We expect to get a non-empty preamble.
|
|
EXPECT_GT(
|
|
cantFail(std::move(Preamble)).Preamble->Preamble.getBounds().Size,
|
|
0u);
|
|
});
|
|
// Wait for the preamble is being built.
|
|
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
|
|
|
|
// Update the file which results in an empty preamble.
|
|
S.update(Foo, getInputs(Foo, WithEmptyPreamble), WantDiagnostics::Auto);
|
|
// Wait for the preamble is being built.
|
|
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
|
|
S.runWithPreamble(
|
|
"getEmptyPreamble", Foo, TUScheduler::Stale,
|
|
[&](Expected<InputsAndPreamble> Preamble) {
|
|
// We expect to get an empty preamble.
|
|
EXPECT_EQ(
|
|
cantFail(std::move(Preamble)).Preamble->Preamble.getBounds().Size,
|
|
0u);
|
|
});
|
|
}
|
|
|
|
TEST_F(TUSchedulerTests, RunWaitsForPreamble) {
|
|
// Testing strategy: we update the file and schedule a few preamble reads at
|
|
// the same time. All reads should get the same non-null preamble.
|
|
TUScheduler S(CDB,
|
|
/*AsyncThreadsCount=*/4, /*StorePreambleInMemory=*/true,
|
|
/*ASTCallbacks=*/nullptr,
|
|
/*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(),
|
|
ASTRetentionPolicy());
|
|
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(10)));
|
|
// Check all actions got the same non-null preamble.
|
|
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,
|
|
/*AsyncThreadsCount=*/getDefaultAsyncThreadsCount(),
|
|
/*StorePreambleInMemory=*/true, captureDiags(),
|
|
/*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(),
|
|
ASTRetentionPolicy());
|
|
|
|
auto Source = testPath("foo.cpp");
|
|
auto Header = testPath("foo.h");
|
|
|
|
Files[Header] = "int a;";
|
|
Timestamps[Header] = time_t(0);
|
|
|
|
auto SourceContents = R"cpp(
|
|
#include "foo.h"
|
|
int b = a;
|
|
)cpp";
|
|
|
|
// Return value indicates if the updated callback was received.
|
|
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(10));
|
|
if (!UpdateFinished)
|
|
ADD_FAILURE() << "Updated has not finished in one second. Threading bug?";
|
|
return Updated;
|
|
};
|
|
|
|
// Test that subsequent updates with the same inputs do not cause rebuilds.
|
|
ASSERT_TRUE(DoUpdate(SourceContents));
|
|
ASSERT_FALSE(DoUpdate(SourceContents));
|
|
|
|
// Update to a header should cause a rebuild, though.
|
|
Timestamps[Header] = time_t(1);
|
|
ASSERT_TRUE(DoUpdate(SourceContents));
|
|
ASSERT_FALSE(DoUpdate(SourceContents));
|
|
|
|
// Update to the contents should cause a rebuild.
|
|
auto OtherSourceContents = R"cpp(
|
|
#include "foo.h"
|
|
int c = d;
|
|
)cpp";
|
|
ASSERT_TRUE(DoUpdate(OtherSourceContents));
|
|
ASSERT_FALSE(DoUpdate(OtherSourceContents));
|
|
|
|
// Update to the compile commands should also cause a rebuild.
|
|
CDB.ExtraClangFlags.push_back("-DSOMETHING");
|
|
ASSERT_TRUE(DoUpdate(OtherSourceContents));
|
|
ASSERT_FALSE(DoUpdate(OtherSourceContents));
|
|
}
|
|
|
|
TEST_F(TUSchedulerTests, NoChangeDiags) {
|
|
TUScheduler S(CDB,
|
|
/*AsyncThreadsCount=*/getDefaultAsyncThreadsCount(),
|
|
/*StorePreambleInMemory=*/true, captureDiags(),
|
|
/*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(),
|
|
ASTRetentionPolicy());
|
|
|
|
auto FooCpp = testPath("foo.cpp");
|
|
auto Contents = "int a; int b;";
|
|
|
|
updateWithDiags(
|
|
S, FooCpp, Contents, WantDiagnostics::No,
|
|
[](std::vector<Diag>) { ADD_FAILURE() << "Should not be called."; });
|
|
S.runWithAST("touchAST", FooCpp, [](Expected<InputsAndAST> IA) {
|
|
// Make sure the AST was actually built.
|
|
cantFail(std::move(IA));
|
|
});
|
|
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
|
|
|
|
// Even though the inputs didn't change and AST can be reused, we need to
|
|
// report the diagnostics, as they were not reported previously.
|
|
std::atomic<bool> SeenDiags(false);
|
|
updateWithDiags(S, FooCpp, Contents, WantDiagnostics::Auto,
|
|
[&](std::vector<Diag>) { SeenDiags = true; });
|
|
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
|
|
ASSERT_TRUE(SeenDiags);
|
|
|
|
// Subsequent request does not get any diagnostics callback because the same
|
|
// diags have previously been reported and the inputs didn't change.
|
|
updateWithDiags(
|
|
S, FooCpp, Contents, WantDiagnostics::Auto,
|
|
[&](std::vector<Diag>) { ADD_FAILURE() << "Should not be called."; });
|
|
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
|
|
}
|
|
|
|
TEST_F(TUSchedulerTests, Run) {
|
|
TUScheduler S(CDB, /*AsyncThreadsCount=*/getDefaultAsyncThreadsCount(),
|
|
/*StorePreambleInMemory=*/true, /*ASTCallbacks=*/nullptr,
|
|
/*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(),
|
|
ASTRetentionPolicy());
|
|
std::atomic<int> Counter(0);
|
|
S.run("add 1", [&] { ++Counter; });
|
|
S.run("add 2", [&] { Counter += 2; });
|
|
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
|
|
EXPECT_EQ(Counter.load(), 3);
|
|
}
|
|
|
|
TEST_F(TUSchedulerTests, TUStatus) {
|
|
class CaptureTUStatus : public DiagnosticsConsumer {
|
|
public:
|
|
void onDiagnosticsReady(PathRef File,
|
|
std::vector<Diag> Diagnostics) override {}
|
|
|
|
void onFileUpdated(PathRef File, const TUStatus &Status) override {
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
AllStatus.push_back(Status);
|
|
}
|
|
|
|
std::vector<TUStatus> allStatus() {
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
return AllStatus;
|
|
}
|
|
|
|
private:
|
|
std::mutex Mutex;
|
|
std::vector<TUStatus> AllStatus;
|
|
} CaptureTUStatus;
|
|
MockFSProvider FS;
|
|
MockCompilationDatabase CDB;
|
|
ClangdServer Server(CDB, FS, CaptureTUStatus, ClangdServer::optsForTest());
|
|
Annotations Code("int m^ain () {}");
|
|
|
|
// We schedule the following tasks in the queue:
|
|
// [Update] [GoToDefinition]
|
|
Server.addDocument(testPath("foo.cpp"), Code.code(), WantDiagnostics::Yes);
|
|
Server.locateSymbolAt(testPath("foo.cpp"), Code.point(),
|
|
[](Expected<std::vector<LocatedSymbol>> Result) {
|
|
ASSERT_TRUE((bool)Result);
|
|
});
|
|
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest());
|
|
|
|
EXPECT_THAT(CaptureTUStatus.allStatus(),
|
|
ElementsAre(
|
|
// Statuses of "Update" action.
|
|
TUState(TUAction::RunningAction, "Update"),
|
|
TUState(TUAction::BuildingPreamble, "Update"),
|
|
TUState(TUAction::BuildingFile, "Update"),
|
|
|
|
// Statuses of "Definitions" action
|
|
TUState(TUAction::RunningAction, "Definitions"),
|
|
TUState(TUAction::Idle, /*No action*/ "")));
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace clangd
|
|
} // namespace clang
|