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 "FeatureModule.h" |
15 | #include "LSPBinder.h" |
16 | #include "LSPClient.h" |
17 | #include "TestFS.h" |
18 | #include "support/Function.h" |
19 | #include "support/Logger.h" |
20 | #include "support/TestTracer.h" |
21 | #include "support/Threading.h" |
22 | #include "clang/Basic/Diagnostic.h" |
23 | #include "clang/Basic/LLVM.h" |
24 | #include "llvm/ADT/FunctionExtras.h" |
25 | #include "llvm/ADT/StringRef.h" |
26 | #include "llvm/Support/Error.h" |
27 | #include "llvm/Support/FormatVariadic.h" |
28 | #include "llvm/Support/JSON.h" |
29 | #include "llvm/Support/raw_ostream.h" |
30 | #include "llvm/Testing/Support/Error.h" |
31 | #include "llvm/Testing/Support/SupportHelpers.h" |
32 | #include "gmock/gmock.h" |
33 | #include "gtest/gtest.h" |
34 | #include <cassert> |
35 | #include <condition_variable> |
36 | #include <cstddef> |
37 | #include <deque> |
38 | #include <memory> |
39 | #include <mutex> |
40 | #include <optional> |
41 | #include <thread> |
42 | #include <utility> |
43 | |
44 | namespace clang { |
45 | namespace clangd { |
46 | namespace { |
47 | using testing::ElementsAre; |
48 | |
49 | MATCHER_P(diagMessage, M, "" ) { |
50 | if (const auto *O = arg.getAsObject()) { |
51 | if (const auto Msg = O->getString("message" )) |
52 | return *Msg == M; |
53 | } |
54 | return false; |
55 | } |
56 | |
57 | class LSPTest : public ::testing::Test { |
58 | protected: |
59 | LSPTest() : LogSession(L) { |
60 | ClangdServer::Options &Base = Opts; |
61 | Base = ClangdServer::optsForTest(); |
62 | // This is needed to we can test index-based operations like call hierarchy. |
63 | Base.BuildDynamicSymbolIndex = true; |
64 | Base.FeatureModules = &FeatureModules; |
65 | } |
66 | |
67 | LSPClient &start() { |
68 | EXPECT_FALSE(Server) << "Already initialized" ; |
69 | Server.emplace(args&: Client.transport(), args&: FS, args&: Opts); |
70 | ServerThread.emplace(args: [&] { EXPECT_TRUE(Server->run()); }); |
71 | Client.call(Method: "initialize" , Params: llvm::json::Object{}); |
72 | return Client; |
73 | } |
74 | |
75 | void stop() { |
76 | assert(Server); |
77 | Client.call(Method: "shutdown" , Params: nullptr); |
78 | Client.notify(Method: "exit" , Params: nullptr); |
79 | Client.stop(); |
80 | ServerThread->join(); |
81 | Server.reset(); |
82 | ServerThread.reset(); |
83 | } |
84 | |
85 | ~LSPTest() { |
86 | if (Server) |
87 | stop(); |
88 | } |
89 | |
90 | MockFS FS; |
91 | ClangdLSPServer::Options Opts; |
92 | FeatureModuleSet FeatureModules; |
93 | |
94 | private: |
95 | class Logger : public clang::clangd::Logger { |
96 | // Color logs so we can distinguish them from test output. |
97 | void log(Level L, const char *Fmt, |
98 | const llvm::formatv_object_base &Message) override { |
99 | raw_ostream::Colors Color; |
100 | switch (L) { |
101 | case Level::Verbose: |
102 | Color = raw_ostream::BLUE; |
103 | break; |
104 | case Level::Error: |
105 | Color = raw_ostream::RED; |
106 | break; |
107 | default: |
108 | Color = raw_ostream::YELLOW; |
109 | break; |
110 | } |
111 | std::lock_guard<std::mutex> Lock(LogMu); |
112 | (llvm::outs().changeColor(Color) << Message << "\n" ).resetColor(); |
113 | } |
114 | std::mutex LogMu; |
115 | }; |
116 | |
117 | Logger L; |
118 | LoggingSession LogSession; |
119 | std::optional<ClangdLSPServer> Server; |
120 | std::optional<std::thread> ServerThread; |
121 | LSPClient Client; |
122 | }; |
123 | |
124 | TEST_F(LSPTest, GoToDefinition) { |
125 | Annotations Code(R"cpp( |
126 | int [[fib]](int n) { |
127 | return n >= 2 ? ^fib(n - 1) + fib(n - 2) : 1; |
128 | } |
129 | )cpp" ); |
130 | auto &Client = start(); |
131 | Client.didOpen(Path: "foo.cpp" , Content: Code.code()); |
132 | auto &Def = Client.call(Method: "textDocument/definition" , |
133 | Params: llvm::json::Object{ |
134 | {.K: "textDocument" , .V: Client.documentID(Path: "foo.cpp" )}, |
135 | {.K: "position" , .V: Code.point()}, |
136 | }); |
137 | llvm::json::Value Want = llvm::json::Array{llvm::json::Object{ |
138 | {.K: "uri" , .V: Client.uri(Path: "foo.cpp" )}, {.K: "range" , .V: Code.range()}}}; |
139 | EXPECT_EQ(Def.takeValue(), Want); |
140 | } |
141 | |
142 | TEST_F(LSPTest, Diagnostics) { |
143 | auto &Client = start(); |
144 | Client.didOpen(Path: "foo.cpp" , Content: "void main(int, char**);" ); |
145 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
146 | llvm::ValueIs(testing::ElementsAre( |
147 | diagMessage("'main' must return 'int' (fix available)" )))); |
148 | |
149 | Client.didChange(Path: "foo.cpp" , Content: "int x = \"42\";" ); |
150 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
151 | llvm::ValueIs(testing::ElementsAre( |
152 | diagMessage("Cannot initialize a variable of type 'int' with " |
153 | "an lvalue of type 'const char[3]'" )))); |
154 | |
155 | Client.didClose(Path: "foo.cpp" ); |
156 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), llvm::ValueIs(testing::IsEmpty())); |
157 | } |
158 | |
159 | TEST_F(LSPTest, DiagnosticsHeaderSaved) { |
160 | auto &Client = start(); |
161 | Client.didOpen(Path: "foo.cpp" , Content: R"cpp( |
162 | #include "foo.h" |
163 | int x = VAR; |
164 | )cpp" ); |
165 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
166 | llvm::ValueIs(testing::ElementsAre( |
167 | diagMessage("'foo.h' file not found" ), |
168 | diagMessage("Use of undeclared identifier 'VAR'" )))); |
169 | // Now create the header. |
170 | FS.Files["foo.h" ] = "#define VAR original" ; |
171 | Client.notify( |
172 | Method: "textDocument/didSave" , |
173 | Params: llvm::json::Object{{.K: "textDocument" , .V: Client.documentID(Path: "foo.h" )}}); |
174 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
175 | llvm::ValueIs(testing::ElementsAre( |
176 | diagMessage("Use of undeclared identifier 'original'" )))); |
177 | // Now modify the header from within the "editor". |
178 | FS.Files["foo.h" ] = "#define VAR changed" ; |
179 | Client.notify( |
180 | Method: "textDocument/didSave" , |
181 | Params: llvm::json::Object{{.K: "textDocument" , .V: Client.documentID(Path: "foo.h" )}}); |
182 | // Foo.cpp should be rebuilt with new diagnostics. |
183 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
184 | llvm::ValueIs(testing::ElementsAre( |
185 | diagMessage("Use of undeclared identifier 'changed'" )))); |
186 | } |
187 | |
188 | TEST_F(LSPTest, RecordsLatencies) { |
189 | trace::TestTracer Tracer; |
190 | auto &Client = start(); |
191 | llvm::StringLiteral MethodName = "method_name" ; |
192 | EXPECT_THAT(Tracer.takeMetric("lsp_latency" , MethodName), testing::SizeIs(0)); |
193 | llvm::consumeError(Err: Client.call(Method: MethodName, Params: {}).take().takeError()); |
194 | stop(); |
195 | EXPECT_THAT(Tracer.takeMetric("lsp_latency" , MethodName), testing::SizeIs(1)); |
196 | } |
197 | |
198 | TEST_F(LSPTest, IncomingCalls) { |
199 | Annotations Code(R"cpp( |
200 | void calle^e(int); |
201 | void caller1() { |
202 | [[callee]](42); |
203 | } |
204 | )cpp" ); |
205 | auto &Client = start(); |
206 | Client.didOpen(Path: "foo.cpp" , Content: Code.code()); |
207 | auto Items = Client |
208 | .call(Method: "textDocument/prepareCallHierarchy" , |
209 | Params: llvm::json::Object{ |
210 | {.K: "textDocument" , .V: Client.documentID(Path: "foo.cpp" )}, |
211 | {.K: "position" , .V: Code.point()}}) |
212 | .takeValue(); |
213 | auto FirstItem = (*Items.getAsArray())[0]; |
214 | auto Calls = Client |
215 | .call(Method: "callHierarchy/incomingCalls" , |
216 | Params: llvm::json::Object{{.K: "item" , .V: FirstItem}}) |
217 | .takeValue(); |
218 | auto FirstCall = *(*Calls.getAsArray())[0].getAsObject(); |
219 | EXPECT_EQ(FirstCall["fromRanges" ], llvm::json::Value{Code.range()}); |
220 | auto From = *FirstCall["from" ].getAsObject(); |
221 | EXPECT_EQ(From["name" ], "caller1" ); |
222 | } |
223 | |
224 | TEST_F(LSPTest, CDBConfigIntegration) { |
225 | auto CfgProvider = |
226 | config::Provider::fromAncestorRelativeYAMLFiles(RelPath: ".clangd" , FS); |
227 | Opts.ConfigProvider = CfgProvider.get(); |
228 | |
229 | // Map bar.cpp to a different compilation database which defines FOO->BAR. |
230 | FS.Files[".clangd" ] = R"yaml( |
231 | If: |
232 | PathMatch: bar.cpp |
233 | CompileFlags: |
234 | CompilationDatabase: bar |
235 | )yaml" ; |
236 | FS.Files["bar/compile_flags.txt" ] = "-DFOO=BAR" ; |
237 | |
238 | auto &Client = start(); |
239 | // foo.cpp gets parsed as normal. |
240 | Client.didOpen(Path: "foo.cpp" , Content: "int x = FOO;" ); |
241 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
242 | llvm::ValueIs(testing::ElementsAre( |
243 | diagMessage("Use of undeclared identifier 'FOO'" )))); |
244 | // bar.cpp shows the configured compile command. |
245 | Client.didOpen(Path: "bar.cpp" , Content: "int x = FOO;" ); |
246 | EXPECT_THAT(Client.diagnostics("bar.cpp" ), |
247 | llvm::ValueIs(testing::ElementsAre( |
248 | diagMessage("Use of undeclared identifier 'BAR'" )))); |
249 | } |
250 | |
251 | TEST_F(LSPTest, ModulesTest) { |
252 | class MathModule final : public FeatureModule { |
253 | OutgoingNotification<int> Changed; |
254 | void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps, |
255 | llvm::json::Object &ServerCaps) override { |
256 | Bind.notification(Method: "add" , This: this, Handler: &MathModule::add); |
257 | Bind.method(Method: "get" , This: this, Handler: &MathModule::get); |
258 | Changed = Bind.outgoingNotification(Method: "changed" ); |
259 | } |
260 | |
261 | int Value = 0; |
262 | |
263 | void add(const int &X) { |
264 | Value += X; |
265 | Changed(Value); |
266 | } |
267 | void get(const std::nullptr_t &, Callback<int> Reply) { |
268 | scheduler().runQuick( |
269 | Name: "get" , Path: "" , |
270 | Action: [Reply(std::move(Reply)), Value(Value)]() mutable { Reply(Value); }); |
271 | } |
272 | }; |
273 | FeatureModules.add(M: std::make_unique<MathModule>()); |
274 | |
275 | auto &Client = start(); |
276 | Client.notify(Method: "add" , Params: 2); |
277 | Client.notify(Method: "add" , Params: 8); |
278 | EXPECT_EQ(10, Client.call("get" , nullptr).takeValue()); |
279 | EXPECT_THAT(Client.takeNotifications("changed" ), |
280 | ElementsAre(llvm::json::Value(2), llvm::json::Value(10))); |
281 | } |
282 | |
283 | // Creates a Callback that writes its received value into an |
284 | // std::optional<Expected>. |
285 | template <typename T> |
286 | llvm::unique_function<void(llvm::Expected<T>)> |
287 | capture(std::optional<llvm::Expected<T>> &Out) { |
288 | Out.reset(); |
289 | return [&Out](llvm::Expected<T> V) { Out.emplace(std::move(V)); }; |
290 | } |
291 | |
292 | TEST_F(LSPTest, FeatureModulesThreadingTest) { |
293 | // A feature module that does its work on a background thread, and so |
294 | // exercises the block/shutdown protocol. |
295 | class AsyncCounter final : public FeatureModule { |
296 | bool ShouldStop = false; |
297 | int State = 0; |
298 | std::deque<Callback<int>> Queue; // null = increment, non-null = read. |
299 | std::condition_variable CV; |
300 | std::mutex Mu; |
301 | std::thread Thread; |
302 | |
303 | void run() { |
304 | std::unique_lock<std::mutex> Lock(Mu); |
305 | while (true) { |
306 | CV.wait(lock&: Lock, p: [&] { return ShouldStop || !Queue.empty(); }); |
307 | if (ShouldStop) { |
308 | Queue.clear(); |
309 | CV.notify_all(); |
310 | return; |
311 | } |
312 | Callback<int> &Task = Queue.front(); |
313 | if (Task) |
314 | Task(State); |
315 | else |
316 | ++State; |
317 | Queue.pop_front(); |
318 | CV.notify_all(); |
319 | } |
320 | } |
321 | |
322 | bool blockUntilIdle(Deadline D) override { |
323 | std::unique_lock<std::mutex> Lock(Mu); |
324 | return clangd::wait(Lock, CV, D, F: [this] { return Queue.empty(); }); |
325 | } |
326 | |
327 | void stop() override { |
328 | { |
329 | std::lock_guard<std::mutex> Lock(Mu); |
330 | ShouldStop = true; |
331 | } |
332 | CV.notify_all(); |
333 | } |
334 | |
335 | public: |
336 | AsyncCounter() : Thread([this] { run(); }) {} |
337 | ~AsyncCounter() { |
338 | // Verify shutdown sequence was performed. |
339 | // Real modules would not do this, to be robust to no ClangdServer. |
340 | { |
341 | // We still need the lock here, as Queue might be empty when |
342 | // ClangdServer calls blockUntilIdle, but run() might not have returned |
343 | // yet. |
344 | std::lock_guard<std::mutex> Lock(Mu); |
345 | EXPECT_TRUE(ShouldStop) << "ClangdServer should request shutdown" ; |
346 | EXPECT_EQ(Queue.size(), 0u) << "ClangdServer should block until idle" ; |
347 | } |
348 | Thread.join(); |
349 | } |
350 | |
351 | void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps, |
352 | llvm::json::Object &ServerCaps) override { |
353 | Bind.notification(Method: "increment" , This: this, Handler: &AsyncCounter::increment); |
354 | } |
355 | |
356 | // Get the current value, bypassing the queue. |
357 | // Used to verify that sync->blockUntilIdle avoids races in tests. |
358 | int getSync() { |
359 | std::lock_guard<std::mutex> Lock(Mu); |
360 | return State; |
361 | } |
362 | |
363 | // Increment the current value asynchronously. |
364 | void increment(const std::nullptr_t &) { |
365 | { |
366 | std::lock_guard<std::mutex> Lock(Mu); |
367 | Queue.push_back(x: nullptr); |
368 | } |
369 | CV.notify_all(); |
370 | } |
371 | }; |
372 | |
373 | FeatureModules.add(M: std::make_unique<AsyncCounter>()); |
374 | auto &Client = start(); |
375 | |
376 | Client.notify(Method: "increment" , Params: nullptr); |
377 | Client.notify(Method: "increment" , Params: nullptr); |
378 | Client.notify(Method: "increment" , Params: nullptr); |
379 | Client.sync(); |
380 | EXPECT_EQ(3, FeatureModules.get<AsyncCounter>()->getSync()); |
381 | // Throw some work on the queue to make sure shutdown blocks on it. |
382 | Client.notify(Method: "increment" , Params: nullptr); |
383 | Client.notify(Method: "increment" , Params: nullptr); |
384 | Client.notify(Method: "increment" , Params: nullptr); |
385 | // And immediately shut down. FeatureModule destructor verifies we blocked. |
386 | } |
387 | |
388 | TEST_F(LSPTest, DiagModuleTest) { |
389 | static constexpr llvm::StringLiteral DiagMsg = "DiagMsg" ; |
390 | class DiagModule final : public FeatureModule { |
391 | struct DiagHooks : public ASTListener { |
392 | void sawDiagnostic(const clang::Diagnostic &, clangd::Diag &D) override { |
393 | D.Message = DiagMsg.str(); |
394 | } |
395 | }; |
396 | |
397 | public: |
398 | std::unique_ptr<ASTListener> astListeners() override { |
399 | return std::make_unique<DiagHooks>(); |
400 | } |
401 | }; |
402 | FeatureModules.add(M: std::make_unique<DiagModule>()); |
403 | |
404 | auto &Client = start(); |
405 | Client.didOpen(Path: "foo.cpp" , Content: "test;" ); |
406 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
407 | llvm::ValueIs(testing::ElementsAre(diagMessage(DiagMsg)))); |
408 | } |
409 | } // namespace |
410 | } // namespace clangd |
411 | } // namespace clang |
412 | |