//===-- LSPBinderTests.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 "LSPBinder.h" #include "llvm/Testing/Support/Error.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include namespace clang { namespace clangd { namespace { using testing::ElementsAre; using testing::HasSubstr; using testing::IsEmpty; using testing::UnorderedElementsAre; // JSON-serializable type for testing. struct Foo { int X; friend bool operator==(Foo A, Foo B) { return A.X == B.X; } }; bool fromJSON(const llvm::json::Value &V, Foo &F, llvm::json::Path P) { return fromJSON(V, F.X, P.field("X")); } llvm::json::Value toJSON(const Foo &F) { return F.X; } // Creates a Callback that writes its received value into an // std::optional. template llvm::unique_function)> capture(std::optional> &Out) { Out.reset(); return [&Out](llvm::Expected V) { Out.emplace(std::move(V)); }; } struct OutgoingRecorder : public LSPBinder::RawOutgoing { llvm::StringMap> Received; void callMethod(llvm::StringRef Method, llvm::json::Value Params, Callback Reply) override { Received[Method].push_back(Params); if (Method == "fail") return Reply(error("Params={0}", Params)); Reply(Params); // echo back the request } void notify(llvm::StringRef Method, llvm::json::Value Params) override { Received[Method].push_back(std::move(Params)); } std::vector take(llvm::StringRef Method) { std::vector Result = Received.lookup(Method); Received.erase(Method); return Result; } }; TEST(LSPBinderTest, IncomingCalls) { LSPBinder::RawHandlers RawHandlers; OutgoingRecorder RawOutgoing; LSPBinder Binder{RawHandlers, RawOutgoing}; struct Handler { void plusOne(const Foo &Params, Callback Reply) { Reply(Foo{Params.X + 1}); } void fail(const Foo &Params, Callback Reply) { Reply(error("X={0}", Params.X)); } void notify(const Foo &Params) { LastNotify = Params.X; ++NotifyCount; } int LastNotify = -1; int NotifyCount = 0; }; Handler H; Binder.method("plusOne", &H, &Handler::plusOne); Binder.method("fail", &H, &Handler::fail); Binder.notification("notify", &H, &Handler::notify); Binder.command("cmdPlusOne", &H, &Handler::plusOne); ASSERT_THAT(RawHandlers.MethodHandlers.keys(), UnorderedElementsAre("plusOne", "fail")); ASSERT_THAT(RawHandlers.NotificationHandlers.keys(), UnorderedElementsAre("notify")); ASSERT_THAT(RawHandlers.CommandHandlers.keys(), UnorderedElementsAre("cmdPlusOne")); std::optional> Reply; auto &RawPlusOne = RawHandlers.MethodHandlers["plusOne"]; RawPlusOne(1, capture(Reply)); ASSERT_TRUE(Reply.has_value()); EXPECT_THAT_EXPECTED(*Reply, llvm::HasValue(2)); RawPlusOne("foo", capture(Reply)); ASSERT_TRUE(Reply.has_value()); EXPECT_THAT_EXPECTED( *Reply, llvm::FailedWithMessage(HasSubstr( "failed to decode plusOne request: expected integer"))); auto &RawFail = RawHandlers.MethodHandlers["fail"]; RawFail(2, capture(Reply)); ASSERT_TRUE(Reply.has_value()); EXPECT_THAT_EXPECTED(*Reply, llvm::FailedWithMessage("X=2")); auto &RawNotify = RawHandlers.NotificationHandlers["notify"]; RawNotify(42); EXPECT_EQ(H.LastNotify, 42); EXPECT_EQ(H.NotifyCount, 1); RawNotify("hi"); // invalid, will be logged EXPECT_EQ(H.LastNotify, 42); EXPECT_EQ(H.NotifyCount, 1); auto &RawCmdPlusOne = RawHandlers.CommandHandlers["cmdPlusOne"]; RawCmdPlusOne(1, capture(Reply)); ASSERT_TRUE(Reply.has_value()); EXPECT_THAT_EXPECTED(*Reply, llvm::HasValue(2)); // None of this generated any outgoing traffic. EXPECT_THAT(RawOutgoing.Received, IsEmpty()); } TEST(LSPBinderTest, OutgoingCalls) { LSPBinder::RawHandlers RawHandlers; OutgoingRecorder RawOutgoing; LSPBinder Binder{RawHandlers, RawOutgoing}; LSPBinder::OutgoingMethod Echo; Echo = Binder.outgoingMethod("echo"); LSPBinder::OutgoingMethod WrongSignature; WrongSignature = Binder.outgoingMethod("wrongSignature"); LSPBinder::OutgoingMethod Fail; Fail = Binder.outgoingMethod("fail"); std::optional> Reply; Echo(Foo{2}, capture(Reply)); EXPECT_THAT(RawOutgoing.take("echo"), ElementsAre(llvm::json::Value(2))); ASSERT_TRUE(Reply.has_value()); EXPECT_THAT_EXPECTED(*Reply, llvm::HasValue(Foo{2})); // JSON response is integer, can't be parsed as string. std::optional> WrongTypeReply; WrongSignature(Foo{2}, capture(WrongTypeReply)); EXPECT_THAT(RawOutgoing.take("wrongSignature"), ElementsAre(llvm::json::Value(2))); ASSERT_TRUE(Reply.has_value()); EXPECT_THAT_EXPECTED(*WrongTypeReply, llvm::FailedWithMessage( HasSubstr("failed to decode wrongSignature reply"))); Fail(Foo{2}, capture(Reply)); EXPECT_THAT(RawOutgoing.take("fail"), ElementsAre(llvm::json::Value(2))); ASSERT_TRUE(Reply.has_value()); EXPECT_THAT_EXPECTED(*Reply, llvm::FailedWithMessage("Params=2")); } } // namespace } // namespace clangd } // namespace clang