mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 09:41:06 +00:00
* 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>
176 lines
5.4 KiB
C++
176 lines
5.4 KiB
C++
#include <gtest/gtest.h>
|
|
#include <QtTest/QSignalSpy>
|
|
#include <QJsonObject>
|
|
#include "logos_mock.h"
|
|
#include "logos_api.h"
|
|
#include "logos_provider_object.h"
|
|
#include "module_proxy.h"
|
|
|
|
// Minimal provider for proxy testing
|
|
class ProxyTestProvider : public LogosProviderBase {
|
|
public:
|
|
QString providerName() const override { return "proxy_test"; }
|
|
QString providerVersion() const override { return "1.0.0"; }
|
|
|
|
QVariant callMethod(const QString& methodName, const QVariantList& args) override
|
|
{
|
|
lastMethodCalled = methodName;
|
|
lastArgs = args;
|
|
return returnValue;
|
|
}
|
|
|
|
// getMethods() returns the whole interface: methods AND events, each tagged
|
|
// with a "type". The proxy slices it into getPluginMethods/Events/Interface.
|
|
QJsonArray getMethods() override
|
|
{
|
|
QJsonArray arr;
|
|
{
|
|
QJsonObject m;
|
|
m["type"] = "method";
|
|
m["name"] = "testMethod";
|
|
arr.append(m);
|
|
}
|
|
{
|
|
QJsonObject e;
|
|
e["type"] = "event";
|
|
e["name"] = "testEvent";
|
|
arr.append(e);
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
// Expose protected emitEvent for testing
|
|
void testEmitEvent(const QString& name, const QVariantList& data) { emitEvent(name, data); }
|
|
|
|
QString lastMethodCalled;
|
|
QVariantList lastArgs;
|
|
QVariant returnValue = QVariant(99);
|
|
};
|
|
|
|
class ModuleProxyTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override
|
|
{
|
|
m_mock = new LogosMockSetup();
|
|
m_provider = new ProxyTestProvider();
|
|
}
|
|
void TearDown() override
|
|
{
|
|
delete m_provider;
|
|
delete m_mock;
|
|
}
|
|
LogosMockSetup* m_mock = nullptr;
|
|
ProxyTestProvider* m_provider = nullptr;
|
|
};
|
|
|
|
TEST_F(ModuleProxyTest, CallRemoteMethodDispatchesToProvider)
|
|
{
|
|
ModuleProxy proxy(m_provider);
|
|
QVariant r = proxy.callRemoteMethod("token", "myMethod", {QVariant(1)});
|
|
EXPECT_EQ(r.toInt(), 99);
|
|
EXPECT_EQ(m_provider->lastMethodCalled, "myMethod");
|
|
EXPECT_EQ(m_provider->lastArgs.size(), 1);
|
|
}
|
|
|
|
TEST_F(ModuleProxyTest, GetPluginMethodsDispatchesToProvider)
|
|
{
|
|
ModuleProxy proxy(m_provider);
|
|
// getPluginMethods() returns the method-typed entries only — the event the
|
|
// provider also reports through getMethods() is filtered out.
|
|
QJsonArray methods = proxy.getPluginMethods();
|
|
ASSERT_EQ(methods.size(), 1);
|
|
EXPECT_EQ(methods[0].toObject()["name"].toString(), "testMethod");
|
|
}
|
|
|
|
TEST_F(ModuleProxyTest, GetPluginEventsReturnsOnlyEvents)
|
|
{
|
|
ModuleProxy proxy(m_provider);
|
|
QJsonArray events = proxy.getPluginEvents();
|
|
ASSERT_EQ(events.size(), 1);
|
|
EXPECT_EQ(events[0].toObject()["name"].toString(), "testEvent");
|
|
}
|
|
|
|
TEST_F(ModuleProxyTest, GetPluginInterfaceReturnsMethodsAndEvents)
|
|
{
|
|
ModuleProxy proxy(m_provider);
|
|
// The whole interface — both the method and the event — in one array.
|
|
EXPECT_EQ(proxy.getPluginInterface().size(), 2);
|
|
}
|
|
|
|
TEST_F(ModuleProxyTest, GetPluginMethodsSpecialCaseInCallRemoteMethod)
|
|
{
|
|
ModuleProxy proxy(m_provider);
|
|
QVariant r = proxy.callRemoteMethod("token", "getPluginMethods");
|
|
// Should return the methods array as QVariant, not dispatch to provider's callMethod
|
|
EXPECT_TRUE(r.toJsonArray().size() > 0);
|
|
// Provider's callMethod should NOT have been called for "getPluginMethods"
|
|
EXPECT_TRUE(m_provider->lastMethodCalled.isEmpty());
|
|
}
|
|
|
|
TEST_F(ModuleProxyTest, GetPluginEventsAndInterfaceSpecialCaseInCallRemoteMethod)
|
|
{
|
|
ModuleProxy proxy(m_provider);
|
|
|
|
QVariant ev = proxy.callRemoteMethod("token", "getPluginEvents");
|
|
EXPECT_EQ(ev.toJsonArray().size(), 1);
|
|
|
|
QVariant iface = proxy.callRemoteMethod("token", "getPluginInterface");
|
|
EXPECT_EQ(iface.toJsonArray().size(), 2);
|
|
|
|
// Both are intercepted by the proxy, never dispatched to the provider.
|
|
EXPECT_TRUE(m_provider->lastMethodCalled.isEmpty());
|
|
}
|
|
|
|
TEST_F(ModuleProxyTest, NullProviderHandling)
|
|
{
|
|
ModuleProxy proxy(nullptr);
|
|
QVariant r = proxy.callRemoteMethod("token", "fn");
|
|
EXPECT_FALSE(r.isValid());
|
|
EXPECT_EQ(proxy.getPluginMethods().size(), 0);
|
|
}
|
|
|
|
TEST_F(ModuleProxyTest, EmptyMethodNameHandling)
|
|
{
|
|
ModuleProxy proxy(m_provider);
|
|
QVariant r = proxy.callRemoteMethod("token", "");
|
|
EXPECT_FALSE(r.isValid());
|
|
EXPECT_TRUE(m_provider->lastMethodCalled.isEmpty());
|
|
}
|
|
|
|
TEST_F(ModuleProxyTest, SaveTokenValidation)
|
|
{
|
|
ModuleProxy proxy(m_provider);
|
|
|
|
EXPECT_TRUE(proxy.saveToken("mod", "tok"));
|
|
EXPECT_FALSE(proxy.saveToken("", "tok")); // empty module name
|
|
EXPECT_FALSE(proxy.saveToken("mod", "")); // empty token
|
|
}
|
|
|
|
TEST_F(ModuleProxyTest, EventForwarding)
|
|
{
|
|
ModuleProxy proxy(m_provider);
|
|
QSignalSpy spy(&proxy, &ModuleProxy::eventResponse);
|
|
|
|
// The proxy sets up event listener on construction.
|
|
// When provider emits event, proxy should emit signal.
|
|
m_provider->testEmitEvent("my_event", {QVariant("data")});
|
|
|
|
EXPECT_EQ(spy.count(), 1);
|
|
EXPECT_EQ(spy.at(0).at(0).toString(), "my_event");
|
|
QVariantList data = spy.at(0).at(1).value<QVariantList>();
|
|
ASSERT_EQ(data.size(), 1);
|
|
EXPECT_EQ(data[0].toString(), "data");
|
|
}
|
|
|
|
TEST_F(ModuleProxyTest, InformModuleTokenDelegatesToProvider)
|
|
{
|
|
LogosAPI api("origin");
|
|
m_provider->init(&api);
|
|
ModuleProxy proxy(m_provider);
|
|
|
|
bool result = proxy.informModuleToken("auth", "target_mod", "tok123");
|
|
EXPECT_TRUE(result);
|
|
// Provider's informModuleToken saves via TokenManager
|
|
EXPECT_EQ(TokenManager::instance().getToken("target_mod"), "tok123");
|
|
}
|