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 | |
45 | namespace clang { |
46 | namespace clangd { |
47 | namespace { |
48 | using testing::ElementsAre; |
49 | |
50 | MATCHER_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 | |
58 | class LSPTest : public ::testing::Test { |
59 | protected: |
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(args&: Client.transport(), args&: FS, args&: Opts); |
71 | ServerThread.emplace(args: [&] { EXPECT_TRUE(Server->run()); }); |
72 | Client.call(Method: "initialize" , Params: llvm::json::Object{}); |
73 | return Client; |
74 | } |
75 | |
76 | void stop() { |
77 | assert(Server); |
78 | Client.call(Method: "shutdown" , Params: nullptr); |
79 | Client.notify(Method: "exit" , Params: 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 | |
95 | private: |
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 | |
125 | TEST_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(Path: "foo.cpp" , Content: Code.code()); |
133 | auto &Def = Client.call(Method: "textDocument/definition" , |
134 | Params: llvm::json::Object{ |
135 | {.K: "textDocument" , .V: Client.documentID(Path: "foo.cpp" )}, |
136 | {.K: "position" , .V: Code.point()}, |
137 | }); |
138 | llvm::json::Value Want = llvm::json::Array{llvm::json::Object{ |
139 | {.K: "uri" , .V: Client.uri(Path: "foo.cpp" )}, {.K: "range" , .V: Code.range()}}}; |
140 | EXPECT_EQ(Def.takeValue(), Want); |
141 | } |
142 | |
143 | TEST_F(LSPTest, Diagnostics) { |
144 | auto &Client = start(); |
145 | Client.didOpen(Path: "foo.cpp" , Content: "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(Path: "foo.cpp" , Content: "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(Path: "foo.cpp" ); |
157 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), llvm::ValueIs(testing::IsEmpty())); |
158 | } |
159 | |
160 | TEST_F(LSPTest, DiagnosticsHeaderSaved) { |
161 | auto &Client = start(); |
162 | Client.didOpen(Path: "foo.cpp" , Content: 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 | Method: "textDocument/didSave" , |
174 | Params: llvm::json::Object{{.K: "textDocument" , .V: Client.documentID(Path: "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 | Method: "textDocument/didSave" , |
182 | Params: llvm::json::Object{{.K: "textDocument" , .V: Client.documentID(Path: "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 | |
189 | TEST_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(Err: Client.call(Method: MethodName, Params: {}).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 |
201 | TEST_F(LSPTest, ClangTidyRename) { |
202 | // This test requires clang-tidy checks to be linked in. |
203 | if (!CLANGD_TIDY_CHECKS) |
204 | return; |
205 | Annotations (R"cpp( |
206 | void [[foo]](); |
207 | )cpp" ); |
208 | Annotations Source(R"cpp( |
209 | void [[foo]]() {} |
210 | )cpp" ); |
211 | Opts.ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts, |
212 | llvm::StringRef) { |
213 | ClangTidyOpts.Checks = {"-*,readability-identifier-naming" }; |
214 | ClangTidyOpts.CheckOptions["readability-identifier-naming.FunctionCase" ] = |
215 | "CamelCase" ; |
216 | }; |
217 | auto &Client = start(); |
218 | Client.didOpen(Path: "foo.hpp" , Content: Header.code()); |
219 | Client.didOpen(Path: "foo.cpp" , Content: Source.code()); |
220 | |
221 | auto Diags = Client.diagnostics(Path: "foo.cpp" ); |
222 | ASSERT_TRUE(Diags && !Diags->empty()); |
223 | auto RenameDiag = Diags->front(); |
224 | |
225 | auto RenameCommand = |
226 | (*Client |
227 | .call(Method: "textDocument/codeAction" , |
228 | Params: llvm::json::Object{ |
229 | {.K: "textDocument" , .V: Client.documentID(Path: "foo.cpp" )}, |
230 | {.K: "context" , |
231 | .V: llvm::json::Object{ |
232 | {.K: "diagnostics" , .V: llvm::json::Array{RenameDiag}}}}, |
233 | {.K: "range" , .V: Source.range()}}) |
234 | .takeValue() |
235 | .getAsArray())[0]; |
236 | |
237 | ASSERT_EQ((*RenameCommand.getAsObject())["title" ], "change 'foo' to 'Foo'" ); |
238 | |
239 | Client.expectServerCall(Method: "workspace/applyEdit" ); |
240 | Client.call(Method: "workspace/executeCommand" , Params: RenameCommand); |
241 | Client.sync(); |
242 | |
243 | auto Params = Client.takeCallParams(Method: "workspace/applyEdit" ); |
244 | auto Uri = [&](llvm::StringRef Path) { |
245 | return Client.uri(Path).getAsString().value().str(); |
246 | }; |
247 | llvm::json::Object ExpectedEdit = llvm::json::Object{ |
248 | {.K: "edit" , .V: llvm::json::Object{ |
249 | {.K: "changes" , |
250 | .V: llvm::json::Object{ |
251 | {.K: Uri("foo.hpp" ), .V: llvm::json::Array{llvm::json::Object{ |
252 | {.K: "range" , .V: Header.range()}, |
253 | {.K: "newText" , .V: "Foo" }, |
254 | }}}, |
255 | |
256 | {.K: Uri("foo.cpp" ), .V: llvm::json::Array{llvm::json::Object{ |
257 | {.K: "range" , .V: Source.range()}, |
258 | {.K: "newText" , .V: "Foo" }, |
259 | }}} |
260 | |
261 | }}}}}; |
262 | EXPECT_EQ(Params, std::vector{llvm::json::Value(std::move(ExpectedEdit))}); |
263 | } |
264 | |
265 | TEST_F(LSPTest, IncomingCalls) { |
266 | Annotations Code(R"cpp( |
267 | void calle^e(int); |
268 | void caller1() { |
269 | [[callee]](42); |
270 | } |
271 | )cpp" ); |
272 | auto &Client = start(); |
273 | Client.didOpen(Path: "foo.cpp" , Content: Code.code()); |
274 | auto Items = Client |
275 | .call(Method: "textDocument/prepareCallHierarchy" , |
276 | Params: llvm::json::Object{ |
277 | {.K: "textDocument" , .V: Client.documentID(Path: "foo.cpp" )}, |
278 | {.K: "position" , .V: Code.point()}}) |
279 | .takeValue(); |
280 | auto FirstItem = (*Items.getAsArray())[0]; |
281 | auto Calls = Client |
282 | .call(Method: "callHierarchy/incomingCalls" , |
283 | Params: llvm::json::Object{{.K: "item" , .V: FirstItem}}) |
284 | .takeValue(); |
285 | auto FirstCall = *(*Calls.getAsArray())[0].getAsObject(); |
286 | EXPECT_EQ(FirstCall["fromRanges" ], llvm::json::Value{Code.range()}); |
287 | auto From = *FirstCall["from" ].getAsObject(); |
288 | EXPECT_EQ(From["name" ], "caller1" ); |
289 | } |
290 | |
291 | TEST_F(LSPTest, CDBConfigIntegration) { |
292 | auto CfgProvider = |
293 | config::Provider::fromAncestorRelativeYAMLFiles(RelPath: ".clangd" , FS); |
294 | Opts.ConfigProvider = CfgProvider.get(); |
295 | |
296 | // Map bar.cpp to a different compilation database which defines FOO->BAR. |
297 | FS.Files[".clangd" ] = R"yaml( |
298 | If: |
299 | PathMatch: bar.cpp |
300 | CompileFlags: |
301 | CompilationDatabase: bar |
302 | )yaml" ; |
303 | FS.Files["bar/compile_flags.txt" ] = "-DFOO=BAR" ; |
304 | |
305 | auto &Client = start(); |
306 | // foo.cpp gets parsed as normal. |
307 | Client.didOpen(Path: "foo.cpp" , Content: "int x = FOO;" ); |
308 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
309 | llvm::ValueIs(testing::ElementsAre( |
310 | diagMessage("Use of undeclared identifier 'FOO'" )))); |
311 | // bar.cpp shows the configured compile command. |
312 | Client.didOpen(Path: "bar.cpp" , Content: "int x = FOO;" ); |
313 | EXPECT_THAT(Client.diagnostics("bar.cpp" ), |
314 | llvm::ValueIs(testing::ElementsAre( |
315 | diagMessage("Use of undeclared identifier 'BAR'" )))); |
316 | } |
317 | |
318 | TEST_F(LSPTest, ModulesTest) { |
319 | class MathModule final : public FeatureModule { |
320 | OutgoingNotification<int> Changed; |
321 | void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps, |
322 | llvm::json::Object &ServerCaps) override { |
323 | Bind.notification(Method: "add" , This: this, Handler: &MathModule::add); |
324 | Bind.method(Method: "get" , This: this, Handler: &MathModule::get); |
325 | Changed = Bind.outgoingNotification(Method: "changed" ); |
326 | } |
327 | |
328 | int Value = 0; |
329 | |
330 | void add(const int &X) { |
331 | Value += X; |
332 | Changed(Value); |
333 | } |
334 | void get(const std::nullptr_t &, Callback<int> Reply) { |
335 | scheduler().runQuick( |
336 | Name: "get" , Path: "" , |
337 | Action: [Reply(std::move(Reply)), Value(Value)]() mutable { Reply(Value); }); |
338 | } |
339 | }; |
340 | FeatureModules.add(M: std::make_unique<MathModule>()); |
341 | |
342 | auto &Client = start(); |
343 | Client.notify(Method: "add" , Params: 2); |
344 | Client.notify(Method: "add" , Params: 8); |
345 | EXPECT_EQ(10, Client.call("get" , nullptr).takeValue()); |
346 | EXPECT_THAT(Client.takeNotifications("changed" ), |
347 | ElementsAre(llvm::json::Value(2), llvm::json::Value(10))); |
348 | } |
349 | |
350 | // Creates a Callback that writes its received value into an |
351 | // std::optional<Expected>. |
352 | template <typename T> |
353 | llvm::unique_function<void(llvm::Expected<T>)> |
354 | capture(std::optional<llvm::Expected<T>> &Out) { |
355 | Out.reset(); |
356 | return [&Out](llvm::Expected<T> V) { Out.emplace(std::move(V)); }; |
357 | } |
358 | |
359 | TEST_F(LSPTest, FeatureModulesThreadingTest) { |
360 | // A feature module that does its work on a background thread, and so |
361 | // exercises the block/shutdown protocol. |
362 | class AsyncCounter final : public FeatureModule { |
363 | bool ShouldStop = false; |
364 | int State = 0; |
365 | std::deque<Callback<int>> Queue; // null = increment, non-null = read. |
366 | std::condition_variable CV; |
367 | std::mutex Mu; |
368 | std::thread Thread; |
369 | |
370 | void run() { |
371 | std::unique_lock<std::mutex> Lock(Mu); |
372 | while (true) { |
373 | CV.wait(lock&: Lock, p: [&] { return ShouldStop || !Queue.empty(); }); |
374 | if (ShouldStop) { |
375 | Queue.clear(); |
376 | CV.notify_all(); |
377 | return; |
378 | } |
379 | Callback<int> &Task = Queue.front(); |
380 | if (Task) |
381 | Task(State); |
382 | else |
383 | ++State; |
384 | Queue.pop_front(); |
385 | CV.notify_all(); |
386 | } |
387 | } |
388 | |
389 | bool blockUntilIdle(Deadline D) override { |
390 | std::unique_lock<std::mutex> Lock(Mu); |
391 | return clangd::wait(Lock, CV, D, F: [this] { return Queue.empty(); }); |
392 | } |
393 | |
394 | void stop() override { |
395 | { |
396 | std::lock_guard<std::mutex> Lock(Mu); |
397 | ShouldStop = true; |
398 | } |
399 | CV.notify_all(); |
400 | } |
401 | |
402 | public: |
403 | AsyncCounter() : Thread([this] { run(); }) {} |
404 | ~AsyncCounter() { |
405 | // Verify shutdown sequence was performed. |
406 | // Real modules would not do this, to be robust to no ClangdServer. |
407 | { |
408 | // We still need the lock here, as Queue might be empty when |
409 | // ClangdServer calls blockUntilIdle, but run() might not have returned |
410 | // yet. |
411 | std::lock_guard<std::mutex> Lock(Mu); |
412 | EXPECT_TRUE(ShouldStop) << "ClangdServer should request shutdown" ; |
413 | EXPECT_EQ(Queue.size(), 0u) << "ClangdServer should block until idle" ; |
414 | } |
415 | Thread.join(); |
416 | } |
417 | |
418 | void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps, |
419 | llvm::json::Object &ServerCaps) override { |
420 | Bind.notification(Method: "increment" , This: this, Handler: &AsyncCounter::increment); |
421 | } |
422 | |
423 | // Get the current value, bypassing the queue. |
424 | // Used to verify that sync->blockUntilIdle avoids races in tests. |
425 | int getSync() { |
426 | std::lock_guard<std::mutex> Lock(Mu); |
427 | return State; |
428 | } |
429 | |
430 | // Increment the current value asynchronously. |
431 | void increment(const std::nullptr_t &) { |
432 | { |
433 | std::lock_guard<std::mutex> Lock(Mu); |
434 | Queue.push_back(x: nullptr); |
435 | } |
436 | CV.notify_all(); |
437 | } |
438 | }; |
439 | |
440 | FeatureModules.add(M: std::make_unique<AsyncCounter>()); |
441 | auto &Client = start(); |
442 | |
443 | Client.notify(Method: "increment" , Params: nullptr); |
444 | Client.notify(Method: "increment" , Params: nullptr); |
445 | Client.notify(Method: "increment" , Params: nullptr); |
446 | Client.sync(); |
447 | EXPECT_EQ(3, FeatureModules.get<AsyncCounter>()->getSync()); |
448 | // Throw some work on the queue to make sure shutdown blocks on it. |
449 | Client.notify(Method: "increment" , Params: nullptr); |
450 | Client.notify(Method: "increment" , Params: nullptr); |
451 | Client.notify(Method: "increment" , Params: nullptr); |
452 | // And immediately shut down. FeatureModule destructor verifies we blocked. |
453 | } |
454 | |
455 | TEST_F(LSPTest, DiagModuleTest) { |
456 | static constexpr llvm::StringLiteral DiagMsg = "DiagMsg" ; |
457 | class DiagModule final : public FeatureModule { |
458 | struct DiagHooks : public ASTListener { |
459 | void sawDiagnostic(const clang::Diagnostic &, clangd::Diag &D) override { |
460 | D.Message = DiagMsg.str(); |
461 | } |
462 | }; |
463 | |
464 | public: |
465 | std::unique_ptr<ASTListener> astListeners() override { |
466 | return std::make_unique<DiagHooks>(); |
467 | } |
468 | }; |
469 | FeatureModules.add(M: std::make_unique<DiagModule>()); |
470 | |
471 | auto &Client = start(); |
472 | Client.didOpen(Path: "foo.cpp" , Content: "test;" ); |
473 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
474 | llvm::ValueIs(testing::ElementsAre(diagMessage(DiagMsg)))); |
475 | } |
476 | } // namespace |
477 | } // namespace clangd |
478 | } // namespace clang |
479 | |