| #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 = tooling::getInsertArgumentAdjuster("-Done=two")(Argv, ""); |
| }); |
| 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; |
| 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; }"; |
| |
| llvm::StringMap<std::string> Storage; |
| size_t CacheHits = 0; |
| MemoryShardStorage MSS(Storage, CacheHits); |
| OverlayCDB CDB(/*Base=*/nullptr); |
| BackgroundIndex::Options Opts; |
| 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() |
| : Source(IndexContents::All), 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 |