forked from OSchip/llvm-project
1163 lines
38 KiB
C++
1163 lines
38 KiB
C++
//===-- ClangdTests.cpp - Clangd unit tests ---------------------*- 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 "ClangdLSPServer.h"
|
|
#include "ClangdServer.h"
|
|
#include "GlobalCompilationDatabase.h"
|
|
#include "Matchers.h"
|
|
#include "SyncAPI.h"
|
|
#include "TestFS.h"
|
|
#include "Threading.h"
|
|
#include "URI.h"
|
|
#include "clang/Config/config.h"
|
|
#include "clang/Sema/CodeCompleteConsumer.h"
|
|
#include "llvm/ADT/SmallVector.h"
|
|
#include "llvm/ADT/StringMap.h"
|
|
#include "llvm/Support/Errc.h"
|
|
#include "llvm/Support/Path.h"
|
|
#include "llvm/Support/Regex.h"
|
|
#include "gmock/gmock.h"
|
|
#include "gtest/gtest.h"
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <iostream>
|
|
#include <random>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
namespace clang {
|
|
namespace clangd {
|
|
|
|
namespace {
|
|
|
|
using ::testing::ElementsAre;
|
|
using ::testing::Field;
|
|
using ::testing::Gt;
|
|
using ::testing::IsEmpty;
|
|
using ::testing::Pair;
|
|
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 ErrorCheckingDiagConsumer : public DiagnosticsConsumer {
|
|
public:
|
|
void onDiagnosticsReady(PathRef File,
|
|
std::vector<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;
|
|
};
|
|
|
|
/// For each file, record whether the last published diagnostics contained at
|
|
/// least one error.
|
|
class MultipleErrorCheckingDiagConsumer : public DiagnosticsConsumer {
|
|
public:
|
|
void onDiagnosticsReady(PathRef File,
|
|
std::vector<Diag> Diagnostics) override {
|
|
bool HadError = diagsContainErrors(Diagnostics);
|
|
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
LastDiagsHadError[File] = HadError;
|
|
}
|
|
|
|
/// Exposes all files consumed by onDiagnosticsReady in an unspecified order.
|
|
/// For each file, a bool value indicates whether the last diagnostics
|
|
/// contained an error.
|
|
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(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;
|
|
};
|
|
|
|
/// Replaces all patterns of the form 0x123abc with spaces
|
|
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 dumpASTWithoutMemoryLocs(ClangdServer &Server, PathRef File) {
|
|
auto DumpWithMemLocs = runDumpAST(Server, File);
|
|
return replacePtrsInDump(DumpWithMemLocs);
|
|
}
|
|
|
|
class ClangdVFSTest : public ::testing::Test {
|
|
protected:
|
|
std::string parseSourceAndDumpAST(
|
|
PathRef SourceFileRelPath, llvm::StringRef SourceContents,
|
|
std::vector<std::pair<PathRef, llvm::StringRef>> ExtraFiles = {},
|
|
bool ExpectErrors = false) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
for (const auto &FileWithContents : ExtraFiles)
|
|
FS.Files[testPath(FileWithContents.first)] = 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_F(ClangdVFSTest, Parse) {
|
|
// FIXME: figure out a stable format for AST dumps, so that we can check the
|
|
// output of the dump itself is equal to the expected one, not just that it's
|
|
// different.
|
|
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_F(ClangdVFSTest, ParseWithHeader) {
|
|
parseSourceAndDumpAST("foo.cpp", "#include \"foo.h\"", {},
|
|
/*ExpectErrors=*/true);
|
|
parseSourceAndDumpAST("foo.cpp", "#include \"foo.h\"", {{"foo.h", ""}},
|
|
/*ExpectErrors=*/false);
|
|
|
|
const auto SourceContents = R"cpp(
|
|
#include "foo.h"
|
|
int b = a;
|
|
)cpp";
|
|
parseSourceAndDumpAST("foo.cpp", SourceContents, {{"foo.h", ""}},
|
|
/*ExpectErrors=*/true);
|
|
parseSourceAndDumpAST("foo.cpp", SourceContents, {{"foo.h", "int a;"}},
|
|
/*ExpectErrors=*/false);
|
|
}
|
|
|
|
TEST_F(ClangdVFSTest, Reparse) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
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);
|
|
auto DumpParse1 = dumpASTWithoutMemoryLocs(Server, FooCpp);
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
|
|
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
|
|
|
|
Server.addDocument(FooCpp, "");
|
|
auto DumpParseEmpty = dumpASTWithoutMemoryLocs(Server, FooCpp);
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
|
|
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
|
|
|
|
Server.addDocument(FooCpp, SourceContents);
|
|
auto DumpParse2 = dumpASTWithoutMemoryLocs(Server, FooCpp);
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
|
|
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
|
|
|
|
EXPECT_EQ(DumpParse1, DumpParse2);
|
|
EXPECT_NE(DumpParse1, DumpParseEmpty);
|
|
}
|
|
|
|
TEST_F(ClangdVFSTest, ReparseOnHeaderChange) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
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);
|
|
auto DumpParse1 = dumpASTWithoutMemoryLocs(Server, FooCpp);
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
|
|
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
|
|
|
|
FS.Files[FooH] = "";
|
|
Server.addDocument(FooCpp, SourceContents);
|
|
auto DumpParseDifferent = dumpASTWithoutMemoryLocs(Server, FooCpp);
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
|
|
EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags());
|
|
|
|
FS.Files[FooH] = "int a;";
|
|
Server.addDocument(FooCpp, SourceContents);
|
|
auto DumpParse2 = dumpASTWithoutMemoryLocs(Server, FooCpp);
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
|
|
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
|
|
|
|
EXPECT_EQ(DumpParse1, DumpParse2);
|
|
EXPECT_NE(DumpParse1, DumpParseDifferent);
|
|
}
|
|
|
|
TEST_F(ClangdVFSTest, PropagatesContexts) {
|
|
static Key<int> Secret;
|
|
struct FSProvider : public FileSystemProvider {
|
|
IntrusiveRefCntPtr<llvm::vfs::FileSystem> getFileSystem() const override {
|
|
Got = Context::current().getExisting(Secret);
|
|
return buildTestFS({});
|
|
}
|
|
mutable int Got;
|
|
} FS;
|
|
struct DiagConsumer : public DiagnosticsConsumer {
|
|
void onDiagnosticsReady(PathRef File,
|
|
std::vector<Diag> Diagnostics) override {
|
|
Got = Context::current().getExisting(Secret);
|
|
}
|
|
int Got;
|
|
} DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
|
|
// Verify that the context is plumbed to the FS provider and diagnostics.
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
{
|
|
WithContextValue Entrypoint(Secret, 42);
|
|
Server.addDocument(testPath("foo.cpp"), "void main(){}");
|
|
}
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest());
|
|
EXPECT_EQ(FS.Got, 42);
|
|
EXPECT_EQ(DiagConsumer.Got, 42);
|
|
}
|
|
|
|
// Only enable this test on Unix
|
|
#ifdef LLVM_ON_UNIX
|
|
TEST_F(ClangdVFSTest, SearchLibDir) {
|
|
// Checks that searches for GCC installation is done through vfs.
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
CDB.ExtraClangFlags.insert(CDB.ExtraClangFlags.end(),
|
|
{"-xc++", "-target", "x86_64-linux-unknown",
|
|
"-m64", "--gcc-toolchain=/randomusr",
|
|
"-stdlib=libstdc++"});
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
// Just a random gcc version string
|
|
SmallString<8> Version("4.9.3");
|
|
|
|
// A lib dir for gcc installation
|
|
SmallString<64> LibDir("/randomusr/lib/gcc/x86_64-linux-gnu");
|
|
llvm::sys::path::append(LibDir, Version);
|
|
|
|
// Put crtbegin.o into LibDir/64 to trick clang into thinking there's a gcc
|
|
// installation there.
|
|
SmallString<64> DummyLibFile;
|
|
llvm::sys::path::append(DummyLibFile, LibDir, "64", "crtbegin.o");
|
|
FS.Files[DummyLibFile] = "";
|
|
|
|
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 // LLVM_ON_UNIX
|
|
|
|
TEST_F(ClangdVFSTest, ForceReparseCompileCommand) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
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] = "";
|
|
|
|
// First parse files in C mode and check they produce errors.
|
|
CDB.ExtraClangFlags = {"-xc"};
|
|
runAddDocument(Server, FooCpp, SourceContents1);
|
|
EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags());
|
|
runAddDocument(Server, FooCpp, SourceContents2);
|
|
EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags());
|
|
|
|
// Now switch to C++ mode.
|
|
CDB.ExtraClangFlags = {"-xc++"};
|
|
runAddDocument(Server, FooCpp, SourceContents2, WantDiagnostics::Auto);
|
|
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
|
|
// Subsequent addDocument calls should finish without errors too.
|
|
runAddDocument(Server, FooCpp, SourceContents1);
|
|
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
|
|
runAddDocument(Server, FooCpp, SourceContents2);
|
|
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
|
|
}
|
|
|
|
TEST_F(ClangdVFSTest, ForceReparseCompileCommandDefines) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
auto FooCpp = testPath("foo.cpp");
|
|
const auto SourceContents = R"cpp(
|
|
#ifdef WITH_ERROR
|
|
this
|
|
#endif
|
|
|
|
int main() { return 0; }
|
|
)cpp";
|
|
FS.Files[FooCpp] = "";
|
|
|
|
// Parse with define, we expect to see the errors.
|
|
CDB.ExtraClangFlags = {"-DWITH_ERROR"};
|
|
runAddDocument(Server, FooCpp, SourceContents);
|
|
EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags());
|
|
|
|
// Parse without the define, no errors should be produced.
|
|
CDB.ExtraClangFlags = {};
|
|
runAddDocument(Server, FooCpp, SourceContents, WantDiagnostics::Auto);
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest());
|
|
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
|
|
// Subsequent addDocument call should finish without errors too.
|
|
runAddDocument(Server, FooCpp, SourceContents);
|
|
EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags());
|
|
}
|
|
|
|
// Test ClangdServer.reparseOpenedFiles.
|
|
TEST_F(ClangdVFSTest, 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");
|
|
|
|
MockFSProvider FS;
|
|
MockCompilationDatabase CDB;
|
|
MultipleErrorCheckingDiagConsumer DiagConsumer;
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
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"))));
|
|
|
|
// Undefine MACRO, close baz.cpp.
|
|
CDB.ExtraClangFlags.clear();
|
|
DiagConsumer.clear();
|
|
Server.removeDocument(BazCpp);
|
|
Server.addDocument(FooCpp, FooSource.code(), WantDiagnostics::Auto);
|
|
Server.addDocument(BarCpp, BarSource.code(), WantDiagnostics::Auto);
|
|
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"))));
|
|
}
|
|
|
|
TEST_F(ClangdVFSTest, MemoryUsage) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
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.getUsedBytesPerFile(), IsEmpty());
|
|
|
|
Server.addDocument(FooCpp, SourceContents);
|
|
Server.addDocument(BarCpp, SourceContents);
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest());
|
|
|
|
EXPECT_THAT(Server.getUsedBytesPerFile(),
|
|
UnorderedElementsAre(Pair(FooCpp, Gt(0u)), Pair(BarCpp, Gt(0u))));
|
|
|
|
Server.removeDocument(FooCpp);
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest());
|
|
EXPECT_THAT(Server.getUsedBytesPerFile(), ElementsAre(Pair(BarCpp, Gt(0u))));
|
|
|
|
Server.removeDocument(BarCpp);
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest());
|
|
EXPECT_THAT(Server.getUsedBytesPerFile(), IsEmpty());
|
|
}
|
|
|
|
TEST_F(ClangdVFSTest, InvalidCompileCommand) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
auto FooCpp = testPath("foo.cpp");
|
|
// clang cannot create CompilerInvocation if we pass two files in the
|
|
// CompileCommand. We pass the file in ExtraFlags once and CDB adds another
|
|
// one in getCompileCommand().
|
|
CDB.ExtraClangFlags.push_back(FooCpp);
|
|
|
|
// Clang can't parse command args in that case, but we shouldn't crash.
|
|
runAddDocument(Server, FooCpp, "int main() {}");
|
|
|
|
EXPECT_EQ(runDumpAST(Server, FooCpp), "<no-ast>");
|
|
EXPECT_ERROR(runLocateSymbolAt(Server, FooCpp, Position()));
|
|
EXPECT_ERROR(runFindDocumentHighlights(Server, FooCpp, Position()));
|
|
EXPECT_ERROR(runRename(Server, FooCpp, Position(), "new_name"));
|
|
// Identifier-based fallback completion.
|
|
EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Position(),
|
|
clangd::CodeCompleteOptions()))
|
|
.Completions,
|
|
ElementsAre(Field(&CodeCompletion::Name, "int"),
|
|
Field(&CodeCompletion::Name, "main")));
|
|
auto SigHelp = runSignatureHelp(Server, FooCpp, Position());
|
|
ASSERT_TRUE(bool(SigHelp)) << "signatureHelp returned an error";
|
|
EXPECT_THAT(SigHelp->signatures, IsEmpty());
|
|
}
|
|
|
|
class ClangdThreadingTest : public ClangdVFSTest {};
|
|
|
|
TEST_F(ClangdThreadingTest, StressTest) {
|
|
// Without 'static' clang gives an error for a usage inside TestDiagConsumer.
|
|
static const unsigned FilesCount = 5;
|
|
const unsigned RequestsCount = 500;
|
|
// Blocking requests wait for the parsing to complete, they slow down the test
|
|
// dramatically, so they are issued rarely. Each
|
|
// BlockingRequestInterval-request will be a blocking one.
|
|
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";
|
|
|
|
// Giving invalid line and column number should not crash ClangdServer, but
|
|
// just to make sure we're sometimes hitting the bounds inside the file we
|
|
// limit the intervals of line and column number that are generated.
|
|
unsigned MaxLineForFileRequests = 7;
|
|
unsigned MaxColumnForFileRequests = 10;
|
|
|
|
std::vector<std::string> FilePaths;
|
|
MockFSProvider 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 DiagnosticsConsumer {
|
|
public:
|
|
TestDiagConsumer() : Stats(FilesCount, FileStat()) {}
|
|
|
|
void onDiagnosticsReady(PathRef File,
|
|
std::vector<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, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
// Prepare some random distributions for the test.
|
|
std::random_device RandGen;
|
|
|
|
std::uniform_int_distribution<unsigned> FileIndexDist(0, FilesCount - 1);
|
|
// Pass a text that contains compiler errors to addDocument in about 20% of
|
|
// all requests.
|
|
std::bernoulli_distribution ShouldHaveErrorsDist(0.2);
|
|
// Line and Column numbers for requests that need them.
|
|
std::uniform_int_distribution<int> LineDist(0, MaxLineForFileRequests);
|
|
std::uniform_int_distribution<int> ColumnDist(0, MaxColumnForFileRequests);
|
|
|
|
// Some helpers.
|
|
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,
|
|
WantDiagnostics::Auto);
|
|
UpdateStatsOnAddDocument(FileIndex, ShouldHaveErrors);
|
|
};
|
|
|
|
// Various requests that we would randomly run.
|
|
auto AddDocumentRequest = [&]() {
|
|
unsigned FileIndex = FileIndexDist(RandGen);
|
|
AddDocument(FileIndex, /*SkipCache=*/false);
|
|
};
|
|
|
|
auto ForceReparseRequest = [&]() {
|
|
unsigned FileIndex = FileIndexDist(RandGen);
|
|
AddDocument(FileIndex, /*SkipCache=*/true);
|
|
};
|
|
|
|
auto RemoveDocumentRequest = [&]() {
|
|
unsigned FileIndex = FileIndexDist(RandGen);
|
|
// Make sure we don't violate the ClangdServer's contract.
|
|
if (ReqStats[FileIndex].FileIsRemoved)
|
|
AddDocument(FileIndex, /*SkipCache=*/false);
|
|
|
|
Server.removeDocument(FilePaths[FileIndex]);
|
|
UpdateStatsOnRemoveDocument(FileIndex);
|
|
};
|
|
|
|
auto CodeCompletionRequest = [&]() {
|
|
unsigned FileIndex = FileIndexDist(RandGen);
|
|
// Make sure we don't violate the ClangdServer's contract.
|
|
if (ReqStats[FileIndex].FileIsRemoved)
|
|
AddDocument(FileIndex, /*SkipCache=*/false);
|
|
|
|
Position Pos;
|
|
Pos.line = LineDist(RandGen);
|
|
Pos.character = ColumnDist(RandGen);
|
|
// FIXME(ibiryukov): Also test async completion requests.
|
|
// Simply putting CodeCompletion into async requests now would make
|
|
// tests slow, since there's no way to cancel previous completion
|
|
// requests as opposed to AddDocument/RemoveDocument, which are implicitly
|
|
// cancelled by any subsequent AddDocument/RemoveDocument request to the
|
|
// same file.
|
|
cantFail(runCodeComplete(Server, FilePaths[FileIndex], Pos,
|
|
clangd::CodeCompleteOptions()));
|
|
};
|
|
|
|
auto LocateSymbolRequest = [&]() {
|
|
unsigned FileIndex = FileIndexDist(RandGen);
|
|
// Make sure we don't violate the ClangdServer's contract.
|
|
if (ReqStats[FileIndex].FileIsRemoved)
|
|
AddDocument(FileIndex, /*SkipCache=*/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};
|
|
|
|
// Bash requests to ClangdServer in a loop.
|
|
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) {
|
|
// Issue an async request most of the time. It should be fast.
|
|
unsigned RequestIndex = AsyncRequestIndexDist(RandGen);
|
|
AsyncRequests[RequestIndex]();
|
|
} else {
|
|
// Issue a blocking request once in a while.
|
|
auto RequestIndex = BlockingRequestIndexDist(RandGen);
|
|
BlockingRequests[RequestIndex]();
|
|
}
|
|
}
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest());
|
|
}
|
|
|
|
// Check some invariants about the state of the program.
|
|
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_F(ClangdVFSTest, CheckSourceHeaderSwitch) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
auto SourceContents = R"cpp(
|
|
#include "foo.h"
|
|
int b = a;
|
|
)cpp";
|
|
|
|
auto FooCpp = testPath("foo.cpp");
|
|
auto FooH = testPath("foo.h");
|
|
auto Invalid = testPath("main.cpp");
|
|
|
|
FS.Files[FooCpp] = SourceContents;
|
|
FS.Files[FooH] = "int a;";
|
|
FS.Files[Invalid] = "int main() { \n return 0; \n }";
|
|
|
|
Optional<Path> PathResult = Server.switchSourceHeader(FooCpp);
|
|
EXPECT_TRUE(PathResult.hasValue());
|
|
ASSERT_EQ(PathResult.getValue(), FooH);
|
|
|
|
PathResult = Server.switchSourceHeader(FooH);
|
|
EXPECT_TRUE(PathResult.hasValue());
|
|
ASSERT_EQ(PathResult.getValue(), FooCpp);
|
|
|
|
SourceContents = R"c(
|
|
#include "foo.HH"
|
|
int b = a;
|
|
)c";
|
|
|
|
// Test with header file in capital letters and different extension, source
|
|
// file with different extension
|
|
auto FooC = testPath("bar.c");
|
|
auto FooHH = testPath("bar.HH");
|
|
|
|
FS.Files[FooC] = SourceContents;
|
|
FS.Files[FooHH] = "int a;";
|
|
|
|
PathResult = Server.switchSourceHeader(FooC);
|
|
EXPECT_TRUE(PathResult.hasValue());
|
|
ASSERT_EQ(PathResult.getValue(), FooHH);
|
|
|
|
// Test with both capital letters
|
|
auto Foo2C = testPath("foo2.C");
|
|
auto Foo2HH = testPath("foo2.HH");
|
|
FS.Files[Foo2C] = SourceContents;
|
|
FS.Files[Foo2HH] = "int a;";
|
|
|
|
PathResult = Server.switchSourceHeader(Foo2C);
|
|
EXPECT_TRUE(PathResult.hasValue());
|
|
ASSERT_EQ(PathResult.getValue(), Foo2HH);
|
|
|
|
// Test with source file as capital letter and .hxx header file
|
|
auto Foo3C = testPath("foo3.C");
|
|
auto Foo3HXX = testPath("foo3.hxx");
|
|
|
|
SourceContents = R"c(
|
|
#include "foo3.hxx"
|
|
int b = a;
|
|
)c";
|
|
|
|
FS.Files[Foo3C] = SourceContents;
|
|
FS.Files[Foo3HXX] = "int a;";
|
|
|
|
PathResult = Server.switchSourceHeader(Foo3C);
|
|
EXPECT_TRUE(PathResult.hasValue());
|
|
ASSERT_EQ(PathResult.getValue(), Foo3HXX);
|
|
|
|
// Test if asking for a corresponding file that doesn't exist returns an empty
|
|
// string.
|
|
PathResult = Server.switchSourceHeader(Invalid);
|
|
EXPECT_FALSE(PathResult.hasValue());
|
|
}
|
|
|
|
TEST_F(ClangdThreadingTest, NoConcurrentDiagnostics) {
|
|
class NoConcurrentAccessDiagConsumer : public DiagnosticsConsumer {
|
|
public:
|
|
std::atomic<int> Count = {0};
|
|
|
|
NoConcurrentAccessDiagConsumer(std::promise<void> StartSecondReparse)
|
|
: StartSecondReparse(std::move(StartSecondReparse)) {}
|
|
|
|
void onDiagnosticsReady(PathRef, std::vector<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 we started the second parse immediately, it might cancel the first.
|
|
// So we don't allow it to start until the first has delivered diags...
|
|
if (FirstRequest) {
|
|
FirstRequest = false;
|
|
StartSecondReparse.set_value();
|
|
// ... but then we wait long enough that the callbacks would overlap.
|
|
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");
|
|
MockFSProvider 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, DiagConsumer, ClangdServer::optsForTest());
|
|
Server.addDocument(FooCpp, SourceContentsWithErrors);
|
|
StartSecond.wait();
|
|
Server.addDocument(FooCpp, SourceContentsWithoutErrors);
|
|
ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
|
|
ASSERT_EQ(DiagConsumer.Count, 2); // Sanity check - we actually ran both?
|
|
}
|
|
|
|
TEST_F(ClangdVFSTest, FormatCode) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
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 = Server.formatFile(Code, Path);
|
|
EXPECT_TRUE(static_cast<bool>(Replaces));
|
|
auto Changed = tooling::applyAllReplacements(Code, *Replaces);
|
|
EXPECT_TRUE(static_cast<bool>(Changed));
|
|
EXPECT_EQ(Expected, *Changed);
|
|
}
|
|
|
|
TEST_F(ClangdVFSTest, ChangedHeaderFromISystem) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
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")));
|
|
// Update the header and rerun addDocument to make sure we get the updated
|
|
// files.
|
|
FS.Files[HeaderPath] = "struct X { int bar; int baz; };";
|
|
runAddDocument(Server, SourcePath, Code.code());
|
|
Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(),
|
|
clangd::CodeCompleteOptions()))
|
|
.Completions;
|
|
// We want to make sure we see the updated version.
|
|
EXPECT_THAT(Completions, ElementsAre(Field(&CodeCompletion::Name, "bar"),
|
|
Field(&CodeCompletion::Name, "baz")));
|
|
}
|
|
|
|
// FIXME(ioeric): make this work for windows again.
|
|
#ifndef _WIN32
|
|
// Check that running code completion doesn't stat() a bunch of files from the
|
|
// preamble again. (They should be using the preamble's stat-cache)
|
|
TEST(ClangdTests, PreambleVFSStatCache) {
|
|
class ListenStatsFSProvider : public FileSystemProvider {
|
|
public:
|
|
ListenStatsFSProvider(llvm::StringMap<unsigned> &CountStats)
|
|
: CountStats(CountStats) {}
|
|
|
|
IntrusiveRefCntPtr<llvm::vfs::FileSystem> getFileSystem() const override {
|
|
class ListenStatVFS : public llvm::vfs::ProxyFileSystem {
|
|
public:
|
|
ListenStatVFS(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<ListenStatVFS>(
|
|
new ListenStatVFS(buildTestFS(Files), CountStats));
|
|
}
|
|
|
|
// If relative paths are used, they are resolved with testPath().
|
|
llvm::StringMap<std::string> Files;
|
|
llvm::StringMap<unsigned> &CountStats;
|
|
};
|
|
|
|
llvm::StringMap<unsigned> CountStats;
|
|
ListenStatsFSProvider FS(CountStats);
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
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_F(ClangdVFSTest, FlagsWithPlugins) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
CDB.ExtraClangFlags = {
|
|
"-Xclang",
|
|
"-add-plugin",
|
|
"-Xclang",
|
|
"random-plugin",
|
|
};
|
|
OverlayCDB OCDB(&CDB);
|
|
ClangdServer Server(OCDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
auto FooCpp = testPath("foo.cpp");
|
|
const auto SourceContents = "int main() { return 0; }";
|
|
FS.Files[FooCpp] = FooCpp;
|
|
Server.addDocument(FooCpp, SourceContents);
|
|
auto Result = dumpASTWithoutMemoryLocs(Server, FooCpp);
|
|
EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics";
|
|
EXPECT_NE(Result, "<no-ast>");
|
|
}
|
|
|
|
TEST_F(ClangdVFSTest, FallbackWhenPreambleIsNotReady) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
MockCompilationDatabase CDB;
|
|
ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
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.AllowFallback = true;
|
|
|
|
// This will make compile command broken and preamble absent.
|
|
CDB.ExtraClangFlags = {"yolo.cc"};
|
|
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);
|
|
// Identifier-based fallback completion doesn't know about "symbol" scope.
|
|
EXPECT_THAT(Res.Completions,
|
|
ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"),
|
|
Field(&CodeCompletion::Scope, ""))));
|
|
|
|
// Make the compile command work again.
|
|
CDB.ExtraClangFlags = {"-std=c++11"};
|
|
Server.addDocument(FooCpp, Code.code());
|
|
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_F(ClangdVFSTest, FallbackWhenWaitingForCompileCommand) {
|
|
MockFSProvider FS;
|
|
ErrorCheckingDiagConsumer DiagConsumer;
|
|
// Returns compile command only when notified.
|
|
class DelayedCompilationDatabase : public GlobalCompilationDatabase {
|
|
public:
|
|
DelayedCompilationDatabase(Notification &CanReturnCommand)
|
|
: CanReturnCommand(CanReturnCommand) {}
|
|
|
|
llvm::Optional<tooling::CompileCommand>
|
|
getCompileCommand(PathRef File, ProjectInfo * = nullptr) const override {
|
|
// FIXME: make this timeout and fail instead of waiting forever in case
|
|
// something goes wrong.
|
|
CanReturnCommand.wait();
|
|
auto FileName = llvm::sys::path::filename(File);
|
|
std::vector<std::string> CommandLine = {"clangd", "-ffreestanding", 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, DiagConsumer, ClangdServer::optsForTest());
|
|
|
|
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());
|
|
|
|
// Sleep for some time to make sure code completion is not run because update
|
|
// hasn't been scheduled.
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
auto Opts = clangd::CodeCompleteOptions();
|
|
Opts.AllowFallback = true;
|
|
|
|
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::"))));
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace clangd
|
|
} // namespace clang
|