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
44namespace clang {
45namespace clangd {
46namespace {
47using testing::ElementsAre;
48
49MATCHER_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
57class LSPTest : public ::testing::Test {
58protected:
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
94private:
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
124TEST_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
142TEST_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
159TEST_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
188TEST_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
198TEST_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
224TEST_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(
231If:
232 PathMatch: bar.cpp
233CompileFlags:
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
251TEST_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>.
285template <typename T>
286llvm::unique_function<void(llvm::Expected<T>)>
287capture(std::optional<llvm::Expected<T>> &Out) {
288 Out.reset();
289 return [&Out](llvm::Expected<T> V) { Out.emplace(std::move(V)); };
290}
291
292TEST_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
388TEST_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

source code of clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp