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 "CodeComplete.h"
12 #include "LSPClient.h"
13 #include "Protocol.h"
14 #include "TestFS.h"
15 #include "refactor/Rename.h"
16 #include "support/Logger.h"
17 #include "support/TestTracer.h"
18 #include "llvm/ADT/StringRef.h"
19 #include "llvm/Support/Error.h"
20 #include "llvm/Support/JSON.h"
21 #include "llvm/Testing/Support/SupportHelpers.h"
22 #include "gmock/gmock.h"
23 #include "gtest/gtest.h"
24
25 namespace clang {
26 namespace clangd {
27 namespace {
28
29 MATCHER_P(DiagMessage, M, "") {
30 if (const auto *O = arg.getAsObject()) {
31 if (const auto Msg = O->getString("message"))
32 return *Msg == M;
33 }
34 return false;
35 }
36
37 class LSPTest : public ::testing::Test, private clangd::Logger {
38 protected:
LSPTest()39 LSPTest() : LogSession(*this) {}
40
start()41 LSPClient &start() {
42 EXPECT_FALSE(Server.hasValue()) << "Already initialized";
43 Server.emplace(Client.transport(), FS, CCOpts, RenameOpts,
44 /*CompileCommandsDir=*/llvm::None, /*UseDirBasedCDB=*/false,
45 /*ForcedOffsetEncoding=*/llvm::None, Opts);
46 ServerThread.emplace([&] { EXPECT_TRUE(Server->run()); });
47 Client.call("initialize", llvm::json::Object{});
48 return Client;
49 }
50
stop()51 void stop() {
52 assert(Server);
53 Client.call("shutdown", nullptr);
54 Client.notify("exit", nullptr);
55 Client.stop();
56 ServerThread->join();
57 Server.reset();
58 ServerThread.reset();
59 }
60
~LSPTest()61 ~LSPTest() {
62 if (Server)
63 stop();
64 }
65
66 MockFS FS;
67 CodeCompleteOptions CCOpts;
68 RenameOptions RenameOpts;
69 ClangdServer::Options Opts = ClangdServer::optsForTest();
70
71 private:
72 // Color logs so we can distinguish them from test output.
log(Level L,const llvm::formatv_object_base & Message)73 void log(Level L, 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 } // namespace
171 } // namespace clangd
172 } // namespace clang
173