498 lines · cpp
1//===-- ClangdLSPServerTests.cpp ------------------------------------------===//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8 9#include "Annotations.h"10#include "ClangdLSPServer.h"11#include "ClangdServer.h"12#include "ConfigProvider.h"13#include "Diagnostics.h"14#include "Feature.h"15#include "FeatureModule.h"16#include "LSPBinder.h"17#include "LSPClient.h"18#include "TestFS.h"19#include "support/Function.h"20#include "support/Logger.h"21#include "support/TestTracer.h"22#include "support/Threading.h"23#include "clang/Basic/Diagnostic.h"24#include "clang/Basic/LLVM.h"25#include "llvm/ADT/FunctionExtras.h"26#include "llvm/ADT/StringRef.h"27#include "llvm/Support/Error.h"28#include "llvm/Support/FormatVariadic.h"29#include "llvm/Support/JSON.h"30#include "llvm/Support/raw_ostream.h"31#include "llvm/Testing/Support/Error.h"32#include "llvm/Testing/Support/SupportHelpers.h"33#include "gmock/gmock.h"34#include "gtest/gtest.h"35#include <cassert>36#include <condition_variable>37#include <cstddef>38#include <deque>39#include <memory>40#include <mutex>41#include <optional>42#include <thread>43#include <utility>44 45namespace clang {46namespace clangd {47namespace {48using testing::ElementsAre;49 50MATCHER_P(diagMessage, M, "") {51 if (const auto *O = arg.getAsObject()) {52 if (const auto Msg = O->getString("message"))53 return *Msg == M;54 }55 return false;56}57 58class LSPTest : public ::testing::Test {59protected:60 LSPTest() : LogSession(L) {61 ClangdServer::Options &Base = Opts;62 Base = ClangdServer::optsForTest();63 // This is needed to we can test index-based operations like call hierarchy.64 Base.BuildDynamicSymbolIndex = true;65 Base.FeatureModules = &FeatureModules;66 }67 68 LSPClient &start() {69 EXPECT_FALSE(Server) << "Already initialized";70 Server.emplace(Client.transport(), FS, Opts);71 ServerThread.emplace([&] { EXPECT_TRUE(Server->run()); });72 Client.call("initialize", llvm::json::Object{});73 return Client;74 }75 76 void stop() {77 assert(Server);78 Client.call("shutdown", nullptr);79 Client.notify("exit", nullptr);80 Client.stop();81 ServerThread->join();82 Server.reset();83 ServerThread.reset();84 }85 86 ~LSPTest() {87 if (Server)88 stop();89 }90 91 MockFS FS;92 ClangdLSPServer::Options Opts;93 FeatureModuleSet FeatureModules;94 95private:96 class Logger : public clang::clangd::Logger {97 // Color logs so we can distinguish them from test output.98 void log(Level L, const char *Fmt,99 const llvm::formatv_object_base &Message) override {100 raw_ostream::Colors Color;101 switch (L) {102 case Level::Verbose:103 Color = raw_ostream::BLUE;104 break;105 case Level::Error:106 Color = raw_ostream::RED;107 break;108 default:109 Color = raw_ostream::YELLOW;110 break;111 }112 std::lock_guard<std::mutex> Lock(LogMu);113 (llvm::outs().changeColor(Color) << Message << "\n").resetColor();114 }115 std::mutex LogMu;116 };117 118 Logger L;119 LoggingSession LogSession;120 std::optional<ClangdLSPServer> Server;121 std::optional<std::thread> ServerThread;122 LSPClient Client;123};124 125TEST_F(LSPTest, GoToDefinition) {126 Annotations Code(R"cpp(127 int [[fib]](int n) {128 return n >= 2 ? ^fib(n - 1) + fib(n - 2) : 1;129 }130 )cpp");131 auto &Client = start();132 Client.didOpen("foo.cpp", Code.code());133 auto &Def = Client.call("textDocument/definition",134 llvm::json::Object{135 {"textDocument", Client.documentID("foo.cpp")},136 {"position", Code.point()},137 });138 llvm::json::Value Want = llvm::json::Array{llvm::json::Object{139 {"uri", Client.uri("foo.cpp")}, {"range", Code.range()}}};140 EXPECT_EQ(Def.takeValue(), Want);141}142 143TEST_F(LSPTest, Diagnostics) {144 auto &Client = start();145 Client.didOpen("foo.cpp", "void main(int, char**);");146 EXPECT_THAT(Client.diagnostics("foo.cpp"),147 llvm::ValueIs(testing::ElementsAre(148 diagMessage("'main' must return 'int' (fix available)"))));149 150 Client.didChange("foo.cpp", "int x = \"42\";");151 EXPECT_THAT(Client.diagnostics("foo.cpp"),152 llvm::ValueIs(testing::ElementsAre(153 diagMessage("Cannot initialize a variable of type 'int' with "154 "an lvalue of type 'const char[3]'"))));155 156 Client.didClose("foo.cpp");157 EXPECT_THAT(Client.diagnostics("foo.cpp"), llvm::ValueIs(testing::IsEmpty()));158}159 160TEST_F(LSPTest, DiagnosticsHeaderSaved) {161 auto &Client = start();162 Client.didOpen("foo.cpp", R"cpp(163 #include "foo.h"164 int x = VAR;165 )cpp");166 EXPECT_THAT(Client.diagnostics("foo.cpp"),167 llvm::ValueIs(testing::ElementsAre(168 diagMessage("'foo.h' file not found"),169 diagMessage("Use of undeclared identifier 'VAR'"))));170 // Now create the header.171 FS.Files["foo.h"] = "#define VAR original";172 Client.notify(173 "textDocument/didSave",174 llvm::json::Object{{"textDocument", Client.documentID("foo.h")}});175 EXPECT_THAT(Client.diagnostics("foo.cpp"),176 llvm::ValueIs(testing::ElementsAre(177 diagMessage("Use of undeclared identifier 'original'"))));178 // Now modify the header from within the "editor".179 FS.Files["foo.h"] = "#define VAR changed";180 Client.notify(181 "textDocument/didSave",182 llvm::json::Object{{"textDocument", Client.documentID("foo.h")}});183 // Foo.cpp should be rebuilt with new diagnostics.184 EXPECT_THAT(Client.diagnostics("foo.cpp"),185 llvm::ValueIs(testing::ElementsAre(186 diagMessage("Use of undeclared identifier 'changed'"))));187}188 189TEST_F(LSPTest, RecordsLatencies) {190 trace::TestTracer Tracer;191 auto &Client = start();192 llvm::StringLiteral MethodName = "method_name";193 EXPECT_THAT(Tracer.takeMetric("lsp_latency", MethodName), testing::SizeIs(0));194 llvm::consumeError(Client.call(MethodName, {}).take().takeError());195 stop();196 EXPECT_THAT(Tracer.takeMetric("lsp_latency", MethodName), testing::SizeIs(1));197}198 199// clang-tidy's renames are converted to clangd's internal rename functionality,200// see clangd#1589 and clangd#741201TEST_F(LSPTest, ClangTidyRename) {202 // This test requires clang-tidy checks to be linked in.203 if (!CLANGD_TIDY_CHECKS)204 return;205 Annotations Header(R"cpp(206 void [[foo]]();207 )cpp");208 Annotations Source(R"cpp(209 void [[foo]]() {}210 )cpp");211 constexpr auto ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts,212 llvm::StringRef) {213 ClangTidyOpts.Checks = {"-*,readability-identifier-naming"};214 ClangTidyOpts.CheckOptions["readability-identifier-naming.FunctionCase"] =215 "CamelCase";216 };217 Opts.ClangTidyProvider = ClangTidyProvider;218 auto &Client = start();219 Client.didOpen("foo.hpp", Header.code());220 Client.didOpen("foo.cpp", Source.code());221 222 auto Diags = Client.diagnostics("foo.cpp");223 ASSERT_TRUE(Diags && !Diags->empty());224 auto RenameDiag = Diags->front();225 226 auto RenameCommand =227 (*Client228 .call("textDocument/codeAction",229 llvm::json::Object{230 {"textDocument", Client.documentID("foo.cpp")},231 {"context",232 llvm::json::Object{233 {"diagnostics", llvm::json::Array{RenameDiag}}}},234 {"range", Source.range()}})235 .takeValue()236 .getAsArray())[0];237 238 ASSERT_EQ((*RenameCommand.getAsObject())["title"],239 "Apply fix: change 'foo' to 'Foo'");240 241 Client.expectServerCall("workspace/applyEdit");242 Client.call("workspace/executeCommand", RenameCommand);243 Client.sync();244 245 auto Params = Client.takeCallParams("workspace/applyEdit");246 auto Uri = [&](llvm::StringRef Path) {247 return Client.uri(Path).getAsString().value().str();248 };249 llvm::json::Object ExpectedEdit = llvm::json::Object{250 {"edit", llvm::json::Object{251 {"changes",252 llvm::json::Object{253 {Uri("foo.hpp"), llvm::json::Array{llvm::json::Object{254 {"range", Header.range()},255 {"newText", "Foo"},256 }}},257 258 {Uri("foo.cpp"), llvm::json::Array{llvm::json::Object{259 {"range", Source.range()},260 {"newText", "Foo"},261 }}}262 263 }}}}};264 EXPECT_EQ(Params, std::vector{llvm::json::Value(std::move(ExpectedEdit))});265}266 267TEST_F(LSPTest, ClangTidyCrash_Issue109367) {268 // This test requires clang-tidy checks to be linked in.269 if (!CLANGD_TIDY_CHECKS)270 return;271 constexpr auto ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts,272 llvm::StringRef) {273 ClangTidyOpts.Checks = {"-*,boost-use-ranges"};274 };275 Opts.ClangTidyProvider = ClangTidyProvider;276 // Check that registering the boost-use-ranges checker's matchers277 // on two different threads does not cause a crash.278 auto &Client = start();279 Client.didOpen("a.cpp", "");280 Client.didOpen("b.cpp", "");281 Client.sync();282}283 284TEST_F(LSPTest, IncomingCalls) {285 Annotations Code(R"cpp(286 void calle^e(int);287 void caller1() {288 [[callee]](42);289 }290 )cpp");291 auto &Client = start();292 Client.didOpen("foo.cpp", Code.code());293 auto Items = Client294 .call("textDocument/prepareCallHierarchy",295 llvm::json::Object{296 {"textDocument", Client.documentID("foo.cpp")},297 {"position", Code.point()}})298 .takeValue();299 auto FirstItem = (*Items.getAsArray())[0];300 auto Calls = Client301 .call("callHierarchy/incomingCalls",302 llvm::json::Object{{"item", FirstItem}})303 .takeValue();304 auto FirstCall = *(*Calls.getAsArray())[0].getAsObject();305 EXPECT_EQ(FirstCall["fromRanges"], llvm::json::Value{Code.range()});306 auto From = *FirstCall["from"].getAsObject();307 EXPECT_EQ(From["name"], "caller1");308}309 310TEST_F(LSPTest, CDBConfigIntegration) {311 auto CfgProvider =312 config::Provider::fromAncestorRelativeYAMLFiles(".clangd", FS);313 Opts.ConfigProvider = CfgProvider.get();314 315 // Map bar.cpp to a different compilation database which defines FOO->BAR.316 FS.Files[".clangd"] = R"yaml(317If:318 PathMatch: bar.cpp319CompileFlags:320 CompilationDatabase: bar321)yaml";322 FS.Files["bar/compile_flags.txt"] = "-DFOO=BAR";323 324 auto &Client = start();325 // foo.cpp gets parsed as normal.326 Client.didOpen("foo.cpp", "int x = FOO;");327 EXPECT_THAT(Client.diagnostics("foo.cpp"),328 llvm::ValueIs(testing::ElementsAre(329 diagMessage("Use of undeclared identifier 'FOO'"))));330 // bar.cpp shows the configured compile command.331 Client.didOpen("bar.cpp", "int x = FOO;");332 EXPECT_THAT(Client.diagnostics("bar.cpp"),333 llvm::ValueIs(testing::ElementsAre(334 diagMessage("Use of undeclared identifier 'BAR'"))));335}336 337TEST_F(LSPTest, ModulesTest) {338 class MathModule final : public FeatureModule {339 OutgoingNotification<int> Changed;340 void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps,341 llvm::json::Object &ServerCaps) override {342 Bind.notification("add", this, &MathModule::add);343 Bind.method("get", this, &MathModule::get);344 Changed = Bind.outgoingNotification("changed");345 }346 347 int Value = 0;348 349 void add(const int &X) {350 Value += X;351 Changed(Value);352 }353 void get(const std::nullptr_t &, Callback<int> Reply) {354 scheduler().runQuick(355 "get", "",356 [Reply(std::move(Reply)), Value(Value)]() mutable { Reply(Value); });357 }358 };359 FeatureModules.add(std::make_unique<MathModule>());360 361 auto &Client = start();362 Client.notify("add", 2);363 Client.notify("add", 8);364 EXPECT_EQ(10, Client.call("get", nullptr).takeValue());365 EXPECT_THAT(Client.takeNotifications("changed"),366 ElementsAre(llvm::json::Value(2), llvm::json::Value(10)));367}368 369// Creates a Callback that writes its received value into an370// std::optional<Expected>.371template <typename T>372llvm::unique_function<void(llvm::Expected<T>)>373capture(std::optional<llvm::Expected<T>> &Out) {374 Out.reset();375 return [&Out](llvm::Expected<T> V) { Out.emplace(std::move(V)); };376}377 378TEST_F(LSPTest, FeatureModulesThreadingTest) {379 // A feature module that does its work on a background thread, and so380 // exercises the block/shutdown protocol.381 class AsyncCounter final : public FeatureModule {382 bool ShouldStop = false;383 int State = 0;384 std::deque<Callback<int>> Queue; // null = increment, non-null = read.385 std::condition_variable CV;386 std::mutex Mu;387 std::thread Thread;388 389 void run() {390 std::unique_lock<std::mutex> Lock(Mu);391 while (true) {392 CV.wait(Lock, [&] { return ShouldStop || !Queue.empty(); });393 if (ShouldStop) {394 Queue.clear();395 CV.notify_all();396 return;397 }398 Callback<int> &Task = Queue.front();399 if (Task)400 Task(State);401 else402 ++State;403 Queue.pop_front();404 CV.notify_all();405 }406 }407 408 bool blockUntilIdle(Deadline D) override {409 std::unique_lock<std::mutex> Lock(Mu);410 return clangd::wait(Lock, CV, D, [this] { return Queue.empty(); });411 }412 413 void stop() override {414 {415 std::lock_guard<std::mutex> Lock(Mu);416 ShouldStop = true;417 }418 CV.notify_all();419 }420 421 public:422 AsyncCounter() : Thread([this] { run(); }) {}423 ~AsyncCounter() {424 // Verify shutdown sequence was performed.425 // Real modules would not do this, to be robust to no ClangdServer.426 {427 // We still need the lock here, as Queue might be empty when428 // ClangdServer calls blockUntilIdle, but run() might not have returned429 // yet.430 std::lock_guard<std::mutex> Lock(Mu);431 EXPECT_TRUE(ShouldStop) << "ClangdServer should request shutdown";432 EXPECT_EQ(Queue.size(), 0u) << "ClangdServer should block until idle";433 }434 Thread.join();435 }436 437 void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps,438 llvm::json::Object &ServerCaps) override {439 Bind.notification("increment", this, &AsyncCounter::increment);440 }441 442 // Get the current value, bypassing the queue.443 // Used to verify that sync->blockUntilIdle avoids races in tests.444 int getSync() {445 std::lock_guard<std::mutex> Lock(Mu);446 return State;447 }448 449 // Increment the current value asynchronously.450 void increment(const std::nullptr_t &) {451 {452 std::lock_guard<std::mutex> Lock(Mu);453 Queue.push_back(nullptr);454 }455 CV.notify_all();456 }457 };458 459 FeatureModules.add(std::make_unique<AsyncCounter>());460 auto &Client = start();461 462 Client.notify("increment", nullptr);463 Client.notify("increment", nullptr);464 Client.notify("increment", nullptr);465 Client.sync();466 EXPECT_EQ(3, FeatureModules.get<AsyncCounter>()->getSync());467 // Throw some work on the queue to make sure shutdown blocks on it.468 Client.notify("increment", nullptr);469 Client.notify("increment", nullptr);470 Client.notify("increment", nullptr);471 // And immediately shut down. FeatureModule destructor verifies we blocked.472}473 474TEST_F(LSPTest, DiagModuleTest) {475 static constexpr llvm::StringLiteral DiagMsg = "DiagMsg";476 class DiagModule final : public FeatureModule {477 struct DiagHooks : public ASTListener {478 void sawDiagnostic(const clang::Diagnostic &, clangd::Diag &D) override {479 D.Message = DiagMsg.str();480 }481 };482 483 public:484 std::unique_ptr<ASTListener> astListeners() override {485 return std::make_unique<DiagHooks>();486 }487 };488 FeatureModules.add(std::make_unique<DiagModule>());489 490 auto &Client = start();491 Client.didOpen("foo.cpp", "test;");492 EXPECT_THAT(Client.diagnostics("foo.cpp"),493 llvm::ValueIs(testing::ElementsAre(diagMessage(DiagMsg))));494}495} // namespace496} // namespace clangd497} // namespace clang498