// End-to-end smoke test for the plain-C++ TCP transport. // // Wires a PlainTransportHost to a fixture QObject that exposes a // Q_INVOKABLE callRemoteMethod + an eventResponse signal (matching the // ModuleProxy shape expected by the transport), and drives it with a // PlainTransportConnection + PlainLogosObject on the same process. Proves // the wire stack + Asio runtime + Qt-boundary adapters round-trip calls // and events correctly. // // NOTE: disabled in this iteration — the in-process ModuleProxy fixture // deadlocks under nix's test sandbox because this file runs the PlainLogos // consumer and the PlainTransportHost provider on the same Qt event loop, // and the synchronous consumer wait prevents the queued QMetaObject::invokeMethod // dispatch on the provider from making progress. The transport itself is // exercised cross-process by the logos-logoscore-py integration matrix // (follow-up PR); what's here is a scaffold for the inevitable follow-up // that runs host + consumer in separate QCoreApplication instances / // processes. #if 0 #include #include "logos_object.h" #include "logos_transport_config.h" #include "module_proxy.h" #include "plain_transport_connection.h" #include "plain_transport_host.h" #include #include #include #include #include #include #include #include #include using namespace logos::plain; namespace { // Minimal stand-in for a real module: a ModuleProxy-like QObject that // answers callRemoteMethod() and can emit eventResponse(). class FakeModule : public ModuleProxy { public: explicit FakeModule(QObject* parent = nullptr) : ModuleProxy(nullptr, parent) {} QVariant callRemoteMethod(const QString& /*authToken*/, const QString& methodName, const QVariantList& args) { lastMethod = methodName; lastArgs = args; if (methodName == "echo" && !args.isEmpty()) return args.first(); if (methodName == "sum" && args.size() == 2) return args[0].toInt() + args[1].toInt(); return QVariant("unhandled: " + methodName); } QJsonArray getPluginMethods() { return QJsonArray{}; } QString lastMethod; QVariantList lastArgs; }; // Ensure the single QCoreApplication instance exists for these tests. QCoreApplication* ensureApp() { static int argc = 0; static char* argv[] = { nullptr }; if (!QCoreApplication::instance()) new QCoreApplication(argc, argv); return QCoreApplication::instance(); } } // anonymous namespace class PlainTcpTransportTest : public ::testing::Test { protected: void SetUp() override { ensureApp(); } void pumpEventLoop(int ms) { auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms); while (std::chrono::steady_clock::now() < end) { QCoreApplication::processEvents(); std::this_thread::sleep_for(std::chrono::milliseconds(5)); } } }; TEST_F(PlainTcpTransportTest, MethodCallRoundTrip) { // Host on 127.0.0.1, ephemeral port. LogosTransportConfig cfg; cfg.protocol = LogosProtocol::Tcp; cfg.host = "127.0.0.1"; cfg.port = 0; auto host = std::make_unique(cfg); ASSERT_TRUE(host->start()); FakeModule fake; ASSERT_TRUE(host->publishObject("fake_mod", &fake)); // Extract the actual bound port from the endpoint() URL. QString endpoint = host->endpoint(); ASSERT_TRUE(endpoint.startsWith("tcp://")); const int colon = endpoint.lastIndexOf(':'); ASSERT_NE(colon, -1); uint16_t boundPort = endpoint.mid(colon + 1).toUShort(); ASSERT_NE(boundPort, 0); LogosTransportConfig clientCfg = cfg; clientCfg.port = boundPort; auto conn = std::make_unique(clientCfg); ASSERT_TRUE(conn->connectToHost()); LogosObject* obj = conn->requestObject("fake_mod", 2000); ASSERT_NE(obj, nullptr); // The inbound Call frame is dispatched to ModuleProxy via // QMetaObject::invokeMethod(..., Qt::QueuedConnection), which requires // the Qt event loop on the main thread to be pumped. callMethod blocks // on a future, so we have to run callMethod on a worker and pump the // event loop here until the worker finishes. QVariant result; std::atomic done{false}; std::thread caller([&] { result = obj->callMethod("", "echo", QVariantList{ QString("hello") }, 3000); done.store(true); }); for (int i = 0; i < 200 && !done.load(); ++i) pumpEventLoop(20); caller.join(); EXPECT_EQ(result.toString(), "hello"); obj->release(); host.reset(); } TEST_F(PlainTcpTransportTest, EventDelivery) { LogosTransportConfig cfg; cfg.protocol = LogosProtocol::Tcp; cfg.host = "127.0.0.1"; cfg.port = 0; auto host = std::make_unique(cfg); ASSERT_TRUE(host->start()); FakeModule fake; ASSERT_TRUE(host->publishObject("emitter", &fake)); QString endpoint = host->endpoint(); uint16_t boundPort = endpoint.mid(endpoint.lastIndexOf(':') + 1).toUShort(); LogosTransportConfig clientCfg = cfg; clientCfg.port = boundPort; auto conn = std::make_unique(clientCfg); ASSERT_TRUE(conn->connectToHost()); LogosObject* obj = conn->requestObject("emitter", 2000); ASSERT_NE(obj, nullptr); std::atomic received{0}; QString lastEvent; QVariantList lastData; obj->onEvent("ping", [&](const QString& name, const QVariantList& data) { lastEvent = name; lastData = data; received.fetch_add(1); }); // Give the subscription frame a moment to reach the host. pumpEventLoop(200); // Emit from the host side — this fires ModuleProxy::eventResponse, which // our transport hook fans out to the subscribed connection. emit fake.eventResponse("ping", QVariantList{ QString("hi"), 42 }); // Wait for the event to traverse the socket. for (int i = 0; i < 40 && received.load() == 0; ++i) pumpEventLoop(50); EXPECT_GE(received.load(), 1); EXPECT_EQ(lastEvent, "ping"); ASSERT_EQ(lastData.size(), 2); EXPECT_EQ(lastData[0].toString(), "hi"); EXPECT_EQ(lastData[1].toInt(), 42); obj->release(); host.reset(); } #endif