Files
logos-cpp-sdk/tests/sdk/test_logos_module_context.cpp
T
Dario Lipicar 8bdbd13848 Extend universal modules with module context (#61)
* extend universal modules with module context

* implement module calls and events for universal modules

* pr comments
2026-05-19 12:49:15 -03:00

248 lines
9.5 KiB
C++

// Unit tests for LogosModuleContext + the codegen-helper SFINAE overloads
// declared in logos_module_context.h.
//
// These cover the pieces the cpp-generator relies on when emitting the
// `onInit(LogosAPI*) override` in every universal module's provider:
//
// 1. The opt-in base class itself: getters default-empty, the
// framework setter populates them in one shot and fires
// onContextReady() exactly once, the typed `modules()` accessor
// reinterprets the stored void* correctly against a translation-
// unit-local `struct LogosModules` (mirroring how each real
// module's codegen-emitted `logos_sdk.h` defines that struct
// at global scope).
//
// 2. The SFINAE-dispatched helpers (`_logos_codegen_::maybeSetContext`
// and `maybeSetLogosModules`): the inheriting overload writes
// through to the base, the non-inheriting overload silently
// no-ops. The latter is the property that lets the generator
// emit the same `onInit` body for every module — modules that
// don't inherit just get the no-op specialisations and compile
// unchanged.
//
// Where the generator's full output is exercised end-to-end (codegen-
// produced provider + a real impl that reads the context) is the
// logos-test-modules integration suite — these tests stay focused on
// the SDK-side primitives.
#include <gtest/gtest.h>
#include "logos_module_context.h"
#include <string>
#include <type_traits>
// Stand-in for a module-specific `LogosModules` struct. The real one
// is generated per-module in `generated_code/logos_sdk.h` at global
// scope (one accessor per dep). Defined here at global scope so it
// matches the forward declaration in `logos_module_context.h` and
// makes the inline `modules()` accessor compile against this TU's
// translation-unit-local definition.
struct LogosModules {
int sentinel = 42;
};
namespace {
// Impl that opts in to the context. Tracks whether onContextReady fired
// and which values were visible at that point so the test can assert
// "framework set everything BEFORE invoking the hook" (which is what
// the generator's order guarantees).
class ContextInherit : public LogosModuleContext {
public:
int onContextReadyCalls = 0;
std::string seenModulePath;
std::string seenInstanceId;
std::string seenInstancePersistencePath;
protected:
void onContextReady() override {
++onContextReadyCalls;
seenModulePath = modulePath();
seenInstanceId = instanceId();
seenInstancePersistencePath = instancePersistencePath();
}
};
// Impl that intentionally does NOT inherit LogosModuleContext, used
// to verify that the SFINAE helpers compile (and silently no-op)
// against arbitrary impl types. If the helpers ever regress to a
// blind `static_cast` the dispatch generator emits, this test fails
// to compile — exactly the failure mode we want.
class NonInheritingImpl {
public:
int touched = 0;
};
} // namespace
// ── LogosModuleContext ───────────────────────────────────────────────────────
TEST(LogosModuleContextTest, GettersDefaultEmptyBeforeSetup)
{
ContextInherit ctx;
EXPECT_TRUE(ctx.modulePath().empty());
EXPECT_TRUE(ctx.instanceId().empty());
EXPECT_TRUE(ctx.instancePersistencePath().empty());
EXPECT_EQ(ctx.onContextReadyCalls, 0);
}
TEST(LogosModuleContextTest, SetContextPopulatesGettersAndFiresHook)
{
ContextInherit ctx;
ctx._logosCoreSetContext_("/lib/my", "abc123", "/data/my/abc123");
EXPECT_EQ(ctx.modulePath(), "/lib/my");
EXPECT_EQ(ctx.instanceId(), "abc123");
EXPECT_EQ(ctx.instancePersistencePath(), "/data/my/abc123");
EXPECT_EQ(ctx.onContextReadyCalls, 1);
}
TEST(LogosModuleContextTest, OnContextReadySeesPopulatedGetters)
{
// The generator's order guarantees the three fields are written
// BEFORE onContextReady fires, so derived impls can rely on the
// getters returning the freshly-set values from inside the hook.
ContextInherit ctx;
ctx._logosCoreSetContext_("/lib/x", "i-7", "/data/x/i-7");
EXPECT_EQ(ctx.seenModulePath, "/lib/x");
EXPECT_EQ(ctx.seenInstanceId, "i-7");
EXPECT_EQ(ctx.seenInstancePersistencePath, "/data/x/i-7");
}
TEST(LogosModuleContextTest, SetContextCalledTwiceFiresHookTwice)
{
// The base class doesn't gate onContextReady — if the framework
// somehow reinitialises (re-load scenarios), the hook fires
// again. Documenting via a test so the behaviour is intentional.
ContextInherit ctx;
ctx._logosCoreSetContext_("/a", "1", "/d/1");
ctx._logosCoreSetContext_("/b", "2", "/d/2");
EXPECT_EQ(ctx.onContextReadyCalls, 2);
EXPECT_EQ(ctx.modulePath(), "/b");
EXPECT_EQ(ctx.instanceId(), "2");
EXPECT_EQ(ctx.instancePersistencePath(), "/d/2");
}
TEST(LogosModuleContextTest, SetLogosModulesPtrAndTypedAccessor)
{
ContextInherit ctx;
LogosModules mods;
mods.sentinel = 7;
// Type-erased framework write…
ctx._logosCoreSetLogosModulesPtr_(&mods);
// …typed read on the way out via the non-template `modules()`
// accessor. The accessor's inline body static_casts the stored
// void* to this TU's `struct LogosModules` (forward-declared in
// logos_module_context.h, defined at file scope above).
LogosModules& got = ctx.modules();
EXPECT_EQ(&got, &mods);
EXPECT_EQ(got.sentinel, 7);
// Pointer aliasing: mutating through the original object is
// visible through the accessor (we hand out a reference, not a
// copy). Demonstrates the generator's lifetime model — the
// provider owns the LogosModules and the context holds a non-
// owning pointer for the impl's lifetime.
mods.sentinel = 99;
EXPECT_EQ(ctx.modules().sentinel, 99);
}
TEST(LogosModuleContextTest, LogosModulesPointerIndependentOfContext)
{
// The two framework setters touch unrelated state. Setting one
// doesn't clobber the other — important because the generator
// calls them in sequence inside the same onInit override and we
// don't want a future re-ordering or refactor to silently couple
// them.
ContextInherit ctx;
LogosModules mods;
ctx._logosCoreSetLogosModulesPtr_(&mods);
ctx._logosCoreSetContext_("/m", "id", "/p");
EXPECT_EQ(&ctx.modules(), &mods);
EXPECT_EQ(ctx.modulePath(), "/m");
}
// ── Tag-dispatched helpers (_logos_codegen_::maybeSet*) ─────────────────────
TEST(LogosModuleContextHelpersTest, MaybeSetContextWritesForInheritingImpl)
{
ContextInherit impl;
_logos_codegen_::maybeSetContext(impl, "/p", "id", "/per");
EXPECT_EQ(impl.modulePath(), "/p");
EXPECT_EQ(impl.instanceId(), "id");
EXPECT_EQ(impl.instancePersistencePath(), "/per");
EXPECT_EQ(impl.onContextReadyCalls, 1);
}
TEST(LogosModuleContextHelpersTest, MaybeSetContextNoOpForNonInheritingImpl)
{
// The whole point: this call has to COMPILE against a type that
// doesn't inherit LogosModuleContext (because the generator emits
// the same line for every module). A regression to a blind
// static_cast would manifest as a build error here, not a
// runtime failure — that's the property we're locking in.
NonInheritingImpl impl;
_logos_codegen_::maybeSetContext(impl, "/p", "id", "/per");
EXPECT_EQ(impl.touched, 0); // genuinely nothing happened
}
TEST(LogosModuleContextHelpersTest, MaybeSetLogosModulesWritesForInheritingImpl)
{
ContextInherit impl;
LogosModules mods;
_logos_codegen_::maybeSetLogosModules(impl, &mods);
EXPECT_EQ(&impl.modules(), &mods);
}
TEST(LogosModuleContextHelpersTest, MaybeSetLogosModulesNoOpForNonInheritingImpl)
{
NonInheritingImpl impl;
LogosModules mods;
// Same compile-time test as above, separate helper.
_logos_codegen_::maybeSetLogosModules(impl, &mods);
EXPECT_EQ(impl.touched, 0);
}
// ── Compile-time enforcement of the SFINAE split ────────────────────────────
// Both overloads must be discoverable by ADL / unqualified lookup.
// Verify at compile time that the right one is selected for each
// branch, so even a silent code-gen regression that picks the wrong
// overload (e.g. removing the std::enable_if_t guard) gets caught
// before any runtime assertion runs.
static_assert(
std::is_same_v<
decltype(_logos_codegen_::maybeSetContext(
std::declval<ContextInherit&>(),
std::string{}, std::string{}, std::string{})),
void>,
"maybeSetContext must resolve to void for impls inheriting LogosModuleContext");
static_assert(
std::is_same_v<
decltype(_logos_codegen_::maybeSetContext(
std::declval<NonInheritingImpl&>(),
std::string{}, std::string{}, std::string{})),
void>,
"maybeSetContext must resolve to void for impls NOT inheriting LogosModuleContext");
static_assert(
std::is_same_v<
decltype(_logos_codegen_::maybeSetLogosModules(
std::declval<ContextInherit&>(),
std::declval<void*>())),
void>,
"maybeSetLogosModules must resolve to void for impls inheriting LogosModuleContext");
static_assert(
std::is_same_v<
decltype(_logos_codegen_::maybeSetLogosModules(
std::declval<NonInheritingImpl&>(),
std::declval<void*>())),
void>,
"maybeSetLogosModules must resolve to void for impls NOT inheriting LogosModuleContext");