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 "LSPClient.h"
14 #include "support/Logger.h"
15 #include "support/TestTracer.h"
16 #include "llvm/ADT/StringRef.h"
17 #include "llvm/Support/Error.h"
18 #include "llvm/Support/JSON.h"
19 #include "llvm/Testing/Support/Error.h"
20 #include "llvm/Testing/Support/SupportHelpers.h"
21 #include "gmock/gmock.h"
22 #include "gtest/gtest.h"
28 using llvm::Succeeded
;
29 using testing::ElementsAre
;
31 MATCHER_P(diagMessage
, M
, "") {
32 if (const auto *O
= arg
.getAsObject()) {
33 if (const auto Msg
= O
->getString("message"))
39 class LSPTest
: public ::testing::Test
{
41 LSPTest() : LogSession(L
) {
42 ClangdServer::Options
&Base
= Opts
;
43 Base
= ClangdServer::optsForTest();
44 // This is needed to we can test index-based operations like call hierarchy.
45 Base
.BuildDynamicSymbolIndex
= true;
46 Base
.FeatureModules
= &FeatureModules
;
50 EXPECT_FALSE(Server
) << "Already initialized";
51 Server
.emplace(Client
.transport(), FS
, Opts
);
52 ServerThread
.emplace([&] { EXPECT_TRUE(Server
->run()); });
53 Client
.call("initialize", llvm::json::Object
{});
59 Client
.call("shutdown", nullptr);
60 Client
.notify("exit", nullptr);
73 ClangdLSPServer::Options Opts
;
74 FeatureModuleSet FeatureModules
;
77 class Logger
: public clang::clangd::Logger
{
78 // Color logs so we can distinguish them from test output.
79 void log(Level L
, const char *Fmt
,
80 const llvm::formatv_object_base
&Message
) override
{
81 raw_ostream::Colors Color
;
84 Color
= raw_ostream::BLUE
;
87 Color
= raw_ostream::RED
;
90 Color
= raw_ostream::YELLOW
;
93 std::lock_guard
<std::mutex
> Lock(LogMu
);
94 (llvm::outs().changeColor(Color
) << Message
<< "\n").resetColor();
100 LoggingSession LogSession
;
101 std::optional
<ClangdLSPServer
> Server
;
102 std::optional
<std::thread
> ServerThread
;
106 TEST_F(LSPTest
, GoToDefinition
) {
107 Annotations
Code(R
"cpp(
109 return n >= 2 ? ^fib(n - 1) + fib(n - 2) : 1;
112 auto &Client
= start();
113 Client
.didOpen("foo.cpp", Code
.code());
114 auto &Def
= Client
.call("textDocument/definition",
116 {"textDocument", Client
.documentID("foo.cpp")},
117 {"position", Code
.point()},
119 llvm::json::Value Want
= llvm::json::Array
{llvm::json::Object
{
120 {"uri", Client
.uri("foo.cpp")}, {"range", Code
.range()}}};
121 EXPECT_EQ(Def
.takeValue(), Want
);
124 TEST_F(LSPTest
, Diagnostics
) {
125 auto &Client
= start();
126 Client
.didOpen("foo.cpp", "void main(int, char**);");
127 EXPECT_THAT(Client
.diagnostics("foo.cpp"),
128 llvm::ValueIs(testing::ElementsAre(
129 diagMessage("'main' must return 'int' (fix available)"))));
131 Client
.didChange("foo.cpp", "int x = \"42\";");
132 EXPECT_THAT(Client
.diagnostics("foo.cpp"),
133 llvm::ValueIs(testing::ElementsAre(
134 diagMessage("Cannot initialize a variable of type 'int' with "
135 "an lvalue of type 'const char[3]'"))));
137 Client
.didClose("foo.cpp");
138 EXPECT_THAT(Client
.diagnostics("foo.cpp"), llvm::ValueIs(testing::IsEmpty()));
141 TEST_F(LSPTest
, DiagnosticsHeaderSaved
) {
142 auto &Client
= start();
143 Client
.didOpen("foo.cpp", R
"cpp(
147 EXPECT_THAT(Client
.diagnostics("foo.cpp"),
148 llvm::ValueIs(testing::ElementsAre(
149 diagMessage("'foo.h' file not found"),
150 diagMessage("Use of undeclared identifier 'VAR'"))));
151 // Now create the header.
152 FS
.Files
["foo.h"] = "#define VAR original";
154 "textDocument/didSave",
155 llvm::json::Object
{{"textDocument", Client
.documentID("foo.h")}});
156 EXPECT_THAT(Client
.diagnostics("foo.cpp"),
157 llvm::ValueIs(testing::ElementsAre(
158 diagMessage("Use of undeclared identifier 'original'"))));
159 // Now modify the header from within the "editor".
160 FS
.Files
["foo.h"] = "#define VAR changed";
162 "textDocument/didSave",
163 llvm::json::Object
{{"textDocument", Client
.documentID("foo.h")}});
164 // Foo.cpp should be rebuilt with new diagnostics.
165 EXPECT_THAT(Client
.diagnostics("foo.cpp"),
166 llvm::ValueIs(testing::ElementsAre(
167 diagMessage("Use of undeclared identifier 'changed'"))));
170 TEST_F(LSPTest
, RecordsLatencies
) {
171 trace::TestTracer Tracer
;
172 auto &Client
= start();
173 llvm::StringLiteral MethodName
= "method_name";
174 EXPECT_THAT(Tracer
.takeMetric("lsp_latency", MethodName
), testing::SizeIs(0));
175 llvm::consumeError(Client
.call(MethodName
, {}).take().takeError());
177 EXPECT_THAT(Tracer
.takeMetric("lsp_latency", MethodName
), testing::SizeIs(1));
180 TEST_F(LSPTest
, IncomingCalls
) {
181 Annotations
Code(R
"cpp(
187 auto &Client
= start();
188 Client
.didOpen("foo.cpp", Code
.code());
190 .call("textDocument/prepareCallHierarchy",
192 {"textDocument", Client
.documentID("foo.cpp")},
193 {"position", Code
.point()}})
195 auto FirstItem
= (*Items
.getAsArray())[0];
197 .call("callHierarchy/incomingCalls",
198 llvm::json::Object
{{"item", FirstItem
}})
200 auto FirstCall
= *(*Calls
.getAsArray())[0].getAsObject();
201 EXPECT_EQ(FirstCall
["fromRanges"], llvm::json::Value
{Code
.range()});
202 auto From
= *FirstCall
["from"].getAsObject();
203 EXPECT_EQ(From
["name"], "caller1");
206 TEST_F(LSPTest
, CDBConfigIntegration
) {
208 config::Provider::fromAncestorRelativeYAMLFiles(".clangd", FS
);
209 Opts
.ConfigProvider
= CfgProvider
.get();
211 // Map bar.cpp to a different compilation database which defines FOO->BAR.
212 FS
.Files
[".clangd"] = R
"yaml(
216 CompilationDatabase: bar
218 FS
.Files
["bar/compile_flags.txt"] = "-DFOO=BAR";
220 auto &Client
= start();
221 // foo.cpp gets parsed as normal.
222 Client
.didOpen("foo.cpp", "int x = FOO;");
223 EXPECT_THAT(Client
.diagnostics("foo.cpp"),
224 llvm::ValueIs(testing::ElementsAre(
225 diagMessage("Use of undeclared identifier 'FOO'"))));
226 // bar.cpp shows the configured compile command.
227 Client
.didOpen("bar.cpp", "int x = FOO;");
228 EXPECT_THAT(Client
.diagnostics("bar.cpp"),
229 llvm::ValueIs(testing::ElementsAre(
230 diagMessage("Use of undeclared identifier 'BAR'"))));
233 TEST_F(LSPTest
, ModulesTest
) {
234 class MathModule final
: public FeatureModule
{
235 OutgoingNotification
<int> Changed
;
236 void initializeLSP(LSPBinder
&Bind
, const llvm::json::Object
&ClientCaps
,
237 llvm::json::Object
&ServerCaps
) override
{
238 Bind
.notification("add", this, &MathModule::add
);
239 Bind
.method("get", this, &MathModule::get
);
240 Changed
= Bind
.outgoingNotification("changed");
245 void add(const int &X
) {
249 void get(const std::nullptr_t
&, Callback
<int> Reply
) {
250 scheduler().runQuick(
252 [Reply(std::move(Reply
)), Value(Value
)]() mutable { Reply(Value
); });
255 FeatureModules
.add(std::make_unique
<MathModule
>());
257 auto &Client
= start();
258 Client
.notify("add", 2);
259 Client
.notify("add", 8);
260 EXPECT_EQ(10, Client
.call("get", nullptr).takeValue());
261 EXPECT_THAT(Client
.takeNotifications("changed"),
262 ElementsAre(llvm::json::Value(2), llvm::json::Value(10)));
265 // Creates a Callback that writes its received value into an
266 // std::optional<Expected>.
267 template <typename T
>
268 llvm::unique_function
<void(llvm::Expected
<T
>)>
269 capture(std::optional
<llvm::Expected
<T
>> &Out
) {
271 return [&Out
](llvm::Expected
<T
> V
) { Out
.emplace(std::move(V
)); };
274 TEST_F(LSPTest
, FeatureModulesThreadingTest
) {
275 // A feature module that does its work on a background thread, and so
276 // exercises the block/shutdown protocol.
277 class AsyncCounter final
: public FeatureModule
{
278 bool ShouldStop
= false;
280 std::deque
<Callback
<int>> Queue
; // null = increment, non-null = read.
281 std::condition_variable CV
;
286 std::unique_lock
<std::mutex
> Lock(Mu
);
288 CV
.wait(Lock
, [&] { return ShouldStop
|| !Queue
.empty(); });
294 Callback
<int> &Task
= Queue
.front();
304 bool blockUntilIdle(Deadline D
) override
{
305 std::unique_lock
<std::mutex
> Lock(Mu
);
306 return clangd::wait(Lock
, CV
, D
, [this] { return Queue
.empty(); });
309 void stop() override
{
311 std::lock_guard
<std::mutex
> Lock(Mu
);
318 AsyncCounter() : Thread([this] { run(); }) {}
320 // Verify shutdown sequence was performed.
321 // Real modules would not do this, to be robust to no ClangdServer.
323 // We still need the lock here, as Queue might be empty when
324 // ClangdServer calls blockUntilIdle, but run() might not have returned
326 std::lock_guard
<std::mutex
> Lock(Mu
);
327 EXPECT_TRUE(ShouldStop
) << "ClangdServer should request shutdown";
328 EXPECT_EQ(Queue
.size(), 0u) << "ClangdServer should block until idle";
333 void initializeLSP(LSPBinder
&Bind
, const llvm::json::Object
&ClientCaps
,
334 llvm::json::Object
&ServerCaps
) override
{
335 Bind
.notification("increment", this, &AsyncCounter::increment
);
338 // Get the current value, bypassing the queue.
339 // Used to verify that sync->blockUntilIdle avoids races in tests.
341 std::lock_guard
<std::mutex
> Lock(Mu
);
345 // Increment the current value asynchronously.
346 void increment(const std::nullptr_t
&) {
348 std::lock_guard
<std::mutex
> Lock(Mu
);
349 Queue
.push_back(nullptr);
355 FeatureModules
.add(std::make_unique
<AsyncCounter
>());
356 auto &Client
= start();
358 Client
.notify("increment", nullptr);
359 Client
.notify("increment", nullptr);
360 Client
.notify("increment", nullptr);
361 EXPECT_THAT_EXPECTED(Client
.call("sync", nullptr).take(), Succeeded());
362 EXPECT_EQ(3, FeatureModules
.get
<AsyncCounter
>()->getSync());
363 // Throw some work on the queue to make sure shutdown blocks on it.
364 Client
.notify("increment", nullptr);
365 Client
.notify("increment", nullptr);
366 Client
.notify("increment", nullptr);
367 // And immediately shut down. FeatureModule destructor verifies we blocked.
370 TEST_F(LSPTest
, DiagModuleTest
) {
371 static constexpr llvm::StringLiteral DiagMsg
= "DiagMsg";
372 class DiagModule final
: public FeatureModule
{
373 struct DiagHooks
: public ASTListener
{
374 void sawDiagnostic(const clang::Diagnostic
&, clangd::Diag
&D
) override
{
375 D
.Message
= DiagMsg
.str();
380 std::unique_ptr
<ASTListener
> astListeners() override
{
381 return std::make_unique
<DiagHooks
>();
384 FeatureModules
.add(std::make_unique
<DiagModule
>());
386 auto &Client
= start();
387 Client
.didOpen("foo.cpp", "test;");
388 EXPECT_THAT(Client
.diagnostics("foo.cpp"),
389 llvm::ValueIs(testing::ElementsAre(diagMessage(DiagMsg
))));
392 } // namespace clangd