Files
logos-cpp-sdk/tests/generator/test_make_source.cpp
Dario LipicarandClaude Opus 4.8 f0fe8cbfeb Make the base SDK Qt-free: Qt developer layer moves to logos-qt-sdk (#83)
* Extract the protocol layer into logos-protocol; consume it as a flake input

The transport/token/IPC layer (transports incl. QRO + plain TCP/TLS,
consumer core LogosAPIClient/LogosAPIConsumer with the capability
auto-requestModule flow, ModuleProxy, token manager, QVariant<->JSON
conversion, the abstract LogosProviderObject interface) now lives in the
logos-protocol repo behind the versioned lp_* C ABI.

This SDK keeps the typed C++ developer layer (LogosAPI, provider base
classes + Qt provider glue, module context, code generator) and still
compiles the protocol sources INTO liblogos_sdk.a from the flake input,
so the installed artifact (archive symbols, include/ + include/cpp
layouts, cmake config) stays byte-compatible: existing consumers need
no changes. Public headers are unchanged; logos_provider_object.h keeps
its name and now re-exports the abstract interface from
logos_provider_interface.h.

Transport/protocol component tests moved to logos-protocol with the
code; the remaining sdk/generator/experimental suites are unchanged
(432/432 green against the local protocol checkout).

* lock: add logos-protocol input

* Make the base SDK Qt-free: move the Qt developer layer to logos-qt-sdk

LogosAPI, LogosAPIProvider, LogosProviderBase/LOGOS_PROVIDER macros, the
QObject provider glue (QtProviderObject) and the legacy PluginInterface
(core/interface.h) move to the new logos-qt-sdk repo. The protocol
sources are no longer compiled into a monolithic archive — consumers
link logos-qt-sdk (which layers on logos-protocol) instead.

What remains here is header-only std C++: logos_module_context.h,
logos_result.h (StdLogosResult), logos_json.h — exported as the CMake
INTERFACE target logos-cpp-sdk::logos_headers — plus the code generator
(a build-time tool; its introspection mode now includes
logos_provider_interface.h from logos-protocol, where
LogosProviderPlugin moved).

Mechanically verified Qt-free: the logos-cpp-lib / logos-cpp-include
closures contain only nlohmann_json. Tests: 245/245 (module-context std
suite + generator + experimental).

* fix: accept the installed source-export layout in the protocol-root check

The fail-fast only tested <root>/cpp/logos_protocol.h, but the LP_SRC
selection right below (and the error message itself) support the
installed export layout <root>/include/cpp as well. Pointing
LOGOS_PROTOCOL_ROOT at an installed export tripped the FATAL_ERROR
before that fallback could apply.

Caught by Copilot review on #82.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* lock: pin logos-protocol to the qt-free-split branch head

The Qt-free SDK (and the cdylib backend stacked on it) reference
LogosProviderPlugin from protocol's logos_provider_interface.h, which
lands on feat/qt-free-split — the P1-branch pin no longer compiles
standalone. Temporary — drop when the chain PRs merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* doctest: pin the logoscore runtime via its {release} placeholder

The spec built logoscore-cli at bare master with only the cpp-sdk inputs
overridden — master's stack cannot compile against the qt-free SDK, so
the suite failed on the chain branches. With the placeholder, CI's
--release-for pins expand it to the workspace's logoscore commit (and
local runs without a pin still fall back to master, unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* doctest: override the nested module builders to {release} too

capability_module (via logoscore's lock) and the cloned accounts module
resolve module-builder from their own locks — pre-split revs whose
LogosModule.cmake still detects the SDK by logos_api.h, which the
qt-free SDK no longer ships ('logos-cpp-sdk not found'). Overriding the
builder itself to the workspace-pinned chain rev (keeping the nested
cpp-sdk override) builds both modules with the split-aware builder.
Verified end-to-end locally with the exact doctest command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* doctest: apply the {release} + nested-builder overrides to all three specs

The runtime spec got the treatment in 210eea1; the composition and
worker-thread specs have the same logoscore/module build commands and
failed identically (pre-split builders from the modules' own locks).
All executed run: blocks now pin logoscore-cli{release} and override
the nested module builders to logos-module-builder{release}; the
displayed code_block: variants stay in their generic master form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* codegen: typed wrappers throw on call failure; dispatch catches escapes

Generated sync client wrappers call the new err-out invokeRemoteMethod
overload and throw logos::LogosCallError when the call fails (e.g. the
bound module is missing) — previously the empty QVariant silently
degraded to the return type's default and a caller could not tell
failure from a legitimate 0 / "". Both generators (legacy + LIDL),
both API styles. Async paths unchanged.

Generated provider dispatch (universal qt glue + LOGOS_PROVIDER) wraps
the method body in a catch-all that logs and returns an invalid QVariant
— an escaped exception becomes an ordinary METHOD_FAILED instead of
unwinding through Qt event dispatch and killing the module process.

* codegen: CallError out-param instead of throwing wrappers

Per review, the generated sync wrappers expose the error channel as an
optional trailing parameter — add(a, b, &err) — rather than throwing:
explicit, stateless, works on temporaries, and existing call sites
compile unchanged (they keep default-on-failure, now with a qWarning so
failures are visible in the module log). The dispatch catch-all from the
previous commit stays: it contains author exceptions, it doesn't
introduce any.

* glue: fire onContextReady AFTER modules()/event wiring

The generated onInit set the context (which fires the impl's
onContextReady hook) before constructing the LogosModules aggregate and
wiring typed event emission — so an impl doing its documented one-time
setup there (typed dependency calls, event subscriptions) dereferenced
a null aggregate and crashed the module process (signal 11). Found by
the first module to subscribe to a dependency's typed event from
onContextReady. Context now goes last.

* ci: run workflows on stacked PRs + workflow_dispatch

Both workflows filtered pull_request to master-based PRs, so stacked PRs
(feat/qt-free-sdk -> feat/extract-logos-protocol, feat/cdylib-authoring
-> feat/qt-free-sdk) ran NO checks at all. Drop the base-branch filter
for pull_request and add workflow_dispatch for manual runs. Same fix as
logos-module-builder 232b8a2.

* lock: protocol at the typed-requestModule port (3de5398)

* ci: chain pins for the doc-tests (drop at merge)

In repo CI only cpp-sdk's {release} is the commit under test —
logoscore-cli and module-builder expanded to master, which doesn't link
against the chain SDK the specs override in ('Build the CLI with the SDK
override' failed on every run since the stacked-PR triggers were
enabled). Pin both to the extraction-chain heads; the workspace pipeline
is unaffected (it pins every repo itself).

* generator: distribute the LIDL frontend for external generators

First step of moving ALL Qt glue emission out of this repo into
logos-qt-sdk's logos-qt-generator (cpp-sdk's generator keeps only the
Qt-free outputs: std typed wrappers, logos_sdk umbrella, cdylib
impl-exports, LIDL derivation).

- Shared emit helpers (lidlToPascalCase, lidlTypeToQt, lidlTypeToStd,
  lidlIsStdConvertible) move to a new lidl_emit_common.{h,cpp} unit, used
  by both generators.
- The frontend set (AST, lexer, parser, serializer, validator,
  impl-header parser, emit-common) is installed under
  share/lidl-frontend/ — the qt generator compiles these sources in
  directly, so the two tools share one frontend without a binary ABI.

* lock: protocol#3 merged — pin advances to protocol master

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 19:56:19 -03:00

235 lines
7.1 KiB
C++

#include <gtest/gtest.h>
#include <QJsonArray>
#include <QJsonObject>
#include "generator_lib.h"
static QJsonObject makeMethod(const QString& name, const QString& retType, int paramCount)
{
QJsonObject m;
m["name"] = name;
m["returnType"] = retType;
m["isInvokable"] = true;
QJsonArray params;
for (int i = 0; i < paramCount; ++i) {
QJsonObject p;
p["type"] = "int";
p["name"] = QString("p%1").arg(i);
params.append(p);
}
m["parameters"] = params;
return m;
}
TEST(MakeSourceTest, ContainsInclude)
{
QString src = makeSource("mod", "Mod", "mod_api.h", QJsonArray());
EXPECT_TRUE(src.contains("#include \"mod_api.h\""));
}
TEST(MakeSourceTest, ConstructorInitializesClient)
{
QString src = makeSource("my_mod", "MyMod", "my_mod_api.h", QJsonArray());
EXPECT_TRUE(src.contains("MyMod::MyMod(LogosAPI* api)"));
EXPECT_TRUE(src.contains("api->getClient(\"my_mod\")"));
}
TEST(MakeSourceTest, EnsureReplicaMethod)
{
QString src = makeSource("mod", "Mod", "mod.h", QJsonArray());
EXPECT_TRUE(src.contains("LogosObject* Mod::ensureReplica()"));
}
TEST(MakeSourceTest, ZeroParams)
{
QJsonArray methods;
methods.append(makeMethod("doStuff", "int", 0));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("m_client->invokeRemoteMethod(\"mod\", \"doStuff\", QVariantList{}, Timeout(), &_err)"));
EXPECT_TRUE(src.contains("return _result.toInt()"));
}
TEST(MakeSourceTest, OneParam)
{
QJsonArray methods;
methods.append(makeMethod("fn", "bool", 1));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("m_client->invokeRemoteMethod(\"mod\", \"fn\", QVariantList{p0}, Timeout(), &_err)"));
EXPECT_TRUE(src.contains("return _result.toBool()"));
}
TEST(MakeSourceTest, TwoParams)
{
QJsonArray methods;
methods.append(makeMethod("fn", "void", 2));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("m_client->invokeRemoteMethod(\"mod\", \"fn\", QVariantList{p0, p1}, Timeout(), &_err)"));
}
TEST(MakeSourceTest, ThreeParams)
{
QJsonArray methods;
methods.append(makeMethod("fn", "QString", 3));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("p0, p1, p2"));
EXPECT_TRUE(src.contains("return _result.toString()"));
}
TEST(MakeSourceTest, FourParams)
{
QJsonArray methods;
methods.append(makeMethod("fn", "double", 4));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("p0, p1, p2, p3"));
EXPECT_TRUE(src.contains("return _result.toDouble()"));
}
TEST(MakeSourceTest, FiveParams)
{
QJsonArray methods;
methods.append(makeMethod("fn", "float", 5));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("p0, p1, p2, p3, p4"));
EXPECT_TRUE(src.contains("return _result.toFloat()"));
}
TEST(MakeSourceTest, MoreThanFiveParamsUsesVariantList)
{
QJsonArray methods;
methods.append(makeMethod("fn", "QVariant", 6));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("QVariantList{p0, p1, p2, p3, p4, p5}"));
EXPECT_TRUE(src.contains("return _result"));
}
TEST(MakeSourceTest, VoidReturnNoConversion)
{
QJsonArray methods;
methods.append(makeMethod("doIt", "void", 0));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_FALSE(src.contains("return _result"));
}
TEST(MakeSourceTest, QStringListReturn)
{
QJsonArray methods;
methods.append(makeMethod("getNames", "QStringList", 0));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("return _result.toStringList()"));
}
TEST(MakeSourceTest, QJsonArrayReturn)
{
QJsonArray methods;
methods.append(makeMethod("getData", "QJsonArray", 0));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("qvariant_cast<QJsonArray>(_result)"));
}
TEST(MakeSourceTest, QVariantListReturn)
{
QJsonArray methods;
methods.append(makeMethod("getItems", "QVariantList", 0));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("return _result.toList()"));
}
TEST(MakeSourceTest, QVariantMapReturn)
{
QJsonArray methods;
methods.append(makeMethod("getData", "QVariantMap", 0));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("return _result.toMap()"));
}
TEST(MakeSourceTest, QVariantListAsync)
{
QJsonArray methods;
methods.append(makeMethod("getItems", "QVariantList", 0));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("Mod::getItemsAsync("));
EXPECT_TRUE(src.contains("std::function<void(QVariantList)> callback"));
EXPECT_TRUE(src.contains("QVariantList()"));
}
TEST(MakeSourceTest, QVariantMapAsync)
{
QJsonArray methods;
methods.append(makeMethod("getData", "QVariantMap", 0));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("Mod::getDataAsync("));
EXPECT_TRUE(src.contains("std::function<void(QVariantMap)> callback"));
EXPECT_TRUE(src.contains("QVariantMap()"));
}
TEST(MakeSourceTest, QVariantListConstRefParam)
{
QJsonArray methods;
{
QJsonObject m;
m["name"] = "process";
m["returnType"] = "void";
m["isInvokable"] = true;
QJsonArray params;
QJsonObject p;
p["type"] = "QVariantList";
p["name"] = "items";
params.append(p);
m["parameters"] = params;
methods.append(m);
}
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("const QVariantList& items"));
}
TEST(MakeSourceTest, QVariantMapConstRefParam)
{
QJsonArray methods;
{
QJsonObject m;
m["name"] = "update";
m["returnType"] = "void";
m["isInvokable"] = true;
QJsonArray params;
QJsonObject p;
p["type"] = "QVariantMap";
p["name"] = "data";
params.append(p);
m["parameters"] = params;
methods.append(m);
}
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("const QVariantMap& data"));
}
TEST(MakeSourceTest, LogosResultReturn)
{
QJsonArray methods;
methods.append(makeMethod("query", "LogosResult", 0));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("_result.value<LogosResult>()"));
}
TEST(MakeSourceTest, AsyncImplementation)
{
QJsonArray methods;
methods.append(makeMethod("fn", "int", 1));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("Mod::fnAsync("));
EXPECT_TRUE(src.contains("invokeRemoteMethodAsync"));
EXPECT_TRUE(src.contains("callback"));
}
TEST(MakeSourceTest, NonInvokableSkipped)
{
QJsonArray methods;
QJsonObject m;
m["name"] = "hidden";
m["returnType"] = "void";
m["isInvokable"] = false;
m["parameters"] = QJsonArray();
methods.append(m);
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_FALSE(src.contains("hidden"));
}