[clang-tools-extra] 1229245 - [clangd] Set up machinery for gtests of ClangdLSPServer.
Sam McCall via cfe-commits
cfe-commits at lists.llvm.org
Thu Apr 9 17:51:07 PDT 2020
Author: Sam McCall
Date: 2020-04-10T02:50:57+02:00
New Revision: 1229245df7c7323fa916054bf20edc01b3606dd2
URL: https://github.com/llvm/llvm-project/commit/1229245df7c7323fa916054bf20edc01b3606dd2
DIFF: https://github.com/llvm/llvm-project/commit/1229245df7c7323fa916054bf20edc01b3606dd2.diff
LOG: [clangd] Set up machinery for gtests of ClangdLSPServer.
Summary:
This is going to be needed to test e.g. diagnostics regeneration on
didSave where files changed on disk. Coordinating such changes is too
hard in lit tests.
Reviewers: kadircet
Subscribers: mgorny, ilya-biryukov, MaskRay, jkorous, arphaman, usaxena95, cfe-commits
Tags: #clang
Differential Revision: https://reviews.llvm.org/D77766
Added:
clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
clang-tools-extra/clangd/unittests/LSPClient.cpp
clang-tools-extra/clangd/unittests/LSPClient.h
Modified:
clang-tools-extra/clangd/unittests/CMakeLists.txt
Removed:
################################################################################
diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt
index 5065a56afc97..541367a1d96a 100644
--- a/clang-tools-extra/clangd/unittests/CMakeLists.txt
+++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt
@@ -30,6 +30,7 @@ add_unittest(ClangdUnitTests ClangdTests
CancellationTests.cpp
CanonicalIncludesTests.cpp
ClangdTests.cpp
+ ClangdLSPServerTests.cpp
CodeCompleteTests.cpp
CodeCompletionStringsTests.cpp
CollectMacrosTests.cpp
@@ -55,6 +56,7 @@ add_unittest(ClangdUnitTests ClangdTests
IndexActionTests.cpp
IndexTests.cpp
JSONTransportTests.cpp
+ LSPClient.cpp
ParsedASTTests.cpp
PathMappingTests.cpp
PrintASTTests.cpp
diff --git a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
new file mode 100644
index 000000000000..b8029b954377
--- /dev/null
+++ b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
@@ -0,0 +1,131 @@
+//===-- ClangdLSPServerTests.cpp ------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "Annotations.h"
+#include "ClangdLSPServer.h"
+#include "CodeComplete.h"
+#include "LSPClient.h"
+#include "Logger.h"
+#include "Protocol.h"
+#include "TestFS.h"
+#include "refactor/Rename.h"
+#include "llvm/Support/JSON.h"
+#include "llvm/Testing/Support/SupportHelpers.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace clang {
+namespace clangd {
+namespace {
+
+MATCHER_P(DiagMessage, M, "") {
+ if (const auto *O = arg.getAsObject()) {
+ if (const auto Msg = O->getString("message"))
+ return *Msg == M;
+ }
+ return false;
+}
+
+class LSPTest : public ::testing::Test, private clangd::Logger {
+protected:
+ LSPTest() : LogSession(*this) {}
+
+ LSPClient &start() {
+ EXPECT_FALSE(Server.hasValue()) << "Already initialized";
+ Server.emplace(Client.transport(), FS, CCOpts, RenameOpts,
+ /*CompileCommandsDir=*/llvm::None, /*UseDirBasedCDB=*/false,
+ /*ForcedOffsetEncoding=*/llvm::None, Opts);
+ ServerThread.emplace([&] { EXPECT_TRUE(Server->run()); });
+ Client.call("initialize", llvm::json::Object{});
+ return Client;
+ }
+
+ void stop() {
+ assert(Server);
+ Client.call("shutdown", nullptr);
+ Client.notify("exit", nullptr);
+ Client.stop();
+ ServerThread->join();
+ Server.reset();
+ ServerThread.reset();
+ }
+
+ ~LSPTest() {
+ if (Server)
+ stop();
+ }
+
+ MockFSProvider FS;
+ CodeCompleteOptions CCOpts;
+ RenameOptions RenameOpts;
+ ClangdServer::Options Opts = ClangdServer::optsForTest();
+
+private:
+ // Color logs so we can distinguish them from test output.
+ void log(Level L, const llvm::formatv_object_base &Message) override {
+ raw_ostream::Colors Color;
+ switch (L) {
+ case Level::Verbose:
+ Color = raw_ostream::BLUE;
+ break;
+ case Level::Error:
+ Color = raw_ostream::RED;
+ break;
+ default:
+ Color = raw_ostream::YELLOW;
+ break;
+ }
+ std::lock_guard<std::mutex> Lock(LogMu);
+ (llvm::outs().changeColor(Color) << Message << "\n").resetColor();
+ }
+ std::mutex LogMu;
+
+ LoggingSession LogSession;
+ llvm::Optional<ClangdLSPServer> Server;
+ llvm::Optional<std::thread> ServerThread;
+ LSPClient Client;
+};
+
+TEST_F(LSPTest, GoToDefinition) {
+ Annotations Code(R"cpp(
+ int [[fib]](int n) {
+ return n >= 2 ? ^fib(n - 1) + fib(n - 2) : 1;
+ }
+ )cpp");
+ auto &Client = start();
+ Client.didOpen("foo.cpp", Code.code());
+ auto &Def = Client.call("textDocument/definition",
+ llvm::json::Object{
+ {"textDocument", Client.documentID("foo.cpp")},
+ {"position", Code.point()},
+ });
+ llvm::json::Value Want = llvm::json::Array{llvm::json::Object{
+ {"uri", Client.uri("foo.cpp")}, {"range", Code.range()}}};
+ EXPECT_EQ(Def.takeValue(), Want);
+}
+
+TEST_F(LSPTest, Diagnostics) {
+ auto &Client = start();
+ Client.didOpen("foo.cpp", "void main(int, char**);");
+ EXPECT_THAT(Client.diagnostics("foo.cpp"),
+ llvm::ValueIs(testing::ElementsAre(
+ DiagMessage("'main' must return 'int' (fix available)"))));
+
+ Client.didChange("foo.cpp", "int x = \"42\";");
+ EXPECT_THAT(Client.diagnostics("foo.cpp"),
+ llvm::ValueIs(testing::ElementsAre(
+ DiagMessage("Cannot initialize a variable of type 'int' with "
+ "an lvalue of type 'const char [3]'"))));
+
+ Client.didClose("foo.cpp");
+ EXPECT_THAT(Client.diagnostics("foo.cpp"), llvm::ValueIs(testing::IsEmpty()));
+}
+
+} // namespace
+} // namespace clangd
+} // namespace clang
diff --git a/clang-tools-extra/clangd/unittests/LSPClient.cpp b/clang-tools-extra/clangd/unittests/LSPClient.cpp
new file mode 100644
index 000000000000..5e43314d1fe5
--- /dev/null
+++ b/clang-tools-extra/clangd/unittests/LSPClient.cpp
@@ -0,0 +1,211 @@
+#include "LSPClient.h"
+#include "gtest/gtest.h"
+#include <condition_variable>
+
+#include "Protocol.h"
+#include "TestFS.h"
+#include "Threading.h"
+#include "Transport.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/raw_ostream.h"
+#include <queue>
+
+namespace clang {
+namespace clangd {
+
+llvm::Expected<llvm::json::Value> clang::clangd::LSPClient::CallResult::take() {
+ std::unique_lock<std::mutex> Lock(Mu);
+ if (!clangd::wait(Lock, CV, timeoutSeconds(10),
+ [this] { return Value.hasValue(); })) {
+ ADD_FAILURE() << "No result from call after 10 seconds!";
+ return llvm::json::Value(nullptr);
+ }
+ return std::move(*Value);
+}
+
+llvm::json::Value LSPClient::CallResult::takeValue() {
+ auto ExpValue = take();
+ if (!ExpValue) {
+ ADD_FAILURE() << "takeValue(): " << llvm::toString(ExpValue.takeError());
+ return llvm::json::Value(nullptr);
+ }
+ return std::move(*ExpValue);
+}
+
+void LSPClient::CallResult::set(llvm::Expected<llvm::json::Value> V) {
+ std::lock_guard<std::mutex> Lock(Mu);
+ if (Value) {
+ ADD_FAILURE() << "Multiple replies";
+ llvm::consumeError(V.takeError());
+ return;
+ }
+ Value = std::move(V);
+ CV.notify_all();
+}
+
+LSPClient::CallResult::~CallResult() {
+ if (Value && !*Value) {
+ ADD_FAILURE() << llvm::toString(Value->takeError());
+ }
+}
+
+static void logBody(llvm::StringRef Method, llvm::json::Value V, bool Send) {
+ // We invert <<< and >>> as the combined log is from the server's viewpoint.
+ vlog("{0} {1}: {2:2}", Send ? "<<<" : ">>>", Method, V);
+}
+
+class LSPClient::TransportImpl : public Transport {
+public:
+ std::pair<llvm::json::Value, CallResult *> addCallSlot() {
+ std::lock_guard<std::mutex> Lock(Mu);
+ unsigned ID = CallResults.size();
+ CallResults.emplace_back();
+ return {ID, &CallResults.back()};
+ }
+
+ // A null action causes the transport to shut down.
+ void enqueue(std::function<void(MessageHandler &)> Action) {
+ std::lock_guard<std::mutex> Lock(Mu);
+ Actions.push(std::move(Action));
+ CV.notify_all();
+ }
+
+ std::vector<llvm::json::Value> takeNotifications(llvm::StringRef Method) {
+ std::vector<llvm::json::Value> Result;
+ {
+ std::lock_guard<std::mutex> Lock(Mu);
+ std::swap(Result, Notifications[Method]);
+ }
+ return Result;
+ }
+
+private:
+ void reply(llvm::json::Value ID,
+ llvm::Expected<llvm::json::Value> V) override {
+ if (V) // Nothing additional to log for error.
+ logBody("reply", *V, /*Send=*/false);
+ std::lock_guard<std::mutex> Lock(Mu);
+ if (auto I = ID.getAsInteger()) {
+ if (*I >= 0 && *I < static_cast<int64_t>(CallResults.size())) {
+ CallResults[*I].set(std::move(V));
+ return;
+ }
+ }
+ ADD_FAILURE() << "Invalid reply to ID " << ID;
+ llvm::consumeError(std::move(V).takeError());
+ }
+
+ void notify(llvm::StringRef Method, llvm::json::Value V) override {
+ logBody(Method, V, /*Send=*/false);
+ std::lock_guard<std::mutex> Lock(Mu);
+ Notifications[Method].push_back(std::move(V));
+ }
+
+ void call(llvm::StringRef Method, llvm::json::Value Params,
+ llvm::json::Value ID) override {
+ logBody(Method, Params, /*Send=*/false);
+ ADD_FAILURE() << "Unexpected server->client call " << Method;
+ }
+
+ llvm::Error loop(MessageHandler &H) override {
+ std::unique_lock<std::mutex> Lock(Mu);
+ while (true) {
+ CV.wait(Lock, [&] { return !Actions.empty(); });
+ if (!Actions.front()) // Stop!
+ return llvm::Error::success();
+ auto Action = std::move(Actions.front());
+ Actions.pop();
+ Lock.unlock();
+ Action(H);
+ Lock.lock();
+ }
+ }
+
+ std::mutex Mu;
+ std::deque<CallResult> CallResults;
+ std::queue<std::function<void(Transport::MessageHandler &)>> Actions;
+ std::condition_variable CV;
+ llvm::StringMap<std::vector<llvm::json::Value>> Notifications;
+};
+
+LSPClient::LSPClient() : T(std::make_unique<TransportImpl>()) {}
+LSPClient::~LSPClient() = default;
+
+LSPClient::CallResult &LSPClient::call(llvm::StringRef Method,
+ llvm::json::Value Params) {
+ auto Slot = T->addCallSlot();
+ T->enqueue([ID(Slot.first), Method(Method.str()),
+ Params(std::move(Params))](Transport::MessageHandler &H) {
+ logBody(Method, Params, /*Send=*/true);
+ H.onCall(Method, std::move(Params), ID);
+ });
+ return *Slot.second;
+}
+
+void LSPClient::notify(llvm::StringRef Method, llvm::json::Value Params) {
+ T->enqueue([Method(Method.str()),
+ Params(std::move(Params))](Transport::MessageHandler &H) {
+ logBody(Method, Params, /*Send=*/true);
+ H.onNotify(Method, std::move(Params));
+ });
+}
+
+std::vector<llvm::json::Value>
+LSPClient::takeNotifications(llvm::StringRef Method) {
+ return T->takeNotifications(Method);
+}
+
+void LSPClient::stop() { T->enqueue(nullptr); }
+
+Transport &LSPClient::transport() { return *T; }
+
+using Obj = llvm::json::Object;
+
+llvm::json::Value LSPClient::uri(llvm::StringRef Path) {
+ std::string Storage;
+ if (!llvm::sys::path::is_absolute(Path))
+ Path = Storage = testPath(Path);
+ return toJSON(URIForFile::canonicalize(Path, Path));
+}
+llvm::json::Value LSPClient::documentID(llvm::StringRef Path) {
+ return Obj{{"uri", uri(Path)}};
+}
+
+void LSPClient::didOpen(llvm::StringRef Path, llvm::StringRef Content) {
+ notify(
+ "textDocument/didOpen",
+ Obj{{"textDocument",
+ Obj{{"uri", uri(Path)}, {"text", Content}, {"languageId", "cpp"}}}});
+}
+void LSPClient::didChange(llvm::StringRef Path, llvm::StringRef Content) {
+ notify("textDocument/didChange",
+ Obj{{"textDocument", documentID(Path)},
+ {"contentChanges", llvm::json::Array{Obj{{"text", Content}}}}});
+}
+void LSPClient::didClose(llvm::StringRef Path) {
+ notify("textDocument/didClose", Obj{{"textDocument", documentID(Path)}});
+}
+
+void LSPClient::sync() { call("sync", nullptr).takeValue(); }
+
+llvm::Optional<std::vector<llvm::json::Value>>
+LSPClient::diagnostics(llvm::StringRef Path) {
+ sync();
+ auto Notifications = takeNotifications("textDocument/publishDiagnostics");
+ for (const auto &Notification : llvm::reverse(Notifications)) {
+ if (const auto *PubDiagsParams = Notification.getAsObject()) {
+ auto U = PubDiagsParams->getString("uri");
+ auto *D = PubDiagsParams->getArray("diagnostics");
+ if (!U || !D) {
+ ADD_FAILURE() << "Bad PublishDiagnosticsParams: " << PubDiagsParams;
+ continue;
+ }
+ if (*U == uri(Path))
+ return std::vector<llvm::json::Value>(D->begin(), D->end());
+ }
+ }
+ return {};
+}
+
+} // namespace clangd
+} // namespace clang
diff --git a/clang-tools-extra/clangd/unittests/LSPClient.h b/clang-tools-extra/clangd/unittests/LSPClient.h
new file mode 100644
index 000000000000..b505c4f13ea2
--- /dev/null
+++ b/clang-tools-extra/clangd/unittests/LSPClient.h
@@ -0,0 +1,82 @@
+//===-- LSPClient.h - Helper for ClangdLSPServer tests ----------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include <condition_variable>
+#include <deque>
+#include <llvm/ADT/Optional.h>
+#include <llvm/Support/Error.h>
+#include <llvm/Support/JSON.h>
+#include <mutex>
+
+namespace clang {
+namespace clangd {
+class Transport;
+
+// A client library for talking to ClangdLSPServer in tests.
+// Manages serialization of messages, pairing requests/repsonses, and implements
+// the Transport abstraction.
+class LSPClient {
+ class TransportImpl;
+ std::unique_ptr<TransportImpl> T;
+
+public:
+ // Represents the result of an LSP call: a promise for a result or error.
+ class CallResult {
+ public:
+ ~CallResult();
+ // Blocks up to 10 seconds for the result to be ready.
+ // Records a test failure if there was no reply.
+ llvm::Expected<llvm::json::Value> take();
+ // Like take(), but records a test failure if the result was an error.
+ llvm::json::Value takeValue();
+
+ private:
+ // Should be called once to provide the value.
+ void set(llvm::Expected<llvm::json::Value> V);
+
+ llvm::Optional<llvm::Expected<llvm::json::Value>> Value;
+ std::mutex Mu;
+ std::condition_variable CV;
+
+ friend TransportImpl; // Calls set().
+ };
+
+ LSPClient();
+ ~LSPClient();
+ LSPClient(LSPClient &&) = delete;
+ LSPClient &operator=(LSPClient &&) = delete;
+
+ // Enqueue an LSP method call, returns a promise for the reply. Threadsafe.
+ CallResult &call(llvm::StringRef Method, llvm::json::Value Params);
+ // Enqueue an LSP notification. Threadsafe.
+ void notify(llvm::StringRef Method, llvm::json::Value Params);
+ // Returns matching notifications since the last call to takeNotifications.
+ std::vector<llvm::json::Value> takeNotifications(llvm::StringRef Method);
+ // The transport is shut down after all pending messages are sent.
+ void stop();
+
+ // Shorthand for common LSP methods. Relative paths are passed to testPath().
+ static llvm::json::Value uri(llvm::StringRef Path);
+ static llvm::json::Value documentID(llvm::StringRef Path);
+ void didOpen(llvm::StringRef Path, llvm::StringRef Content);
+ void didChange(llvm::StringRef Path, llvm::StringRef Content);
+ void didClose(llvm::StringRef Path);
+ // Blocks until the server is idle (using the 'sync' protocol extension).
+ void sync();
+ // sync()s to ensure pending diagnostics arrive, and returns the newest set.
+ llvm::Optional<std::vector<llvm::json::Value>>
+ diagnostics(llvm::StringRef Path);
+
+ // Get the transport used to connect this client to a ClangdLSPServer.
+ Transport &transport();
+
+private:
+};
+
+} // namespace clangd
+} // namespace clang
More information about the cfe-commits
mailing list