#include "Annotations.h"
#include "ClangdServer.h"
#include "CodeComplete.h"
#include "CompileCommands.h"
#include "ConfigFragment.h"
#include "GlobalCompilationDatabase.h"
#include "Matchers.h"
#include "SyncAPI.h"
#include "TestFS.h"
#include "TestTU.h"
#include "TidyProvider.h"
#include "refactor/Tweak.h"
#include "support/MemoryTree.h"
#include "support/Path.h"
#include "support/Threading.h"
#include "clang/Config/config.h"
#include "clang/Sema/CodeCompleteConsumer.h"
#include "clang/Tooling/ArgumentsAdjusters.h"
#include "clang/Tooling/Core/Replacement.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Allocator.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Regex.h"
#include "llvm/Support/VirtualFileSystem.h"
#include "llvm/Testing/Support/Error.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <algorithm>
#include <chrono>
#include <iostream>
#include <optional>
#include <random>
#include <string>
#include <thread>
#include <vector>
namespace clang {
namespace clangd {
namespace {
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::Pair;
using ::testing::SizeIs;
using ::testing::UnorderedElementsAre;
MATCHER_P2(DeclAt, File, Range, "") {
return arg.PreferredDeclaration ==
Location{URIForFile::canonicalize(File, testRoot()), Range};
}
bool diagsContainErrors(const std::vector<Diag> &Diagnostics) {
for (auto D : Diagnostics) {
if (D.Severity == DiagnosticsEngine::Error ||
D.Severity == DiagnosticsEngine::Fatal)
return true;
}
return false;
}
class ErrorCheckingCallbacks : public ClangdServer::Callbacks {
public:
void onDiagnosticsReady(PathRef File, llvm::StringRef Version,
llvm::ArrayRef<Diag> Diagnostics) override {
bool HadError = diagsContainErrors(Diagnostics);
std::lock_guard<std::mutex> Lock(Mutex);
HadErrorInLastDiags = HadError;
}
bool hadErrorInLastDiags() {
std::lock_guard<std::mutex> Lock(Mutex);
return HadErrorInLastDiags;
}
private:
std::mutex Mutex;
bool HadErrorInLastDiags = false;
};
class MultipleErrorCheckingCallbacks : public ClangdServer::Callbacks {
public:
void onDiagnosticsReady(PathRef File, llvm::StringRef Version,
llvm::ArrayRef<Diag> Diagnostics) override {
bool HadError = diagsContainErrors(Diagnostics);
std::lock_guard<std::mutex> Lock(Mutex);
LastDiagsHadError[File] = HadError;
}
std::vector<std::pair<Path, bool>> filesWithDiags() const {
std::vector<std::pair<Path, bool>> Result;
std::lock_guard<std::mutex> Lock(Mutex);
for (const auto &It : LastDiagsHadError)
Result.emplace_back(std::string(It.first()), It.second);
return Result;
}
void clear() {
std::lock_guard<std::mutex> Lock(Mutex);
LastDiagsHadError.clear();
}
private:
mutable std::mutex Mutex;
llvm::StringMap<bool> LastDiagsHadError;
};
std::string replacePtrsInDump(std::string const &Dump) {
llvm::Regex RE("0x[0-9a-fA-F]+");
llvm::SmallVector<llvm::StringRef, 1> Matches;
llvm::StringRef Pending = Dump;
std::string Result;
while (RE.match(Pending, &Matches)) {
assert(Matches.size() == 1 && "Exactly one match expected");
auto MatchPos = Matches[0].data() - Pending.data();
Result += Pending.take_front(MatchPos);
Pending = Pending.drop_front(MatchPos + Matches[0].size());
}
Result += Pending;
return Result;
}
std::string dumpAST(ClangdServer &Server, PathRef File) {
std::string Result;
Notification Done;
Server.customAction(File, "DumpAST", [&](llvm::Expected<InputsAndAST> AST) {
if (AST) {
llvm::raw_string_ostream ResultOS(Result);
AST->AST.getASTContext().getTranslationUnitDecl()->dump(ResultOS, true);
} else {
llvm::consumeError(AST.takeError());
Result = "<no-ast>";
}
Done.notify();
});
Done.wait();
return Result;
}
std::string dumpASTWithoutMemoryLocs(ClangdServer &Server, PathRef File) {
return replacePtrsInDump(dumpAST(Server, File));
}
std::string parseSourceAndDumpAST(
PathRef SourceFileRelPath, llvm::StringRef SourceContents,
std::vector<std::pair<PathRef, llvm::StringRef>> ExtraFiles = {},
bool ExpectErrors = false) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
for (const auto &FileWithContents : ExtraFiles)
FS.Files[testPath(FileWithContents.first)] =
std::string(FileWithContents.second);
auto SourceFilename = testPath(SourceFileRelPath);
Server.addDocument(SourceFilename, SourceContents);
auto Result = dumpASTWithoutMemoryLocs(Server, SourceFilename);
EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
EXPECT_EQ(ExpectErrors, DiagConsumer.hadErrorInLastDiags());
return Result;
}
TEST(ClangdServerTest, Parse) {
auto Empty = parseSourceAndDumpAST("foo.cpp", "");
auto OneDecl = parseSourceAndDumpAST("foo.cpp", "int a;");
auto SomeDecls = parseSourceAndDumpAST("foo.cpp", "int a; int b; int c;");
EXPECT_NE(Empty, OneDecl);
EXPECT_NE(Empty, SomeDecls);
EXPECT_NE(SomeDecls, OneDecl);
auto Empty2 = parseSourceAndDumpAST("foo.cpp", "");
auto OneDecl2 = parseSourceAndDumpAST("foo.cpp", "int a;");
auto SomeDecls2 = parseSourceAndDumpAST("foo.cpp", "int a; int b; int c;");
EXPECT_EQ(Empty, Empty2);
EXPECT_EQ(OneDecl, OneDecl2);
EXPECT_EQ(SomeDecls, SomeDecls2);
}
TEST(ClangdServerTest, ParseWithHeader) {
parseSourceAndDumpAST("foo.cpp", "#include \"foo.h\"", {},
true);
parseSourceAndDumpAST("foo.cpp", "#include \"foo.h\"", {{"foo.h", ""}},
false);
const auto *SourceContents = R"cpp(
#include "foo.h"
int b = a;
)cpp";
parseSourceAndDumpAST("foo.cpp", SourceContents, {{"foo.h", ""}},
true);
parseSourceAndDumpAST("foo.cpp", SourceContents, {{"foo.h", "int a;"}},
false);
}
TEST(ClangdServerTest, Reparse) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
const auto *SourceContents = R"cpp(
#include "foo.h"
int b = a;
)cpp";
auto FooCpp = testPath("foo.cpp");
FS.Files[testPath("foo.h")] = "int a;";
FS.Files[FooCpp] = SourceContents;
Server.addDocument(FooCpp, SourceContents);
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
auto DumpParse1 = dumpASTWithoutMemoryLocs(Server, FooCpp);
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
Server.addDocument(FooCpp, "");
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
auto DumpParseEmpty = dumpASTWithoutMemoryLocs(Server, FooCpp);
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
Server.addDocument(FooCpp, SourceContents);
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
auto DumpParse2 = dumpASTWithoutMemoryLocs(Server, FooCpp);
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
EXPECT_EQ(DumpParse1, DumpParse2);
EXPECT_NE(DumpParse1, DumpParseEmpty);
}
TEST(ClangdServerTest, ReparseOnHeaderChange) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
const auto *SourceContents = R"cpp(
#include "foo.h"
int b = a;
)cpp";
auto FooCpp = testPath("foo.cpp");
auto FooH = testPath("foo.h");
FS.Files[FooH] = "int a;";
FS.Files[FooCpp] = SourceContents;
Server.addDocument(FooCpp, SourceContents);
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
auto DumpParse1 = dumpASTWithoutMemoryLocs(Server, FooCpp);
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
FS.Files[FooH] = "";
Server.addDocument(FooCpp, SourceContents);
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
auto DumpParseDifferent = dumpASTWithoutMemoryLocs(Server, FooCpp);
EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags());
FS.Files[FooH] = "int a;";
Server.addDocument(FooCpp, SourceContents);
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
auto DumpParse2 = dumpASTWithoutMemoryLocs(Server, FooCpp);
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
EXPECT_EQ(DumpParse1, DumpParse2);
EXPECT_NE(DumpParse1, DumpParseDifferent);
}
TEST(ClangdServerTest, PropagatesContexts) {
static Key<int> Secret;
struct ContextReadingFS : public ThreadsafeFS {
mutable int Got;
private:
IntrusiveRefCntPtr<llvm::vfs::FileSystem> viewImpl() const override {
Got = Context::current().getExisting(Secret);
return buildTestFS({});
}
} FS;
struct Callbacks : public ClangdServer::Callbacks {
void onDiagnosticsReady(PathRef File, llvm::StringRef Version,
llvm::ArrayRef<Diag> Diagnostics) override {
Got = Context::current().getExisting(Secret);
}
int Got;
} Callbacks;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &Callbacks);
{
WithContextValue Entrypoint(Secret, 42);
Server.addDocument(testPath("foo.cpp"), "void main(){}");
}
ASSERT_TRUE(Server.blockUntilIdleForTest());
EXPECT_EQ(FS.Got, 42);
EXPECT_EQ(Callbacks.Got, 42);
}
TEST(ClangdServerTest, RespectsConfig) {
Annotations Example(R"cpp(
#ifdef FOO
int [[x]];
#else
int x;
#endif
int y = ^x;
)cpp");
class ConfigProvider : public config::Provider {
std::vector<config::CompiledFragment>
getFragments(const config::Params &,
config::DiagnosticCallback DC) const override {
config::Fragment F;
F.If.PathMatch.emplace_back(".*foo.cc");
F.CompileFlags.Add.emplace_back("-DFOO=1");
return {std::move(F).compile(DC)};
}
} CfgProvider;
auto Opts = ClangdServer::optsForTest();
Opts.ContextProvider =
ClangdServer::createConfiguredContextProvider(&CfgProvider, nullptr);
OverlayCDB CDB(nullptr, {},
CommandMangler::forTests());
MockFS FS;
ClangdServer Server(CDB, FS, Opts);
Server.addDocument(testPath("foo.cc"), Example.code());
auto Result = runLocateSymbolAt(Server, testPath("foo.cc"), Example.point());
ASSERT_TRUE(bool(Result)) << Result.takeError();
ASSERT_THAT(*Result, SizeIs(1));
EXPECT_EQ(Result->front().PreferredDeclaration.range, Example.range());
Server.addDocument(testPath("bar.cc"), Example.code());
Result = runLocateSymbolAt(Server, testPath("bar.cc"), Example.point());
ASSERT_TRUE(bool(Result)) << Result.takeError();
ASSERT_THAT(*Result, SizeIs(1));
EXPECT_NE(Result->front().PreferredDeclaration.range, Example.range());
}
TEST(ClangdServerTest, PropagatesVersion) {
MockCompilationDatabase CDB;
MockFS FS;
struct Callbacks : public ClangdServer::Callbacks {
void onDiagnosticsReady(PathRef File, llvm::StringRef Version,
llvm::ArrayRef<Diag> Diagnostics) override {
Got = Version.str();
}
std::string Got = "";
} Callbacks;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &Callbacks);
runAddDocument(Server, testPath("foo.cpp"), "void main(){}", "42");
EXPECT_EQ(Callbacks.Got, "42");
}
#ifdef LLVM_ON_UNIX
TEST(ClangdServerTest, SearchLibDir) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
CDB.ExtraClangFlags.insert(CDB.ExtraClangFlags.end(),
{"-xc++", "--target=x86_64-unknown-linux-gnu",
"-m64", "--gcc-toolchain=/randomusr",
"-stdlib=libstdc++"});
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
SmallString<8> Version("4.9.3");
SmallString<64> LibDir("/randomusr/lib/gcc/x86_64-linux-gnu");
llvm::sys::path::append(LibDir, Version);
SmallString<64> MockLibFile;
llvm::sys::path::append(MockLibFile, LibDir, "64", "crtbegin.o");
FS.Files[MockLibFile] = "";
SmallString<64> IncludeDir("/randomusr/include/c++");
llvm::sys::path::append(IncludeDir, Version);
SmallString<64> StringPath;
llvm::sys::path::append(StringPath, IncludeDir, "string");
FS.Files[StringPath] = "class mock_string {};";
auto FooCpp = testPath("foo.cpp");
const auto *SourceContents = R"cpp(
#include <string>
mock_string x;
)cpp";
FS.Files[FooCpp] = SourceContents;
runAddDocument(Server, FooCpp, SourceContents);
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
const auto *SourceContentsWithError = R"cpp(
#include <string>
std::string x;
)cpp";
runAddDocument(Server, FooCpp, SourceContentsWithError);
EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags());
}
#endif
TEST(ClangdServerTest, ForceReparseCompileCommand) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
auto FooCpp = testPath("foo.cpp");
const auto *SourceContents1 = R"cpp(
template <class T>
struct foo { T x; };
)cpp";
const auto *SourceContents2 = R"cpp(
template <class T>
struct bar { T x; };
)cpp";
FS.Files[FooCpp] = "";
CDB.ExtraClangFlags = {"-xc"};
runAddDocument(Server, FooCpp, SourceContents1);
EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags());
runAddDocument(Server, FooCpp, SourceContents2);
EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags());
CDB.ExtraClangFlags = {"-xc++"};
runAddDocument(Server, FooCpp, SourceContents2);
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
runAddDocument(Server, FooCpp, SourceContents1);
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
runAddDocument(Server, FooCpp, SourceContents2);
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
}
TEST(ClangdServerTest, ForceReparseCompileCommandDefines) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
auto FooCpp = testPath("foo.cpp");
const auto *SourceContents = R"cpp(
#ifdef WITH_ERROR
this
#endif
int main() { return 0; }
)cpp";
FS.Files[FooCpp] = "";
CDB.ExtraClangFlags = {"-DWITH_ERROR"};
runAddDocument(Server, FooCpp, SourceContents);
EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags());
CDB.ExtraClangFlags = {};
runAddDocument(Server, FooCpp, SourceContents);
ASSERT_TRUE(Server.blockUntilIdleForTest());
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
runAddDocument(Server, FooCpp, SourceContents);
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
}
TEST(ClangdServerTest, ReparseOpenedFiles) {
Annotations FooSource(R"cpp(
#ifdef MACRO
static void $one[[bob]]() {}
#else
static void $two[[bob]]() {}
#endif
int main () { bo^b (); return 0; }
)cpp");
Annotations BarSource(R"cpp(
#ifdef MACRO
this is an error
#endif
)cpp");
Annotations BazSource(R"cpp(
int hello;
)cpp");
MockFS FS;
MockCompilationDatabase CDB;
MultipleErrorCheckingCallbacks DiagConsumer;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
auto FooCpp = testPath("foo.cpp");
auto BarCpp = testPath("bar.cpp");
auto BazCpp = testPath("baz.cpp");
FS.Files[FooCpp] = "";
FS.Files[BarCpp] = "";
FS.Files[BazCpp] = "";
CDB.ExtraClangFlags = {"-DMACRO=1"};
Server.addDocument(FooCpp, FooSource.code());
Server.addDocument(BarCpp, BarSource.code());
Server.addDocument(BazCpp, BazSource.code());
ASSERT_TRUE(Server.blockUntilIdleForTest());
EXPECT_THAT(DiagConsumer.filesWithDiags(),
UnorderedElementsAre(Pair(FooCpp, false), Pair(BarCpp, true),
Pair(BazCpp, false)));
auto Locations = runLocateSymbolAt(Server, FooCpp, FooSource.point());
EXPECT_TRUE(bool(Locations));
EXPECT_THAT(*Locations, ElementsAre(DeclAt(FooCpp, FooSource.range("one"))));
CDB.ExtraClangFlags.clear();
DiagConsumer.clear();
Server.removeDocument(BazCpp);
Server.addDocument(FooCpp, FooSource.code());
Server.addDocument(BarCpp, BarSource.code());
ASSERT_TRUE(Server.blockUntilIdleForTest());
EXPECT_THAT(DiagConsumer.filesWithDiags(),
UnorderedElementsAre(Pair(FooCpp, false), Pair(BarCpp, false)));
Locations = runLocateSymbolAt(Server, FooCpp, FooSource.point());
EXPECT_TRUE(bool(Locations));
EXPECT_THAT(*Locations, ElementsAre(DeclAt(FooCpp, FooSource.range("two"))));
}
MATCHER_P4(Stats, Name, UsesMemory, PreambleBuilds, ASTBuilds, "") {
return arg.first() == Name &&
(arg.second.UsedBytesAST + arg.second.UsedBytesPreamble != 0) ==
UsesMemory &&
std::tie(arg.second.PreambleBuilds, ASTBuilds) ==
std::tie(PreambleBuilds, ASTBuilds);
}
TEST(ClangdServerTest, FileStats) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
Path FooCpp = testPath("foo.cpp");
const auto *SourceContents = R"cpp(
struct Something {
int method();
};
)cpp";
Path BarCpp = testPath("bar.cpp");
FS.Files[FooCpp] = "";
FS.Files[BarCpp] = "";
EXPECT_THAT(Server.fileStats(), IsEmpty());
Server.addDocument(FooCpp, SourceContents);
Server.addDocument(BarCpp, SourceContents);
ASSERT_TRUE(Server.blockUntilIdleForTest());
EXPECT_THAT(Server.fileStats(),
UnorderedElementsAre(Stats(FooCpp, true, 1, 1),
Stats(BarCpp, true, 1, 1)));
Server.removeDocument(FooCpp);
ASSERT_TRUE(Server.blockUntilIdleForTest());
EXPECT_THAT(Server.fileStats(), ElementsAre(Stats(BarCpp, true, 1, 1)));
Server.removeDocument(BarCpp);
ASSERT_TRUE(Server.blockUntilIdleForTest());
EXPECT_THAT(Server.fileStats(), IsEmpty());
}
TEST(ClangdServerTest, InvalidCompileCommand) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
auto FooCpp = testPath("foo.cpp");
CDB.ExtraClangFlags.push_back("-###");
runAddDocument(Server, FooCpp, "int main() {}");
EXPECT_EQ(dumpAST(Server, FooCpp), "<no-ast>");
EXPECT_ERROR(runLocateSymbolAt(Server, FooCpp, Position()));
EXPECT_ERROR(runFindDocumentHighlights(Server, FooCpp, Position()));
EXPECT_ERROR(runRename(Server, FooCpp, Position(), "new_name",
clangd::RenameOptions()));
EXPECT_ERROR(
runSignatureHelp(Server, FooCpp, Position(), MarkupKind::PlainText));
EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Position(),
clangd::CodeCompleteOptions()))
.Completions,
ElementsAre(Field(&CodeCompletion::Name, "int"),
Field(&CodeCompletion::Name, "main")));
}
TEST(ClangdThreadingTest, StressTest) {
static const unsigned FilesCount = 5;
const unsigned RequestsCount = 500;
const unsigned BlockingRequestInterval = 40;
const auto *SourceContentsWithoutErrors = R"cpp(
int a;
int b;
int c;
int d;
)cpp";
const auto *SourceContentsWithErrors = R"cpp(
int a = x;
int b;
int c;
int d;
)cpp";
unsigned MaxLineForFileRequests = 7;
unsigned MaxColumnForFileRequests = 10;
std::vector<std::string> FilePaths;
MockFS FS;
for (unsigned I = 0; I < FilesCount; ++I) {
std::string Name = std::string("Foo") + std::to_string(I) + ".cpp";
FS.Files[Name] = "";
FilePaths.push_back(testPath(Name));
}
struct FileStat {
unsigned HitsWithoutErrors = 0;
unsigned HitsWithErrors = 0;
bool HadErrorsInLastDiags = false;
};
class TestDiagConsumer : public ClangdServer::Callbacks {
public:
TestDiagConsumer() : Stats(FilesCount, FileStat()) {}
void onDiagnosticsReady(PathRef File, llvm::StringRef Version,
llvm::ArrayRef<Diag> Diagnostics) override {
StringRef FileIndexStr = llvm::sys::path::stem(File);
ASSERT_TRUE(FileIndexStr.consume_front("Foo"));
unsigned long FileIndex = std::stoul(FileIndexStr.str());
bool HadError = diagsContainErrors(Diagnostics);
std::lock_guard<std::mutex> Lock(Mutex);
if (HadError)
Stats[FileIndex].HitsWithErrors++;
else
Stats[FileIndex].HitsWithoutErrors++;
Stats[FileIndex].HadErrorsInLastDiags = HadError;
}
std::vector<FileStat> takeFileStats() {
std::lock_guard<std::mutex> Lock(Mutex);
return std::move(Stats);
}
private:
std::mutex Mutex;
std::vector<FileStat> Stats;
};
struct RequestStats {
unsigned RequestsWithoutErrors = 0;
unsigned RequestsWithErrors = 0;
bool LastContentsHadErrors = false;
bool FileIsRemoved = true;
};
std::vector<RequestStats> ReqStats;
ReqStats.reserve(FilesCount);
for (unsigned FileIndex = 0; FileIndex < FilesCount; ++FileIndex)
ReqStats.emplace_back();
TestDiagConsumer DiagConsumer;
{
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
std::random_device RandGen;
std::uniform_int_distribution<unsigned> FileIndexDist(0, FilesCount - 1);
std::bernoulli_distribution ShouldHaveErrorsDist(0.2);
std::uniform_int_distribution<int> LineDist(0, MaxLineForFileRequests);
std::uniform_int_distribution<int> ColumnDist(0, MaxColumnForFileRequests);
auto UpdateStatsOnAddDocument = [&](unsigned FileIndex, bool HadErrors) {
auto &Stats = ReqStats[FileIndex];
if (HadErrors)
++Stats.RequestsWithErrors;
else
++Stats.RequestsWithoutErrors;
Stats.LastContentsHadErrors = HadErrors;
Stats.FileIsRemoved = false;
};
auto UpdateStatsOnRemoveDocument = [&](unsigned FileIndex) {
auto &Stats = ReqStats[FileIndex];
Stats.FileIsRemoved = true;
};
auto AddDocument = [&](unsigned FileIndex, bool SkipCache) {
bool ShouldHaveErrors = ShouldHaveErrorsDist(RandGen);
Server.addDocument(FilePaths[FileIndex],
ShouldHaveErrors ? SourceContentsWithErrors
: SourceContentsWithoutErrors);
UpdateStatsOnAddDocument(FileIndex, ShouldHaveErrors);
};
auto AddDocumentRequest = [&]() {
unsigned FileIndex = FileIndexDist(RandGen);
AddDocument(FileIndex, false);
};
auto ForceReparseRequest = [&]() {
unsigned FileIndex = FileIndexDist(RandGen);
AddDocument(FileIndex, true);
};
auto RemoveDocumentRequest = [&]() {
unsigned FileIndex = FileIndexDist(RandGen);
if (ReqStats[FileIndex].FileIsRemoved)
AddDocument(FileIndex, false);
Server.removeDocument(FilePaths[FileIndex]);
UpdateStatsOnRemoveDocument(FileIndex);
};
auto CodeCompletionRequest = [&]() {
unsigned FileIndex = FileIndexDist(RandGen);
if (ReqStats[FileIndex].FileIsRemoved)
AddDocument(FileIndex, false);
Position Pos;
Pos.line = LineDist(RandGen);
Pos.character = ColumnDist(RandGen);
cantFail(runCodeComplete(Server, FilePaths[FileIndex], Pos,
clangd::CodeCompleteOptions()));
};
auto LocateSymbolRequest = [&]() {
unsigned FileIndex = FileIndexDist(RandGen);
if (ReqStats[FileIndex].FileIsRemoved)
AddDocument(FileIndex, false);
Position Pos;
Pos.line = LineDist(RandGen);
Pos.character = ColumnDist(RandGen);
ASSERT_TRUE(!!runLocateSymbolAt(Server, FilePaths[FileIndex], Pos));
};
std::vector<std::function<void()>> AsyncRequests = {
AddDocumentRequest, ForceReparseRequest, RemoveDocumentRequest};
std::vector<std::function<void()>> BlockingRequests = {
CodeCompletionRequest, LocateSymbolRequest};
std::uniform_int_distribution<int> AsyncRequestIndexDist(
0, AsyncRequests.size() - 1);
std::uniform_int_distribution<int> BlockingRequestIndexDist(
0, BlockingRequests.size() - 1);
for (unsigned I = 1; I <= RequestsCount; ++I) {
if (I % BlockingRequestInterval != 0) {
unsigned RequestIndex = AsyncRequestIndexDist(RandGen);
AsyncRequests[RequestIndex]();
} else {
auto RequestIndex = BlockingRequestIndexDist(RandGen);
BlockingRequests[RequestIndex]();
}
}
ASSERT_TRUE(Server.blockUntilIdleForTest());
}
std::vector<FileStat> Stats = DiagConsumer.takeFileStats();
for (unsigned I = 0; I < FilesCount; ++I) {
if (!ReqStats[I].FileIsRemoved) {
ASSERT_EQ(Stats[I].HadErrorsInLastDiags,
ReqStats[I].LastContentsHadErrors);
}
ASSERT_LE(Stats[I].HitsWithErrors, ReqStats[I].RequestsWithErrors);
ASSERT_LE(Stats[I].HitsWithoutErrors, ReqStats[I].RequestsWithoutErrors);
}
}
TEST(ClangdThreadingTest, NoConcurrentDiagnostics) {
class NoConcurrentAccessDiagConsumer : public ClangdServer::Callbacks {
public:
std::atomic<int> Count = {0};
NoConcurrentAccessDiagConsumer(std::promise<void> StartSecondReparse)
: StartSecondReparse(std::move(StartSecondReparse)) {}
void onDiagnosticsReady(PathRef, llvm::StringRef,
llvm::ArrayRef<Diag>) override {
++Count;
std::unique_lock<std::mutex> Lock(Mutex, std::try_to_lock_t());
ASSERT_TRUE(Lock.owns_lock())
<< "Detected concurrent onDiagnosticsReady calls for the same file.";
if (FirstRequest) {
FirstRequest = false;
StartSecondReparse.set_value();
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
private:
std::mutex Mutex;
bool FirstRequest = true;
std::promise<void> StartSecondReparse;
};
const auto *SourceContentsWithoutErrors = R"cpp(
int a;
int b;
int c;
int d;
)cpp";
const auto *SourceContentsWithErrors = R"cpp(
int a = x;
int b;
int c;
int d;
)cpp";
auto FooCpp = testPath("foo.cpp");
MockFS FS;
FS.Files[FooCpp] = "";
std::promise<void> StartSecondPromise;
std::future<void> StartSecond = StartSecondPromise.get_future();
NoConcurrentAccessDiagConsumer DiagConsumer(std::move(StartSecondPromise));
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
Server.addDocument(FooCpp, SourceContentsWithErrors);
StartSecond.wait();
Server.addDocument(FooCpp, SourceContentsWithoutErrors);
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
ASSERT_EQ(DiagConsumer.Count, 2);
}
TEST(ClangdServerTest, FormatCode) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
auto Path = testPath("foo.cpp");
std::string Code = R"cpp(
#include "x.h"
#include "y.h"
void f( ) {}
)cpp";
std::string Expected = R"cpp(
#include "x.h"
#include "y.h"
void f() {}
)cpp";
FS.Files[Path] = Code;
runAddDocument(Server, Path, Code);
auto Replaces = runFormatFile(Server, Path, std::nullopt);
EXPECT_TRUE(static_cast<bool>(Replaces));
auto Changed = tooling::applyAllReplacements(Code, *Replaces);
EXPECT_TRUE(static_cast<bool>(Changed));
EXPECT_EQ(Expected, *Changed);
}
TEST(ClangdServerTest, ChangedHeaderFromISystem) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
auto SourcePath = testPath("source/foo.cpp");
auto HeaderPath = testPath("headers/foo.h");
FS.Files[HeaderPath] = "struct X { int bar; };";
Annotations Code(R"cpp(
#include "foo.h"
int main() {
X().ba^
})cpp");
CDB.ExtraClangFlags.push_back("-xc++");
CDB.ExtraClangFlags.push_back("-isystem" + testPath("headers"));
runAddDocument(Server, SourcePath, Code.code());
auto Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(),
clangd::CodeCompleteOptions()))
.Completions;
EXPECT_THAT(Completions, ElementsAre(Field(&CodeCompletion::Name, "bar")));
FS.Files[HeaderPath] = "struct X { int bar; int baz; };";
runAddDocument(Server, SourcePath, Code.code());
Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(),
clangd::CodeCompleteOptions()))
.Completions;
EXPECT_THAT(Completions, ElementsAre(Field(&CodeCompletion::Name, "bar"),
Field(&CodeCompletion::Name, "baz")));
}
#ifndef _WIN32
TEST(ClangdTests, PreambleVFSStatCache) {
class StatRecordingFS : public ThreadsafeFS {
llvm::StringMap<unsigned> &CountStats;
public:
llvm::StringMap<std::string> Files;
StatRecordingFS(llvm::StringMap<unsigned> &CountStats)
: CountStats(CountStats) {}
private:
IntrusiveRefCntPtr<llvm::vfs::FileSystem> viewImpl() const override {
class StatRecordingVFS : public llvm::vfs::ProxyFileSystem {
public:
StatRecordingVFS(IntrusiveRefCntPtr<llvm::vfs::FileSystem> FS,
llvm::StringMap<unsigned> &CountStats)
: ProxyFileSystem(std::move(FS)), CountStats(CountStats) {}
llvm::ErrorOr<std::unique_ptr<llvm::vfs::File>>
openFileForRead(const Twine &Path) override {
++CountStats[llvm::sys::path::filename(Path.str())];
return ProxyFileSystem::openFileForRead(Path);
}
llvm::ErrorOr<llvm::vfs::Status> status(const Twine &Path) override {
++CountStats[llvm::sys::path::filename(Path.str())];
return ProxyFileSystem::status(Path);
}
private:
llvm::StringMap<unsigned> &CountStats;
};
return IntrusiveRefCntPtr<StatRecordingVFS>(
new StatRecordingVFS(buildTestFS(Files), CountStats));
}
};
llvm::StringMap<unsigned> CountStats;
StatRecordingFS FS(CountStats);
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
auto SourcePath = testPath("foo.cpp");
auto HeaderPath = testPath("foo.h");
FS.Files[HeaderPath] = "struct TestSym {};";
Annotations Code(R"cpp(
#include "foo.h"
int main() {
TestSy^
})cpp");
runAddDocument(Server, SourcePath, Code.code());
unsigned Before = CountStats["foo.h"];
EXPECT_GT(Before, 0u);
auto Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(),
clangd::CodeCompleteOptions()))
.Completions;
EXPECT_EQ(CountStats["foo.h"], Before);
EXPECT_THAT(Completions,
ElementsAre(Field(&CodeCompletion::Name, "TestSym")));
}
#endif
TEST(ClangdServerTest, FallbackWhenPreambleIsNotReady) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
auto FooCpp = testPath("foo.cpp");
Annotations Code(R"cpp(
namespace ns { int xyz; }
using namespace ns;
int main() {
xy^
})cpp");
FS.Files[FooCpp] = FooCpp;
auto Opts = clangd::CodeCompleteOptions();
Opts.RunParser = CodeCompleteOptions::ParseIfReady;
CDB.ExtraClangFlags = {"-###"};
Server.addDocument(FooCpp, Code.code());
ASSERT_TRUE(Server.blockUntilIdleForTest());
auto Res = cantFail(runCodeComplete(Server, FooCpp, Code.point(), Opts));
EXPECT_EQ(Res.Context, CodeCompletionContext::CCC_Recovery);
EXPECT_THAT(Res.Completions,
ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"),
Field(&CodeCompletion::Scope, ""))));
CDB.ExtraClangFlags = {"-std=c++11"};
Server.addDocument(FooCpp, Code.code());
ASSERT_TRUE(Server.blockUntilIdleForTest());
EXPECT_THAT(
cantFail(runCodeComplete(Server, FooCpp, Code.point(), Opts)).Completions,
ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"),
Field(&CodeCompletion::Scope, "ns::"))));
Opts.RunParser = CodeCompleteOptions::NeverParse;
EXPECT_THAT(
cantFail(runCodeComplete(Server, FooCpp, Code.point(), Opts)).Completions,
ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"),
Field(&CodeCompletion::Scope, ""))));
}
TEST(ClangdServerTest, FallbackWhenWaitingForCompileCommand) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
class DelayedCompilationDatabase : public GlobalCompilationDatabase {
public:
DelayedCompilationDatabase(Notification &CanReturnCommand)
: CanReturnCommand(CanReturnCommand) {}
std::optional<tooling::CompileCommand>
getCompileCommand(PathRef File) const override {
CanReturnCommand.wait();
auto FileName = llvm::sys::path::filename(File);
std::vector<std::string> CommandLine = {"clangd", "-ffreestanding",
std::string(File)};
return {tooling::CompileCommand(llvm::sys::path::parent_path(File),
FileName, std::move(CommandLine), "")};
}
std::vector<std::string> ExtraClangFlags;
private:
Notification &CanReturnCommand;
};
Notification CanReturnCommand;
DelayedCompilationDatabase CDB(CanReturnCommand);
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
auto FooCpp = testPath("foo.cpp");
Annotations Code(R"cpp(
namespace ns { int xyz; }
using namespace ns;
int main() {
xy^
})cpp");
FS.Files[FooCpp] = FooCpp;
Server.addDocument(FooCpp, Code.code());
std::this_thread::sleep_for(std::chrono::milliseconds(10));
auto Opts = clangd::CodeCompleteOptions();
Opts.RunParser = CodeCompleteOptions::ParseIfReady;
auto Res = cantFail(runCodeComplete(Server, FooCpp, Code.point(), Opts));
EXPECT_EQ(Res.Context, CodeCompletionContext::CCC_Recovery);
CanReturnCommand.notify();
ASSERT_TRUE(Server.blockUntilIdleForTest());
EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Code.point(),
clangd::CodeCompleteOptions()))
.Completions,
ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"),
Field(&CodeCompletion::Scope, "ns::"))));
}
TEST(ClangdServerTest, CustomAction) {
OverlayCDB CDB(nullptr);
MockFS FS;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest());
Server.addDocument(testPath("foo.cc"), "void x();");
Decl::Kind XKind = Decl::TranslationUnit;
EXPECT_THAT_ERROR(runCustomAction(Server, testPath("foo.cc"),
[&](InputsAndAST AST) {
XKind = findDecl(AST.AST, "x").getKind();
}),
llvm::Succeeded());
EXPECT_EQ(XKind, Decl::Function);
}
#if !defined(__has_feature) || !__has_feature(address_sanitizer)
TEST(ClangdServerTest, TestStackOverflow) {
MockFS FS;
ErrorCheckingCallbacks DiagConsumer;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &DiagConsumer);
const char *SourceContents = R"cpp(
constexpr int foo() { return foo(); }
static_assert(foo());
)cpp";
auto FooCpp = testPath("foo.cpp");
FS.Files[FooCpp] = SourceContents;
Server.addDocument(FooCpp, SourceContents);
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags());
}
#endif
TEST(ClangdServer, TidyOverrideTest) {
struct DiagsCheckingCallback : public ClangdServer::Callbacks {
public:
void onDiagnosticsReady(PathRef File, llvm::StringRef Version,
llvm::ArrayRef<Diag> Diagnostics) override {
std::lock_guard<std::mutex> Lock(Mutex);
HadDiagsInLastCallback = !Diagnostics.empty();
}
std::mutex Mutex;
bool HadDiagsInLastCallback = false;
} DiagConsumer;
MockFS FS;
FS.Files[testPath(".clang-tidy")] = R"(
Checks: -*,bugprone-use-after-move,llvm-header-guard
)";
MockCompilationDatabase CDB;
std::vector<TidyProvider> Stack;
Stack.push_back(provideClangTidyFiles(FS));
Stack.push_back(disableUnusableChecks());
TidyProvider Provider = combine(std::move(Stack));
CDB.ExtraClangFlags = {"-xc++"};
auto Opts = ClangdServer::optsForTest();
Opts.ClangTidyProvider = Provider;
ClangdServer Server(CDB, FS, Opts, &DiagConsumer);
const char *SourceContents = R"cpp(
struct Foo { Foo(); Foo(Foo&); Foo(Foo&&); };
namespace std { Foo&& move(Foo&); }
void foo() {
Foo x;
Foo y = std::move(x);
Foo z = x;
})cpp";
Server.addDocument(testPath("foo.h"), SourceContents);
ASSERT_TRUE(Server.blockUntilIdleForTest());
EXPECT_FALSE(DiagConsumer.HadDiagsInLastCallback);
}
TEST(ClangdServer, MemoryUsageTest) {
MockFS FS;
MockCompilationDatabase CDB;
ClangdServer Server(CDB, FS, ClangdServer::optsForTest());
auto FooCpp = testPath("foo.cpp");
Server.addDocument(FooCpp, "");
ASSERT_TRUE(Server.blockUntilIdleForTest());
llvm::BumpPtrAllocator Alloc;
MemoryTree MT(&Alloc);
Server.profile(MT);
ASSERT_TRUE(MT.children().count("tuscheduler"));
EXPECT_TRUE(MT.child("tuscheduler").children().count(FooCpp));
}
TEST(ClangdServer, RespectsTweakFormatting) {
static constexpr const char *TweakID = "ModuleTweak";
static constexpr const char *NewContents = "{not;\nformatted;}";
struct TweakContributingModule final : public FeatureModule {
struct ModuleTweak final : public Tweak {
const char *id() const override { return TweakID; }
bool prepare(const Selection &Sel) override { return true; }
Expected<Effect> apply(const Selection &Sel) override {
auto &SM = Sel.AST->getSourceManager();
llvm::StringRef FilePath = SM.getFilename(Sel.Cursor);
tooling::Replacements Reps;
llvm::cantFail(
Reps.add(tooling::Replacement(FilePath, 0, 0, NewContents)));
auto E = llvm::cantFail(Effect::mainFileEdit(SM, std::move(Reps)));
E.FormatEdits = false;
return E;
}
std::string title() const override { return id(); }
llvm::StringLiteral kind() const override {
return llvm::StringLiteral("");
};
};
void contributeTweaks(std::vector<std::unique_ptr<Tweak>> &Out) override {
Out.emplace_back(new ModuleTweak);
}
};
MockFS FS;
MockCompilationDatabase CDB;
auto Opts = ClangdServer::optsForTest();
FeatureModuleSet Set;
Set.add(std::make_unique<TweakContributingModule>());
Opts.FeatureModules = &Set;
ClangdServer Server(CDB, FS, Opts);
auto FooCpp = testPath("foo.cpp");
Server.addDocument(FooCpp, "");
ASSERT_TRUE(Server.blockUntilIdleForTest());
Notification N;
Server.applyTweak(FooCpp, {}, TweakID, [&](llvm::Expected<Tweak::Effect> E) {
ASSERT_TRUE(static_cast<bool>(E));
EXPECT_THAT(llvm::cantFail(E->ApplyEdits.lookup(FooCpp).apply()),
NewContents);
N.notify();
});
N.wait();
}
TEST(ClangdServer, InactiveRegions) {
struct InactiveRegionsCallback : ClangdServer::Callbacks {
std::vector<std::vector<Range>> FoundInactiveRegions;
void onInactiveRegionsReady(PathRef FIle,
std::vector<Range> InactiveRegions) override {
FoundInactiveRegions.push_back(std::move(InactiveRegions));
}
};
MockFS FS;
MockCompilationDatabase CDB;
CDB.ExtraClangFlags.push_back("-DCMDMACRO");
auto Opts = ClangdServer::optsForTest();
Opts.PublishInactiveRegions = true;
InactiveRegionsCallback Callback;
ClangdServer Server(CDB, FS, Opts, &Callback);
Annotations Source(R"cpp(
#define PREAMBLEMACRO 42
#if PREAMBLEMACRO > 40
#define ACTIVE
#else
$inactive1[[ #define INACTIVE]]
#endif
int endPreamble;
#ifndef CMDMACRO
$inactive2[[ int inactiveInt;]]
#endif
#undef CMDMACRO
#ifdef CMDMACRO
$inactive3[[ int inactiveInt2;]]
#elif PREAMBLEMACRO > 0
int activeInt1;
int activeInt2;
#else
$inactive4[[ int inactiveInt3;]]
#endif
#ifdef CMDMACRO
#endif // empty inactive range, gets dropped
)cpp");
Server.addDocument(testPath("foo.cpp"), Source.code());
ASSERT_TRUE(Server.blockUntilIdleForTest());
EXPECT_THAT(Callback.FoundInactiveRegions,
ElementsAre(ElementsAre(
Source.range("inactive1"), Source.range("inactive2"),
Source.range("inactive3"), Source.range("inactive4"))));
}
}
}
}