Files
logos-protocol/tests/protocol/test_plain_transport_tcp.cpp
Dario Lipicar 29afbac532 Extract the Logos protocol layer from logos-cpp-sdk (lp_* C ABI + protocol semver) (#2)
* Extract the Logos protocol layer from logos-cpp-sdk

Transports (plain TCP/TLS, qt_local, qt_remote/QRO, mock), token manager,
consumer core (LogosAPIClient/LogosAPIConsumer incl. the capability
auto-requestModule flow), ModuleProxy, the abstract LogosProviderObject
interface, and the canonical QVariant<->JSON conversion — now behind the
language-neutral lp_* C ABI (logos_protocol.h) carrying the protocol
semver (LOGOS_PROTOCOL_VERSION_*, lp_protocol_version()).

Bytes crossing the ABI use the lossless {"_bytes": base64url} tagging
(NUL-safe), matching the plain wire encoding.

Provider lp_* surface is compiled groundwork; serving lands with module
authoring.

* consumer: typed requestModule for the capability flow

Port of logos-cpp-sdk master f5a127dd ('use updated capability module',
cpp-sdk#85, Iuri Matias) — the touched files (logos_api_client.cpp,
logos_api_consumer.{h,cpp}) moved into this repo in the P1 extraction.
The capability auto-requestModule path now calls a typed std::string
helper on the consumer (which acquires the capability object directly)
instead of a stringly invokeRemoteMethod round-trip. 111/111 tests.
2026-06-12 18:59:01 -03:00

199 lines
6.3 KiB
C++

// 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 <gtest/gtest.h>
#include "logos_object.h"
#include "logos_transport_config.h"
#include "module_proxy.h"
#include "plain_transport_connection.h"
#include "plain_transport_host.h"
#include <QCoreApplication>
#include <QJsonArray>
#include <QObject>
#include <QString>
#include <QVariant>
#include <QVariantList>
#include <chrono>
#include <memory>
#include <thread>
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<PlainTransportHost>(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<PlainTransportConnection>(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<bool> 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<PlainTransportHost>(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<PlainTransportConnection>(clientCfg);
ASSERT_TRUE(conn->connectToHost());
LogosObject* obj = conn->requestObject("emitter", 2000);
ASSERT_NE(obj, nullptr);
std::atomic<int> 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