llvm-project/clang-tools-extra/clangd/unittests/BackgroundIndexTests.cpp

941 lines
32 KiB
C++

#include "CompileCommands.h"
#include "Config.h"
#include "Headers.h"
#include "SyncAPI.h"
#include "TestFS.h"
#include "TestIndex.h"
#include "TestTU.h"
#include "index/Background.h"
#include "index/BackgroundRebuild.h"
#include "clang/Tooling/ArgumentsAdjusters.h"
#include "clang/Tooling/CompilationDatabase.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/ScopedPrinter.h"
#include "llvm/Support/Threading.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <deque>
#include <thread>
using ::testing::_;
using ::testing::AllOf;
using ::testing::Contains;
using ::testing::ElementsAre;
using ::testing::Not;
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
namespace clang {
namespace clangd {
MATCHER_P(Named, N, "") { return arg.Name == N; }
MATCHER_P(QName, N, "") { return (arg.Scope + arg.Name).str() == N; }
MATCHER(Declared, "") {
return !StringRef(arg.CanonicalDeclaration.FileURI).empty();
}
MATCHER(Defined, "") { return !StringRef(arg.Definition.FileURI).empty(); }
MATCHER_P(FileURI, F, "") { return StringRef(arg.Location.FileURI) == F; }
::testing::Matcher<const RefSlab &>
RefsAre(std::vector<::testing::Matcher<Ref>> Matchers) {
return ElementsAre(::testing::Pair(_, UnorderedElementsAreArray(Matchers)));
}
// URI cannot be empty since it references keys in the IncludeGraph.
MATCHER(EmptyIncludeNode, "") {
return arg.Flags == IncludeGraphNode::SourceFlag::None && !arg.URI.empty() &&
arg.Digest == FileDigest{{0}} && arg.DirectIncludes.empty();
}
MATCHER(HadErrors, "") {
return arg.Flags & IncludeGraphNode::SourceFlag::HadErrors;
}
MATCHER_P(NumReferences, N, "") { return arg.References == N; }
class MemoryShardStorage : public BackgroundIndexStorage {
mutable std::mutex StorageMu;
llvm::StringMap<std::string> &Storage;
size_t &CacheHits;
public:
MemoryShardStorage(llvm::StringMap<std::string> &Storage, size_t &CacheHits)
: Storage(Storage), CacheHits(CacheHits) {}
llvm::Error storeShard(llvm::StringRef ShardIdentifier,
IndexFileOut Shard) const override {
std::lock_guard<std::mutex> Lock(StorageMu);
AccessedPaths.insert(ShardIdentifier);
Storage[ShardIdentifier] = llvm::to_string(Shard);
return llvm::Error::success();
}
std::unique_ptr<IndexFileIn>
loadShard(llvm::StringRef ShardIdentifier) const override {
std::lock_guard<std::mutex> Lock(StorageMu);
AccessedPaths.insert(ShardIdentifier);
if (Storage.find(ShardIdentifier) == Storage.end()) {
return nullptr;
}
auto IndexFile = readIndexFile(Storage[ShardIdentifier]);
if (!IndexFile) {
ADD_FAILURE() << "Error while reading " << ShardIdentifier << ':'
<< IndexFile.takeError();
return nullptr;
}
CacheHits++;
return std::make_unique<IndexFileIn>(std::move(*IndexFile));
}
mutable llvm::StringSet<> AccessedPaths;
};
class BackgroundIndexTest : public ::testing::Test {
protected:
BackgroundIndexTest() { BackgroundQueue::preventThreadStarvationInTests(); }
};
TEST_F(BackgroundIndexTest, NoCrashOnErrorFile) {
MockFS FS;
FS.Files[testPath("root/A.cc")] = "error file";
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
tooling::CompileCommand Cmd;
Cmd.Filename = testPath("root/A.cc");
Cmd.Directory = testPath("root");
Cmd.CommandLine = {"clang++", "-DA=1", testPath("root/A.cc")};
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
}
TEST_F(BackgroundIndexTest, Config) {
MockFS FS;
// Set up two identical TUs, foo and bar.
// They define foo::one and bar::one.
std::vector<tooling::CompileCommand> Cmds;
for (std::string Name : {"foo", "bar", "baz"}) {
std::string Filename = Name + ".cpp";
std::string Header = Name + ".h";
FS.Files[Filename] = "#include \"" + Header + "\"";
FS.Files[Header] = "namespace " + Name + " { int one; }";
tooling::CompileCommand Cmd;
Cmd.Filename = Filename;
Cmd.Directory = testRoot();
Cmd.CommandLine = {"clang++", Filename};
Cmds.push_back(std::move(Cmd));
}
// Context provider that installs a configuration mutating foo's command.
// This causes it to define foo::two instead of foo::one.
// It also disables indexing of baz entirely.
BackgroundIndex::Options Opts;
Opts.ContextProvider = [](PathRef P) {
Config C;
if (P.endswith("foo.cpp"))
C.CompileFlags.Edits.push_back(
[](std::vector<std::string> &Argv) { Argv.push_back("-Done=two"); });
if (P.endswith("baz.cpp"))
C.Index.Background = Config::BackgroundPolicy::Skip;
return Context::current().derive(Config::Key, std::move(C));
};
// Create the background index.
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
// We need the CommandMangler, because that applies the config we're testing.
OverlayCDB CDB(/*Base=*/nullptr, /*FallbackFlags=*/{},
tooling::ArgumentsAdjuster(CommandMangler::forTests()));
BackgroundIndex Idx(
FS, CDB, [&](llvm::StringRef) { return &MSS; }, std::move(Opts));
// Index the two files.
for (auto &Cmd : Cmds) {
std::string FullPath = testPath(Cmd.Filename);
CDB.setCompileCommand(FullPath, std::move(Cmd));
}
// Wait for both files to be indexed.
ASSERT_TRUE(Idx.blockUntilIdleForTest());
EXPECT_THAT(runFuzzyFind(Idx, ""),
UnorderedElementsAre(QName("foo"), QName("foo::two"),
QName("bar"), QName("bar::one")));
}
TEST_F(BackgroundIndexTest, IndexTwoFiles) {
MockFS FS;
// a.h yields different symbols when included by A.cc vs B.cc.
FS.Files[testPath("root/A.h")] = R"cpp(
void common();
void f_b();
#if A
class A_CC {};
#else
class B_CC{};
#endif
)cpp";
FS.Files[testPath("root/A.cc")] =
"#include \"A.h\"\nstatic void g() { (void)common; }";
FS.Files[testPath("root/B.cc")] =
R"cpp(
#define A 0
#include "A.h"
void f_b() {
(void)common;
(void)common;
(void)common;
(void)common;
})cpp";
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex::Options Opts;
Opts.CollectMainFileRefs = true;
BackgroundIndex Idx(
FS, CDB, [&](llvm::StringRef) { return &MSS; }, Opts);
tooling::CompileCommand Cmd;
Cmd.Filename = testPath("root/A.cc");
Cmd.Directory = testPath("root");
Cmd.CommandLine = {"clang++", "-DA=1", testPath("root/A.cc")};
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
EXPECT_THAT(runFuzzyFind(Idx, ""),
UnorderedElementsAre(AllOf(Named("common"), NumReferences(1U)),
AllOf(Named("A_CC"), NumReferences(0U)),
AllOf(Named("g"), NumReferences(1U)),
AllOf(Named("f_b"), Declared(),
Not(Defined()), NumReferences(0U))));
Cmd.Filename = testPath("root/B.cc");
Cmd.CommandLine = {"clang++", Cmd.Filename};
CDB.setCompileCommand(testPath("root/B.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
// B_CC is dropped as we don't collect symbols from A.h in this compilation.
EXPECT_THAT(runFuzzyFind(Idx, ""),
UnorderedElementsAre(AllOf(Named("common"), NumReferences(5U)),
AllOf(Named("A_CC"), NumReferences(0U)),
AllOf(Named("g"), NumReferences(1U)),
AllOf(Named("f_b"), Declared(), Defined(),
NumReferences(1U))));
auto Syms = runFuzzyFind(Idx, "common");
EXPECT_THAT(Syms, UnorderedElementsAre(Named("common")));
auto Common = *Syms.begin();
EXPECT_THAT(getRefs(Idx, Common.ID),
RefsAre({FileURI("unittest:///root/A.h"),
FileURI("unittest:///root/A.cc"),
FileURI("unittest:///root/B.cc"),
FileURI("unittest:///root/B.cc"),
FileURI("unittest:///root/B.cc"),
FileURI("unittest:///root/B.cc")}));
}
TEST_F(BackgroundIndexTest, MainFileRefs) {
MockFS FS;
FS.Files[testPath("root/A.h")] = R"cpp(
void header_sym();
)cpp";
FS.Files[testPath("root/A.cc")] =
"#include \"A.h\"\nstatic void main_sym() { (void)header_sym; }";
// Check the behaviour with CollectMainFileRefs = false (the default
// at the SymbolCollector level).
{
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
tooling::CompileCommand Cmd;
Cmd.Filename = testPath("root/A.cc");
Cmd.Directory = testPath("root");
Cmd.CommandLine = {"clang++", testPath("root/A.cc")};
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
EXPECT_THAT(
runFuzzyFind(Idx, ""),
UnorderedElementsAre(AllOf(Named("header_sym"), NumReferences(1U)),
AllOf(Named("main_sym"), NumReferences(0U))));
}
// Check the behaviour with CollectMainFileRefs = true.
{
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex::Options Opts;
Opts.CollectMainFileRefs = true;
BackgroundIndex Idx(
FS, CDB, [&](llvm::StringRef) { return &MSS; }, Opts);
tooling::CompileCommand Cmd;
Cmd.Filename = testPath("root/A.cc");
Cmd.Directory = testPath("root");
Cmd.CommandLine = {"clang++", testPath("root/A.cc")};
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
EXPECT_THAT(
runFuzzyFind(Idx, ""),
UnorderedElementsAre(AllOf(Named("header_sym"), NumReferences(1U)),
AllOf(Named("main_sym"), NumReferences(1U))));
}
}
TEST_F(BackgroundIndexTest, ShardStorageTest) {
MockFS FS;
FS.Files[testPath("root/A.h")] = R"cpp(
void common();
void f_b();
class A_CC {};
)cpp";
std::string A_CC = "";
FS.Files[testPath("root/A.cc")] = R"cpp(
#include "A.h"
void g() { (void)common; }
class B_CC : public A_CC {};
)cpp";
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
tooling::CompileCommand Cmd;
Cmd.Filename = testPath("root/A.cc");
Cmd.Directory = testPath("root");
Cmd.CommandLine = {"clang++", testPath("root/A.cc")};
// Check nothing is loaded from Storage, but A.cc and A.h has been stored.
{
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
}
EXPECT_EQ(CacheHits, 0U);
EXPECT_EQ(Storage.size(), 2U);
{
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
}
EXPECT_EQ(CacheHits, 2U); // Check both A.cc and A.h loaded from cache.
EXPECT_EQ(Storage.size(), 2U);
auto ShardHeader = MSS.loadShard(testPath("root/A.h"));
EXPECT_NE(ShardHeader, nullptr);
EXPECT_THAT(
*ShardHeader->Symbols,
UnorderedElementsAre(Named("common"), Named("A_CC"),
AllOf(Named("f_b"), Declared(), Not(Defined()))));
for (const auto &Ref : *ShardHeader->Refs)
EXPECT_THAT(Ref.second,
UnorderedElementsAre(FileURI("unittest:///root/A.h")));
auto ShardSource = MSS.loadShard(testPath("root/A.cc"));
EXPECT_NE(ShardSource, nullptr);
EXPECT_THAT(*ShardSource->Symbols,
UnorderedElementsAre(Named("g"), Named("B_CC")));
for (const auto &Ref : *ShardSource->Refs)
EXPECT_THAT(Ref.second,
UnorderedElementsAre(FileURI("unittest:///root/A.cc")));
// The BaseOf relationship between A_CC and B_CC is stored in both the file
// containing the definition of the subject (A_CC) and the file containing
// the definition of the object (B_CC).
SymbolID A = findSymbol(*ShardHeader->Symbols, "A_CC").ID;
SymbolID B = findSymbol(*ShardSource->Symbols, "B_CC").ID;
EXPECT_THAT(*ShardHeader->Relations,
UnorderedElementsAre(Relation{A, RelationKind::BaseOf, B}));
EXPECT_THAT(*ShardSource->Relations,
UnorderedElementsAre(Relation{A, RelationKind::BaseOf, B}));
}
TEST_F(BackgroundIndexTest, DirectIncludesTest) {
MockFS FS;
FS.Files[testPath("root/B.h")] = "";
FS.Files[testPath("root/A.h")] = R"cpp(
#include "B.h"
void common();
void f_b();
class A_CC {};
)cpp";
std::string A_CC = "#include \"A.h\"\nvoid g() { (void)common; }";
FS.Files[testPath("root/A.cc")] = A_CC;
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
tooling::CompileCommand Cmd;
Cmd.Filename = testPath("root/A.cc");
Cmd.Directory = testPath("root");
Cmd.CommandLine = {"clang++", testPath("root/A.cc")};
{
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
}
auto ShardSource = MSS.loadShard(testPath("root/A.cc"));
EXPECT_TRUE(ShardSource->Sources);
EXPECT_EQ(ShardSource->Sources->size(), 2U); // A.cc, A.h
EXPECT_THAT(
ShardSource->Sources->lookup("unittest:///root/A.cc").DirectIncludes,
UnorderedElementsAre("unittest:///root/A.h"));
EXPECT_NE(ShardSource->Sources->lookup("unittest:///root/A.cc").Digest,
FileDigest{{0}});
EXPECT_THAT(ShardSource->Sources->lookup("unittest:///root/A.h"),
EmptyIncludeNode());
auto ShardHeader = MSS.loadShard(testPath("root/A.h"));
EXPECT_TRUE(ShardHeader->Sources);
EXPECT_EQ(ShardHeader->Sources->size(), 2U); // A.h, B.h
EXPECT_THAT(
ShardHeader->Sources->lookup("unittest:///root/A.h").DirectIncludes,
UnorderedElementsAre("unittest:///root/B.h"));
EXPECT_NE(ShardHeader->Sources->lookup("unittest:///root/A.h").Digest,
FileDigest{{0}});
EXPECT_THAT(ShardHeader->Sources->lookup("unittest:///root/B.h"),
EmptyIncludeNode());
}
TEST_F(BackgroundIndexTest, ShardStorageLoad) {
MockFS FS;
FS.Files[testPath("root/A.h")] = R"cpp(
void common();
void f_b();
class A_CC {};
)cpp";
FS.Files[testPath("root/A.cc")] =
"#include \"A.h\"\nvoid g() { (void)common; }";
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
tooling::CompileCommand Cmd;
Cmd.Filename = testPath("root/A.cc");
Cmd.Directory = testPath("root");
Cmd.CommandLine = {"clang++", testPath("root/A.cc")};
// Check nothing is loaded from Storage, but A.cc and A.h has been stored.
{
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
}
// Change header.
FS.Files[testPath("root/A.h")] = R"cpp(
void common();
void f_b();
class A_CC {};
class A_CCnew {};
)cpp";
{
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
}
EXPECT_EQ(CacheHits, 2U); // Check both A.cc and A.h loaded from cache.
// Check if the new symbol has arrived.
auto ShardHeader = MSS.loadShard(testPath("root/A.h"));
EXPECT_NE(ShardHeader, nullptr);
EXPECT_THAT(*ShardHeader->Symbols, Contains(Named("A_CCnew")));
// Change source.
FS.Files[testPath("root/A.cc")] =
"#include \"A.h\"\nvoid g() { (void)common; }\nvoid f_b() {}";
{
CacheHits = 0;
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
}
EXPECT_EQ(CacheHits, 2U); // Check both A.cc and A.h loaded from cache.
// Check if the new symbol has arrived.
ShardHeader = MSS.loadShard(testPath("root/A.h"));
EXPECT_NE(ShardHeader, nullptr);
EXPECT_THAT(*ShardHeader->Symbols, Contains(Named("A_CCnew")));
auto ShardSource = MSS.loadShard(testPath("root/A.cc"));
EXPECT_NE(ShardSource, nullptr);
EXPECT_THAT(*ShardSource->Symbols,
Contains(AllOf(Named("f_b"), Declared(), Defined())));
}
TEST_F(BackgroundIndexTest, ShardStorageEmptyFile) {
MockFS FS;
FS.Files[testPath("root/A.h")] = R"cpp(
void common();
void f_b();
class A_CC {};
)cpp";
FS.Files[testPath("root/B.h")] = R"cpp(
#include "A.h"
)cpp";
FS.Files[testPath("root/A.cc")] =
"#include \"B.h\"\nvoid g() { (void)common; }";
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
tooling::CompileCommand Cmd;
Cmd.Filename = testPath("root/A.cc");
Cmd.Directory = testPath("root");
Cmd.CommandLine = {"clang++", testPath("root/A.cc")};
// Check that A.cc, A.h and B.h has been stored.
{
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
}
EXPECT_THAT(Storage.keys(),
UnorderedElementsAre(testPath("root/A.cc"), testPath("root/A.h"),
testPath("root/B.h")));
auto ShardHeader = MSS.loadShard(testPath("root/B.h"));
EXPECT_NE(ShardHeader, nullptr);
EXPECT_TRUE(ShardHeader->Symbols->empty());
// Check that A.cc, A.h and B.h has been loaded.
{
CacheHits = 0;
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
}
EXPECT_EQ(CacheHits, 3U);
// Update B.h to contain some symbols.
FS.Files[testPath("root/B.h")] = R"cpp(
#include "A.h"
void new_func();
)cpp";
// Check that B.h has been stored with new contents.
{
CacheHits = 0;
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
CDB.setCompileCommand(testPath("root/A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
}
EXPECT_EQ(CacheHits, 3U);
ShardHeader = MSS.loadShard(testPath("root/B.h"));
EXPECT_NE(ShardHeader, nullptr);
EXPECT_THAT(*ShardHeader->Symbols,
Contains(AllOf(Named("new_func"), Declared(), Not(Defined()))));
}
TEST_F(BackgroundIndexTest, NoDotsInAbsPath) {
MockFS FS;
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
ASSERT_TRUE(Idx.blockUntilIdleForTest());
tooling::CompileCommand Cmd;
FS.Files[testPath("root/A.cc")] = "";
Cmd.Filename = "../A.cc";
Cmd.Directory = testPath("root/build");
Cmd.CommandLine = {"clang++", "../A.cc"};
CDB.setCompileCommand(testPath("root/build/../A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
FS.Files[testPath("root/B.cc")] = "";
Cmd.Filename = "./B.cc";
Cmd.Directory = testPath("root");
Cmd.CommandLine = {"clang++", "./B.cc"};
CDB.setCompileCommand(testPath("root/./B.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
for (llvm::StringRef AbsPath : MSS.AccessedPaths.keys()) {
EXPECT_FALSE(AbsPath.contains("./")) << AbsPath;
EXPECT_FALSE(AbsPath.contains("../")) << AbsPath;
}
}
TEST_F(BackgroundIndexTest, UncompilableFiles) {
MockFS FS;
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
tooling::CompileCommand Cmd;
FS.Files[testPath("A.h")] = "void foo();";
FS.Files[testPath("B.h")] = "#include \"C.h\"\nasdf;";
FS.Files[testPath("C.h")] = "";
FS.Files[testPath("A.cc")] = R"cpp(
#include "A.h"
#include "B.h"
#include "not_found_header.h"
void foo() {}
)cpp";
Cmd.Filename = "../A.cc";
Cmd.Directory = testPath("build");
Cmd.CommandLine = {"clang++", "../A.cc"};
CDB.setCompileCommand(testPath("build/../A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
EXPECT_THAT(Storage.keys(), ElementsAre(testPath("A.cc"), testPath("A.h"),
testPath("B.h"), testPath("C.h")));
{
auto Shard = MSS.loadShard(testPath("A.cc"));
EXPECT_THAT(*Shard->Symbols, UnorderedElementsAre(Named("foo")));
EXPECT_THAT(Shard->Sources->keys(),
UnorderedElementsAre("unittest:///A.cc", "unittest:///A.h",
"unittest:///B.h"));
EXPECT_THAT(Shard->Sources->lookup("unittest:///A.cc"), HadErrors());
}
{
auto Shard = MSS.loadShard(testPath("A.h"));
EXPECT_THAT(*Shard->Symbols, UnorderedElementsAre(Named("foo")));
EXPECT_THAT(Shard->Sources->keys(),
UnorderedElementsAre("unittest:///A.h"));
EXPECT_THAT(Shard->Sources->lookup("unittest:///A.h"), HadErrors());
}
{
auto Shard = MSS.loadShard(testPath("B.h"));
EXPECT_THAT(*Shard->Symbols, UnorderedElementsAre(Named("asdf")));
EXPECT_THAT(Shard->Sources->keys(),
UnorderedElementsAre("unittest:///B.h", "unittest:///C.h"));
EXPECT_THAT(Shard->Sources->lookup("unittest:///B.h"), HadErrors());
}
{
auto Shard = MSS.loadShard(testPath("C.h"));
EXPECT_THAT(*Shard->Symbols, UnorderedElementsAre());
EXPECT_THAT(Shard->Sources->keys(),
UnorderedElementsAre("unittest:///C.h"));
EXPECT_THAT(Shard->Sources->lookup("unittest:///C.h"), HadErrors());
}
}
TEST_F(BackgroundIndexTest, CmdLineHash) {
MockFS FS;
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
tooling::CompileCommand Cmd;
FS.Files[testPath("A.cc")] = "#include \"A.h\"";
FS.Files[testPath("A.h")] = "";
Cmd.Filename = "../A.cc";
Cmd.Directory = testPath("build");
Cmd.CommandLine = {"clang++", "../A.cc", "-fsyntax-only"};
CDB.setCompileCommand(testPath("build/../A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
EXPECT_THAT(Storage.keys(), ElementsAre(testPath("A.cc"), testPath("A.h")));
// Make sure we only store the Cmd for main file.
EXPECT_FALSE(MSS.loadShard(testPath("A.h"))->Cmd);
tooling::CompileCommand CmdStored = *MSS.loadShard(testPath("A.cc"))->Cmd;
EXPECT_EQ(CmdStored.CommandLine, Cmd.CommandLine);
EXPECT_EQ(CmdStored.Directory, Cmd.Directory);
}
TEST_F(BackgroundIndexTest, Reindex) {
MockFS FS;
llvm::StringMap<std::string> Storage;
size_t CacheHits = 0;
MemoryShardStorage MSS(Storage, CacheHits);
OverlayCDB CDB(/*Base=*/nullptr);
BackgroundIndex Idx(FS, CDB, [&](llvm::StringRef) { return &MSS; },
/*Opts=*/{});
// Index a file.
FS.Files[testPath("A.cc")] = "int theOldFunction();";
tooling::CompileCommand Cmd;
Cmd.Filename = "../A.cc";
Cmd.Directory = testPath("build");
Cmd.CommandLine = {"clang++", "../A.cc", "-fsyntax-only"};
CDB.setCompileCommand(testPath("A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
// Verify the result is indexed and stored.
EXPECT_EQ(1u, runFuzzyFind(Idx, "theOldFunction").size());
EXPECT_EQ(0u, runFuzzyFind(Idx, "theNewFunction").size());
std::string OldShard = Storage.lookup(testPath("A.cc"));
EXPECT_NE("", OldShard);
// Change the content and command, and notify to reindex it.
Cmd.CommandLine.push_back("-DFOO");
FS.Files[testPath("A.cc")] = "int theNewFunction();";
CDB.setCompileCommand(testPath("A.cc"), Cmd);
ASSERT_TRUE(Idx.blockUntilIdleForTest());
// Currently, we will never index the same main file again.
EXPECT_EQ(1u, runFuzzyFind(Idx, "theOldFunction").size());
EXPECT_EQ(0u, runFuzzyFind(Idx, "theNewFunction").size());
EXPECT_EQ(OldShard, Storage.lookup(testPath("A.cc")));
}
class BackgroundIndexRebuilderTest : public testing::Test {
protected:
BackgroundIndexRebuilderTest()
: Target(std::make_unique<MemIndex>()),
Rebuilder(&Target, &Source, /*Threads=*/10) {
// Prepare FileSymbols with TestSymbol in it, for checkRebuild.
TestSymbol.ID = SymbolID("foo");
}
// Perform Action and determine whether it rebuilt the index or not.
bool checkRebuild(std::function<void()> Action) {
// Update name so we can tell if the index updates.
VersionStorage.push_back("Sym" + std::to_string(++VersionCounter));
TestSymbol.Name = VersionStorage.back();
SymbolSlab::Builder SB;
SB.insert(TestSymbol);
Source.update("", std::make_unique<SymbolSlab>(std::move(SB).build()),
nullptr, nullptr, false);
// Now maybe update the index.
Action();
// Now query the index to get the name count.
std::string ReadName;
LookupRequest Req;
Req.IDs.insert(TestSymbol.ID);
Target.lookup(Req,
[&](const Symbol &S) { ReadName = std::string(S.Name); });
// The index was rebuild if the name is up to date.
return ReadName == VersionStorage.back();
}
Symbol TestSymbol;
FileSymbols Source;
SwapIndex Target;
BackgroundIndexRebuilder Rebuilder;
unsigned VersionCounter = 0;
std::deque<std::string> VersionStorage;
};
TEST_F(BackgroundIndexRebuilderTest, IndexingTUs) {
for (unsigned I = 0; I < Rebuilder.TUsBeforeFirstBuild - 1; ++I)
EXPECT_FALSE(checkRebuild([&] { Rebuilder.indexedTU(); }));
EXPECT_TRUE(checkRebuild([&] { Rebuilder.indexedTU(); }));
for (unsigned I = 0; I < Rebuilder.TUsBeforeRebuild - 1; ++I)
EXPECT_FALSE(checkRebuild([&] { Rebuilder.indexedTU(); }));
EXPECT_TRUE(checkRebuild([&] { Rebuilder.indexedTU(); }));
}
TEST_F(BackgroundIndexRebuilderTest, LoadingShards) {
Rebuilder.startLoading();
Rebuilder.loadedShard(10);
Rebuilder.loadedShard(20);
EXPECT_TRUE(checkRebuild([&] { Rebuilder.doneLoading(); }));
// No rebuild for no shards.
Rebuilder.startLoading();
EXPECT_FALSE(checkRebuild([&] { Rebuilder.doneLoading(); }));
// Loads can overlap.
Rebuilder.startLoading();
Rebuilder.loadedShard(1);
Rebuilder.startLoading();
Rebuilder.loadedShard(1);
EXPECT_FALSE(checkRebuild([&] { Rebuilder.doneLoading(); }));
Rebuilder.loadedShard(1);
EXPECT_TRUE(checkRebuild([&] { Rebuilder.doneLoading(); }));
// No rebuilding for indexed files while loading.
Rebuilder.startLoading();
for (unsigned I = 0; I < 3 * Rebuilder.TUsBeforeRebuild; ++I)
EXPECT_FALSE(checkRebuild([&] { Rebuilder.indexedTU(); }));
// But they get indexed when we're done, even if no shards were loaded.
EXPECT_TRUE(checkRebuild([&] { Rebuilder.doneLoading(); }));
}
TEST(BackgroundQueueTest, Priority) {
// Create high and low priority tasks.
// Once a bunch of high priority tasks have run, the queue is stopped.
// So the low priority tasks should never run.
BackgroundQueue Q;
std::atomic<unsigned> HiRan(0), LoRan(0);
BackgroundQueue::Task Lo([&] { ++LoRan; });
BackgroundQueue::Task Hi([&] {
if (++HiRan >= 10)
Q.stop();
});
Hi.QueuePri = 100;
// Enqueuing the low-priority ones first shouldn't make them run first.
Q.append(std::vector<BackgroundQueue::Task>(30, Lo));
for (unsigned I = 0; I < 30; ++I)
Q.push(Hi);
AsyncTaskRunner ThreadPool;
for (unsigned I = 0; I < 5; ++I)
ThreadPool.runAsync("worker", [&] { Q.work(); });
// We should test enqueue with active workers, but it's hard to avoid races.
// Just make sure we don't crash.
Q.push(Lo);
Q.append(std::vector<BackgroundQueue::Task>(2, Hi));
// After finishing, check the tasks that ran.
ThreadPool.wait();
EXPECT_GE(HiRan, 10u);
EXPECT_EQ(LoRan, 0u);
}
TEST(BackgroundQueueTest, Boost) {
std::string Sequence;
BackgroundQueue::Task A([&] { Sequence.push_back('A'); });
A.Tag = "A";
A.QueuePri = 1;
BackgroundQueue::Task B([&] { Sequence.push_back('B'); });
B.QueuePri = 2;
B.Tag = "B";
{
BackgroundQueue Q;
Q.append({A, B});
Q.work([&] { Q.stop(); });
EXPECT_EQ("BA", Sequence) << "priority order";
}
Sequence.clear();
{
BackgroundQueue Q;
Q.boost("A", 3);
Q.append({A, B});
Q.work([&] { Q.stop(); });
EXPECT_EQ("AB", Sequence) << "A was boosted before enqueueing";
}
Sequence.clear();
{
BackgroundQueue Q;
Q.append({A, B});
Q.boost("A", 3);
Q.work([&] { Q.stop(); });
EXPECT_EQ("AB", Sequence) << "A was boosted after enqueueing";
}
}
TEST(BackgroundQueueTest, Duplicates) {
std::string Sequence;
BackgroundQueue::Task A([&] { Sequence.push_back('A'); });
A.QueuePri = 100;
A.Key = 1;
BackgroundQueue::Task B([&] { Sequence.push_back('B'); });
// B has no key, and is not subject to duplicate detection.
B.QueuePri = 50;
BackgroundQueue Q;
Q.append({A, B, A, B}); // One A is dropped, the other is high priority.
Q.work(/*OnIdle=*/[&] {
// The first time we go idle, we enqueue the same task again.
if (!llvm::is_contained(Sequence, ' ')) {
Sequence.push_back(' ');
Q.append({A, B, A, B}); // Both As are dropped.
} else {
Q.stop();
}
});
// This could reasonably be "ABB BBA", if we had good *re*indexing support.
EXPECT_EQ("ABB BB", Sequence);
}
TEST(BackgroundQueueTest, Progress) {
using testing::AnyOf;
BackgroundQueue::Stats S;
BackgroundQueue Q([&](BackgroundQueue::Stats New) {
// Verify values are sane.
// Items are enqueued one at a time (at least in this test).
EXPECT_THAT(New.Enqueued, AnyOf(S.Enqueued, S.Enqueued + 1));
// Items are completed one at a time.
EXPECT_THAT(New.Completed, AnyOf(S.Completed, S.Completed + 1));
// Items are started or completed one at a time.
EXPECT_THAT(New.Active, AnyOf(S.Active - 1, S.Active, S.Active + 1));
// Idle point only advances in time.
EXPECT_GE(New.LastIdle, S.LastIdle);
// Idle point is a task that has been completed in the past.
EXPECT_LE(New.LastIdle, New.Completed);
// LastIdle is now only if we're really idle.
EXPECT_EQ(New.LastIdle == New.Enqueued,
New.Completed == New.Enqueued && New.Active == 0u);
S = New;
});
// Two types of tasks: a ping task enqueues a pong task.
// This avoids all enqueues followed by all completions (boring!)
std::atomic<int> PingCount(0), PongCount(0);
BackgroundQueue::Task Pong([&] { ++PongCount; });
BackgroundQueue::Task Ping([&] {
++PingCount;
Q.push(Pong);
});
for (int I = 0; I < 1000; ++I)
Q.push(Ping);
// Spin up some workers and stop while idle.
AsyncTaskRunner ThreadPool;
for (unsigned I = 0; I < 5; ++I)
ThreadPool.runAsync("worker", [&] { Q.work([&] { Q.stop(); }); });
ThreadPool.wait();
// Everything's done, check final stats.
// Assertions above ensure we got from 0 to 2000 in a reasonable way.
EXPECT_EQ(PingCount.load(), 1000);
EXPECT_EQ(PongCount.load(), 1000);
EXPECT_EQ(S.Active, 0u);
EXPECT_EQ(S.Enqueued, 2000u);
EXPECT_EQ(S.Completed, 2000u);
EXPECT_EQ(S.LastIdle, 2000u);
}
TEST(BackgroundIndex, Profile) {
MockFS FS;
MockCompilationDatabase CDB;
BackgroundIndex Idx(FS, CDB, [](llvm::StringRef) { return nullptr; },
/*Opts=*/{});
llvm::BumpPtrAllocator Alloc;
MemoryTree MT(&Alloc);
Idx.profile(MT);
ASSERT_THAT(MT.children(),
UnorderedElementsAre(Pair("slabs", _), Pair("index", _)));
}
} // namespace clangd
} // namespace clang