// 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 #include "logos_module_context.h" #include #include // 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(), 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(), 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(), std::declval())), void>, "maybeSetLogosModules must resolve to void for impls inheriting LogosModuleContext"); static_assert( std::is_same_v< decltype(_logos_codegen_::maybeSetLogosModules( std::declval(), std::declval())), void>, "maybeSetLogosModules must resolve to void for impls NOT inheriting LogosModuleContext");