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 "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/SupportHelpers.h"
20 #include "gmock/gmock.h"
21 #include "gtest/gtest.h"
22
23 namespace clang {
24 namespace clangd {
25 namespace {
26
27 MATCHER_P(DiagMessage, M, "") {
28 if (const auto *O = arg.getAsObject()) {
29 if (const auto Msg = O->getString("message"))
30 return *Msg == M;
31 }
32 return false;
33 }
34
35 class LSPTest : public ::testing::Test, private clangd::Logger {
36 protected:
LSPTest()37 LSPTest() : LogSession(*this) {
38 ClangdServer::Options &Base = Opts;
39 Base = ClangdServer::optsForTest();
40 // This is needed to we can test index-based operations like call hierarchy.
41 Base.BuildDynamicSymbolIndex = true;
42 }
43
start()44 LSPClient &start() {
45 EXPECT_FALSE(Server.hasValue()) << "Already initialized";
46 Server.emplace(Client.transport(), FS, Opts);
47 ServerThread.emplace([&] { EXPECT_TRUE(Server->run()); });
48 Client.call("initialize", llvm::json::Object{});
49 return Client;
50 }
51
stop()52 void stop() {
53 assert(Server);
54 Client.call("shutdown", nullptr);
55 Client.notify("exit", nullptr);
56 Client.stop();
57 ServerThread->join();
58 Server.reset();
59 ServerThread.reset();
60 }
61
~LSPTest()62 ~LSPTest() {
63 if (Server)
64 stop();
65 }
66
67 MockFS FS;
68 ClangdLSPServer::Options Opts;
69
70 private:
71 // Color logs so we can distinguish them from test output.
log(Level L,const char * Fmt,const llvm::formatv_object_base & Message)72 void log(Level L, const char *Fmt,
73 const llvm::formatv_object_base &Message) override {
74 raw_ostream::Colors Color;
75 switch (L) {
76 case Level::Verbose:
77 Color = raw_ostream::BLUE;
78 break;
79 case Level::Error:
80 Color = raw_ostream::RED;
81 break;
82 default:
83 Color = raw_ostream::YELLOW;
84 break;
85 }
86 std::lock_guard<std::mutex> Lock(LogMu);
87 (llvm::outs().changeColor(Color) << Message << "\n").resetColor();
88 }
89 std::mutex LogMu;
90
91 LoggingSession LogSession;
92 llvm::Optional<ClangdLSPServer> Server;
93 llvm::Optional<std::thread> ServerThread;
94 LSPClient Client;
95 };
96
TEST_F(LSPTest,GoToDefinition)97 TEST_F(LSPTest, GoToDefinition) {
98 Annotations Code(R"cpp(
99 int [[fib]](int n) {
100 return n >= 2 ? ^fib(n - 1) + fib(n - 2) : 1;
101 }
102 )cpp");
103 auto &Client = start();
104 Client.didOpen("foo.cpp", Code.code());
105 auto &Def = Client.call("textDocument/definition",
106 llvm::json::Object{
107 {"textDocument", Client.documentID("foo.cpp")},
108 {"position", Code.point()},
109 });
110 llvm::json::Value Want = llvm::json::Array{llvm::json::Object{
111 {"uri", Client.uri("foo.cpp")}, {"range", Code.range()}}};
112 EXPECT_EQ(Def.takeValue(), Want);
113 }
114
TEST_F(LSPTest,Diagnostics)115 TEST_F(LSPTest, Diagnostics) {
116 auto &Client = start();
117 Client.didOpen("foo.cpp", "void main(int, char**);");
118 EXPECT_THAT(Client.diagnostics("foo.cpp"),
119 llvm::ValueIs(testing::ElementsAre(
120 DiagMessage("'main' must return 'int' (fix available)"))));
121
122 Client.didChange("foo.cpp", "int x = \"42\";");
123 EXPECT_THAT(Client.diagnostics("foo.cpp"),
124 llvm::ValueIs(testing::ElementsAre(
125 DiagMessage("Cannot initialize a variable of type 'int' with "
126 "an lvalue of type 'const char [3]'"))));
127
128 Client.didClose("foo.cpp");
129 EXPECT_THAT(Client.diagnostics("foo.cpp"), llvm::ValueIs(testing::IsEmpty()));
130 }
131
TEST_F(LSPTest,DiagnosticsHeaderSaved)132 TEST_F(LSPTest, DiagnosticsHeaderSaved) {
133 auto &Client = start();
134 Client.didOpen("foo.cpp", R"cpp(
135 #include "foo.h"
136 int x = VAR;
137 )cpp");
138 EXPECT_THAT(Client.diagnostics("foo.cpp"),
139 llvm::ValueIs(testing::ElementsAre(
140 DiagMessage("'foo.h' file not found"),
141 DiagMessage("Use of undeclared identifier 'VAR'"))));
142 // Now create the header.
143 FS.Files["foo.h"] = "#define VAR original";
144 Client.notify(
145 "textDocument/didSave",
146 llvm::json::Object{{"textDocument", Client.documentID("foo.h")}});
147 EXPECT_THAT(Client.diagnostics("foo.cpp"),
148 llvm::ValueIs(testing::ElementsAre(
149 DiagMessage("Use of undeclared identifier 'original'"))));
150 // Now modify the header from within the "editor".
151 FS.Files["foo.h"] = "#define VAR changed";
152 Client.notify(
153 "textDocument/didSave",
154 llvm::json::Object{{"textDocument", Client.documentID("foo.h")}});
155 // Foo.cpp should be rebuilt with new diagnostics.
156 EXPECT_THAT(Client.diagnostics("foo.cpp"),
157 llvm::ValueIs(testing::ElementsAre(
158 DiagMessage("Use of undeclared identifier 'changed'"))));
159 }
160
TEST_F(LSPTest,RecordsLatencies)161 TEST_F(LSPTest, RecordsLatencies) {
162 trace::TestTracer Tracer;
163 auto &Client = start();
164 llvm::StringLiteral MethodName = "method_name";
165 EXPECT_THAT(Tracer.takeMetric("lsp_latency", MethodName), testing::SizeIs(0));
166 llvm::consumeError(Client.call(MethodName, {}).take().takeError());
167 stop();
168 EXPECT_THAT(Tracer.takeMetric("lsp_latency", MethodName), testing::SizeIs(1));
169 }
170
TEST_F(LSPTest,IncomingCalls)171 TEST_F(LSPTest, IncomingCalls) {
172 Annotations Code(R"cpp(
173 void calle^e(int);
174 void caller1() {
175 [[callee]](42);
176 }
177 )cpp");
178 auto &Client = start();
179 Client.didOpen("foo.cpp", Code.code());
180 auto Items = Client
181 .call("textDocument/prepareCallHierarchy",
182 llvm::json::Object{
183 {"textDocument", Client.documentID("foo.cpp")},
184 {"position", Code.point()}})
185 .takeValue();
186 auto FirstItem = (*Items.getAsArray())[0];
187 auto Calls = Client
188 .call("callHierarchy/incomingCalls",
189 llvm::json::Object{{"item", FirstItem}})
190 .takeValue();
191 auto FirstCall = *(*Calls.getAsArray())[0].getAsObject();
192 EXPECT_EQ(FirstCall["fromRanges"], llvm::json::Value{Code.range()});
193 auto From = *FirstCall["from"].getAsObject();
194 EXPECT_EQ(From["name"], "caller1");
195 }
196
TEST_F(LSPTest,CDBConfigIntegration)197 TEST_F(LSPTest, CDBConfigIntegration) {
198 auto CfgProvider =
199 config::Provider::fromAncestorRelativeYAMLFiles(".clangd", FS);
200 Opts.ConfigProvider = CfgProvider.get();
201
202 // Map bar.cpp to a different compilation database which defines FOO->BAR.
203 FS.Files[".clangd"] = R"yaml(
204 If:
205 PathMatch: bar.cpp
206 CompileFlags:
207 CompilationDatabase: bar
208 )yaml";
209 FS.Files["bar/compile_flags.txt"] = "-DFOO=BAR";
210
211 auto &Client = start();
212 // foo.cpp gets parsed as normal.
213 Client.didOpen("foo.cpp", "int x = FOO;");
214 EXPECT_THAT(Client.diagnostics("foo.cpp"),
215 llvm::ValueIs(testing::ElementsAre(
216 DiagMessage("Use of undeclared identifier 'FOO'"))));
217 // bar.cpp shows the configured compile command.
218 Client.didOpen("bar.cpp", "int x = FOO;");
219 EXPECT_THAT(Client.diagnostics("bar.cpp"),
220 llvm::ValueIs(testing::ElementsAre(
221 DiagMessage("Use of undeclared identifier 'BAR'"))));
222 }
223
224 } // namespace
225 } // namespace clangd
226 } // namespace clang
227