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