mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 12:01:15 +00:00
* 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.
199 lines
6.3 KiB
C++
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
|