|  | //===-- TUSchedulerTests.cpp ------------------------------------*- C++ -*-===// | 
|  | // | 
|  | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | 
|  | // See https://llvm.org/LICENSE.txt for license information. | 
|  | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | 
|  | // | 
|  | //===----------------------------------------------------------------------===// | 
|  |  | 
|  | #include "Annotations.h" | 
|  | #include "ClangdServer.h" | 
|  | #include "Compiler.h" | 
|  | #include "Config.h" | 
|  | #include "Diagnostics.h" | 
|  | #include "GlobalCompilationDatabase.h" | 
|  | #include "Matchers.h" | 
|  | #include "ParsedAST.h" | 
|  | #include "Preamble.h" | 
|  | #include "TUScheduler.h" | 
|  | #include "TestFS.h" | 
|  | #include "TestIndex.h" | 
|  | #include "clang-include-cleaner/Record.h" | 
|  | #include "support/Cancellation.h" | 
|  | #include "support/Context.h" | 
|  | #include "support/Path.h" | 
|  | #include "support/TestTracer.h" | 
|  | #include "support/Threading.h" | 
|  | #include "clang/Basic/DiagnosticDriver.h" | 
|  | #include "llvm/ADT/ArrayRef.h" | 
|  | #include "llvm/ADT/FunctionExtras.h" | 
|  | #include "llvm/ADT/ScopeExit.h" | 
|  | #include "llvm/ADT/StringExtras.h" | 
|  | #include "llvm/ADT/StringMap.h" | 
|  | #include "llvm/ADT/StringRef.h" | 
|  | #include "gmock/gmock.h" | 
|  | #include "gtest/gtest.h" | 
|  | #include <atomic> | 
|  | #include <chrono> | 
|  | #include <condition_variable> | 
|  | #include <cstdint> | 
|  | #include <functional> | 
|  | #include <memory> | 
|  | #include <mutex> | 
|  | #include <optional> | 
|  | #include <string> | 
|  | #include <utility> | 
|  | #include <vector> | 
|  |  | 
|  | namespace clang { | 
|  | namespace clangd { | 
|  | namespace { | 
|  |  | 
|  | using ::testing::AllOf; | 
|  | using ::testing::AnyOf; | 
|  | using ::testing::Contains; | 
|  | using ::testing::Each; | 
|  | using ::testing::ElementsAre; | 
|  | using ::testing::Eq; | 
|  | using ::testing::Field; | 
|  | using ::testing::IsEmpty; | 
|  | using ::testing::Not; | 
|  | using ::testing::Pair; | 
|  | using ::testing::Pointee; | 
|  | using ::testing::SizeIs; | 
|  | using ::testing::UnorderedElementsAre; | 
|  |  | 
|  | MATCHER_P2(TUState, PreambleActivity, ASTActivity, "") { | 
|  | if (arg.PreambleActivity != PreambleActivity) { | 
|  | *result_listener << "preamblestate is " | 
|  | << static_cast<uint8_t>(arg.PreambleActivity); | 
|  | return false; | 
|  | } | 
|  | if (arg.ASTActivity.K != ASTActivity) { | 
|  | *result_listener << "aststate is " << arg.ASTActivity.K; | 
|  | return false; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // Simple ContextProvider to verify the provider is invoked & contexts are used. | 
|  | static Key<std::string> BoundPath; | 
|  | Context bindPath(PathRef F) { | 
|  | return Context::current().derive(BoundPath, F.str()); | 
|  | } | 
|  | llvm::StringRef boundPath() { | 
|  | const std::string *V = Context::current().get(BoundPath); | 
|  | return V ? *V : llvm::StringRef(""); | 
|  | } | 
|  |  | 
|  | TUScheduler::Options optsForTest() { | 
|  | TUScheduler::Options Opts(ClangdServer::optsForTest()); | 
|  | Opts.ContextProvider = bindPath; | 
|  | return Opts; | 
|  | } | 
|  |  | 
|  | class TUSchedulerTests : public ::testing::Test { | 
|  | protected: | 
|  | ParseInputs getInputs(PathRef File, std::string Contents) { | 
|  | ParseInputs Inputs; | 
|  | Inputs.CompileCommand = *CDB.getCompileCommand(File); | 
|  | Inputs.TFS = &FS; | 
|  | Inputs.Contents = std::move(Contents); | 
|  | Inputs.Opts = ParseOptions(); | 
|  | return Inputs; | 
|  | } | 
|  |  | 
|  | void updateWithCallback(TUScheduler &S, PathRef File, | 
|  | llvm::StringRef Contents, WantDiagnostics WD, | 
|  | llvm::unique_function<void()> CB) { | 
|  | updateWithCallback(S, File, getInputs(File, std::string(Contents)), WD, | 
|  | std::move(CB)); | 
|  | } | 
|  |  | 
|  | void updateWithCallback(TUScheduler &S, PathRef File, ParseInputs Inputs, | 
|  | WantDiagnostics WD, | 
|  | llvm::unique_function<void()> CB) { | 
|  | WithContextValue Ctx(llvm::make_scope_exit(std::move(CB))); | 
|  | S.update(File, Inputs, WD); | 
|  | } | 
|  |  | 
|  | static Key<llvm::unique_function<void(PathRef File, std::vector<Diag>)>> | 
|  | DiagsCallbackKey; | 
|  |  | 
|  | /// A diagnostics callback that should be passed to TUScheduler when it's used | 
|  | /// in updateWithDiags. | 
|  | static std::unique_ptr<ParsingCallbacks> captureDiags() { | 
|  | class CaptureDiags : public ParsingCallbacks { | 
|  | public: | 
|  | void onMainAST(PathRef File, ParsedAST &AST, PublishFn Publish) override { | 
|  | reportDiagnostics(File, AST.getDiagnostics(), Publish); | 
|  | } | 
|  |  | 
|  | void onFailedAST(PathRef File, llvm::StringRef Version, | 
|  | std::vector<Diag> Diags, PublishFn Publish) override { | 
|  | reportDiagnostics(File, Diags, Publish); | 
|  | } | 
|  |  | 
|  | private: | 
|  | void reportDiagnostics(PathRef File, llvm::ArrayRef<Diag> Diags, | 
|  | PublishFn Publish) { | 
|  | auto *D = Context::current().get(DiagsCallbackKey); | 
|  | if (!D) | 
|  | return; | 
|  | Publish([&]() { | 
|  | const_cast<llvm::unique_function<void(PathRef, std::vector<Diag>)> &>( | 
|  | *D)(File, Diags); | 
|  | }); | 
|  | } | 
|  | }; | 
|  | return std::make_unique<CaptureDiags>(); | 
|  | } | 
|  |  | 
|  | /// Schedule an update and call \p CB with the diagnostics it produces, if | 
|  | /// any. The TUScheduler should be created with captureDiags as a | 
|  | /// DiagsCallback for this to work. | 
|  | void updateWithDiags(TUScheduler &S, PathRef File, ParseInputs Inputs, | 
|  | WantDiagnostics WD, | 
|  | llvm::unique_function<void(std::vector<Diag>)> CB) { | 
|  | Path OrigFile = File.str(); | 
|  | WithContextValue Ctx(DiagsCallbackKey, | 
|  | [OrigFile, CB = std::move(CB)]( | 
|  | PathRef File, std::vector<Diag> Diags) mutable { | 
|  | assert(File == OrigFile); | 
|  | CB(std::move(Diags)); | 
|  | }); | 
|  | S.update(File, std::move(Inputs), WD); | 
|  | } | 
|  |  | 
|  | void updateWithDiags(TUScheduler &S, PathRef File, llvm::StringRef Contents, | 
|  | WantDiagnostics WD, | 
|  | llvm::unique_function<void(std::vector<Diag>)> CB) { | 
|  | return updateWithDiags(S, File, getInputs(File, std::string(Contents)), WD, | 
|  | std::move(CB)); | 
|  | } | 
|  |  | 
|  | MockFS FS; | 
|  | MockCompilationDatabase CDB; | 
|  | }; | 
|  |  | 
|  | Key<llvm::unique_function<void(PathRef File, std::vector<Diag>)>> | 
|  | TUSchedulerTests::DiagsCallbackKey; | 
|  |  | 
|  | TEST_F(TUSchedulerTests, MissingFiles) { | 
|  | TUScheduler S(CDB, optsForTest()); | 
|  |  | 
|  | auto Added = testPath("added.cpp"); | 
|  | FS.Files[Added] = "x"; | 
|  |  | 
|  | auto Missing = testPath("missing.cpp"); | 
|  | FS.Files[Missing] = ""; | 
|  |  | 
|  | S.update(Added, getInputs(Added, "x"), WantDiagnostics::No); | 
|  |  | 
|  | // Assert each operation for missing file is an error (even if it's | 
|  | // available in VFS). | 
|  | S.runWithAST("", Missing, | 
|  | [&](Expected<InputsAndAST> AST) { EXPECT_ERROR(AST); }); | 
|  | S.runWithPreamble( | 
|  | "", Missing, TUScheduler::Stale, | 
|  | [&](Expected<InputsAndPreamble> Preamble) { EXPECT_ERROR(Preamble); }); | 
|  | // remove() shouldn't crash on missing files. | 
|  | S.remove(Missing); | 
|  |  | 
|  | // Assert there aren't any errors for added file. | 
|  | S.runWithAST("", Added, | 
|  | [&](Expected<InputsAndAST> AST) { EXPECT_TRUE(bool(AST)); }); | 
|  | S.runWithPreamble("", Added, TUScheduler::Stale, | 
|  | [&](Expected<InputsAndPreamble> Preamble) { | 
|  | EXPECT_TRUE(bool(Preamble)); | 
|  | }); | 
|  | S.remove(Added); | 
|  |  | 
|  | // Assert that all operations fail after removing the file. | 
|  | S.runWithAST("", Added, | 
|  | [&](Expected<InputsAndAST> AST) { EXPECT_ERROR(AST); }); | 
|  | S.runWithPreamble("", Added, TUScheduler::Stale, | 
|  | [&](Expected<InputsAndPreamble> Preamble) { | 
|  | ASSERT_FALSE(bool(Preamble)); | 
|  | llvm::consumeError(Preamble.takeError()); | 
|  | }); | 
|  | // remove() shouldn't crash on missing files. | 
|  | S.remove(Added); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, WantDiagnostics) { | 
|  | std::atomic<int> CallbackCount(0); | 
|  | { | 
|  | // To avoid a racy test, don't allow tasks to actually run on the worker | 
|  | // thread until we've scheduled them all. | 
|  | Notification Ready; | 
|  | TUScheduler S(CDB, optsForTest(), captureDiags()); | 
|  | auto Path = testPath("foo.cpp"); | 
|  | // Semicolons here and in the following inputs are significant. They ensure | 
|  | // preamble stays the same across runs. Otherwise we might get multiple | 
|  | // diagnostics callbacks, once with the stale preamble and another with the | 
|  | // fresh preamble. | 
|  | updateWithDiags(S, Path, ";", WantDiagnostics::Yes, | 
|  | [&](std::vector<Diag>) { Ready.wait(); }); | 
|  | updateWithDiags(S, Path, ";request diags", WantDiagnostics::Yes, | 
|  | [&](std::vector<Diag>) { ++CallbackCount; }); | 
|  | updateWithDiags(S, Path, ";auto (clobbered)", WantDiagnostics::Auto, | 
|  | [&](std::vector<Diag>) { | 
|  | ADD_FAILURE() | 
|  | << "auto should have been cancelled by auto"; | 
|  | }); | 
|  | updateWithDiags(S, Path, ";request no diags", WantDiagnostics::No, | 
|  | [&](std::vector<Diag>) { | 
|  | ADD_FAILURE() << "no diags should not be called back"; | 
|  | }); | 
|  | updateWithDiags(S, Path, ";auto (produces)", WantDiagnostics::Auto, | 
|  | [&](std::vector<Diag>) { ++CallbackCount; }); | 
|  | Ready.notify(); | 
|  |  | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | } | 
|  | EXPECT_EQ(2, CallbackCount); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, Debounce) { | 
|  | auto Opts = optsForTest(); | 
|  | Opts.UpdateDebounce = DebouncePolicy::fixed(std::chrono::milliseconds(500)); | 
|  | TUScheduler S(CDB, Opts, captureDiags()); | 
|  | auto Path = testPath("foo.cpp"); | 
|  | // Issue a write that's going to be debounced away. | 
|  | updateWithDiags(S, Path, "auto (debounced)", WantDiagnostics::Auto, | 
|  | [&](std::vector<Diag>) { | 
|  | ADD_FAILURE() | 
|  | << "auto should have been debounced and canceled"; | 
|  | }); | 
|  | // Sleep a bit to verify that it's really debounce that's holding diagnostics. | 
|  | std::this_thread::sleep_for(std::chrono::milliseconds(50)); | 
|  |  | 
|  | // Issue another write, this time we'll wait for its diagnostics. | 
|  | Notification N; | 
|  | updateWithDiags(S, Path, "auto (timed out)", WantDiagnostics::Auto, | 
|  | [&](std::vector<Diag>) { N.notify(); }); | 
|  | EXPECT_TRUE(N.wait(timeoutSeconds(60))); | 
|  |  | 
|  | // Once we start shutting down the TUScheduler, this one becomes a dead write. | 
|  | updateWithDiags(S, Path, "auto (discarded)", WantDiagnostics::Auto, | 
|  | [&](std::vector<Diag>) { | 
|  | ADD_FAILURE() | 
|  | << "auto should have been discarded (dead write)"; | 
|  | }); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, Cancellation) { | 
|  | // We have the following update/read sequence | 
|  | //   U0 | 
|  | //   U1(WantDiags=Yes) <-- cancelled | 
|  | //    R1               <-- cancelled | 
|  | //   U2(WantDiags=Yes) <-- cancelled | 
|  | //    R2A              <-- cancelled | 
|  | //    R2B | 
|  | //   U3(WantDiags=Yes) | 
|  | //    R3               <-- cancelled | 
|  | std::vector<StringRef> DiagsSeen, ReadsSeen, ReadsCanceled; | 
|  | { | 
|  | Notification Proceed; // Ensure we schedule everything. | 
|  | TUScheduler S(CDB, optsForTest(), captureDiags()); | 
|  | auto Path = testPath("foo.cpp"); | 
|  | // Helper to schedule a named update and return a function to cancel it. | 
|  | auto Update = [&](StringRef ID) -> Canceler { | 
|  | auto T = cancelableTask(); | 
|  | WithContext C(std::move(T.first)); | 
|  | updateWithDiags( | 
|  | S, Path, ("//" + ID).str(), WantDiagnostics::Yes, | 
|  | [&, ID](std::vector<Diag> Diags) { DiagsSeen.push_back(ID); }); | 
|  | return std::move(T.second); | 
|  | }; | 
|  | // Helper to schedule a named read and return a function to cancel it. | 
|  | auto Read = [&](StringRef ID) -> Canceler { | 
|  | auto T = cancelableTask(); | 
|  | WithContext C(std::move(T.first)); | 
|  | S.runWithAST(ID, Path, [&, ID](llvm::Expected<InputsAndAST> E) { | 
|  | if (auto Err = E.takeError()) { | 
|  | if (Err.isA<CancelledError>()) { | 
|  | ReadsCanceled.push_back(ID); | 
|  | consumeError(std::move(Err)); | 
|  | } else { | 
|  | ADD_FAILURE() << "Non-cancelled error for " << ID << ": " | 
|  | << llvm::toString(std::move(Err)); | 
|  | } | 
|  | } else { | 
|  | ReadsSeen.push_back(ID); | 
|  | } | 
|  | }); | 
|  | return std::move(T.second); | 
|  | }; | 
|  |  | 
|  | updateWithCallback(S, Path, "", WantDiagnostics::Yes, | 
|  | [&]() { Proceed.wait(); }); | 
|  | // The second parens indicate cancellation, where present. | 
|  | Update("U1")(); | 
|  | Read("R1")(); | 
|  | Update("U2")(); | 
|  | Read("R2A")(); | 
|  | Read("R2B"); | 
|  | Update("U3"); | 
|  | Read("R3")(); | 
|  | Proceed.notify(); | 
|  |  | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | } | 
|  | EXPECT_THAT(DiagsSeen, ElementsAre("U2", "U3")) | 
|  | << "U1 and all dependent reads were cancelled. " | 
|  | "U2 has a dependent read R2A. " | 
|  | "U3 was not cancelled."; | 
|  | EXPECT_THAT(ReadsSeen, ElementsAre("R2B")) | 
|  | << "All reads other than R2B were cancelled"; | 
|  | EXPECT_THAT(ReadsCanceled, ElementsAre("R1", "R2A", "R3")) | 
|  | << "All reads other than R2B were cancelled"; | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, InvalidationNoCrash) { | 
|  | auto Path = testPath("foo.cpp"); | 
|  | TUScheduler S(CDB, optsForTest(), captureDiags()); | 
|  |  | 
|  | Notification StartedRunning; | 
|  | Notification ScheduledChange; | 
|  | // We expect invalidation logic to not crash by trying to invalidate a running | 
|  | // request. | 
|  | S.update(Path, getInputs(Path, ""), WantDiagnostics::Auto); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | S.runWithAST( | 
|  | "invalidatable-but-running", Path, | 
|  | [&](llvm::Expected<InputsAndAST> AST) { | 
|  | StartedRunning.notify(); | 
|  | ScheduledChange.wait(); | 
|  | ASSERT_TRUE(bool(AST)); | 
|  | }, | 
|  | TUScheduler::InvalidateOnUpdate); | 
|  | StartedRunning.wait(); | 
|  | S.update(Path, getInputs(Path, ""), WantDiagnostics::Auto); | 
|  | ScheduledChange.notify(); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, Invalidation) { | 
|  | auto Path = testPath("foo.cpp"); | 
|  | TUScheduler S(CDB, optsForTest(), captureDiags()); | 
|  | std::atomic<int> Builds(0), Actions(0); | 
|  |  | 
|  | Notification Start; | 
|  | updateWithDiags(S, Path, "a", WantDiagnostics::Yes, [&](std::vector<Diag>) { | 
|  | ++Builds; | 
|  | Start.wait(); | 
|  | }); | 
|  | S.runWithAST( | 
|  | "invalidatable", Path, | 
|  | [&](llvm::Expected<InputsAndAST> AST) { | 
|  | ++Actions; | 
|  | EXPECT_FALSE(bool(AST)); | 
|  | llvm::Error E = AST.takeError(); | 
|  | EXPECT_TRUE(E.isA<CancelledError>()); | 
|  | handleAllErrors(std::move(E), [&](const CancelledError &E) { | 
|  | EXPECT_EQ(E.Reason, static_cast<int>(ErrorCode::ContentModified)); | 
|  | }); | 
|  | }, | 
|  | TUScheduler::InvalidateOnUpdate); | 
|  | S.runWithAST( | 
|  | "not-invalidatable", Path, | 
|  | [&](llvm::Expected<InputsAndAST> AST) { | 
|  | ++Actions; | 
|  | EXPECT_TRUE(bool(AST)); | 
|  | }, | 
|  | TUScheduler::NoInvalidation); | 
|  | updateWithDiags(S, Path, "b", WantDiagnostics::Auto, [&](std::vector<Diag>) { | 
|  | ++Builds; | 
|  | ADD_FAILURE() << "Shouldn't build, all dependents invalidated"; | 
|  | }); | 
|  | S.runWithAST( | 
|  | "invalidatable", Path, | 
|  | [&](llvm::Expected<InputsAndAST> AST) { | 
|  | ++Actions; | 
|  | EXPECT_FALSE(bool(AST)); | 
|  | llvm::Error E = AST.takeError(); | 
|  | EXPECT_TRUE(E.isA<CancelledError>()); | 
|  | consumeError(std::move(E)); | 
|  | }, | 
|  | TUScheduler::InvalidateOnUpdate); | 
|  | updateWithDiags(S, Path, "c", WantDiagnostics::Auto, | 
|  | [&](std::vector<Diag>) { ++Builds; }); | 
|  | S.runWithAST( | 
|  | "invalidatable", Path, | 
|  | [&](llvm::Expected<InputsAndAST> AST) { | 
|  | ++Actions; | 
|  | EXPECT_TRUE(bool(AST)) << "Shouldn't be invalidated, no update follows"; | 
|  | }, | 
|  | TUScheduler::InvalidateOnUpdate); | 
|  | Start.notify(); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  |  | 
|  | EXPECT_EQ(2, Builds.load()) << "Middle build should be skipped"; | 
|  | EXPECT_EQ(4, Actions.load()) << "All actions should run (some with error)"; | 
|  | } | 
|  |  | 
|  | // We don't invalidate requests for updates that don't change the file content. | 
|  | // These are mostly "refresh this file" events synthesized inside clangd itself. | 
|  | // (Usually the AST rebuild is elided after verifying that all inputs are | 
|  | // unchanged, but invalidation decisions happen earlier and so independently). | 
|  | // See https://github.com/clangd/clangd/issues/620 | 
|  | TEST_F(TUSchedulerTests, InvalidationUnchanged) { | 
|  | auto Path = testPath("foo.cpp"); | 
|  | TUScheduler S(CDB, optsForTest(), captureDiags()); | 
|  | std::atomic<int> Actions(0); | 
|  |  | 
|  | Notification Start; | 
|  | updateWithDiags(S, Path, "a", WantDiagnostics::Yes, [&](std::vector<Diag>) { | 
|  | Start.wait(); | 
|  | }); | 
|  | S.runWithAST( | 
|  | "invalidatable", Path, | 
|  | [&](llvm::Expected<InputsAndAST> AST) { | 
|  | ++Actions; | 
|  | EXPECT_TRUE(bool(AST)) | 
|  | << "Should not invalidate based on an update with same content: " | 
|  | << llvm::toString(AST.takeError()); | 
|  | }, | 
|  | TUScheduler::InvalidateOnUpdate); | 
|  | updateWithDiags(S, Path, "a", WantDiagnostics::Yes, [&](std::vector<Diag>) { | 
|  | ADD_FAILURE() << "Shouldn't build, identical to previous"; | 
|  | }); | 
|  | Start.notify(); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  |  | 
|  | EXPECT_EQ(1, Actions.load()) << "All actions should run"; | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, ManyUpdates) { | 
|  | const int FilesCount = 3; | 
|  | const int UpdatesPerFile = 10; | 
|  |  | 
|  | std::mutex Mut; | 
|  | int TotalASTReads = 0; | 
|  | int TotalPreambleReads = 0; | 
|  | int TotalUpdates = 0; | 
|  | llvm::StringMap<int> LatestDiagVersion; | 
|  |  | 
|  | // Run TUScheduler and collect some stats. | 
|  | { | 
|  | auto Opts = optsForTest(); | 
|  | Opts.UpdateDebounce = DebouncePolicy::fixed(std::chrono::milliseconds(50)); | 
|  | TUScheduler S(CDB, Opts, captureDiags()); | 
|  |  | 
|  | std::vector<std::string> Files; | 
|  | for (int I = 0; I < FilesCount; ++I) { | 
|  | std::string Name = "foo" + std::to_string(I) + ".cpp"; | 
|  | Files.push_back(testPath(Name)); | 
|  | this->FS.Files[Files.back()] = ""; | 
|  | } | 
|  |  | 
|  | StringRef Contents1 = R"cpp(int a;)cpp"; | 
|  | StringRef Contents2 = R"cpp(int main() { return 1; })cpp"; | 
|  | StringRef Contents3 = R"cpp(int a; int b; int sum() { return a + b; })cpp"; | 
|  |  | 
|  | StringRef AllContents[] = {Contents1, Contents2, Contents3}; | 
|  | const int AllContentsSize = 3; | 
|  |  | 
|  | // Scheduler may run tasks asynchronously, but should propagate the | 
|  | // context. We stash a nonce in the context, and verify it in the task. | 
|  | static Key<int> NonceKey; | 
|  | int Nonce = 0; | 
|  |  | 
|  | for (int FileI = 0; FileI < FilesCount; ++FileI) { | 
|  | for (int UpdateI = 0; UpdateI < UpdatesPerFile; ++UpdateI) { | 
|  | auto Contents = AllContents[(FileI + UpdateI) % AllContentsSize]; | 
|  |  | 
|  | auto File = Files[FileI]; | 
|  | auto Inputs = getInputs(File, Contents.str()); | 
|  | { | 
|  | WithContextValue WithNonce(NonceKey, ++Nonce); | 
|  | Inputs.Version = std::to_string(UpdateI); | 
|  | updateWithDiags( | 
|  | S, File, Inputs, WantDiagnostics::Auto, | 
|  | [File, Nonce, Version(Inputs.Version), &Mut, &TotalUpdates, | 
|  | &LatestDiagVersion](std::vector<Diag>) { | 
|  | EXPECT_THAT(Context::current().get(NonceKey), Pointee(Nonce)); | 
|  | EXPECT_EQ(File, boundPath()); | 
|  |  | 
|  | std::lock_guard<std::mutex> Lock(Mut); | 
|  | ++TotalUpdates; | 
|  | EXPECT_EQ(File, *TUScheduler::getFileBeingProcessedInContext()); | 
|  | // Make sure Diags are for a newer version. | 
|  | auto It = LatestDiagVersion.try_emplace(File, -1); | 
|  | const int PrevVersion = It.first->second; | 
|  | int CurVersion; | 
|  | ASSERT_TRUE(llvm::to_integer(Version, CurVersion, 10)); | 
|  | EXPECT_LT(PrevVersion, CurVersion); | 
|  | It.first->getValue() = CurVersion; | 
|  | }); | 
|  | } | 
|  | { | 
|  | WithContextValue WithNonce(NonceKey, ++Nonce); | 
|  | S.runWithAST( | 
|  | "CheckAST", File, | 
|  | [File, Inputs, Nonce, &Mut, | 
|  | &TotalASTReads](Expected<InputsAndAST> AST) { | 
|  | EXPECT_THAT(Context::current().get(NonceKey), Pointee(Nonce)); | 
|  | EXPECT_EQ(File, boundPath()); | 
|  |  | 
|  | ASSERT_TRUE((bool)AST); | 
|  | EXPECT_EQ(AST->Inputs.Contents, Inputs.Contents); | 
|  | EXPECT_EQ(AST->Inputs.Version, Inputs.Version); | 
|  | EXPECT_EQ(AST->AST.version(), Inputs.Version); | 
|  |  | 
|  | std::lock_guard<std::mutex> Lock(Mut); | 
|  | ++TotalASTReads; | 
|  | EXPECT_EQ(File, *TUScheduler::getFileBeingProcessedInContext()); | 
|  | }); | 
|  | } | 
|  |  | 
|  | { | 
|  | WithContextValue WithNonce(NonceKey, ++Nonce); | 
|  | S.runWithPreamble( | 
|  | "CheckPreamble", File, TUScheduler::Stale, | 
|  | [File, Inputs, Nonce, &Mut, | 
|  | &TotalPreambleReads](Expected<InputsAndPreamble> Preamble) { | 
|  | EXPECT_THAT(Context::current().get(NonceKey), Pointee(Nonce)); | 
|  | EXPECT_EQ(File, boundPath()); | 
|  |  | 
|  | ASSERT_TRUE((bool)Preamble); | 
|  | EXPECT_EQ(Preamble->Contents, Inputs.Contents); | 
|  |  | 
|  | std::lock_guard<std::mutex> Lock(Mut); | 
|  | ++TotalPreambleReads; | 
|  | EXPECT_EQ(File, *TUScheduler::getFileBeingProcessedInContext()); | 
|  | }); | 
|  | } | 
|  | } | 
|  | } | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | } // TUScheduler destructor waits for all operations to finish. | 
|  |  | 
|  | std::lock_guard<std::mutex> Lock(Mut); | 
|  | // Updates might get coalesced in preamble thread and result in dropping | 
|  | // diagnostics for intermediate snapshots. | 
|  | EXPECT_GE(TotalUpdates, FilesCount); | 
|  | EXPECT_LE(TotalUpdates, FilesCount * UpdatesPerFile); | 
|  | // We should receive diags for last update. | 
|  | for (const auto &Entry : LatestDiagVersion) | 
|  | EXPECT_EQ(Entry.second, UpdatesPerFile - 1); | 
|  | EXPECT_EQ(TotalASTReads, FilesCount * UpdatesPerFile); | 
|  | EXPECT_EQ(TotalPreambleReads, FilesCount * UpdatesPerFile); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, EvictedAST) { | 
|  | std::atomic<int> BuiltASTCounter(0); | 
|  | auto Opts = optsForTest(); | 
|  | Opts.AsyncThreadsCount = 1; | 
|  | Opts.RetentionPolicy.MaxRetainedASTs = 2; | 
|  | trace::TestTracer Tracer; | 
|  | TUScheduler S(CDB, Opts); | 
|  |  | 
|  | llvm::StringLiteral SourceContents = R"cpp( | 
|  | int* a; | 
|  | double* b = a; | 
|  | )cpp"; | 
|  | llvm::StringLiteral OtherSourceContents = R"cpp( | 
|  | int* a; | 
|  | double* b = a + 0; | 
|  | )cpp"; | 
|  |  | 
|  | auto Foo = testPath("foo.cpp"); | 
|  | auto Bar = testPath("bar.cpp"); | 
|  | auto Baz = testPath("baz.cpp"); | 
|  |  | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "hit"), SizeIs(0)); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "miss"), SizeIs(0)); | 
|  | // Build one file in advance. We will not access it later, so it will be the | 
|  | // one that the cache will evict. | 
|  | updateWithCallback(S, Foo, SourceContents, WantDiagnostics::Yes, | 
|  | [&BuiltASTCounter]() { ++BuiltASTCounter; }); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | ASSERT_EQ(BuiltASTCounter.load(), 1); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "hit"), SizeIs(0)); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "miss"), SizeIs(1)); | 
|  |  | 
|  | // Build two more files. Since we can retain only 2 ASTs, these should be | 
|  | // the ones we see in the cache later. | 
|  | updateWithCallback(S, Bar, SourceContents, WantDiagnostics::Yes, | 
|  | [&BuiltASTCounter]() { ++BuiltASTCounter; }); | 
|  | updateWithCallback(S, Baz, SourceContents, WantDiagnostics::Yes, | 
|  | [&BuiltASTCounter]() { ++BuiltASTCounter; }); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | ASSERT_EQ(BuiltASTCounter.load(), 3); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "hit"), SizeIs(0)); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "miss"), SizeIs(2)); | 
|  |  | 
|  | // Check only the last two ASTs are retained. | 
|  | ASSERT_THAT(S.getFilesWithCachedAST(), UnorderedElementsAre(Bar, Baz)); | 
|  |  | 
|  | // Access the old file again. | 
|  | updateWithCallback(S, Foo, OtherSourceContents, WantDiagnostics::Yes, | 
|  | [&BuiltASTCounter]() { ++BuiltASTCounter; }); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | ASSERT_EQ(BuiltASTCounter.load(), 4); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "hit"), SizeIs(0)); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "miss"), SizeIs(1)); | 
|  |  | 
|  | // Check the AST for foo.cpp is retained now and one of the others got | 
|  | // evicted. | 
|  | EXPECT_THAT(S.getFilesWithCachedAST(), | 
|  | UnorderedElementsAre(Foo, AnyOf(Bar, Baz))); | 
|  | } | 
|  |  | 
|  | // We send "empty" changes to TUScheduler when we think some external event | 
|  | // *might* have invalidated current state (e.g. a header was edited). | 
|  | // Verify that this doesn't evict our cache entries. | 
|  | TEST_F(TUSchedulerTests, NoopChangesDontThrashCache) { | 
|  | auto Opts = optsForTest(); | 
|  | Opts.RetentionPolicy.MaxRetainedASTs = 1; | 
|  | TUScheduler S(CDB, Opts); | 
|  |  | 
|  | auto Foo = testPath("foo.cpp"); | 
|  | auto FooInputs = getInputs(Foo, "int x=1;"); | 
|  | auto Bar = testPath("bar.cpp"); | 
|  | auto BarInputs = getInputs(Bar, "int x=2;"); | 
|  |  | 
|  | // After opening Foo then Bar, AST cache contains Bar. | 
|  | S.update(Foo, FooInputs, WantDiagnostics::Auto); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | S.update(Bar, BarInputs, WantDiagnostics::Auto); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | ASSERT_THAT(S.getFilesWithCachedAST(), ElementsAre(Bar)); | 
|  |  | 
|  | // Any number of no-op updates to Foo don't dislodge Bar from the cache. | 
|  | S.update(Foo, FooInputs, WantDiagnostics::Auto); | 
|  | S.update(Foo, FooInputs, WantDiagnostics::Auto); | 
|  | S.update(Foo, FooInputs, WantDiagnostics::Auto); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | ASSERT_THAT(S.getFilesWithCachedAST(), ElementsAre(Bar)); | 
|  | // In fact each file has been built only once. | 
|  | ASSERT_EQ(S.fileStats().lookup(Foo).ASTBuilds, 1u); | 
|  | ASSERT_EQ(S.fileStats().lookup(Bar).ASTBuilds, 1u); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, EmptyPreamble) { | 
|  | TUScheduler S(CDB, optsForTest()); | 
|  |  | 
|  | auto Foo = testPath("foo.cpp"); | 
|  | auto Header = testPath("foo.h"); | 
|  |  | 
|  | FS.Files[Header] = "void foo()"; | 
|  | FS.Timestamps[Header] = time_t(0); | 
|  | auto *WithPreamble = R"cpp( | 
|  | #include "foo.h" | 
|  | int main() {} | 
|  | )cpp"; | 
|  | auto *WithEmptyPreamble = R"cpp(int main() {})cpp"; | 
|  | S.update(Foo, getInputs(Foo, WithPreamble), WantDiagnostics::Auto); | 
|  | S.runWithPreamble( | 
|  | "getNonEmptyPreamble", Foo, TUScheduler::Stale, | 
|  | [&](Expected<InputsAndPreamble> Preamble) { | 
|  | // We expect to get a non-empty preamble. | 
|  | EXPECT_GT( | 
|  | cantFail(std::move(Preamble)).Preamble->Preamble.getBounds().Size, | 
|  | 0u); | 
|  | }); | 
|  | // Wait while the preamble is being built. | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  |  | 
|  | // Update the file which results in an empty preamble. | 
|  | S.update(Foo, getInputs(Foo, WithEmptyPreamble), WantDiagnostics::Auto); | 
|  | // Wait while the preamble is being built. | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | S.runWithPreamble( | 
|  | "getEmptyPreamble", Foo, TUScheduler::Stale, | 
|  | [&](Expected<InputsAndPreamble> Preamble) { | 
|  | // We expect to get an empty preamble. | 
|  | EXPECT_EQ( | 
|  | cantFail(std::move(Preamble)).Preamble->Preamble.getBounds().Size, | 
|  | 0u); | 
|  | }); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, ASTSignalsSmokeTests) { | 
|  | TUScheduler S(CDB, optsForTest()); | 
|  | auto Foo = testPath("foo.cpp"); | 
|  | auto Header = testPath("foo.h"); | 
|  |  | 
|  | FS.Files[Header] = "namespace tar { int foo(); }"; | 
|  | const char *Contents = R"cpp( | 
|  | #include "foo.h" | 
|  | namespace ns { | 
|  | int func() { | 
|  | return tar::foo()); | 
|  | } | 
|  | } // namespace ns | 
|  | )cpp"; | 
|  | // Update the file which results in an empty preamble. | 
|  | S.update(Foo, getInputs(Foo, Contents), WantDiagnostics::Yes); | 
|  | // Wait while the preamble is being built. | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | Notification TaskRun; | 
|  | S.runWithPreamble( | 
|  | "ASTSignals", Foo, TUScheduler::Stale, | 
|  | [&](Expected<InputsAndPreamble> IP) { | 
|  | ASSERT_FALSE(!IP); | 
|  | std::vector<std::pair<StringRef, int>> NS; | 
|  | for (const auto &P : IP->Signals->RelatedNamespaces) | 
|  | NS.emplace_back(P.getKey(), P.getValue()); | 
|  | EXPECT_THAT(NS, | 
|  | UnorderedElementsAre(Pair("ns::", 1), Pair("tar::", 1))); | 
|  |  | 
|  | std::vector<std::pair<SymbolID, int>> Sym; | 
|  | for (const auto &P : IP->Signals->ReferencedSymbols) | 
|  | Sym.emplace_back(P.getFirst(), P.getSecond()); | 
|  | EXPECT_THAT(Sym, UnorderedElementsAre(Pair(ns("tar").ID, 1), | 
|  | Pair(ns("ns").ID, 1), | 
|  | Pair(func("tar::foo").ID, 1), | 
|  | Pair(func("ns::func").ID, 1))); | 
|  | TaskRun.notify(); | 
|  | }); | 
|  | TaskRun.wait(); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, RunWaitsForPreamble) { | 
|  | // Testing strategy: we update the file and schedule a few preamble reads at | 
|  | // the same time. All reads should get the same non-null preamble. | 
|  | TUScheduler S(CDB, optsForTest()); | 
|  | auto Foo = testPath("foo.cpp"); | 
|  | auto *NonEmptyPreamble = R"cpp( | 
|  | #define FOO 1 | 
|  | #define BAR 2 | 
|  |  | 
|  | int main() {} | 
|  | )cpp"; | 
|  | constexpr int ReadsToSchedule = 10; | 
|  | std::mutex PreamblesMut; | 
|  | std::vector<const void *> Preambles(ReadsToSchedule, nullptr); | 
|  | S.update(Foo, getInputs(Foo, NonEmptyPreamble), WantDiagnostics::Auto); | 
|  | for (int I = 0; I < ReadsToSchedule; ++I) { | 
|  | S.runWithPreamble( | 
|  | "test", Foo, TUScheduler::Stale, | 
|  | [I, &PreamblesMut, &Preambles](Expected<InputsAndPreamble> IP) { | 
|  | std::lock_guard<std::mutex> Lock(PreamblesMut); | 
|  | Preambles[I] = cantFail(std::move(IP)).Preamble; | 
|  | }); | 
|  | } | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | // Check all actions got the same non-null preamble. | 
|  | std::lock_guard<std::mutex> Lock(PreamblesMut); | 
|  | ASSERT_NE(Preambles[0], nullptr); | 
|  | ASSERT_THAT(Preambles, Each(Preambles[0])); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, NoopOnEmptyChanges) { | 
|  | TUScheduler S(CDB, optsForTest(), captureDiags()); | 
|  |  | 
|  | auto Source = testPath("foo.cpp"); | 
|  | auto Header = testPath("foo.h"); | 
|  |  | 
|  | FS.Files[Header] = "int a;"; | 
|  | FS.Timestamps[Header] = time_t(0); | 
|  |  | 
|  | std::string SourceContents = R"cpp( | 
|  | #include "foo.h" | 
|  | int b = a; | 
|  | )cpp"; | 
|  |  | 
|  | // Return value indicates if the updated callback was received. | 
|  | auto DoUpdate = [&](std::string Contents) -> bool { | 
|  | std::atomic<bool> Updated(false); | 
|  | Updated = false; | 
|  | updateWithDiags(S, Source, Contents, WantDiagnostics::Yes, | 
|  | [&Updated](std::vector<Diag>) { Updated = true; }); | 
|  | bool UpdateFinished = S.blockUntilIdle(timeoutSeconds(60)); | 
|  | if (!UpdateFinished) | 
|  | ADD_FAILURE() << "Updated has not finished in one second. Threading bug?"; | 
|  | return Updated; | 
|  | }; | 
|  |  | 
|  | // Test that subsequent updates with the same inputs do not cause rebuilds. | 
|  | ASSERT_TRUE(DoUpdate(SourceContents)); | 
|  | ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 1u); | 
|  | ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 1u); | 
|  | ASSERT_FALSE(DoUpdate(SourceContents)); | 
|  | ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 1u); | 
|  | ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 1u); | 
|  |  | 
|  | // Update to a header should cause a rebuild, though. | 
|  | FS.Timestamps[Header] = time_t(1); | 
|  | ASSERT_TRUE(DoUpdate(SourceContents)); | 
|  | ASSERT_FALSE(DoUpdate(SourceContents)); | 
|  | ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 2u); | 
|  | ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 2u); | 
|  |  | 
|  | // Update to the contents should cause a rebuild. | 
|  | SourceContents += "\nint c = b;"; | 
|  | ASSERT_TRUE(DoUpdate(SourceContents)); | 
|  | ASSERT_FALSE(DoUpdate(SourceContents)); | 
|  | ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 3u); | 
|  | ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 2u); | 
|  |  | 
|  | // Update to the compile commands should also cause a rebuild. | 
|  | CDB.ExtraClangFlags.push_back("-DSOMETHING"); | 
|  | ASSERT_TRUE(DoUpdate(SourceContents)); | 
|  | ASSERT_FALSE(DoUpdate(SourceContents)); | 
|  | // This causes 2 AST builds always. We first build an AST with the stale | 
|  | // preamble, and build a second AST once the fresh preamble is ready. | 
|  | ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 5u); | 
|  | ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 3u); | 
|  | } | 
|  |  | 
|  | // We rebuild if a completely missing header exists, but not if one is added | 
|  | // on a higher-priority include path entry (for performance). | 
|  | // (Previously we wouldn't automatically rebuild when files were added). | 
|  | TEST_F(TUSchedulerTests, MissingHeader) { | 
|  | CDB.ExtraClangFlags.push_back("-I" + testPath("a")); | 
|  | CDB.ExtraClangFlags.push_back("-I" + testPath("b")); | 
|  | // Force both directories to exist so they don't get pruned. | 
|  | FS.Files.try_emplace("a/__unused__"); | 
|  | FS.Files.try_emplace("b/__unused__"); | 
|  | TUScheduler S(CDB, optsForTest(), captureDiags()); | 
|  |  | 
|  | auto Source = testPath("foo.cpp"); | 
|  | auto HeaderA = testPath("a/foo.h"); | 
|  | auto HeaderB = testPath("b/foo.h"); | 
|  |  | 
|  | auto *SourceContents = R"cpp( | 
|  | #include "foo.h" | 
|  | int c = b; | 
|  | )cpp"; | 
|  |  | 
|  | ParseInputs Inputs = getInputs(Source, SourceContents); | 
|  | std::atomic<size_t> DiagCount(0); | 
|  |  | 
|  | // Update the source contents, which should trigger an initial build with | 
|  | // the header file missing. | 
|  | updateWithDiags( | 
|  | S, Source, Inputs, WantDiagnostics::Yes, | 
|  | [&DiagCount](std::vector<Diag> Diags) { | 
|  | ++DiagCount; | 
|  | EXPECT_THAT(Diags, | 
|  | ElementsAre(Field(&Diag::Message, "'foo.h' file not found"), | 
|  | Field(&Diag::Message, | 
|  | "use of undeclared identifier 'b'"))); | 
|  | }); | 
|  | S.blockUntilIdle(timeoutSeconds(60)); | 
|  |  | 
|  | FS.Files[HeaderB] = "int b;"; | 
|  | FS.Timestamps[HeaderB] = time_t(1); | 
|  |  | 
|  | // The addition of the missing header file triggers a rebuild, no errors. | 
|  | updateWithDiags(S, Source, Inputs, WantDiagnostics::Yes, | 
|  | [&DiagCount](std::vector<Diag> Diags) { | 
|  | ++DiagCount; | 
|  | EXPECT_THAT(Diags, IsEmpty()); | 
|  | }); | 
|  |  | 
|  | // Ensure previous assertions are done before we touch the FS again. | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | // Add the high-priority header file, which should reintroduce the error. | 
|  | FS.Files[HeaderA] = "int a;"; | 
|  | FS.Timestamps[HeaderA] = time_t(1); | 
|  |  | 
|  | // This isn't detected: we don't stat a/foo.h to validate the preamble. | 
|  | updateWithDiags(S, Source, Inputs, WantDiagnostics::Yes, | 
|  | [&DiagCount](std::vector<Diag> Diags) { | 
|  | ++DiagCount; | 
|  | ADD_FAILURE() | 
|  | << "Didn't expect new diagnostics when adding a/foo.h"; | 
|  | }); | 
|  |  | 
|  | // Forcing the reload should cause a rebuild. | 
|  | Inputs.ForceRebuild = true; | 
|  | updateWithDiags( | 
|  | S, Source, Inputs, WantDiagnostics::Yes, | 
|  | [&DiagCount](std::vector<Diag> Diags) { | 
|  | ++DiagCount; | 
|  | ElementsAre(Field(&Diag::Message, "use of undeclared identifier 'b'")); | 
|  | }); | 
|  |  | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | EXPECT_EQ(DiagCount, 3U); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, NoChangeDiags) { | 
|  | trace::TestTracer Tracer; | 
|  | TUScheduler S(CDB, optsForTest(), captureDiags()); | 
|  |  | 
|  | auto FooCpp = testPath("foo.cpp"); | 
|  | const auto *Contents = "int a; int b;"; | 
|  |  | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_read", "hit"), SizeIs(0)); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_read", "miss"), SizeIs(0)); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "hit"), SizeIs(0)); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "miss"), SizeIs(0)); | 
|  | updateWithDiags( | 
|  | S, FooCpp, Contents, WantDiagnostics::No, | 
|  | [](std::vector<Diag>) { ADD_FAILURE() << "Should not be called."; }); | 
|  | S.runWithAST("touchAST", FooCpp, [](Expected<InputsAndAST> IA) { | 
|  | // Make sure the AST was actually built. | 
|  | cantFail(std::move(IA)); | 
|  | }); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_read", "hit"), SizeIs(0)); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_read", "miss"), SizeIs(1)); | 
|  |  | 
|  | // Even though the inputs didn't change and AST can be reused, we need to | 
|  | // report the diagnostics, as they were not reported previously. | 
|  | std::atomic<bool> SeenDiags(false); | 
|  | updateWithDiags(S, FooCpp, Contents, WantDiagnostics::Auto, | 
|  | [&](std::vector<Diag>) { SeenDiags = true; }); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | ASSERT_TRUE(SeenDiags); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "hit"), SizeIs(1)); | 
|  | EXPECT_THAT(Tracer.takeMetric("ast_access_diag", "miss"), SizeIs(0)); | 
|  |  | 
|  | // Subsequent request does not get any diagnostics callback because the same | 
|  | // diags have previously been reported and the inputs didn't change. | 
|  | updateWithDiags( | 
|  | S, FooCpp, Contents, WantDiagnostics::Auto, | 
|  | [&](std::vector<Diag>) { ADD_FAILURE() << "Should not be called."; }); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, Run) { | 
|  | for (bool Sync : {false, true}) { | 
|  | auto Opts = optsForTest(); | 
|  | if (Sync) | 
|  | Opts.AsyncThreadsCount = 0; | 
|  | TUScheduler S(CDB, Opts); | 
|  | std::atomic<int> Counter(0); | 
|  | S.run("add 1", /*Path=*/"", [&] { ++Counter; }); | 
|  | S.run("add 2", /*Path=*/"", [&] { Counter += 2; }); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | EXPECT_EQ(Counter.load(), 3); | 
|  |  | 
|  | Notification TaskRun; | 
|  | Key<int> TestKey; | 
|  | WithContextValue CtxWithKey(TestKey, 10); | 
|  | const char *Path = "somepath"; | 
|  | S.run("props context", Path, [&] { | 
|  | EXPECT_EQ(Context::current().getExisting(TestKey), 10); | 
|  | EXPECT_EQ(Path, boundPath()); | 
|  | TaskRun.notify(); | 
|  | }); | 
|  | TaskRun.wait(); | 
|  | } | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, TUStatus) { | 
|  | class CaptureTUStatus : public ClangdServer::Callbacks { | 
|  | public: | 
|  | void onFileUpdated(PathRef File, const TUStatus &Status) override { | 
|  | auto ASTAction = Status.ASTActivity.K; | 
|  | auto PreambleAction = Status.PreambleActivity; | 
|  | std::lock_guard<std::mutex> Lock(Mutex); | 
|  | // Only push the action if it has changed. Since TUStatus can be published | 
|  | // from either Preamble or AST thread and when one changes the other stays | 
|  | // the same. | 
|  | // Note that this can result in missing some updates when something other | 
|  | // than action kind changes, e.g. when AST is built/reused the action kind | 
|  | // stays as Building. | 
|  | if (ASTActions.empty() || ASTActions.back() != ASTAction) | 
|  | ASTActions.push_back(ASTAction); | 
|  | if (PreambleActions.empty() || PreambleActions.back() != PreambleAction) | 
|  | PreambleActions.push_back(PreambleAction); | 
|  | } | 
|  |  | 
|  | std::vector<PreambleAction> preambleStatuses() { | 
|  | std::lock_guard<std::mutex> Lock(Mutex); | 
|  | return PreambleActions; | 
|  | } | 
|  |  | 
|  | std::vector<ASTAction::Kind> astStatuses() { | 
|  | std::lock_guard<std::mutex> Lock(Mutex); | 
|  | return ASTActions; | 
|  | } | 
|  |  | 
|  | private: | 
|  | std::mutex Mutex; | 
|  | std::vector<ASTAction::Kind> ASTActions; | 
|  | std::vector<PreambleAction> PreambleActions; | 
|  | } CaptureTUStatus; | 
|  | MockFS FS; | 
|  | MockCompilationDatabase CDB; | 
|  | ClangdServer Server(CDB, FS, ClangdServer::optsForTest(), &CaptureTUStatus); | 
|  | Annotations Code("int m^ain () {}"); | 
|  |  | 
|  | // We schedule the following tasks in the queue: | 
|  | //   [Update] [GoToDefinition] | 
|  | Server.addDocument(testPath("foo.cpp"), Code.code(), "1", | 
|  | WantDiagnostics::Auto); | 
|  | ASSERT_TRUE(Server.blockUntilIdleForTest()); | 
|  | Server.locateSymbolAt(testPath("foo.cpp"), Code.point(), | 
|  | [](Expected<std::vector<LocatedSymbol>> Result) { | 
|  | ASSERT_TRUE((bool)Result); | 
|  | }); | 
|  | ASSERT_TRUE(Server.blockUntilIdleForTest()); | 
|  |  | 
|  | EXPECT_THAT(CaptureTUStatus.preambleStatuses(), | 
|  | ElementsAre( | 
|  | // PreambleThread starts idle, as the update is first handled | 
|  | // by ASTWorker. | 
|  | PreambleAction::Idle, | 
|  | // Then it starts building first preamble and releases that to | 
|  | // ASTWorker. | 
|  | PreambleAction::Building, | 
|  | // Then goes idle and stays that way as we don't receive any | 
|  | // more update requests. | 
|  | PreambleAction::Idle)); | 
|  | EXPECT_THAT(CaptureTUStatus.astStatuses(), | 
|  | ElementsAre( | 
|  | // Starts handling the update action and blocks until the | 
|  | // first preamble is built. | 
|  | ASTAction::RunningAction, | 
|  | // Afterwards it builds an AST for that preamble to publish | 
|  | // diagnostics. | 
|  | ASTAction::Building, | 
|  | // Then goes idle. | 
|  | ASTAction::Idle, | 
|  | // Afterwards we start executing go-to-def. | 
|  | ASTAction::RunningAction, | 
|  | // Then go idle. | 
|  | ASTAction::Idle)); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, CommandLineErrors) { | 
|  | // We should see errors from command-line parsing inside the main file. | 
|  | CDB.ExtraClangFlags = {"-fsome-unknown-flag"}; | 
|  |  | 
|  | // (!) 'Ready' must live longer than TUScheduler. | 
|  | Notification Ready; | 
|  |  | 
|  | TUScheduler S(CDB, optsForTest(), captureDiags()); | 
|  | std::vector<Diag> Diagnostics; | 
|  | updateWithDiags(S, testPath("foo.cpp"), "void test() {}", | 
|  | WantDiagnostics::Yes, [&](std::vector<Diag> D) { | 
|  | Diagnostics = std::move(D); | 
|  | Ready.notify(); | 
|  | }); | 
|  | Ready.wait(); | 
|  |  | 
|  | EXPECT_THAT( | 
|  | Diagnostics, | 
|  | ElementsAre(AllOf( | 
|  | Field(&Diag::ID, Eq(diag::err_drv_unknown_argument)), | 
|  | Field(&Diag::Name, Eq("drv_unknown_argument")), | 
|  | Field(&Diag::Message, "unknown argument: '-fsome-unknown-flag'")))); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, CommandLineWarnings) { | 
|  | // We should not see warnings from command-line parsing. | 
|  | CDB.ExtraClangFlags = {"-Wsome-unknown-warning"}; | 
|  |  | 
|  | // (!) 'Ready' must live longer than TUScheduler. | 
|  | Notification Ready; | 
|  |  | 
|  | TUScheduler S(CDB, optsForTest(), captureDiags()); | 
|  | std::vector<Diag> Diagnostics; | 
|  | updateWithDiags(S, testPath("foo.cpp"), "void test() {}", | 
|  | WantDiagnostics::Yes, [&](std::vector<Diag> D) { | 
|  | Diagnostics = std::move(D); | 
|  | Ready.notify(); | 
|  | }); | 
|  | Ready.wait(); | 
|  |  | 
|  | EXPECT_THAT(Diagnostics, IsEmpty()); | 
|  | } | 
|  |  | 
|  | TEST(DebouncePolicy, Compute) { | 
|  | namespace c = std::chrono; | 
|  | DebouncePolicy::clock::duration History[] = { | 
|  | c::seconds(0), | 
|  | c::seconds(5), | 
|  | c::seconds(10), | 
|  | c::seconds(20), | 
|  | }; | 
|  | DebouncePolicy Policy; | 
|  | Policy.Min = c::seconds(3); | 
|  | Policy.Max = c::seconds(25); | 
|  | // Call Policy.compute(History) and return seconds as a float. | 
|  | auto Compute = [&](llvm::ArrayRef<DebouncePolicy::clock::duration> History) { | 
|  | return c::duration_cast<c::duration<float, c::seconds::period>>( | 
|  | Policy.compute(History)) | 
|  | .count(); | 
|  | }; | 
|  | EXPECT_NEAR(10, Compute(History), 0.01) << "(upper) median = 10"; | 
|  | Policy.RebuildRatio = 1.5; | 
|  | EXPECT_NEAR(15, Compute(History), 0.01) << "median = 10, ratio = 1.5"; | 
|  | Policy.RebuildRatio = 3; | 
|  | EXPECT_NEAR(25, Compute(History), 0.01) << "constrained by max"; | 
|  | Policy.RebuildRatio = 0; | 
|  | EXPECT_NEAR(3, Compute(History), 0.01) << "constrained by min"; | 
|  | EXPECT_NEAR(25, Compute({}), 0.01) << "no history -> max"; | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, AsyncPreambleThread) { | 
|  | // Blocks preamble thread while building preamble with \p BlockVersion until | 
|  | // \p N is notified. | 
|  | class BlockPreambleThread : public ParsingCallbacks { | 
|  | public: | 
|  | BlockPreambleThread(llvm::StringRef BlockVersion, Notification &N) | 
|  | : BlockVersion(BlockVersion), N(N) {} | 
|  | void onPreambleAST( | 
|  | PathRef Path, llvm::StringRef Version, CapturedASTCtx, | 
|  | std::shared_ptr<const include_cleaner::PragmaIncludes>) override { | 
|  | if (Version == BlockVersion) | 
|  | N.wait(); | 
|  | } | 
|  |  | 
|  | private: | 
|  | llvm::StringRef BlockVersion; | 
|  | Notification &N; | 
|  | }; | 
|  |  | 
|  | static constexpr llvm::StringLiteral InputsV0 = "v0"; | 
|  | static constexpr llvm::StringLiteral InputsV1 = "v1"; | 
|  | Notification Ready; | 
|  | TUScheduler S(CDB, optsForTest(), | 
|  | std::make_unique<BlockPreambleThread>(InputsV1, Ready)); | 
|  |  | 
|  | Path File = testPath("foo.cpp"); | 
|  | auto PI = getInputs(File, ""); | 
|  | PI.Version = InputsV0.str(); | 
|  | S.update(File, PI, WantDiagnostics::Auto); | 
|  | S.blockUntilIdle(timeoutSeconds(60)); | 
|  |  | 
|  | // Block preamble builds. | 
|  | PI.Version = InputsV1.str(); | 
|  | // Issue second update which will block preamble thread. | 
|  | S.update(File, PI, WantDiagnostics::Auto); | 
|  |  | 
|  | Notification RunASTAction; | 
|  | // Issue an AST read, which shouldn't be blocked and see latest version of the | 
|  | // file. | 
|  | S.runWithAST("test", File, [&](Expected<InputsAndAST> AST) { | 
|  | ASSERT_TRUE(bool(AST)); | 
|  | // Make sure preamble is built with stale inputs, but AST was built using | 
|  | // new ones. | 
|  | EXPECT_THAT(AST->AST.preambleVersion(), InputsV0); | 
|  | EXPECT_THAT(AST->Inputs.Version, InputsV1.str()); | 
|  | RunASTAction.notify(); | 
|  | }); | 
|  | RunASTAction.wait(); | 
|  | Ready.notify(); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, OnlyPublishWhenPreambleIsBuilt) { | 
|  | struct PreamblePublishCounter : public ParsingCallbacks { | 
|  | PreamblePublishCounter(int &PreamblePublishCount) | 
|  | : PreamblePublishCount(PreamblePublishCount) {} | 
|  | void onPreamblePublished(PathRef File) override { ++PreamblePublishCount; } | 
|  | int &PreamblePublishCount; | 
|  | }; | 
|  |  | 
|  | int PreamblePublishCount = 0; | 
|  | TUScheduler S(CDB, optsForTest(), | 
|  | std::make_unique<PreamblePublishCounter>(PreamblePublishCount)); | 
|  |  | 
|  | Path File = testPath("foo.cpp"); | 
|  | S.update(File, getInputs(File, ""), WantDiagnostics::Auto); | 
|  | S.blockUntilIdle(timeoutSeconds(60)); | 
|  | EXPECT_EQ(PreamblePublishCount, 1); | 
|  | // Same contents, no publish. | 
|  | S.update(File, getInputs(File, ""), WantDiagnostics::Auto); | 
|  | S.blockUntilIdle(timeoutSeconds(60)); | 
|  | EXPECT_EQ(PreamblePublishCount, 1); | 
|  | // New contents, should publish. | 
|  | S.update(File, getInputs(File, "#define FOO"), WantDiagnostics::Auto); | 
|  | S.blockUntilIdle(timeoutSeconds(60)); | 
|  | EXPECT_EQ(PreamblePublishCount, 2); | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, PublishWithStalePreamble) { | 
|  | // Callbacks that blocks the preamble thread after the first preamble is | 
|  | // built and stores preamble/main-file versions for diagnostics released. | 
|  | class BlockPreambleThread : public ParsingCallbacks { | 
|  | public: | 
|  | using DiagsCB = std::function<void(ParsedAST &)>; | 
|  | BlockPreambleThread(Notification &UnblockPreamble, DiagsCB CB) | 
|  | : UnblockPreamble(UnblockPreamble), CB(std::move(CB)) {} | 
|  |  | 
|  | void onPreambleAST( | 
|  | PathRef Path, llvm::StringRef Version, CapturedASTCtx, | 
|  | std::shared_ptr<const include_cleaner::PragmaIncludes>) override { | 
|  | if (BuildBefore) | 
|  | ASSERT_TRUE(UnblockPreamble.wait(timeoutSeconds(60))) | 
|  | << "Expected notification"; | 
|  | BuildBefore = true; | 
|  | } | 
|  |  | 
|  | void onMainAST(PathRef File, ParsedAST &AST, PublishFn Publish) override { | 
|  | CB(AST); | 
|  | } | 
|  |  | 
|  | void onFailedAST(PathRef File, llvm::StringRef Version, | 
|  | std::vector<Diag> Diags, PublishFn Publish) override { | 
|  | ADD_FAILURE() << "Received failed ast for: " << File << " with version " | 
|  | << Version << '\n'; | 
|  | } | 
|  |  | 
|  | private: | 
|  | bool BuildBefore = false; | 
|  | Notification &UnblockPreamble; | 
|  | std::function<void(ParsedAST &)> CB; | 
|  | }; | 
|  |  | 
|  | // Helpers for issuing blocking update requests on a TUScheduler, whose | 
|  | // onMainAST callback would call onDiagnostics. | 
|  | class DiagCollector { | 
|  | public: | 
|  | void onDiagnostics(ParsedAST &AST) { | 
|  | std::scoped_lock<std::mutex> Lock(DiagMu); | 
|  | DiagVersions.emplace_back( | 
|  | std::make_pair(AST.preambleVersion()->str(), AST.version().str())); | 
|  | DiagsReceived.notify_all(); | 
|  | } | 
|  |  | 
|  | std::pair<std::string, std::string> | 
|  | waitForNewDiags(TUScheduler &S, PathRef File, ParseInputs PI) { | 
|  | std::unique_lock<std::mutex> Lock(DiagMu); | 
|  | // Perform the update under the lock to make sure it isn't handled until | 
|  | // we're waiting for it. | 
|  | S.update(File, std::move(PI), WantDiagnostics::Auto); | 
|  | size_t OldSize = DiagVersions.size(); | 
|  | bool ReceivedDiags = DiagsReceived.wait_for( | 
|  | Lock, std::chrono::seconds(5), | 
|  | [this, OldSize] { return OldSize + 1 == DiagVersions.size(); }); | 
|  | if (!ReceivedDiags) { | 
|  | ADD_FAILURE() << "Timed out waiting for diags"; | 
|  | return {"invalid", "version"}; | 
|  | } | 
|  | return DiagVersions.back(); | 
|  | } | 
|  |  | 
|  | std::vector<std::pair<std::string, std::string>> diagVersions() { | 
|  | std::scoped_lock<std::mutex> Lock(DiagMu); | 
|  | return DiagVersions; | 
|  | } | 
|  |  | 
|  | private: | 
|  | std::condition_variable DiagsReceived; | 
|  | std::mutex DiagMu; | 
|  | std::vector<std::pair</*PreambleVersion*/ std::string, | 
|  | /*MainFileVersion*/ std::string>> | 
|  | DiagVersions; | 
|  | }; | 
|  |  | 
|  | DiagCollector Collector; | 
|  | Notification UnblockPreamble; | 
|  | auto DiagCallbacks = std::make_unique<BlockPreambleThread>( | 
|  | UnblockPreamble, | 
|  | [&Collector](ParsedAST &AST) { Collector.onDiagnostics(AST); }); | 
|  | TUScheduler S(CDB, optsForTest(), std::move(DiagCallbacks)); | 
|  | Path File = testPath("foo.cpp"); | 
|  | auto BlockForDiags = [&](ParseInputs PI) { | 
|  | return Collector.waitForNewDiags(S, File, std::move(PI)); | 
|  | }; | 
|  |  | 
|  | // Build first preamble. | 
|  | auto PI = getInputs(File, ""); | 
|  | PI.Version = PI.Contents = "1"; | 
|  | ASSERT_THAT(BlockForDiags(PI), testing::Pair("1", "1")); | 
|  |  | 
|  | // Now preamble thread is blocked, so rest of the requests sees only the | 
|  | // stale preamble. | 
|  | PI.Version = "2"; | 
|  | PI.Contents = "#define BAR\n" + PI.Version; | 
|  | ASSERT_THAT(BlockForDiags(PI), testing::Pair("1", "2")); | 
|  |  | 
|  | PI.Version = "3"; | 
|  | PI.Contents = "#define FOO\n" + PI.Version; | 
|  | ASSERT_THAT(BlockForDiags(PI), testing::Pair("1", "3")); | 
|  |  | 
|  | UnblockPreamble.notify(); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  |  | 
|  | // Make sure that we have eventual consistency. | 
|  | EXPECT_THAT(Collector.diagVersions().back(), Pair(PI.Version, PI.Version)); | 
|  |  | 
|  | // Check that WantDiagnostics::No doesn't emit any diags. | 
|  | PI.Version = "4"; | 
|  | PI.Contents = "#define FOO\n" + PI.Version; | 
|  | S.update(File, PI, WantDiagnostics::No); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | EXPECT_THAT(Collector.diagVersions().back(), Pair("3", "3")); | 
|  | } | 
|  |  | 
|  | // If a header file is missing from the CDB (or inferred using heuristics), and | 
|  | // it's included by another open file, then we parse it using that files flags. | 
|  | TEST_F(TUSchedulerTests, IncluderCache) { | 
|  | static std::string Main = testPath("main.cpp"), Main2 = testPath("main2.cpp"), | 
|  | Main3 = testPath("main3.cpp"), | 
|  | NoCmd = testPath("no_cmd.h"), | 
|  | Unreliable = testPath("unreliable.h"), | 
|  | OK = testPath("ok.h"), | 
|  | NotIncluded = testPath("not_included.h"); | 
|  | struct NoHeadersCDB : public GlobalCompilationDatabase { | 
|  | std::optional<tooling::CompileCommand> | 
|  | getCompileCommand(PathRef File) const override { | 
|  | if (File == NoCmd || File == NotIncluded || FailAll) | 
|  | return std::nullopt; | 
|  | auto Basic = getFallbackCommand(File); | 
|  | Basic.Heuristic.clear(); | 
|  | if (File == Unreliable) { | 
|  | Basic.Heuristic = "not reliable"; | 
|  | } else if (File == Main) { | 
|  | Basic.CommandLine.push_back("-DMAIN"); | 
|  | } else if (File == Main2) { | 
|  | Basic.CommandLine.push_back("-DMAIN2"); | 
|  | } else if (File == Main3) { | 
|  | Basic.CommandLine.push_back("-DMAIN3"); | 
|  | } | 
|  | return Basic; | 
|  | } | 
|  |  | 
|  | std::atomic<bool> FailAll{false}; | 
|  | } CDB; | 
|  | TUScheduler S(CDB, optsForTest()); | 
|  | auto GetFlags = [&](PathRef Header) { | 
|  | S.update(Header, getInputs(Header, ";"), WantDiagnostics::Yes); | 
|  | EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | Notification CmdDone; | 
|  | tooling::CompileCommand Cmd; | 
|  | S.runWithPreamble("GetFlags", Header, TUScheduler::StaleOrAbsent, | 
|  | [&](llvm::Expected<InputsAndPreamble> Inputs) { | 
|  | ASSERT_FALSE(!Inputs) << Inputs.takeError(); | 
|  | Cmd = std::move(Inputs->Command); | 
|  | CmdDone.notify(); | 
|  | }); | 
|  | CmdDone.wait(); | 
|  | EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | return Cmd.CommandLine; | 
|  | }; | 
|  |  | 
|  | for (const auto &Path : {NoCmd, Unreliable, OK, NotIncluded}) | 
|  | FS.Files[Path] = ";"; | 
|  |  | 
|  | // Initially these files have normal commands from the CDB. | 
|  | EXPECT_THAT(GetFlags(Main), Contains("-DMAIN")) << "sanity check"; | 
|  | EXPECT_THAT(GetFlags(NoCmd), Not(Contains("-DMAIN"))) << "no includes yet"; | 
|  |  | 
|  | // Now make Main include the others, and some should pick up its flags. | 
|  | const char *AllIncludes = R"cpp( | 
|  | #include "no_cmd.h" | 
|  | #include "ok.h" | 
|  | #include "unreliable.h" | 
|  | )cpp"; | 
|  | S.update(Main, getInputs(Main, AllIncludes), WantDiagnostics::Yes); | 
|  | EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | EXPECT_THAT(GetFlags(NoCmd), Contains("-DMAIN")) | 
|  | << "Included from main file, has no own command"; | 
|  | EXPECT_THAT(GetFlags(Unreliable), Contains("-DMAIN")) | 
|  | << "Included from main file, own command is heuristic"; | 
|  | EXPECT_THAT(GetFlags(OK), Not(Contains("-DMAIN"))) | 
|  | << "Included from main file, but own command is used"; | 
|  | EXPECT_THAT(GetFlags(NotIncluded), Not(Contains("-DMAIN"))) | 
|  | << "Not included from main file"; | 
|  |  | 
|  | // Open another file - it won't overwrite the associations with Main. | 
|  | std::string SomeIncludes = R"cpp( | 
|  | #include "no_cmd.h" | 
|  | #include "not_included.h" | 
|  | )cpp"; | 
|  | S.update(Main2, getInputs(Main2, SomeIncludes), WantDiagnostics::Yes); | 
|  | EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | EXPECT_THAT(GetFlags(NoCmd), | 
|  | AllOf(Contains("-DMAIN"), Not(Contains("-DMAIN2")))) | 
|  | << "mainfile association is stable"; | 
|  | EXPECT_THAT(GetFlags(NotIncluded), | 
|  | AllOf(Contains("-DMAIN2"), Not(Contains("-DMAIN")))) | 
|  | << "new headers are associated with new mainfile"; | 
|  |  | 
|  | // Remove includes from main - this marks the associations as invalid but | 
|  | // doesn't actually remove them until another preamble claims them. | 
|  | S.update(Main, getInputs(Main, ""), WantDiagnostics::Yes); | 
|  | EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | EXPECT_THAT(GetFlags(NoCmd), | 
|  | AllOf(Contains("-DMAIN"), Not(Contains("-DMAIN2")))) | 
|  | << "mainfile association not updated yet!"; | 
|  |  | 
|  | // Open yet another file - this time it claims the associations. | 
|  | S.update(Main3, getInputs(Main3, SomeIncludes), WantDiagnostics::Yes); | 
|  | EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | EXPECT_THAT(GetFlags(NoCmd), Contains("-DMAIN3")) | 
|  | << "association invalidated and then claimed by main3"; | 
|  | EXPECT_THAT(GetFlags(Unreliable), Contains("-DMAIN")) | 
|  | << "association invalidated but not reclaimed"; | 
|  | EXPECT_THAT(GetFlags(NotIncluded), Contains("-DMAIN2")) | 
|  | << "association still valid"; | 
|  |  | 
|  | // Delete the file from CDB, it should invalidate the associations. | 
|  | CDB.FailAll = true; | 
|  | EXPECT_THAT(GetFlags(NoCmd), Not(Contains("-DMAIN3"))) | 
|  | << "association should've been invalidated."; | 
|  | // Also run update for Main3 to invalidate the preeamble to make sure next | 
|  | // update populates include cache associations. | 
|  | S.update(Main3, getInputs(Main3, SomeIncludes), WantDiagnostics::Yes); | 
|  | EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | // Re-add the file and make sure nothing crashes. | 
|  | CDB.FailAll = false; | 
|  | S.update(Main3, getInputs(Main3, SomeIncludes), WantDiagnostics::Yes); | 
|  | EXPECT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | EXPECT_THAT(GetFlags(NoCmd), Contains("-DMAIN3")) | 
|  | << "association invalidated and then claimed by main3"; | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, PreservesLastActiveFile) { | 
|  | for (bool Sync : {false, true}) { | 
|  | auto Opts = optsForTest(); | 
|  | if (Sync) | 
|  | Opts.AsyncThreadsCount = 0; | 
|  | TUScheduler S(CDB, Opts); | 
|  |  | 
|  | auto CheckNoFileActionsSeesLastActiveFile = | 
|  | [&](llvm::StringRef LastActiveFile) { | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | std::atomic<int> Counter(0); | 
|  | // We only check for run and runQuick as runWithAST and | 
|  | // runWithPreamble is always bound to a file. | 
|  | S.run("run-UsesLastActiveFile", /*Path=*/"", [&] { | 
|  | ++Counter; | 
|  | EXPECT_EQ(LastActiveFile, boundPath()); | 
|  | }); | 
|  | S.runQuick("runQuick-UsesLastActiveFile", /*Path=*/"", [&] { | 
|  | ++Counter; | 
|  | EXPECT_EQ(LastActiveFile, boundPath()); | 
|  | }); | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  | EXPECT_EQ(2, Counter.load()); | 
|  | }; | 
|  |  | 
|  | // Check that we see no file initially | 
|  | CheckNoFileActionsSeesLastActiveFile(""); | 
|  |  | 
|  | // Now check that every action scheduled with a particular file changes the | 
|  | // LastActiveFile. | 
|  | auto Path = testPath("run.cc"); | 
|  | S.run(Path, Path, [] {}); | 
|  | CheckNoFileActionsSeesLastActiveFile(Path); | 
|  |  | 
|  | Path = testPath("runQuick.cc"); | 
|  | S.runQuick(Path, Path, [] {}); | 
|  | CheckNoFileActionsSeesLastActiveFile(Path); | 
|  |  | 
|  | Path = testPath("runWithAST.cc"); | 
|  | S.update(Path, getInputs(Path, ""), WantDiagnostics::No); | 
|  | S.runWithAST(Path, Path, [](llvm::Expected<InputsAndAST> Inp) { | 
|  | EXPECT_TRUE(bool(Inp)); | 
|  | }); | 
|  | CheckNoFileActionsSeesLastActiveFile(Path); | 
|  |  | 
|  | Path = testPath("runWithPreamble.cc"); | 
|  | S.update(Path, getInputs(Path, ""), WantDiagnostics::No); | 
|  | S.runWithPreamble( | 
|  | Path, Path, TUScheduler::Stale, | 
|  | [](llvm::Expected<InputsAndPreamble> Inp) { EXPECT_TRUE(bool(Inp)); }); | 
|  | CheckNoFileActionsSeesLastActiveFile(Path); | 
|  |  | 
|  | Path = testPath("update.cc"); | 
|  | S.update(Path, getInputs(Path, ""), WantDiagnostics::No); | 
|  | CheckNoFileActionsSeesLastActiveFile(Path); | 
|  |  | 
|  | // An update with the same contents should not change LastActiveFile. | 
|  | auto LastActive = Path; | 
|  | Path = testPath("runWithAST.cc"); | 
|  | S.update(Path, getInputs(Path, ""), WantDiagnostics::No); | 
|  | CheckNoFileActionsSeesLastActiveFile(LastActive); | 
|  | } | 
|  | } | 
|  |  | 
|  | TEST_F(TUSchedulerTests, PreambleThrottle) { | 
|  | const int NumRequests = 4; | 
|  | // Silly throttler that waits for 4 requests, and services them in reverse. | 
|  | // Doesn't honor cancellation but records it. | 
|  | struct : public PreambleThrottler { | 
|  | std::mutex Mu; | 
|  | std::vector<std::string> Acquires; | 
|  | std::vector<RequestID> Releases; | 
|  | llvm::DenseMap<RequestID, Callback> Callbacks; | 
|  | // If set, the notification is signalled after acquiring the specified ID. | 
|  | std::optional<std::pair<RequestID, Notification *>> Notify; | 
|  |  | 
|  | RequestID acquire(llvm::StringRef Filename, Callback CB) override { | 
|  | RequestID ID; | 
|  | Callback Invoke; | 
|  | { | 
|  | std::lock_guard<std::mutex> Lock(Mu); | 
|  | ID = Acquires.size(); | 
|  | Acquires.emplace_back(Filename); | 
|  | // If we're full, satisfy this request immediately. | 
|  | if (Acquires.size() == NumRequests) { | 
|  | Invoke = std::move(CB); | 
|  | } else { | 
|  | Callbacks.try_emplace(ID, std::move(CB)); | 
|  | } | 
|  | } | 
|  | if (Invoke) | 
|  | Invoke(); | 
|  | { | 
|  | std::lock_guard<std::mutex> Lock(Mu); | 
|  | if (Notify && ID == Notify->first) { | 
|  | Notify->second->notify(); | 
|  | Notify.reset(); | 
|  | } | 
|  | } | 
|  | return ID; | 
|  | } | 
|  |  | 
|  | void release(RequestID ID) override { | 
|  | Callback SatisfyNext; | 
|  | { | 
|  | std::lock_guard<std::mutex> Lock(Mu); | 
|  | Releases.push_back(ID); | 
|  | if (ID > 0 && Acquires.size() == NumRequests) | 
|  | SatisfyNext = std::move(Callbacks[ID - 1]); | 
|  | } | 
|  | if (SatisfyNext) | 
|  | SatisfyNext(); | 
|  | } | 
|  |  | 
|  | void reset() { | 
|  | Acquires.clear(); | 
|  | Releases.clear(); | 
|  | Callbacks.clear(); | 
|  | } | 
|  | } Throttler; | 
|  |  | 
|  | struct CaptureBuiltFilenames : public ParsingCallbacks { | 
|  | std::vector<std::string> &Filenames; | 
|  | CaptureBuiltFilenames(std::vector<std::string> &Filenames) | 
|  | : Filenames(Filenames) {} | 
|  | void onPreambleAST( | 
|  | PathRef Path, llvm::StringRef Version, CapturedASTCtx, | 
|  | std::shared_ptr<const include_cleaner::PragmaIncludes> PI) override { | 
|  | // Deliberately no synchronization. | 
|  | // The PreambleThrottler should serialize these calls, if not then tsan | 
|  | // will find a bug here. | 
|  | Filenames.emplace_back(Path); | 
|  | } | 
|  | }; | 
|  |  | 
|  | auto Opts = optsForTest(); | 
|  | Opts.AsyncThreadsCount = 2 * NumRequests; // throttler is the bottleneck | 
|  | Opts.PreambleThrottler = &Throttler; | 
|  |  | 
|  | std::vector<std::string> Filenames; | 
|  |  | 
|  | { | 
|  | std::vector<std::string> BuiltFilenames; | 
|  | TUScheduler S(CDB, Opts, | 
|  | std::make_unique<CaptureBuiltFilenames>(BuiltFilenames)); | 
|  | for (unsigned I = 0; I < NumRequests; ++I) { | 
|  | auto Path = testPath(std::to_string(I) + ".cc"); | 
|  | Filenames.push_back(Path); | 
|  | S.update(Path, getInputs(Path, ""), WantDiagnostics::Yes); | 
|  | } | 
|  | ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(60))); | 
|  |  | 
|  | // The throttler saw all files, and we built them. | 
|  | EXPECT_THAT(Throttler.Acquires, | 
|  | testing::UnorderedElementsAreArray(Filenames)); | 
|  | EXPECT_THAT(BuiltFilenames, | 
|  | testing::UnorderedElementsAreArray(Filenames)); | 
|  | // We built the files in reverse order that the throttler saw them. | 
|  | EXPECT_THAT(BuiltFilenames, | 
|  | testing::ElementsAreArray(Throttler.Acquires.rbegin(), | 
|  | Throttler.Acquires.rend())); | 
|  | // Resources for each file were correctly released. | 
|  | EXPECT_THAT(Throttler.Releases, ElementsAre(3, 2, 1, 0)); | 
|  | } | 
|  |  | 
|  | Throttler.reset(); | 
|  |  | 
|  | // This time, enqueue 2 files, then cancel one of them while still waiting. | 
|  | // Finally shut down the server. Observe that everything gets cleaned up. | 
|  | Notification AfterAcquire2; | 
|  | Notification AfterFinishA; | 
|  | Throttler.Notify = {1, &AfterAcquire2}; | 
|  | std::vector<std::string> BuiltFilenames; | 
|  | auto A = testPath("a.cc"); | 
|  | auto B = testPath("b.cc"); | 
|  | Filenames = {A, B}; | 
|  | { | 
|  | TUScheduler S(CDB, Opts, | 
|  | std::make_unique<CaptureBuiltFilenames>(BuiltFilenames)); | 
|  | updateWithCallback(S, A, getInputs(A, ""), WantDiagnostics::Yes, | 
|  | [&] { AfterFinishA.notify(); }); | 
|  | S.update(B, getInputs(B, ""), WantDiagnostics::Yes); | 
|  | AfterAcquire2.wait(); | 
|  |  | 
|  | // The throttler saw all files, but we built none. | 
|  | EXPECT_THAT(Throttler.Acquires, | 
|  | testing::UnorderedElementsAreArray(Filenames)); | 
|  | EXPECT_THAT(BuiltFilenames, testing::IsEmpty()); | 
|  | // We haven't released anything yet, we're still waiting. | 
|  | EXPECT_THAT(Throttler.Releases, testing::IsEmpty()); | 
|  |  | 
|  | // FIXME: This is flaky, because the request can be destroyed after shutdown | 
|  | // if it hasn't been dequeued yet (stop() resets NextRequest). | 
|  | #if 0 | 
|  | // Now close file A, which will shut down its AST worker. | 
|  | S.remove(A); | 
|  | // Request is destroyed after the queue shutdown, so release() has happened. | 
|  | AfterFinishA.wait(); | 
|  | // We still didn't build anything. | 
|  | EXPECT_THAT(BuiltFilenames, testing::IsEmpty()); | 
|  | // But we've cancelled the request to build A (not sure which its ID is). | 
|  | EXPECT_THAT(Throttler.Releases, ElementsAre(AnyOf(1, 0))); | 
|  | #endif | 
|  |  | 
|  | // Now shut down the TU Scheduler. | 
|  | } | 
|  | // The throttler saw all files, but we built none. | 
|  | EXPECT_THAT(Throttler.Acquires, | 
|  | testing::UnorderedElementsAreArray(Filenames)); | 
|  | EXPECT_THAT(BuiltFilenames, testing::IsEmpty()); | 
|  | // We gave up waiting and everything got released (in some order). | 
|  | EXPECT_THAT(Throttler.Releases, UnorderedElementsAre(1, 0)); | 
|  | } | 
|  |  | 
|  | } // namespace | 
|  | } // namespace clangd | 
|  | } // namespace clang |