| //===-- ClangdLSPServerTests.cpp ------------------------------------------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "Annotations.h" |
| #include "ClangdLSPServer.h" |
| #include "LSPClient.h" |
| #include "Protocol.h" |
| #include "TestFS.h" |
| #include "support/Logger.h" |
| #include "support/TestTracer.h" |
| #include "llvm/ADT/StringRef.h" |
| #include "llvm/Support/Error.h" |
| #include "llvm/Support/JSON.h" |
| #include "llvm/Testing/Support/Error.h" |
| #include "llvm/Testing/Support/SupportHelpers.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| |
| namespace clang { |
| namespace clangd { |
| namespace { |
| using llvm::Succeeded; |
| using testing::ElementsAre; |
| |
| MATCHER_P(DiagMessage, M, "") { |
| if (const auto *O = arg.getAsObject()) { |
| if (const auto Msg = O->getString("message")) |
| return *Msg == M; |
| } |
| return false; |
| } |
| |
| class LSPTest : public ::testing::Test { |
| protected: |
| LSPTest() : LogSession(L) { |
| ClangdServer::Options &Base = Opts; |
| Base = ClangdServer::optsForTest(); |
| // This is needed to we can test index-based operations like call hierarchy. |
| Base.BuildDynamicSymbolIndex = true; |
| Base.FeatureModules = &FeatureModules; |
| } |
| |
| LSPClient &start() { |
| EXPECT_FALSE(Server.hasValue()) << "Already initialized"; |
| Server.emplace(Client.transport(), FS, Opts); |
| ServerThread.emplace([&] { EXPECT_TRUE(Server->run()); }); |
| Client.call("initialize", llvm::json::Object{}); |
| return Client; |
| } |
| |
| void stop() { |
| assert(Server); |
| Client.call("shutdown", nullptr); |
| Client.notify("exit", nullptr); |
| Client.stop(); |
| ServerThread->join(); |
| Server.reset(); |
| ServerThread.reset(); |
| } |
| |
| ~LSPTest() { |
| if (Server) |
| stop(); |
| } |
| |
| MockFS FS; |
| ClangdLSPServer::Options Opts; |
| FeatureModuleSet FeatureModules; |
| |
| private: |
| class Logger : public clang::clangd::Logger { |
| // Color logs so we can distinguish them from test output. |
| void log(Level L, const char *Fmt, |
| const llvm::formatv_object_base &Message) override { |
| raw_ostream::Colors Color; |
| switch (L) { |
| case Level::Verbose: |
| Color = raw_ostream::BLUE; |
| break; |
| case Level::Error: |
| Color = raw_ostream::RED; |
| break; |
| default: |
| Color = raw_ostream::YELLOW; |
| break; |
| } |
| std::lock_guard<std::mutex> Lock(LogMu); |
| (llvm::outs().changeColor(Color) << Message << "\n").resetColor(); |
| } |
| std::mutex LogMu; |
| }; |
| |
| Logger L; |
| LoggingSession LogSession; |
| llvm::Optional<ClangdLSPServer> Server; |
| llvm::Optional<std::thread> ServerThread; |
| LSPClient Client; |
| }; |
| |
| TEST_F(LSPTest, GoToDefinition) { |
| Annotations Code(R"cpp( |
| int [[fib]](int n) { |
| return n >= 2 ? ^fib(n - 1) + fib(n - 2) : 1; |
| } |
| )cpp"); |
| auto &Client = start(); |
| Client.didOpen("foo.cpp", Code.code()); |
| auto &Def = Client.call("textDocument/definition", |
| llvm::json::Object{ |
| {"textDocument", Client.documentID("foo.cpp")}, |
| {"position", Code.point()}, |
| }); |
| llvm::json::Value Want = llvm::json::Array{llvm::json::Object{ |
| {"uri", Client.uri("foo.cpp")}, {"range", Code.range()}}}; |
| EXPECT_EQ(Def.takeValue(), Want); |
| } |
| |
| TEST_F(LSPTest, Diagnostics) { |
| auto &Client = start(); |
| Client.didOpen("foo.cpp", "void main(int, char**);"); |
| EXPECT_THAT(Client.diagnostics("foo.cpp"), |
| llvm::ValueIs(testing::ElementsAre( |
| DiagMessage("'main' must return 'int' (fix available)")))); |
| |
| Client.didChange("foo.cpp", "int x = \"42\";"); |
| EXPECT_THAT(Client.diagnostics("foo.cpp"), |
| llvm::ValueIs(testing::ElementsAre( |
| DiagMessage("Cannot initialize a variable of type 'int' with " |
| "an lvalue of type 'const char[3]'")))); |
| |
| Client.didClose("foo.cpp"); |
| EXPECT_THAT(Client.diagnostics("foo.cpp"), llvm::ValueIs(testing::IsEmpty())); |
| } |
| |
| TEST_F(LSPTest, DiagnosticsHeaderSaved) { |
| auto &Client = start(); |
| Client.didOpen("foo.cpp", R"cpp( |
| #include "foo.h" |
| int x = VAR; |
| )cpp"); |
| EXPECT_THAT(Client.diagnostics("foo.cpp"), |
| llvm::ValueIs(testing::ElementsAre( |
| DiagMessage("'foo.h' file not found"), |
| DiagMessage("Use of undeclared identifier 'VAR'")))); |
| // Now create the header. |
| FS.Files["foo.h"] = "#define VAR original"; |
| Client.notify( |
| "textDocument/didSave", |
| llvm::json::Object{{"textDocument", Client.documentID("foo.h")}}); |
| EXPECT_THAT(Client.diagnostics("foo.cpp"), |
| llvm::ValueIs(testing::ElementsAre( |
| DiagMessage("Use of undeclared identifier 'original'")))); |
| // Now modify the header from within the "editor". |
| FS.Files["foo.h"] = "#define VAR changed"; |
| Client.notify( |
| "textDocument/didSave", |
| llvm::json::Object{{"textDocument", Client.documentID("foo.h")}}); |
| // Foo.cpp should be rebuilt with new diagnostics. |
| EXPECT_THAT(Client.diagnostics("foo.cpp"), |
| llvm::ValueIs(testing::ElementsAre( |
| DiagMessage("Use of undeclared identifier 'changed'")))); |
| } |
| |
| TEST_F(LSPTest, RecordsLatencies) { |
| trace::TestTracer Tracer; |
| auto &Client = start(); |
| llvm::StringLiteral MethodName = "method_name"; |
| EXPECT_THAT(Tracer.takeMetric("lsp_latency", MethodName), testing::SizeIs(0)); |
| llvm::consumeError(Client.call(MethodName, {}).take().takeError()); |
| stop(); |
| EXPECT_THAT(Tracer.takeMetric("lsp_latency", MethodName), testing::SizeIs(1)); |
| } |
| |
| TEST_F(LSPTest, IncomingCalls) { |
| Annotations Code(R"cpp( |
| void calle^e(int); |
| void caller1() { |
| [[callee]](42); |
| } |
| )cpp"); |
| auto &Client = start(); |
| Client.didOpen("foo.cpp", Code.code()); |
| auto Items = Client |
| .call("textDocument/prepareCallHierarchy", |
| llvm::json::Object{ |
| {"textDocument", Client.documentID("foo.cpp")}, |
| {"position", Code.point()}}) |
| .takeValue(); |
| auto FirstItem = (*Items.getAsArray())[0]; |
| auto Calls = Client |
| .call("callHierarchy/incomingCalls", |
| llvm::json::Object{{"item", FirstItem}}) |
| .takeValue(); |
| auto FirstCall = *(*Calls.getAsArray())[0].getAsObject(); |
| EXPECT_EQ(FirstCall["fromRanges"], llvm::json::Value{Code.range()}); |
| auto From = *FirstCall["from"].getAsObject(); |
| EXPECT_EQ(From["name"], "caller1"); |
| } |
| |
| TEST_F(LSPTest, CDBConfigIntegration) { |
| auto CfgProvider = |
| config::Provider::fromAncestorRelativeYAMLFiles(".clangd", FS); |
| Opts.ConfigProvider = CfgProvider.get(); |
| |
| // Map bar.cpp to a different compilation database which defines FOO->BAR. |
| FS.Files[".clangd"] = R"yaml( |
| If: |
| PathMatch: bar.cpp |
| CompileFlags: |
| CompilationDatabase: bar |
| )yaml"; |
| FS.Files["bar/compile_flags.txt"] = "-DFOO=BAR"; |
| |
| auto &Client = start(); |
| // foo.cpp gets parsed as normal. |
| Client.didOpen("foo.cpp", "int x = FOO;"); |
| EXPECT_THAT(Client.diagnostics("foo.cpp"), |
| llvm::ValueIs(testing::ElementsAre( |
| DiagMessage("Use of undeclared identifier 'FOO'")))); |
| // bar.cpp shows the configured compile command. |
| Client.didOpen("bar.cpp", "int x = FOO;"); |
| EXPECT_THAT(Client.diagnostics("bar.cpp"), |
| llvm::ValueIs(testing::ElementsAre( |
| DiagMessage("Use of undeclared identifier 'BAR'")))); |
| } |
| |
| TEST_F(LSPTest, ModulesTest) { |
| class MathModule final : public FeatureModule { |
| OutgoingNotification<int> Changed; |
| void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps, |
| llvm::json::Object &ServerCaps) override { |
| Bind.notification("add", this, &MathModule::add); |
| Bind.method("get", this, &MathModule::get); |
| Changed = Bind.outgoingNotification("changed"); |
| } |
| |
| int Value = 0; |
| |
| void add(const int &X) { |
| Value += X; |
| Changed(Value); |
| } |
| void get(const std::nullptr_t &, Callback<int> Reply) { |
| scheduler().runQuick( |
| "get", "", |
| [Reply(std::move(Reply)), Value(Value)]() mutable { Reply(Value); }); |
| } |
| }; |
| FeatureModules.add(std::make_unique<MathModule>()); |
| |
| auto &Client = start(); |
| Client.notify("add", 2); |
| Client.notify("add", 8); |
| EXPECT_EQ(10, Client.call("get", nullptr).takeValue()); |
| EXPECT_THAT(Client.takeNotifications("changed"), |
| ElementsAre(llvm::json::Value(2), llvm::json::Value(10))); |
| } |
| |
| // Creates a Callback that writes its received value into an Optional<Expected>. |
| template <typename T> |
| llvm::unique_function<void(llvm::Expected<T>)> |
| capture(llvm::Optional<llvm::Expected<T>> &Out) { |
| Out.reset(); |
| return [&Out](llvm::Expected<T> V) { Out.emplace(std::move(V)); }; |
| } |
| |
| TEST_F(LSPTest, FeatureModulesThreadingTest) { |
| // A feature module that does its work on a background thread, and so |
| // exercises the block/shutdown protocol. |
| class AsyncCounter final : public FeatureModule { |
| bool ShouldStop = false; |
| int State = 0; |
| std::deque<Callback<int>> Queue; // null = increment, non-null = read. |
| std::condition_variable CV; |
| std::mutex Mu; |
| std::thread Thread; |
| |
| void run() { |
| std::unique_lock<std::mutex> Lock(Mu); |
| while (true) { |
| CV.wait(Lock, [&] { return ShouldStop || !Queue.empty(); }); |
| if (ShouldStop) { |
| Queue.clear(); |
| CV.notify_all(); |
| return; |
| } |
| Callback<int> &Task = Queue.front(); |
| if (Task) |
| Task(State); |
| else |
| ++State; |
| Queue.pop_front(); |
| CV.notify_all(); |
| } |
| } |
| |
| bool blockUntilIdle(Deadline D) override { |
| std::unique_lock<std::mutex> Lock(Mu); |
| return clangd::wait(Lock, CV, D, [this] { return Queue.empty(); }); |
| } |
| |
| void stop() override { |
| { |
| std::lock_guard<std::mutex> Lock(Mu); |
| ShouldStop = true; |
| } |
| CV.notify_all(); |
| } |
| |
| public: |
| AsyncCounter() : Thread([this] { run(); }) {} |
| ~AsyncCounter() { |
| // Verify shutdown sequence was performed. |
| // Real modules would not do this, to be robust to no ClangdServer. |
| { |
| // We still need the lock here, as Queue might be empty when |
| // ClangdServer calls blockUntilIdle, but run() might not have returned |
| // yet. |
| std::lock_guard<std::mutex> Lock(Mu); |
| EXPECT_TRUE(ShouldStop) << "ClangdServer should request shutdown"; |
| EXPECT_EQ(Queue.size(), 0u) << "ClangdServer should block until idle"; |
| } |
| Thread.join(); |
| } |
| |
| void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps, |
| llvm::json::Object &ServerCaps) override { |
| Bind.notification("increment", this, &AsyncCounter::increment); |
| } |
| |
| // Get the current value, bypassing the queue. |
| // Used to verify that sync->blockUntilIdle avoids races in tests. |
| int getSync() { |
| std::lock_guard<std::mutex> Lock(Mu); |
| return State; |
| } |
| |
| // Increment the current value asynchronously. |
| void increment(const std::nullptr_t &) { |
| { |
| std::lock_guard<std::mutex> Lock(Mu); |
| Queue.push_back(nullptr); |
| } |
| CV.notify_all(); |
| } |
| }; |
| |
| FeatureModules.add(std::make_unique<AsyncCounter>()); |
| auto &Client = start(); |
| |
| Client.notify("increment", nullptr); |
| Client.notify("increment", nullptr); |
| Client.notify("increment", nullptr); |
| EXPECT_THAT_EXPECTED(Client.call("sync", nullptr).take(), Succeeded()); |
| EXPECT_EQ(3, FeatureModules.get<AsyncCounter>()->getSync()); |
| // Throw some work on the queue to make sure shutdown blocks on it. |
| Client.notify("increment", nullptr); |
| Client.notify("increment", nullptr); |
| Client.notify("increment", nullptr); |
| // And immediately shut down. FeatureModule destructor verifies we blocked. |
| } |
| |
| TEST_F(LSPTest, DiagModuleTest) { |
| static constexpr llvm::StringLiteral DiagMsg = "DiagMsg"; |
| class DiagModule final : public FeatureModule { |
| struct DiagHooks : public ASTListener { |
| void sawDiagnostic(const clang::Diagnostic &, clangd::Diag &D) override { |
| D.Message = DiagMsg.str(); |
| } |
| }; |
| |
| public: |
| std::unique_ptr<ASTListener> astListeners() override { |
| return std::make_unique<DiagHooks>(); |
| } |
| }; |
| FeatureModules.add(std::make_unique<DiagModule>()); |
| |
| auto &Client = start(); |
| Client.didOpen("foo.cpp", "test;"); |
| EXPECT_THAT(Client.diagnostics("foo.cpp"), |
| llvm::ValueIs(testing::ElementsAre(DiagMessage(DiagMsg)))); |
| } |
| } // namespace |
| } // namespace clangd |
| } // namespace clang |