Files
logos-cpp-sdk/tests/experimental/test_lidl_gen_provider.cpp
Dario LipicarandClaude Opus 4.8 7b62ac2017 Per-event documentation + getPluginEvents introspection (#71)
* Add per-event documentation + getPluginEvents introspection

Mirror the per-method documentation pipeline for events. Events
(declared in a universal module's logos_events: section) now carry a
description parsed from their /// doc comments, and are introspectable
at runtime via a new getPluginEvents framework call.

- lidl_ast: EventDecl gains a description field.
- impl_header_parser: capture the event's doc comment (previously
  discarded) and an optional metadata.json events[].description.
- lidl_gen_provider: generated universal provider emits
  getEvents() override, mirroring getMethods() (name/signature/
  parameters/description; no returnType/isInvokable — events are void).
- logos_provider_object: default-empty virtual getEvents() so the
  legacy provider path and QtProviderObject inherit empty.
- module_proxy / qt_provider_object: intercept getPluginEvents next to
  the getPluginMethods special-case.
- docs: spec + README event-documentation notes.

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

* Add unit tests for event documentation + getEvents generation

Address review feedback (#71): cover the event-introspection paths that
previously only had method-side tests.

- impl_header_parser test: assert metadata.json events[].description is
  parsed; new documented_events fixture asserts `///` doc-comment
  capture on a logos_events: block (multi-line joined with \n,
  adjacent-only, plain // ignored).
- lidl_gen_provider test: assert the generated dispatch contains
  getEvents() emitting each event's name/signature/parameters and an
  escaped description, and that events carry no returnType/isInvokable.

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

* Fold event introspection into getMethods() to keep the provider ABI stable

The previous approach added a getEvents() virtual to LogosProviderObject,
which inserted a new vtable slot and shifted every later slot — an ABI
break that would misdispatch virtual calls whenever an old and new
host/module were mixed across the in-process plugin boundary.

Instead, report events INSIDE the existing getMethods() call: it now
returns the module's whole interface, with each entry tagged
type "method" or "event" (events omit returnType/isInvokable). The
provider vtable is therefore byte-for-byte unchanged, so old/new hosts
and modules stay binary-compatible — a new host reading an old module
sees no event entries (zero events), and an old host reading a new
module just ignores the "type" field (cosmetic). An entry with no
"type" is treated as a method.

- logos_provider_object.h: remove the getEvents() virtual; document that
  getMethods() carries both, and why.
- generator (lidl_gen_provider): emit events as type "event" entries
  inside getMethods(); tag methods type "method"; no getEvents() output.
- module_proxy / qt_provider_object: getPluginMethods()/getPluginEvents()
  are now type-filtered views of getMethods(), plus a new
  getPluginInterface() returning the whole list. (These are name-
  dispatched Q_INVOKABLEs, not vtable surface — adding them is safe.)
- tests: generator asserts events fold into getMethods() tagged "event";
  ModuleProxy asserts the three filtered views; parser tests unchanged.
- docs: spec/project/docs/README updated, incl. an ABI rationale note.

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

---------

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

463 lines
16 KiB
C++

#include <gtest/gtest.h>
#include "lidl_gen_provider.h"
#include "lidl_gen_client.h"
static ModuleDecl makeTestModule()
{
ModuleDecl m;
m.name = "test_module";
m.version = "1.0.0";
m.description = "Test module";
// Method: std::string greet(const std::string& name) → tstr
{
MethodDecl md;
md.name = "greet";
md.returnType = { TypeExpr::Primitive, "tstr", {} };
ParamDecl p; p.name = "name"; p.type = { TypeExpr::Primitive, "tstr", {} };
md.params.append(p);
m.methods.append(md);
}
// Method: bool isValid() → bool
{
MethodDecl md;
md.name = "isValid";
md.returnType = { TypeExpr::Primitive, "bool", {} };
m.methods.append(md);
}
// Method: int64_t getCount() → int
{
MethodDecl md;
md.name = "getCount";
md.returnType = { TypeExpr::Primitive, "int", {} };
m.methods.append(md);
}
// Method: std::vector<std::string> getNames() → [tstr]
{
MethodDecl md;
md.name = "getNames";
TypeExpr elem = { TypeExpr::Primitive, "tstr", {} };
md.returnType = { TypeExpr::Array, "", { elem } };
m.methods.append(md);
}
// Method: void doNothing() → void
{
MethodDecl md;
md.name = "doNothing";
md.returnType = { TypeExpr::Primitive, "void", {} };
m.methods.append(md);
}
return m;
}
// ---------------------------------------------------------------------------
// Provider header generation
// ---------------------------------------------------------------------------
TEST(LidlGenProvider, HeaderContainsClassName)
{
auto m = makeTestModule();
QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h");
EXPECT_TRUE(h.contains("TestModuleProviderObject"));
EXPECT_TRUE(h.contains("TestModulePlugin"));
}
TEST(LidlGenProvider, HeaderIncludesImplHeader)
{
auto m = makeTestModule();
QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h");
EXPECT_TRUE(h.contains("#include \"test_module_impl.h\""));
}
TEST(LidlGenProvider, HeaderIncludesFrameworkHeaders)
{
auto m = makeTestModule();
QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h");
EXPECT_TRUE(h.contains("#include \"logos_provider_object.h\""));
EXPECT_TRUE(h.contains("#include \"interface.h\""));
EXPECT_TRUE(h.contains("#include \"logos_types.h\""));
}
TEST(LidlGenProvider, HeaderHasPluginMetadata)
{
auto m = makeTestModule();
QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h");
EXPECT_TRUE(h.contains("Q_PLUGIN_METADATA"));
EXPECT_TRUE(h.contains("Q_INTERFACES(PluginInterface LogosProviderPlugin)"));
EXPECT_TRUE(h.contains("Q_OBJECT"));
}
TEST(LidlGenProvider, HeaderHasLogosMacro)
{
auto m = makeTestModule();
QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h");
EXPECT_TRUE(h.contains("LOGOS_PROVIDER(TestModuleProviderObject, \"test_module\", \"1.0.0\")"));
}
TEST(LidlGenProvider, HeaderHasWrapperMethods)
{
auto m = makeTestModule();
QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h");
EXPECT_TRUE(h.contains("QString greet("));
EXPECT_TRUE(h.contains("bool isValid("));
EXPECT_TRUE(h.contains("int getCount("));
EXPECT_TRUE(h.contains("QStringList getNames("));
EXPECT_TRUE(h.contains("void doNothing("));
}
TEST(LidlGenProvider, HeaderConvertsStringParams)
{
auto m = makeTestModule();
QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h");
// greet should convert QString→std::string and back
EXPECT_TRUE(h.contains(".toStdString()"));
EXPECT_TRUE(h.contains("QString::fromStdString("));
}
TEST(LidlGenProvider, HeaderHasStringVecHelpers)
{
auto m = makeTestModule();
QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h");
// getNames returns [tstr], so helpers should be generated
EXPECT_TRUE(h.contains("lidlToQStringList"));
EXPECT_TRUE(h.contains("lidlToStdStringVector"));
}
TEST(LidlGenProvider, HeaderHasImplMember)
{
auto m = makeTestModule();
QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h");
EXPECT_TRUE(h.contains("TestModuleImpl m_impl"));
}
TEST(LidlGenProvider, HeaderHasCreateProviderObject)
{
auto m = makeTestModule();
QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h");
EXPECT_TRUE(h.contains("createProviderObject()"));
EXPECT_TRUE(h.contains("new TestModuleProviderObject()"));
}
TEST(LidlGenProvider, HeaderPluginReturnsCorrectName)
{
auto m = makeTestModule();
QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h");
EXPECT_TRUE(h.contains("return QStringLiteral(\"test_module\")"));
EXPECT_TRUE(h.contains("return QStringLiteral(\"1.0.0\")"));
}
// ---------------------------------------------------------------------------
// No string vec helpers when not needed
// ---------------------------------------------------------------------------
TEST(LidlGenProvider, NoStringVecHelpersWhenNotNeeded)
{
ModuleDecl m;
m.name = "simple";
m.version = "1.0.0";
MethodDecl md;
md.name = "getFlag";
md.returnType = { TypeExpr::Primitive, "bool", {} };
m.methods.append(md);
QString h = lidlMakeProviderHeader(m, "SimpleImpl", "simple_impl.h");
EXPECT_FALSE(h.contains("lidlToQStringList"));
}
// ---------------------------------------------------------------------------
// Dispatch generation
// ---------------------------------------------------------------------------
TEST(LidlGenProvider, DispatchContainsCallMethod)
{
auto m = makeTestModule();
QString d = lidlMakeProviderDispatch(m);
EXPECT_TRUE(d.contains("::callMethod(const QString& methodName, const QVariantList& args)"));
}
TEST(LidlGenProvider, DispatchHasMethodChecks)
{
auto m = makeTestModule();
QString d = lidlMakeProviderDispatch(m);
EXPECT_TRUE(d.contains("if (methodName == \"greet\")"));
EXPECT_TRUE(d.contains("if (methodName == \"isValid\")"));
EXPECT_TRUE(d.contains("if (methodName == \"getCount\")"));
EXPECT_TRUE(d.contains("if (methodName == \"getNames\")"));
EXPECT_TRUE(d.contains("if (methodName == \"doNothing\")"));
}
TEST(LidlGenProvider, DispatchVoidReturnsTrueVariant)
{
auto m = makeTestModule();
QString d = lidlMakeProviderDispatch(m);
// doNothing is void, dispatch should return QVariant(true)
EXPECT_TRUE(d.contains("return QVariant(true)"));
}
TEST(LidlGenProvider, DispatchHasUnknownMethodWarning)
{
auto m = makeTestModule();
QString d = lidlMakeProviderDispatch(m);
EXPECT_TRUE(d.contains("unknown method"));
}
TEST(LidlGenProvider, DispatchContainsGetMethods)
{
auto m = makeTestModule();
QString d = lidlMakeProviderDispatch(m);
EXPECT_TRUE(d.contains("::getMethods()"));
EXPECT_TRUE(d.contains("QJsonArray methods"));
}
TEST(LidlGenProvider, DispatchGetMethodsHasAllMethods)
{
auto m = makeTestModule();
QString d = lidlMakeProviderDispatch(m);
EXPECT_TRUE(d.contains("\"greet\""));
EXPECT_TRUE(d.contains("\"isValid\""));
EXPECT_TRUE(d.contains("\"getCount\""));
EXPECT_TRUE(d.contains("\"getNames\""));
EXPECT_TRUE(d.contains("\"doNothing\""));
}
TEST(LidlGenProvider, DispatchGetMethodsHasSignature)
{
auto m = makeTestModule();
QString d = lidlMakeProviderDispatch(m);
EXPECT_TRUE(d.contains("\"signature\""));
EXPECT_TRUE(d.contains("greet(QString)"));
}
TEST(LidlGenProvider, DispatchGetMethodsHasReturnType)
{
auto m = makeTestModule();
QString d = lidlMakeProviderDispatch(m);
EXPECT_TRUE(d.contains("\"returnType\""));
}
TEST(LidlGenProvider, DispatchGetMethodsHasParameters)
{
auto m = makeTestModule();
QString d = lidlMakeProviderDispatch(m);
EXPECT_TRUE(d.contains("\"parameters\""));
}
TEST(LidlGenProvider, DispatchIncludesGlueHeader)
{
auto m = makeTestModule();
QString d = lidlMakeProviderDispatch(m);
EXPECT_TRUE(d.contains("#include \"test_module_qt_glue.h\""));
}
// ---------------------------------------------------------------------------
// Events in provider header
// ---------------------------------------------------------------------------
TEST(LidlGenProvider, EventEmitters)
{
ModuleDecl m;
m.name = "evented";
m.version = "1.0.0";
EventDecl ed;
ed.name = "onUpdate";
ParamDecl p; p.name = "data"; p.type = { TypeExpr::Primitive, "tstr", {} };
ed.params.append(p);
m.events.append(ed);
QString h = lidlMakeProviderHeader(m, "EventedImpl", "evented_impl.h");
// The provider class still gains a protected `emit<EventName>(...)`
// helper per declared event (callable from custom provider code,
// unused by the standard impl flow which goes via the typed
// `logos_events:` path).
EXPECT_TRUE(h.contains("emitOnupdate"));
EXPECT_TRUE(h.contains("emitEvent(\"onUpdate\""));
// Events declared in ModuleDecl.events flow through the
// `_logos_codegen_::maybeSetEmitEvent` SFINAE helper invoked from
// the generated `onInit`. There is no constructor-based wiring — the
// old `m_impl.emitEvent = …` path was removed along with the legacy
// `std::function emitEvent` member it supported.
EXPECT_TRUE(h.contains("_logos_codegen_::maybeSetEmitEvent(m_impl"));
EXPECT_TRUE(h.contains("emitEvent(QString::fromStdString(name)"));
EXPECT_FALSE(h.contains("m_impl.emitEvent ="));
}
TEST(LidlGenProvider, HeaderIncludesNlohmannConversionForJsonMapReturn)
{
ModuleDecl m;
m.name = "jm";
m.version = "1.0.0";
MethodDecl md;
md.name = "getPayload";
md.returnType = { TypeExpr::Map, "", { { TypeExpr::Primitive, "tstr", {} },
{ TypeExpr::Primitive, "any", {} } } };
md.jsonReturn = true;
m.methods.append(md);
QString h = lidlMakeProviderHeader(m, "JmImpl", "jm_impl.h");
EXPECT_TRUE(h.contains("#include <nlohmann/json.hpp>"));
EXPECT_TRUE(h.contains("nlohmannToQVariant"));
EXPECT_TRUE(h.contains("auto _result = m_impl.getPayload("));
EXPECT_TRUE(h.contains("return nlohmannToQVariant(_result).toMap()"));
}
TEST(LidlGenProvider, HeaderUsesToListForJsonListReturn)
{
ModuleDecl m;
m.name = "jl";
m.version = "1.0.0";
MethodDecl md;
md.name = "getItems";
md.returnType = { TypeExpr::Array, "", { { TypeExpr::Primitive, "any", {} } } };
md.jsonReturn = true;
m.methods.append(md);
QString h = lidlMakeProviderHeader(m, "JlImpl", "jl_impl.h");
EXPECT_TRUE(h.contains("return nlohmannToQVariant(_result).toList()"));
}
TEST(LidlGenProvider, NoNlohmannBlockWithoutJsonReturnMethods)
{
ModuleDecl m = makeTestModule();
QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h");
EXPECT_FALSE(h.contains("nlohmann/json.hpp"));
EXPECT_FALSE(h.contains("nlohmannToQVariant"));
}
TEST(LidlGenProvider, HeaderIncludesNlohmannAndStdResultForResultReturn)
{
ModuleDecl m;
m.name = "resmod";
m.version = "1.0.0";
MethodDecl md;
md.name = "getResult";
md.returnType = { TypeExpr::Primitive, "result", {} };
md.resultReturn = true;
m.methods.append(md);
QString h = lidlMakeProviderHeader(m, "ResModImpl", "resmod_impl.h");
EXPECT_TRUE(h.contains("#include <nlohmann/json.hpp>"));
EXPECT_TRUE(h.contains("nlohmannToQVariant"));
EXPECT_TRUE(h.contains("#include \"logos_result.h\""));
EXPECT_TRUE(h.contains("stdResultToQt"));
EXPECT_TRUE(h.contains("auto _result = m_impl.getResult("));
EXPECT_TRUE(h.contains("return stdResultToQt(_result)"));
}
TEST(LidlGenProvider, StdResultMethodHasLogosResultReturnType)
{
ModuleDecl m;
m.name = "resmod2";
m.version = "1.0.0";
MethodDecl md;
md.name = "doWork";
md.returnType = { TypeExpr::Primitive, "result", {} };
md.resultReturn = true;
m.methods.append(md);
QString h = lidlMakeProviderHeader(m, "ResModImpl2", "resmod2_impl.h");
EXPECT_TRUE(h.contains("LogosResult doWork("));
}
// ---------------------------------------------------------------------------
// Empty module
// ---------------------------------------------------------------------------
TEST(LidlGenProvider, EmptyModuleGeneratesValidCode)
{
ModuleDecl m;
m.name = "empty";
m.version = "0.0.1";
QString h = lidlMakeProviderHeader(m, "EmptyImpl", "empty_impl.h");
EXPECT_TRUE(h.contains("EmptyProviderObject"));
EXPECT_TRUE(h.contains("EmptyPlugin"));
QString d = lidlMakeProviderDispatch(m);
EXPECT_TRUE(d.contains("::callMethod("));
EXPECT_TRUE(d.contains("::getMethods()"));
// There is no separate getEvents(): events (when present) ride inside
// getMethods(), so the provider vtable never gains a slot.
EXPECT_FALSE(d.contains("::getEvents()"));
}
// ---------------------------------------------------------------------------
// Event introspection generation — events are folded INTO getMethods() (each
// tagged type "event"), not a separate getEvents() vtable method. This keeps
// LogosProviderObject's vtable layout stable across SDK versions.
// ---------------------------------------------------------------------------
static ModuleDecl makeEventModule()
{
ModuleDecl m;
m.name = "evt_module";
m.version = "1.0.0";
// Documented event with two params (multi-line description).
{
EventDecl ed;
ed.name = "userLoggedIn";
ed.description = "Auth done.\nToken issued.";
ParamDecl p1; p1.name = "userId"; p1.type = { TypeExpr::Primitive, "tstr", {} };
ParamDecl p2; p2.name = "token"; p2.type = { TypeExpr::Primitive, "tstr", {} };
ed.params.append(p1);
ed.params.append(p2);
m.events.append(ed);
}
// Undocumented, no-arg event.
{
EventDecl ed;
ed.name = "tick";
m.events.append(ed);
}
return m;
}
TEST(LidlGenProvider, GetMethodsContainsEventsTaggedEvent)
{
auto m = makeEventModule();
QString d = lidlMakeProviderDispatch(m);
// No separate getEvents() override is generated…
EXPECT_FALSE(d.contains("::getEvents()"));
// …instead events appear inside getMethods(), each tagged type "event".
EXPECT_TRUE(d.contains("::getMethods()"));
EXPECT_TRUE(d.contains("QStringLiteral(\"event\")"));
EXPECT_TRUE(d.contains("\"userLoggedIn\""));
EXPECT_TRUE(d.contains("\"tick\""));
}
TEST(LidlGenProvider, GetMethodsEventHasSignatureAndParams)
{
auto m = makeEventModule();
QString d = lidlMakeProviderDispatch(m);
// Event signature is computed from its params (tstr → QString).
EXPECT_TRUE(d.contains("userLoggedIn(QString,QString)"));
EXPECT_TRUE(d.contains("\"userId\""));
EXPECT_TRUE(d.contains("\"token\""));
}
TEST(LidlGenProvider, GetMethodsEventEmitsDescription)
{
auto m = makeEventModule();
QString d = lidlMakeProviderDispatch(m);
// The multi-line description is emitted with its newline escaped (\n).
EXPECT_TRUE(d.contains("Auth done.\\nToken issued."));
}
TEST(LidlGenProvider, EventEntriesHaveNoReturnType)
{
// An events-only module: events are void, so no returnType/isInvokable key
// is emitted anywhere (those belong to methods only).
auto m = makeEventModule();
QString d = lidlMakeProviderDispatch(m);
EXPECT_FALSE(d.contains("\"returnType\""));
EXPECT_FALSE(d.contains("\"isInvokable\""));
}
TEST(LidlGenProvider, MethodEntriesTaggedMethod)
{
// A module with methods: each getMethods() entry is tagged type "method".
auto m = makeTestModule();
QString d = lidlMakeProviderDispatch(m);
EXPECT_TRUE(d.contains("QStringLiteral(\"method\")"));
}