//===----------------------------------------------------------------------===// // // 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 "lldb/Host/JSONTransport.h" #include "TestingSupport/Host/JSONTransportTestUtilities.h" #include "TestingSupport/Host/PipeTestUtilities.h" #include "TestingSupport/SubsystemRAII.h" #include "lldb/Host/File.h" #include "lldb/Host/MainLoop.h" #include "lldb/Host/MainLoopBase.h" #include "lldb/Utility/Log.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" #include "llvm/Support/ErrorHandling.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/JSON.h" #include "llvm/Support/raw_ostream.h" #include "llvm/Testing/Support/Error.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include #include #include #include #include #include using namespace llvm; using namespace lldb_private; using namespace lldb_private::transport; using testing::_; using testing::HasSubstr; using testing::InSequence; using testing::Ref; namespace llvm::json { static bool fromJSON(const Value &V, Value &T, Path P) { T = V; return true; } } // namespace llvm::json namespace { namespace test_protocol { struct Req { int id = 0; std::string name; std::optional params; }; json::Value toJSON(const Req &T) { return json::Object{{"name", T.name}, {"id", T.id}, {"params", T.params}}; } bool fromJSON(const json::Value &V, Req &T, json::Path P) { json::ObjectMapper O(V, P); return O && O.map("name", T.name) && O.map("id", T.id) && O.map("params", T.params); } bool operator==(const Req &a, const Req &b) { return a.name == b.name && a.id == b.id && a.params == b.params; } inline llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Req &V) { OS << toJSON(V); return OS; } void PrintTo(const Req &message, std::ostream *os) { std::string O; llvm::raw_string_ostream OS(O); OS << message; *os << O; } struct Resp { int id = 0; int errorCode = 0; std::optional result; }; json::Value toJSON(const Resp &T) { return json::Object{ {"id", T.id}, {"errorCode", T.errorCode}, {"result", T.result}}; } bool fromJSON(const json::Value &V, Resp &T, json::Path P) { json::ObjectMapper O(V, P); return O && O.map("id", T.id) && O.mapOptional("errorCode", T.errorCode) && O.map("result", T.result); } bool operator==(const Resp &a, const Resp &b) { return a.id == b.id && a.errorCode == b.errorCode && a.result == b.result; } inline llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Resp &V) { OS << toJSON(V); return OS; } void PrintTo(const Resp &message, std::ostream *os) { std::string O; llvm::raw_string_ostream OS(O); OS << message; *os << O; } struct Evt { std::string name; std::optional params; }; json::Value toJSON(const Evt &T) { return json::Object{{"name", T.name}, {"params", T.params}}; } bool fromJSON(const json::Value &V, Evt &T, json::Path P) { json::ObjectMapper O(V, P); return O && O.map("name", T.name) && O.map("params", T.params); } bool operator==(const Evt &a, const Evt &b) { return a.name == b.name; } inline llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Evt &V) { OS << toJSON(V); return OS; } void PrintTo(const Evt &message, std::ostream *os) { std::string O; llvm::raw_string_ostream OS(O); OS << message; *os << O; } using Message = std::variant; json::Value toJSON(const Message &msg) { return std::visit([](const auto &msg) { return toJSON(msg); }, msg); } bool fromJSON(const json::Value &V, Message &msg, json::Path P) { const json::Object *O = V.getAsObject(); if (!O) { P.report("expected object"); return false; } if (O->find("id") == O->end()) { Evt E; if (!fromJSON(V, E, P)) return false; msg = std::move(E); return true; } if (O->get("name")) { Req R; if (!fromJSON(V, R, P)) return false; msg = std::move(R); return true; } Resp R; if (!fromJSON(V, R, P)) return false; msg = std::move(R); return true; } struct MyFnParams { int a = 0; int b = 0; }; json::Value toJSON(const MyFnParams &T) { return json::Object{{"a", T.a}, {"b", T.b}}; } bool fromJSON(const json::Value &V, MyFnParams &T, json::Path P) { json::ObjectMapper O(V, P); return O && O.map("a", T.a) && O.map("b", T.b); } struct MyFnResult { int c = 0; }; json::Value toJSON(const MyFnResult &T) { return json::Object{{"c", T.c}}; } bool fromJSON(const json::Value &V, MyFnResult &T, json::Path P) { json::ObjectMapper O(V, P); return O && O.map("c", T.c); } struct ProtoDesc { using Id = int; using Req = Req; using Resp = Resp; using Evt = Evt; static inline Id InitialId() { return 0; } static inline Req Make(Id id, llvm::StringRef method, std::optional params) { return Req{id, method.str(), params}; } static inline Evt Make(llvm::StringRef method, std::optional params) { return Evt{method.str(), params}; } static inline Resp Make(Req req, llvm::Error error) { Resp resp; resp.id = req.id; llvm::handleAllErrors( std::move(error), [&](const llvm::ErrorInfoBase &err) { std::error_code cerr = err.convertToErrorCode(); resp.errorCode = cerr == llvm::inconvertibleErrorCode() ? 1 : cerr.value(); resp.result = err.message(); }); return resp; } static inline Resp Make(Req req, std::optional result) { return Resp{req.id, 0, std::move(result)}; } static inline Id KeyFor(Resp r) { return r.id; } static inline std::string KeyFor(Req r) { return r.name; } static inline std::string KeyFor(Evt e) { return e.name; } static inline std::optional Extract(Req r) { return r.params; } static inline llvm::Expected Extract(Resp r) { if (r.errorCode != 0) return llvm::createStringError( std::error_code(r.errorCode, std::generic_category()), r.result && r.result->getAsString() ? *r.result->getAsString() : "no-message"); return r.result; } static inline std::optional Extract(Evt e) { return e.params; } }; using Transport = TestTransport; using Binder = lldb_private::transport::Binder; using MessageHandler = MockMessageHandler; } // namespace test_protocol template class JSONTransportTest : public PipePairTest { protected: SubsystemRAII subsystems; test_protocol::MessageHandler message_handler; std::unique_ptr transport; MainLoop loop; void SetUp() override { PipePairTest::SetUp(); transport = std::make_unique( std::make_shared(input.GetReadFileDescriptor(), File::eOpenOptionReadOnly, NativeFile::Unowned), std::make_shared(output.GetWriteFileDescriptor(), File::eOpenOptionWriteOnly, NativeFile::Unowned)); } /// Run the transport MainLoop and return any messages received. Error Run(bool close_input = true, std::chrono::milliseconds timeout = std::chrono::milliseconds(5000)) { if (close_input) { input.CloseWriteFileDescriptor(); EXPECT_CALL(message_handler, OnClosed()).WillOnce([this]() { loop.RequestTermination(); }); } loop.AddCallback( [](MainLoopBase &loop) { loop.RequestTermination(); FAIL() << "timeout"; }, timeout); auto handle = transport->RegisterMessageHandler(loop, message_handler); if (!handle) return handle.takeError(); return loop.Run().takeError(); } template void Write(Ts... args) { std::string message; for (const auto &arg : {args...}) message += Encode(arg); EXPECT_THAT_EXPECTED(input.Write(message.data(), message.size()), Succeeded()); } virtual std::string Encode(const json::Value &) = 0; }; class TestHTTPDelimitedJSONTransport final : public HTTPDelimitedJSONTransport { public: using HTTPDelimitedJSONTransport::HTTPDelimitedJSONTransport; void Log(llvm::StringRef message) override { log_messages.emplace_back(message); } std::vector log_messages; }; class HTTPDelimitedJSONTransportTest : public JSONTransportTest { public: using JSONTransportTest::JSONTransportTest; std::string Encode(const json::Value &V) override { std::string msg; raw_string_ostream OS(msg); OS << formatv("{0}", V); return formatv("Content-Length: {0}\r\nContent-type: " "text/json\r\n\r\n{1}", msg.size(), msg) .str(); } }; class TestJSONRPCTransport final : public JSONRPCTransport { public: using JSONRPCTransport::JSONRPCTransport; void Log(llvm::StringRef message) override { log_messages.emplace_back(message); } std::vector log_messages; }; class JSONRPCTransportTest : public JSONTransportTest { public: using JSONTransportTest::JSONTransportTest; std::string Encode(const json::Value &V) override { std::string msg; raw_string_ostream OS(msg); OS << formatv("{0}\n", V); return msg; } }; class TransportBinderTest : public testing::Test { protected: SubsystemRAII subsystems; std::unique_ptr to_remote; std::unique_ptr from_remote; std::unique_ptr binder; test_protocol::MessageHandler remote; MainLoop loop; void SetUp() override { std::tie(to_remote, from_remote) = test_protocol::Transport::createPair(); binder = std::make_unique(*to_remote); auto binder_handle = to_remote->RegisterMessageHandler(loop, remote); EXPECT_THAT_EXPECTED(binder_handle, Succeeded()); auto remote_handle = from_remote->RegisterMessageHandler(loop, *binder); EXPECT_THAT_EXPECTED(remote_handle, Succeeded()); } void Run() { loop.AddPendingCallback([](auto &loop) { loop.RequestTermination(); }); EXPECT_THAT_ERROR(loop.Run().takeError(), Succeeded()); } }; } // namespace // Failing on Windows, see https://github.com/llvm/llvm-project/issues/153446. #ifndef _WIN32 using namespace test_protocol; TEST_F(HTTPDelimitedJSONTransportTest, MalformedRequests) { std::string malformed_header = "COnTent-LenGth: -1\r\nContent-Type: text/json\r\n\r\nnotjosn"; ASSERT_THAT_EXPECTED( input.Write(malformed_header.data(), malformed_header.size()), Succeeded()); EXPECT_CALL(message_handler, OnError(_)).WillOnce([](llvm::Error err) { ASSERT_THAT_ERROR(std::move(err), FailedWithMessage("invalid content length: -1")); }); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(HTTPDelimitedJSONTransportTest, Read) { Write(Req{6, "foo", std::nullopt}); EXPECT_CALL(message_handler, Received(Req{6, "foo", std::nullopt})); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(HTTPDelimitedJSONTransportTest, ReadMultipleMessagesInSingleWrite) { InSequence seq; Write( Message{ Req{6, "one", std::nullopt}, }, Message{ Evt{"two", std::nullopt}, }, Message{ Resp{2, 0, std::nullopt}, }); EXPECT_CALL(message_handler, Received(Req{6, "one", std::nullopt})); EXPECT_CALL(message_handler, Received(Evt{"two", std::nullopt})); EXPECT_CALL(message_handler, Received(Resp{2, 0, std::nullopt})); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(HTTPDelimitedJSONTransportTest, ReadAcrossMultipleChunks) { std::string long_str = std::string( HTTPDelimitedJSONTransport::kReadBufferSize * 2, 'x'); Write(Req{5, long_str, std::nullopt}); EXPECT_CALL(message_handler, Received(Req{5, long_str, std::nullopt})); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(HTTPDelimitedJSONTransportTest, ReadPartialMessage) { std::string message = Encode(Req{5, "foo", std::nullopt}); auto split_at = message.size() / 2; std::string part1 = message.substr(0, split_at); std::string part2 = message.substr(split_at); EXPECT_CALL(message_handler, Received(Req{5, "foo", std::nullopt})); ASSERT_THAT_EXPECTED(input.Write(part1.data(), part1.size()), Succeeded()); loop.AddPendingCallback( [](MainLoopBase &loop) { loop.RequestTermination(); }); ASSERT_THAT_ERROR(Run(/*close_stdin=*/false), Succeeded()); ASSERT_THAT_EXPECTED(input.Write(part2.data(), part2.size()), Succeeded()); input.CloseWriteFileDescriptor(); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(HTTPDelimitedJSONTransportTest, ReadWithZeroByteWrites) { std::string message = Encode(Req{6, "foo", std::nullopt}); auto split_at = message.size() / 2; std::string part1 = message.substr(0, split_at); std::string part2 = message.substr(split_at); EXPECT_CALL(message_handler, Received(Req{6, "foo", std::nullopt})); ASSERT_THAT_EXPECTED(input.Write(part1.data(), part1.size()), Succeeded()); // Run the main loop once for the initial read. loop.AddPendingCallback( [](MainLoopBase &loop) { loop.RequestTermination(); }); ASSERT_THAT_ERROR(Run(/*close_stdin=*/false), Succeeded()); // zero-byte write. ASSERT_THAT_EXPECTED(input.Write(part1.data(), 0), Succeeded()); // zero-byte write. loop.AddPendingCallback( [](MainLoopBase &loop) { loop.RequestTermination(); }); ASSERT_THAT_ERROR(Run(/*close_stdin=*/false), Succeeded()); // Write the remaining part of the message. ASSERT_THAT_EXPECTED(input.Write(part2.data(), part2.size()), Succeeded()); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(HTTPDelimitedJSONTransportTest, ReadWithEOF) { ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(HTTPDelimitedJSONTransportTest, ReaderWithUnhandledData) { std::string json = R"json({"str": "foo"})json"; std::string message = formatv("Content-Length: {0}\r\nContent-type: text/json\r\n\r\n{1}", json.size(), json) .str(); EXPECT_CALL(message_handler, OnError(_)).WillOnce([](llvm::Error err) { // The error should indicate that there are unhandled contents. ASSERT_THAT_ERROR(std::move(err), Failed()); }); // Write an incomplete message and close the handle. ASSERT_THAT_EXPECTED(input.Write(message.data(), message.size() - 1), Succeeded()); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(HTTPDelimitedJSONTransportTest, InvalidTransport) { transport = std::make_unique(nullptr, nullptr); ASSERT_THAT_ERROR(Run(/*close_input=*/false), FailedWithMessage("IO object is not valid.")); } TEST_F(HTTPDelimitedJSONTransportTest, Write) { ASSERT_THAT_ERROR(transport->Send(Req{7, "foo", std::nullopt}), Succeeded()); ASSERT_THAT_ERROR(transport->Send(Resp{5, 0, "bar"}), Succeeded()); ASSERT_THAT_ERROR(transport->Send(Evt{"baz", std::nullopt}), Succeeded()); output.CloseWriteFileDescriptor(); char buf[1024]; Expected bytes_read = output.Read(buf, sizeof(buf), std::chrono::milliseconds(1)); ASSERT_THAT_EXPECTED(bytes_read, Succeeded()); ASSERT_EQ(StringRef(buf, *bytes_read), StringRef("Content-Length: 35\r\n\r\n" R"({"id":7,"name":"foo","params":null})" "Content-Length: 37\r\n\r\n" R"({"errorCode":0,"id":5,"result":"bar"})" "Content-Length: 28\r\n\r\n" R"({"name":"baz","params":null})")); } TEST_F(JSONRPCTransportTest, MalformedRequests) { std::string malformed_header = "notjson\n"; ASSERT_THAT_EXPECTED( input.Write(malformed_header.data(), malformed_header.size()), Succeeded()); EXPECT_CALL(message_handler, OnError(_)).WillOnce([](llvm::Error err) { ASSERT_THAT_ERROR(std::move(err), FailedWithMessage(HasSubstr("Invalid JSON value"))); }); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(JSONRPCTransportTest, Read) { Write(Message{Req{1, "foo", std::nullopt}}); EXPECT_CALL(message_handler, Received(Req{1, "foo", std::nullopt})); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(JSONRPCTransportTest, ReadMultipleMessagesInSingleWrite) { InSequence seq; Write(Message{Req{1, "one", std::nullopt}}, Message{Evt{"two", std::nullopt}}, Message{Resp{3, 0, "three"}}); EXPECT_CALL(message_handler, Received(Req{1, "one", std::nullopt})); EXPECT_CALL(message_handler, Received(Evt{"two", std::nullopt})); EXPECT_CALL(message_handler, Received(Resp{3, 0, "three"})); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(JSONRPCTransportTest, ReadAcrossMultipleChunks) { // Use a string longer than the chunk size to ensure we split the message // across the chunk boundary. std::string long_str = std::string( IOTransport::kReadBufferSize * 2, 'x'); Write(Req{42, long_str, std::nullopt}); EXPECT_CALL(message_handler, Received(Req{42, long_str, std::nullopt})); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(JSONRPCTransportTest, ReadPartialMessage) { std::string message = R"({"id":42,"name":"foo","params":null})" "\n"; std::string part1 = message.substr(0, 7); std::string part2 = message.substr(7); EXPECT_CALL(message_handler, Received(Req{42, "foo", std::nullopt})); ASSERT_THAT_EXPECTED(input.Write(part1.data(), part1.size()), Succeeded()); loop.AddPendingCallback( [](MainLoopBase &loop) { loop.RequestTermination(); }); ASSERT_THAT_ERROR(Run(/*close_input=*/false), Succeeded()); ASSERT_THAT_EXPECTED(input.Write(part2.data(), part2.size()), Succeeded()); input.CloseWriteFileDescriptor(); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(JSONRPCTransportTest, ReadWithEOF) { ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(JSONRPCTransportTest, ReaderWithUnhandledData) { std::string message = R"json({"req": "foo")json"; // Write an incomplete message and close the handle. ASSERT_THAT_EXPECTED(input.Write(message.data(), message.size() - 1), Succeeded()); EXPECT_CALL(message_handler, OnError(_)).WillOnce([](llvm::Error err) { ASSERT_THAT_ERROR(std::move(err), Failed()); }); ASSERT_THAT_ERROR(Run(), Succeeded()); } TEST_F(JSONRPCTransportTest, Write) { ASSERT_THAT_ERROR(transport->Send(Req{11, "foo", std::nullopt}), Succeeded()); ASSERT_THAT_ERROR(transport->Send(Resp{14, 0, "bar"}), Succeeded()); ASSERT_THAT_ERROR(transport->Send(Evt{"baz", std::nullopt}), Succeeded()); output.CloseWriteFileDescriptor(); char buf[1024]; Expected bytes_read = output.Read(buf, sizeof(buf), std::chrono::milliseconds(1)); ASSERT_THAT_EXPECTED(bytes_read, Succeeded()); ASSERT_EQ(StringRef(buf, *bytes_read), StringRef(R"({"id":11,"name":"foo","params":null})" "\n" R"({"errorCode":0,"id":14,"result":"bar"})" "\n" R"({"name":"baz","params":null})" "\n")); } TEST_F(JSONRPCTransportTest, InvalidTransport) { transport = std::make_unique(nullptr, nullptr); ASSERT_THAT_ERROR(Run(/*close_input=*/false), FailedWithMessage("IO object is not valid.")); } // Out-bound binding request handler. TEST_F(TransportBinderTest, OutBoundRequests) { OutgoingRequest addFn = binder->Bind("add"); bool replied = false; addFn(MyFnParams{1, 2}, [&](Expected result) { EXPECT_THAT_EXPECTED(result, Succeeded()); EXPECT_EQ(result->c, 3); replied = true; }); EXPECT_CALL(remote, Received(Req{1, "add", MyFnParams{1, 2}})); EXPECT_THAT_ERROR(from_remote->Send(Resp{1, 0, toJSON(MyFnResult{3})}), Succeeded()); Run(); EXPECT_TRUE(replied); } TEST_F(TransportBinderTest, OutBoundRequestsVoidParams) { OutgoingRequest voidParamFn = binder->Bind("voidParam"); bool replied = false; voidParamFn([&](Expected result) { EXPECT_THAT_EXPECTED(result, Succeeded()); EXPECT_EQ(result->c, 3); replied = true; }); EXPECT_CALL(remote, Received(Req{1, "voidParam", std::nullopt})); EXPECT_THAT_ERROR(from_remote->Send(Resp{1, 0, toJSON(MyFnResult{3})}), Succeeded()); Run(); EXPECT_TRUE(replied); } TEST_F(TransportBinderTest, OutBoundRequestsVoidResult) { OutgoingRequest voidResultFn = binder->Bind("voidResult"); bool replied = false; voidResultFn(MyFnParams{4, 5}, [&](llvm::Error error) { EXPECT_THAT_ERROR(std::move(error), Succeeded()); replied = true; }); EXPECT_CALL(remote, Received(Req{1, "voidResult", MyFnParams{4, 5}})); EXPECT_THAT_ERROR(from_remote->Send(Resp{1, 0, std::nullopt}), Succeeded()); Run(); EXPECT_TRUE(replied); } TEST_F(TransportBinderTest, OutBoundRequestsVoidParamsAndVoidResult) { OutgoingRequest voidParamAndResultFn = binder->Bind("voidParamAndResult"); bool replied = false; voidParamAndResultFn([&](llvm::Error error) { EXPECT_THAT_ERROR(std::move(error), Succeeded()); replied = true; }); EXPECT_CALL(remote, Received(Req{1, "voidParamAndResult", std::nullopt})); EXPECT_THAT_ERROR(from_remote->Send(Resp{1, 0, std::nullopt}), Succeeded()); Run(); EXPECT_TRUE(replied); } // In-bound binding request handler. TEST_F(TransportBinderTest, InBoundRequests) { bool called = false; binder->Bind( "add", [&](const int captured_param, const MyFnParams ¶ms) -> Expected { called = true; return MyFnResult{params.a + params.b + captured_param}; }, 2); EXPECT_THAT_ERROR(from_remote->Send(Req{1, "add", MyFnParams{3, 4}}), Succeeded()); EXPECT_CALL(remote, Received(Resp{1, 0, MyFnResult{9}})); Run(); EXPECT_TRUE(called); } TEST_F(TransportBinderTest, InBoundRequestsVoidParams) { bool called = false; binder->Bind( "voidParam", [&](const int captured_param) -> Expected { called = true; return MyFnResult{captured_param}; }, 2); EXPECT_THAT_ERROR(from_remote->Send(Req{2, "voidParam", std::nullopt}), Succeeded()); EXPECT_CALL(remote, Received(Resp{2, 0, MyFnResult{2}})); Run(); EXPECT_TRUE(called); } TEST_F(TransportBinderTest, InBoundRequestsVoidResult) { bool called = false; binder->Bind( "voidResult", [&](const int captured_param, const MyFnParams ¶ms) -> llvm::Error { called = true; EXPECT_EQ(captured_param, 2); EXPECT_EQ(params.a, 3); EXPECT_EQ(params.b, 4); return llvm::Error::success(); }, 2); EXPECT_THAT_ERROR(from_remote->Send(Req{3, "voidResult", MyFnParams{3, 4}}), Succeeded()); EXPECT_CALL(remote, Received(Resp{3, 0, std::nullopt})); Run(); EXPECT_TRUE(called); } TEST_F(TransportBinderTest, InBoundRequestsVoidParamsAndResult) { bool called = false; binder->Bind( "voidParamAndResult", [&](const int captured_param) -> llvm::Error { called = true; EXPECT_EQ(captured_param, 2); return llvm::Error::success(); }, 2); EXPECT_THAT_ERROR( from_remote->Send(Req{4, "voidParamAndResult", std::nullopt}), Succeeded()); EXPECT_CALL(remote, Received(Resp{4, 0, std::nullopt})); Run(); EXPECT_TRUE(called); } // Out-bound binding event handler. TEST_F(TransportBinderTest, OutBoundEvents) { OutgoingEvent emitEvent = binder->Bind("evt"); emitEvent(MyFnParams{1, 2}); EXPECT_CALL(remote, Received(Evt{"evt", MyFnParams{1, 2}})); Run(); } TEST_F(TransportBinderTest, OutBoundEventsVoidParams) { OutgoingEvent emitEvent = binder->Bind("evt"); emitEvent(); EXPECT_CALL(remote, Received(Evt{"evt", std::nullopt})); Run(); } // In-bound binding event handler. TEST_F(TransportBinderTest, InBoundEvents) { bool called = false; binder->Bind( "evt", [&](const int captured_arg, const MyFnParams ¶ms) { EXPECT_EQ(captured_arg, 42); EXPECT_EQ(params.a, 3); EXPECT_EQ(params.b, 4); called = true; }, 42); EXPECT_THAT_ERROR(from_remote->Send(Evt{"evt", MyFnParams{3, 4}}), Succeeded()); Run(); EXPECT_TRUE(called); } TEST_F(TransportBinderTest, InBoundEventsVoidParams) { bool called = false; binder->Bind( "evt", [&](const int captured_arg) { EXPECT_EQ(captured_arg, 42); called = true; }, 42); EXPECT_THAT_ERROR(from_remote->Send(Evt{"evt", std::nullopt}), Succeeded()); Run(); EXPECT_TRUE(called); } #endif