[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