sammccall created this revision. sammccall added a reviewer: kadircet. Herald added subscribers: cfe-commits, usaxena95, arphaman, jkorous, MaskRay, ilya-biryukov, mgorny. Herald added a project: clang. sammccall added a comment.
It seems fairly likely that this is a big complicated thing we end up using for 1 or 2 tests that are too hard to test with lit but can't be tested at a lower level than ClangdLSPServer. Suggestions on how to make it more lightweight would be welcome! 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. Repository: rG LLVM Github Monorepo https://reviews.llvm.org/D77766 Files: clang-tools-extra/clangd/unittests/CMakeLists.txt clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp clang-tools-extra/clangd/unittests/LSPClient.cpp clang-tools-extra/clangd/unittests/LSPClient.h
Index: clang-tools-extra/clangd/unittests/LSPClient.h =================================================================== --- /dev/null +++ clang-tools-extra/clangd/unittests/LSPClient.h @@ -0,0 +1,80 @@ +//===-- 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: + llvm::Optional<llvm::Expected<llvm::json::Value>> Value; + std::mutex Mu; + std::condition_variable CV; + + friend TransportImpl; + void set(llvm::Expected<llvm::json::Value> V); + }; + + 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 Index: clang-tools-extra/clangd/unittests/LSPClient.cpp =================================================================== --- /dev/null +++ clang-tools-extra/clangd/unittests/LSPClient.cpp @@ -0,0 +1,208 @@ +#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 { + std::mutex Mu; + std::deque<CallResult> CallResults; + std::queue<std::function<void(Transport::MessageHandler &)>> Actions; + std::condition_variable CV; + bool Stop = false; + llvm::StringMap<std::vector<llvm::json::Value>> Notifications; + + void reply(llvm::json::Value ID, + llvm::Expected<llvm::json::Value> V) override { + if (V) + logBody("reply", *V, /*Send=*/false); + std::lock_guard<std::mutex> Lock(Mu); + if (auto I = ID.getAsInteger()) { + if (*I >= 0 && *I < (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 Stop || !Actions.empty(); }); + auto Action = std::move(Actions.front()); + Actions.pop(); + Lock.unlock(); + if (!Action) // stop! + return llvm::Error::success(); + Action(H); + Lock.lock(); + } + } + +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()}; + } + + 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; + } +}; + +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"); + if (!U || *U != uri(Path)) + continue; + if (const auto *Diagnostics = PubDiagsParams->getArray("diagnostics")) + return std::vector<llvm::json::Value>(Diagnostics->begin(), + Diagnostics->end()); + } + } + return {}; +} + +} // namespace clangd +} // namespace clang Index: clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp =================================================================== --- /dev/null +++ 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 { + +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); +} + +MATCHER_P(DiagMessage, M, "") { + if (const auto *O = arg.getAsObject()) { + if (const auto Msg = O->getString("message")) + return *Msg == M; + } + return false; +} + +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 Index: clang-tools-extra/clangd/unittests/CMakeLists.txt =================================================================== --- clang-tools-extra/clangd/unittests/CMakeLists.txt +++ clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -30,6 +30,7 @@ CancellationTests.cpp CanonicalIncludesTests.cpp ClangdTests.cpp + ClangdLSPServerTests.cpp CodeCompleteTests.cpp CodeCompletionStringsTests.cpp CollectMacrosTests.cpp @@ -55,6 +56,7 @@ IndexActionTests.cpp IndexTests.cpp JSONTransportTests.cpp + LSPClient.cpp ParsedASTTests.cpp PathMappingTests.cpp PrintASTTests.cpp
_______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits