1 //===-- ClangdLSPServerTests.cpp ------------------------------------------===//
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
7 //===----------------------------------------------------------------------===//
9 #include "Annotations.h"
10 #include "ClangdLSPServer.h"
11 #include "ClangdServer.h"
12 #include "ConfigProvider.h"
13 #include "Diagnostics.h"
15 #include "FeatureModule.h"
16 #include "LSPBinder.h"
17 #include "LSPClient.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"
36 #include <condition_variable>
48 using testing::ElementsAre
;
50 MATCHER_P(diagMessage
, M
, "") {
51 if (const auto *O
= arg
.getAsObject()) {
52 if (const auto Msg
= O
->getString("message"))
58 class LSPTest
: public ::testing::Test
{
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
;
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
{});
78 Client
.call("shutdown", nullptr);
79 Client
.notify("exit", nullptr);
92 ClangdLSPServer::Options Opts
;
93 FeatureModuleSet FeatureModules
;
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
;
103 Color
= raw_ostream::BLUE
;
106 Color
= raw_ostream::RED
;
109 Color
= raw_ostream::YELLOW
;
112 std::lock_guard
<std::mutex
> Lock(LogMu
);
113 (llvm::outs().changeColor(Color
) << Message
<< "\n").resetColor();
119 LoggingSession LogSession
;
120 std::optional
<ClangdLSPServer
> Server
;
121 std::optional
<std::thread
> ServerThread
;
125 TEST_F(LSPTest
, GoToDefinition
) {
126 Annotations
Code(R
"cpp(
128 return n >= 2 ? ^fib(n - 1) + fib(n - 2) : 1;
131 auto &Client
= start();
132 Client
.didOpen("foo.cpp", Code
.code());
133 auto &Def
= Client
.call("textDocument/definition",
135 {"textDocument", Client
.documentID("foo.cpp")},
136 {"position", Code
.point()},
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
);
143 TEST_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)"))));
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]'"))));
156 Client
.didClose("foo.cpp");
157 EXPECT_THAT(Client
.diagnostics("foo.cpp"), llvm::ValueIs(testing::IsEmpty()));
160 TEST_F(LSPTest
, DiagnosticsHeaderSaved
) {
161 auto &Client
= start();
162 Client
.didOpen("foo.cpp", R
"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";
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";
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'"))));
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(Client
.call(MethodName
, {}).take().takeError());
196 EXPECT_THAT(Tracer
.takeMetric("lsp_latency", MethodName
), testing::SizeIs(1));
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
)
205 Annotations
Header(R
"cpp(
208 Annotations
Source(R
"cpp(
211 Opts
.ClangTidyProvider
= [](tidy::ClangTidyOptions
&ClangTidyOpts
,
213 ClangTidyOpts
.Checks
= {"-*,readability-identifier-naming"};
214 ClangTidyOpts
.CheckOptions
["readability-identifier-naming.FunctionCase"] =
217 auto &Client
= start();
218 Client
.didOpen("foo.hpp", Header
.code());
219 Client
.didOpen("foo.cpp", Source
.code());
221 auto Diags
= Client
.diagnostics("foo.cpp");
222 ASSERT_TRUE(Diags
&& !Diags
->empty());
223 auto RenameDiag
= Diags
->front();
227 .call("textDocument/codeAction",
229 {"textDocument", Client
.documentID("foo.cpp")},
232 {"diagnostics", llvm::json::Array
{RenameDiag
}}}},
233 {"range", Source
.range()}})
237 ASSERT_EQ((*RenameCommand
.getAsObject())["title"], "change 'foo' to 'Foo'");
239 Client
.expectServerCall("workspace/applyEdit");
240 Client
.call("workspace/executeCommand", RenameCommand
);
243 auto Params
= Client
.takeCallParams("workspace/applyEdit");
244 auto Uri
= [&](llvm::StringRef Path
) {
245 return Client
.uri(Path
).getAsString().value().str();
247 llvm::json::Object ExpectedEdit
= llvm::json::Object
{
248 {"edit", llvm::json::Object
{
251 {Uri("foo.hpp"), llvm::json::Array
{llvm::json::Object
{
252 {"range", Header
.range()},
256 {Uri("foo.cpp"), llvm::json::Array
{llvm::json::Object
{
257 {"range", Source
.range()},
262 EXPECT_EQ(Params
, std::vector
{llvm::json::Value(std::move(ExpectedEdit
))});
265 TEST_F(LSPTest
, ClangTidyCrash_Issue109367
) {
266 // This test requires clang-tidy checks to be linked in.
267 if (!CLANGD_TIDY_CHECKS
)
269 Opts
.ClangTidyProvider
= [](tidy::ClangTidyOptions
&ClangTidyOpts
,
271 ClangTidyOpts
.Checks
= {"-*,boost-use-ranges"};
273 // Check that registering the boost-use-ranges checker's matchers
274 // on two different threads does not cause a crash.
275 auto &Client
= start();
276 Client
.didOpen("a.cpp", "");
277 Client
.didOpen("b.cpp", "");
281 TEST_F(LSPTest
, IncomingCalls
) {
282 Annotations
Code(R
"cpp(
288 auto &Client
= start();
289 Client
.didOpen("foo.cpp", Code
.code());
291 .call("textDocument/prepareCallHierarchy",
293 {"textDocument", Client
.documentID("foo.cpp")},
294 {"position", Code
.point()}})
296 auto FirstItem
= (*Items
.getAsArray())[0];
298 .call("callHierarchy/incomingCalls",
299 llvm::json::Object
{{"item", FirstItem
}})
301 auto FirstCall
= *(*Calls
.getAsArray())[0].getAsObject();
302 EXPECT_EQ(FirstCall
["fromRanges"], llvm::json::Value
{Code
.range()});
303 auto From
= *FirstCall
["from"].getAsObject();
304 EXPECT_EQ(From
["name"], "caller1");
307 TEST_F(LSPTest
, CDBConfigIntegration
) {
309 config::Provider::fromAncestorRelativeYAMLFiles(".clangd", FS
);
310 Opts
.ConfigProvider
= CfgProvider
.get();
312 // Map bar.cpp to a different compilation database which defines FOO->BAR.
313 FS
.Files
[".clangd"] = R
"yaml(
317 CompilationDatabase: bar
319 FS
.Files
["bar/compile_flags.txt"] = "-DFOO=BAR";
321 auto &Client
= start();
322 // foo.cpp gets parsed as normal.
323 Client
.didOpen("foo.cpp", "int x = FOO;");
324 EXPECT_THAT(Client
.diagnostics("foo.cpp"),
325 llvm::ValueIs(testing::ElementsAre(
326 diagMessage("Use of undeclared identifier 'FOO'"))));
327 // bar.cpp shows the configured compile command.
328 Client
.didOpen("bar.cpp", "int x = FOO;");
329 EXPECT_THAT(Client
.diagnostics("bar.cpp"),
330 llvm::ValueIs(testing::ElementsAre(
331 diagMessage("Use of undeclared identifier 'BAR'"))));
334 TEST_F(LSPTest
, ModulesTest
) {
335 class MathModule final
: public FeatureModule
{
336 OutgoingNotification
<int> Changed
;
337 void initializeLSP(LSPBinder
&Bind
, const llvm::json::Object
&ClientCaps
,
338 llvm::json::Object
&ServerCaps
) override
{
339 Bind
.notification("add", this, &MathModule::add
);
340 Bind
.method("get", this, &MathModule::get
);
341 Changed
= Bind
.outgoingNotification("changed");
346 void add(const int &X
) {
350 void get(const std::nullptr_t
&, Callback
<int> Reply
) {
351 scheduler().runQuick(
353 [Reply(std::move(Reply
)), Value(Value
)]() mutable { Reply(Value
); });
356 FeatureModules
.add(std::make_unique
<MathModule
>());
358 auto &Client
= start();
359 Client
.notify("add", 2);
360 Client
.notify("add", 8);
361 EXPECT_EQ(10, Client
.call("get", nullptr).takeValue());
362 EXPECT_THAT(Client
.takeNotifications("changed"),
363 ElementsAre(llvm::json::Value(2), llvm::json::Value(10)));
366 // Creates a Callback that writes its received value into an
367 // std::optional<Expected>.
368 template <typename T
>
369 llvm::unique_function
<void(llvm::Expected
<T
>)>
370 capture(std::optional
<llvm::Expected
<T
>> &Out
) {
372 return [&Out
](llvm::Expected
<T
> V
) { Out
.emplace(std::move(V
)); };
375 TEST_F(LSPTest
, FeatureModulesThreadingTest
) {
376 // A feature module that does its work on a background thread, and so
377 // exercises the block/shutdown protocol.
378 class AsyncCounter final
: public FeatureModule
{
379 bool ShouldStop
= false;
381 std::deque
<Callback
<int>> Queue
; // null = increment, non-null = read.
382 std::condition_variable CV
;
387 std::unique_lock
<std::mutex
> Lock(Mu
);
389 CV
.wait(Lock
, [&] { return ShouldStop
|| !Queue
.empty(); });
395 Callback
<int> &Task
= Queue
.front();
405 bool blockUntilIdle(Deadline D
) override
{
406 std::unique_lock
<std::mutex
> Lock(Mu
);
407 return clangd::wait(Lock
, CV
, D
, [this] { return Queue
.empty(); });
410 void stop() override
{
412 std::lock_guard
<std::mutex
> Lock(Mu
);
419 AsyncCounter() : Thread([this] { run(); }) {}
421 // Verify shutdown sequence was performed.
422 // Real modules would not do this, to be robust to no ClangdServer.
424 // We still need the lock here, as Queue might be empty when
425 // ClangdServer calls blockUntilIdle, but run() might not have returned
427 std::lock_guard
<std::mutex
> Lock(Mu
);
428 EXPECT_TRUE(ShouldStop
) << "ClangdServer should request shutdown";
429 EXPECT_EQ(Queue
.size(), 0u) << "ClangdServer should block until idle";
434 void initializeLSP(LSPBinder
&Bind
, const llvm::json::Object
&ClientCaps
,
435 llvm::json::Object
&ServerCaps
) override
{
436 Bind
.notification("increment", this, &AsyncCounter::increment
);
439 // Get the current value, bypassing the queue.
440 // Used to verify that sync->blockUntilIdle avoids races in tests.
442 std::lock_guard
<std::mutex
> Lock(Mu
);
446 // Increment the current value asynchronously.
447 void increment(const std::nullptr_t
&) {
449 std::lock_guard
<std::mutex
> Lock(Mu
);
450 Queue
.push_back(nullptr);
456 FeatureModules
.add(std::make_unique
<AsyncCounter
>());
457 auto &Client
= start();
459 Client
.notify("increment", nullptr);
460 Client
.notify("increment", nullptr);
461 Client
.notify("increment", nullptr);
463 EXPECT_EQ(3, FeatureModules
.get
<AsyncCounter
>()->getSync());
464 // Throw some work on the queue to make sure shutdown blocks on it.
465 Client
.notify("increment", nullptr);
466 Client
.notify("increment", nullptr);
467 Client
.notify("increment", nullptr);
468 // And immediately shut down. FeatureModule destructor verifies we blocked.
471 TEST_F(LSPTest
, DiagModuleTest
) {
472 static constexpr llvm::StringLiteral DiagMsg
= "DiagMsg";
473 class DiagModule final
: public FeatureModule
{
474 struct DiagHooks
: public ASTListener
{
475 void sawDiagnostic(const clang::Diagnostic
&, clangd::Diag
&D
) override
{
476 D
.Message
= DiagMsg
.str();
481 std::unique_ptr
<ASTListener
> astListeners() override
{
482 return std::make_unique
<DiagHooks
>();
485 FeatureModules
.add(std::make_unique
<DiagModule
>());
487 auto &Client
= start();
488 Client
.didOpen("foo.cpp", "test;");
489 EXPECT_THAT(Client
.diagnostics("foo.cpp"),
490 llvm::ValueIs(testing::ElementsAre(diagMessage(DiagMsg
))));
493 } // namespace clangd