clang-tools 24.0.0git
ClangdLSPServerTests.cpp
Go to the documentation of this file.
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-exception
6//
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;
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, std::move(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#741
201TEST_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 (*Client
228 .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 matchers
277 // 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 = Client
294 .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 = Client
301 .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 Opts.ConfigProvider =
313
314 // Map bar.cpp to a different compilation database which defines FOO->BAR.
315 FS.Files[".clangd"] = R"yaml(
316If:
317 PathMatch: bar.cpp
318CompileFlags:
319 CompilationDatabase: bar
320)yaml";
321 FS.Files["bar/compile_flags.txt"] = "-DFOO=BAR";
322
323 auto &Client = start();
324 // foo.cpp gets parsed as normal.
325 Client.didOpen("foo.cpp", "int x = FOO;");
326 EXPECT_THAT(Client.diagnostics("foo.cpp"),
327 llvm::ValueIs(testing::ElementsAre(
328 diagMessage("Use of undeclared identifier 'FOO'"))));
329 // bar.cpp shows the configured compile command.
330 Client.didOpen("bar.cpp", "int x = FOO;");
331 EXPECT_THAT(Client.diagnostics("bar.cpp"),
332 llvm::ValueIs(testing::ElementsAre(
333 diagMessage("Use of undeclared identifier 'BAR'"))));
334}
335
336TEST_F(LSPTest, ModulesTest) {
337 class MathModule final : public FeatureModule {
338 OutgoingNotification<int> Changed;
339 void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps,
340 llvm::json::Object &ServerCaps) override {
341 Bind.notification("add", this, &MathModule::add);
342 Bind.method("get", this, &MathModule::get);
343 Changed = Bind.outgoingNotification("changed");
344 }
345
346 int Value = 0;
347
348 void add(const int &X) {
349 Value += X;
350 Changed(Value);
351 }
352 void get(const std::nullptr_t &, Callback<int> Reply) {
353 scheduler().runQuick(
354 "get", "",
355 [Reply(std::move(Reply)), Value(Value)]() mutable { Reply(Value); });
356 }
357 };
358 FeatureModules.add(std::make_unique<MathModule>());
359
360 auto &Client = start();
361 Client.notify("add", 2);
362 Client.notify("add", 8);
363 EXPECT_EQ(10, Client.call("get", nullptr).takeValue());
364 EXPECT_THAT(Client.takeNotifications("changed"),
365 ElementsAre(llvm::json::Value(2), llvm::json::Value(10)));
366}
367
368TEST_F(LSPTest, FeatureModulesThreadingTest) {
369 // A feature module that does its work on a background thread, and so
370 // exercises the block/shutdown protocol.
371 class AsyncCounter final : public FeatureModule {
372 bool ShouldStop = false;
373 int State = 0;
374 std::deque<Callback<int>> Queue; // null = increment, non-null = read.
375 std::condition_variable CV;
376 std::mutex Mu;
377 std::thread Thread;
378
379 void run() {
380 std::unique_lock<std::mutex> Lock(Mu);
381 while (true) {
382 CV.wait(Lock, [&] { return ShouldStop || !Queue.empty(); });
383 if (ShouldStop) {
384 Queue.clear();
385 CV.notify_all();
386 return;
387 }
388 Callback<int> &Task = Queue.front();
389 if (Task)
390 Task(State);
391 else
392 ++State;
393 Queue.pop_front();
394 CV.notify_all();
395 }
396 }
397
398 bool blockUntilIdle(Deadline D) override {
399 std::unique_lock<std::mutex> Lock(Mu);
400 return clangd::wait(Lock, CV, D, [this] { return Queue.empty(); });
401 }
402
403 void stop() override {
404 {
405 std::lock_guard<std::mutex> Lock(Mu);
406 ShouldStop = true;
407 }
408 CV.notify_all();
409 }
410
411 public:
412 AsyncCounter() : Thread([this] { run(); }) {}
413 ~AsyncCounter() {
414 // Verify shutdown sequence was performed.
415 // Real modules would not do this, to be robust to no ClangdServer.
416 {
417 // We still need the lock here, as Queue might be empty when
418 // ClangdServer calls blockUntilIdle, but run() might not have returned
419 // yet.
420 std::lock_guard<std::mutex> Lock(Mu);
421 EXPECT_TRUE(ShouldStop) << "ClangdServer should request shutdown";
422 EXPECT_EQ(Queue.size(), 0u) << "ClangdServer should block until idle";
423 }
424 Thread.join();
425 }
426
427 void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps,
428 llvm::json::Object &ServerCaps) override {
429 Bind.notification("increment", this, &AsyncCounter::increment);
430 }
431
432 // Get the current value, bypassing the queue.
433 // Used to verify that sync->blockUntilIdle avoids races in tests.
434 int getSync() {
435 std::lock_guard<std::mutex> Lock(Mu);
436 return State;
437 }
438
439 // Increment the current value asynchronously.
440 void increment(const std::nullptr_t &) {
441 {
442 std::lock_guard<std::mutex> Lock(Mu);
443 Queue.push_back(nullptr);
444 }
445 CV.notify_all();
446 }
447 };
448
449 FeatureModules.add(std::make_unique<AsyncCounter>());
450 auto &Client = start();
451
452 Client.notify("increment", nullptr);
453 Client.notify("increment", nullptr);
454 Client.notify("increment", nullptr);
455 Client.sync();
456 EXPECT_EQ(3, FeatureModules.get<AsyncCounter>()->getSync());
457 // Throw some work on the queue to make sure shutdown blocks on it.
458 Client.notify("increment", nullptr);
459 Client.notify("increment", nullptr);
460 Client.notify("increment", nullptr);
461 // And immediately shut down. FeatureModule destructor verifies we blocked.
462}
463
464TEST_F(LSPTest, DiagModuleTest) {
465 static constexpr llvm::StringLiteral DiagMsg = "DiagMsg";
466 class DiagModule final : public FeatureModule {
467 struct DiagHooks : public ASTListener {
468 void sawDiagnostic(const clang::Diagnostic &, clangd::Diag &D) override {
469 D.Message = DiagMsg.str();
470 }
471 };
472
473 public:
474 std::unique_ptr<ASTListener> astListeners() override {
475 return std::make_unique<DiagHooks>();
476 }
477 };
478 FeatureModules.add(std::make_unique<DiagModule>());
479
480 auto &Client = start();
481 Client.didOpen("foo.cpp", "test;");
482 EXPECT_THAT(Client.diagnostics("foo.cpp"),
483 llvm::ValueIs(testing::ElementsAre(diagMessage(DiagMsg))));
484}
485
486// Regression test for https://github.com/llvm/llvm-project/issues/196072.
487TEST_F(LSPTest, CompletionOutOfRangePosition) {
488 auto &Client = start();
489 Client.didOpen("foo.cpp", "int x;");
490 auto &Reply = Client.call(
491 "textDocument/completion",
492 llvm::json::Object{
493 {"textDocument", Client.documentID("foo.cpp")},
494 {"position", llvm::json::Object{{"line", 97}, {"character", 0}}},
495 {"context",
496 llvm::json::Object{
497 {"triggerKind", 2},
498 {"triggerCharacter", ">"},
499 }},
500 });
501 auto Result = Reply.take();
502 ASSERT_TRUE(!!Result) << "Expected a response, not a server crash";
503}
504
505// https://github.com/llvm/llvm-project/issues/196225
506TEST_F(LSPTest, ShutdownDuringRename) {
507 Annotations Code("void ^foo();");
508 auto &Client = start();
509 Client.didOpen("foo.cpp", Code.code());
510 auto &Reply = Client.call("textDocument/rename",
511 llvm::json::Object{
512 {"textDocument", Client.documentID("foo.cpp")},
513 {"position", Code.point()},
514 {"newName", "bar"},
515 });
516 stop();
517 auto Result = Reply.take();
518 ASSERT_TRUE(!!Result) << "Expected a response, not a server crash";
519}
520
521} // namespace
522} // namespace clangd
523} // namespace clang
static clang::FrontendPluginRegistry::Add< clang::tidy::ClangTidyPluginAction > X("clang-tidy", "clang-tidy")
Same as llvm::Annotations, but adjusts functions to LSP-specific types for positions and ranges.
Definition Annotations.h:23
A FeatureModule contributes a vertical feature to clangd.
static std::unique_ptr< Provider > fromAncestorRelativeYAMLFiles(llvm::StringRef RelPath, const ThreadsafeFS &, bool Trusted=false)
A RAII Tracer that can be used by tests.
Definition TestTracer.h:28
FIXME: Skip testing on windows temporarily due to the different escaping code mode.
Definition AST.cpp:44
@ Changed
The file got changed.
Definition Protocol.h:833
TEST_F(BackgroundIndexTest, NoCrashOnErrorFile)
MATCHER_P(named, N, "")
void log(const char *Fmt, Ts &&... Vals)
Definition Logger.h:67
std::string Path
A typedef to represent a file path.
Definition Path.h:26
===– Representation.cpp - ClangDoc Representation --------—*- C++ -*-===//