2026-04-21 15:15:46 -04:00
|
|
|
// =============================================================================
|
2026-06-16 17:56:06 -04:00
|
|
|
// Tests for the ModuleLoader abstraction seam.
|
2026-04-21 15:15:46 -04:00
|
|
|
//
|
2026-06-16 17:56:06 -04:00
|
|
|
// Installs a FakeModuleLoader into ModuleManager's ModuleLoaderRegistry and drives the
|
2026-04-23 09:20:45 -04:00
|
|
|
// full ModuleManager load/unload/terminateAll path. Proves that:
|
2026-04-21 15:15:46 -04:00
|
|
|
// - load(), sendToken(), terminate(), terminateAll() are routed through the
|
2026-06-16 17:56:06 -04:00
|
|
|
// loader abstraction (not directly to a subprocess or Qt mechanism).
|
2026-04-21 15:15:46 -04:00
|
|
|
// - Dependency-ordered loads call load() in the correct (topo) order.
|
|
|
|
|
// - Error paths (load returns false) prevent sendToken from being called.
|
|
|
|
|
// No child processes are spawned; no Qt Remote Objects are used.
|
|
|
|
|
// =============================================================================
|
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
#include "logos_core.h"
|
|
|
|
|
#include "qt_test_adapter.h"
|
2026-04-23 09:20:45 -04:00
|
|
|
#include "module_manager.h"
|
|
|
|
|
#include "module_registry.h"
|
2026-06-16 17:56:06 -04:00
|
|
|
#include "module_loader_registry.h"
|
|
|
|
|
#include "module_loader.h"
|
2026-05-08 10:35:05 -04:00
|
|
|
#include "containers/subprocess/subprocess_manager.h"
|
2026-04-21 15:15:46 -04:00
|
|
|
#include <string>
|
|
|
|
|
#include <vector>
|
|
|
|
|
#include <unordered_map>
|
|
|
|
|
#include <unordered_set>
|
|
|
|
|
#include <memory>
|
|
|
|
|
|
|
|
|
|
using namespace LogosCore;
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-06-16 17:56:06 -04:00
|
|
|
// FakeModuleLoader: records all calls; configurable per-module load result.
|
|
|
|
|
// Placed in an anonymous namespace to avoid ODR conflicts with the FakeModuleLoader
|
|
|
|
|
// stub in test_module_loader_registry.cpp (same binary, different definition).
|
2026-04-21 15:15:46 -04:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
namespace {
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
struct FakeModuleLoader : public ModuleLoader {
|
2026-04-21 15:15:46 -04:00
|
|
|
std::string id() const override { return "fake"; }
|
|
|
|
|
|
|
|
|
|
bool canHandle(const ModuleDescriptor&) const override { return true; }
|
|
|
|
|
|
|
|
|
|
bool load(const ModuleDescriptor& desc,
|
|
|
|
|
std::function<void(const std::string&)>,
|
|
|
|
|
LoadedModuleHandle& out) override {
|
|
|
|
|
loadCalls.push_back(desc.name);
|
|
|
|
|
if (failOn.count(desc.name)) return false;
|
|
|
|
|
out.name = desc.name;
|
|
|
|
|
out.pid = 1234;
|
|
|
|
|
out.endpoint = "fake://" + desc.name;
|
|
|
|
|
activeModules.insert(desc.name);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool sendToken(const std::string& name, const std::string& token) override {
|
|
|
|
|
sendTokenCalls.push_back({name, token});
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void terminate(const std::string& name) override {
|
|
|
|
|
terminateCalls.push_back(name);
|
|
|
|
|
activeModules.erase(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void terminateAll() override {
|
|
|
|
|
terminateAllCount++;
|
|
|
|
|
activeModules.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool hasModule(const std::string& name) const override {
|
|
|
|
|
return activeModules.count(name) > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Call records
|
|
|
|
|
std::vector<std::string> loadCalls;
|
|
|
|
|
std::vector<std::pair<std::string,std::string>> sendTokenCalls;
|
|
|
|
|
std::vector<std::string> terminateCalls;
|
|
|
|
|
int terminateAllCount = 0;
|
|
|
|
|
|
|
|
|
|
// Modules to fail on load
|
|
|
|
|
std::unordered_set<std::string> failOn;
|
|
|
|
|
// Modules currently "running"
|
|
|
|
|
std::unordered_set<std::string> activeModules;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} // anonymous namespace
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-06-16 17:56:06 -04:00
|
|
|
// Test fixture: installs FakeModuleLoader, cleans up registry after each test.
|
2026-04-21 15:15:46 -04:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
class ModuleLoaderAbstractionTest : public ::testing::Test {
|
2026-04-21 15:15:46 -04:00
|
|
|
protected:
|
2026-06-16 17:56:06 -04:00
|
|
|
std::shared_ptr<FakeModuleLoader> fake;
|
2026-04-21 15:15:46 -04:00
|
|
|
|
|
|
|
|
void SetUp() override {
|
|
|
|
|
logos_core_terminate_all();
|
|
|
|
|
logos_core_clear();
|
|
|
|
|
SubprocessManager::clearAll();
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
fake = std::make_shared<FakeModuleLoader>();
|
|
|
|
|
ModuleManager::loaders().clearForTests();
|
|
|
|
|
ModuleManager::loaders().registerLoader(fake);
|
2026-04-21 15:15:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TearDown() override {
|
|
|
|
|
logos_core_terminate_all();
|
|
|
|
|
logos_core_clear();
|
|
|
|
|
SubprocessManager::clearAll();
|
2026-06-16 17:56:06 -04:00
|
|
|
// Restore default loader so other test suites aren't affected.
|
|
|
|
|
ModuleManager::loaders().clearForTests();
|
|
|
|
|
ModuleManager::loaders().registerLoader(
|
2026-04-21 15:15:46 -04:00
|
|
|
std::make_shared<SubprocessManager>());
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 09:20:45 -04:00
|
|
|
void registerModule(const std::string& name,
|
2026-04-21 15:15:46 -04:00
|
|
|
const std::vector<std::string>& deps = {}) {
|
|
|
|
|
std::string path = "/fake/" + name + "_plugin.so";
|
2026-04-23 09:20:45 -04:00
|
|
|
logos_core_register_module(name.c_str(), path.c_str());
|
2026-04-21 15:15:46 -04:00
|
|
|
std::vector<const char*> depPtrs;
|
|
|
|
|
for (const auto& d : deps) depPtrs.push_back(d.c_str());
|
2026-04-23 09:20:45 -04:00
|
|
|
logos_core_register_module_dependencies(
|
2026-04-21 15:15:46 -04:00
|
|
|
name.c_str(),
|
|
|
|
|
depPtrs.empty() ? nullptr : depPtrs.data(),
|
|
|
|
|
static_cast<int>(depPtrs.size()));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// =============================================================================
|
|
|
|
|
// Basic load/unload routing
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, LoadModule_CallsFakeModuleLoaderLoad) {
|
2026-04-23 09:20:45 -04:00
|
|
|
registerModule("foo");
|
2026-04-21 15:15:46 -04:00
|
|
|
|
2026-05-19 16:20:32 -04:00
|
|
|
int result = logos_core_load_module("foo", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
ASSERT_EQ(result, 1);
|
|
|
|
|
|
|
|
|
|
ASSERT_EQ(fake->loadCalls.size(), 1u);
|
|
|
|
|
EXPECT_EQ(fake->loadCalls[0], "foo");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, LoadModule_CallsSendTokenAfterLoad) {
|
2026-04-23 09:20:45 -04:00
|
|
|
registerModule("foo");
|
2026-04-21 15:15:46 -04:00
|
|
|
|
2026-05-19 16:20:32 -04:00
|
|
|
logos_core_load_module("foo", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
|
|
|
|
|
ASSERT_EQ(fake->sendTokenCalls.size(), 1u);
|
|
|
|
|
EXPECT_EQ(fake->sendTokenCalls[0].first, "foo");
|
|
|
|
|
EXPECT_FALSE(fake->sendTokenCalls[0].second.empty());
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, LoadModule_MarksModuleAsLoaded) {
|
2026-04-23 09:20:45 -04:00
|
|
|
registerModule("foo");
|
2026-04-21 15:15:46 -04:00
|
|
|
|
2026-05-19 16:20:32 -04:00
|
|
|
logos_core_load_module("foo", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
|
2026-04-23 09:20:45 -04:00
|
|
|
EXPECT_EQ(logos_core_is_module_loaded("foo"), 1);
|
2026-04-21 15:15:46 -04:00
|
|
|
}
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, LoadModule_StoresLoaderInRegistry) {
|
2026-04-23 09:20:45 -04:00
|
|
|
registerModule("foo");
|
2026-05-19 16:20:32 -04:00
|
|
|
logos_core_load_module("foo", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
auto rt = ModuleManager::registry().loaderFor("foo");
|
2026-04-21 15:15:46 -04:00
|
|
|
EXPECT_EQ(rt.get(), fake.get());
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, UnloadModule_CallsFakeModuleLoaderTerminate) {
|
2026-04-23 09:20:45 -04:00
|
|
|
registerModule("foo");
|
2026-05-19 16:20:32 -04:00
|
|
|
logos_core_load_module("foo", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
|
2026-05-19 16:20:32 -04:00
|
|
|
int result = logos_core_unload_module("foo", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
ASSERT_EQ(result, 1);
|
|
|
|
|
|
|
|
|
|
ASSERT_EQ(fake->terminateCalls.size(), 1u);
|
|
|
|
|
EXPECT_EQ(fake->terminateCalls[0], "foo");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, UnloadModule_MarksModuleAsUnloaded) {
|
2026-04-23 09:20:45 -04:00
|
|
|
registerModule("foo");
|
2026-05-19 16:20:32 -04:00
|
|
|
logos_core_load_module("foo", false);
|
|
|
|
|
logos_core_unload_module("foo", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
|
2026-04-23 09:20:45 -04:00
|
|
|
EXPECT_EQ(logos_core_is_module_loaded("foo"), 0);
|
2026-04-21 15:15:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =============================================================================
|
|
|
|
|
// Dependency-ordered loads
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, LoadWithDeps_LoadsInTopologicalOrder) {
|
2026-04-21 15:15:46 -04:00
|
|
|
// Chain: c depends on b, b depends on a.
|
|
|
|
|
// Expected load order: a, b, c.
|
2026-04-23 09:20:45 -04:00
|
|
|
registerModule("a");
|
|
|
|
|
registerModule("b", {"a"});
|
|
|
|
|
registerModule("c", {"b"});
|
2026-04-21 15:15:46 -04:00
|
|
|
|
2026-05-19 16:20:32 -04:00
|
|
|
int result = logos_core_load_module("c", true);
|
2026-04-21 15:15:46 -04:00
|
|
|
ASSERT_EQ(result, 1);
|
|
|
|
|
|
|
|
|
|
ASSERT_EQ(fake->loadCalls.size(), 3u);
|
|
|
|
|
EXPECT_EQ(fake->loadCalls[0], "a");
|
|
|
|
|
EXPECT_EQ(fake->loadCalls[1], "b");
|
|
|
|
|
EXPECT_EQ(fake->loadCalls[2], "c");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, LoadWithDeps_SkipsAlreadyLoadedModules) {
|
2026-04-23 09:20:45 -04:00
|
|
|
registerModule("a");
|
|
|
|
|
registerModule("b", {"a"});
|
2026-04-21 15:15:46 -04:00
|
|
|
|
2026-05-19 16:20:32 -04:00
|
|
|
logos_core_load_module("a", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
fake->loadCalls.clear();
|
|
|
|
|
|
2026-05-19 16:20:32 -04:00
|
|
|
logos_core_load_module("b", true);
|
2026-04-21 15:15:46 -04:00
|
|
|
|
|
|
|
|
ASSERT_EQ(fake->loadCalls.size(), 1u);
|
|
|
|
|
EXPECT_EQ(fake->loadCalls[0], "b");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 22:22:12 -03:00
|
|
|
// LoadsInTopologicalOrder (above) pins the *call sequence*. This one pins the
|
|
|
|
|
// *observable end state*: requesting a single top-level module with
|
|
|
|
|
// with_dependencies=true must leave that module's entire transitive
|
|
|
|
|
// dependency closure loaded, as reported by the public query API. This is the
|
|
|
|
|
// guarantee callers actually rely on ("load app, get everything it needs"),
|
|
|
|
|
// and it exercises a diamond (app → ui, core; ui → core) so a dependency
|
|
|
|
|
// reachable by two paths is still loaded exactly once and not skipped.
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, LoadWithDeps_LeavesTransitiveClosureLoaded) {
|
2026-06-09 22:22:12 -03:00
|
|
|
registerModule("core");
|
|
|
|
|
registerModule("ui", {"core"});
|
|
|
|
|
registerModule("app", {"ui", "core"});
|
|
|
|
|
|
|
|
|
|
// Precondition: nothing in the closure is loaded yet.
|
|
|
|
|
ASSERT_EQ(logos_core_is_module_loaded("app"), 0);
|
|
|
|
|
ASSERT_EQ(logos_core_is_module_loaded("ui"), 0);
|
|
|
|
|
ASSERT_EQ(logos_core_is_module_loaded("core"), 0);
|
|
|
|
|
|
|
|
|
|
// Only the top module is requested.
|
|
|
|
|
int result = logos_core_load_module("app", true);
|
|
|
|
|
ASSERT_EQ(result, 1);
|
|
|
|
|
|
|
|
|
|
// The whole closure ends up loaded — the deps were auto-resolved.
|
|
|
|
|
EXPECT_EQ(logos_core_is_module_loaded("app"), 1);
|
|
|
|
|
EXPECT_EQ(logos_core_is_module_loaded("ui"), 1);
|
|
|
|
|
EXPECT_EQ(logos_core_is_module_loaded("core"), 1);
|
|
|
|
|
|
|
|
|
|
// The diamond's shared dependency loads exactly once despite two paths.
|
|
|
|
|
int coreLoads = 0;
|
|
|
|
|
for (const auto& n : fake->loadCalls)
|
|
|
|
|
if (n == "core") ++coreLoads;
|
|
|
|
|
EXPECT_EQ(coreLoads, 1);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 15:15:46 -04:00
|
|
|
// =============================================================================
|
|
|
|
|
// terminateAll routing
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, TerminateAll_CallsFakeTerminateAll) {
|
2026-04-23 09:20:45 -04:00
|
|
|
registerModule("foo");
|
2026-05-19 16:20:32 -04:00
|
|
|
logos_core_load_module("foo", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
|
|
|
|
|
logos_core_terminate_all();
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(fake->terminateAllCount, 1);
|
2026-04-23 09:20:45 -04:00
|
|
|
EXPECT_EQ(logos_core_is_module_loaded("foo"), 0);
|
2026-04-21 15:15:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =============================================================================
|
|
|
|
|
// Error paths
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, LoadModule_ReturnsFalseWhenLoaderLoadFails) {
|
2026-04-23 09:20:45 -04:00
|
|
|
registerModule("bad");
|
2026-04-21 15:15:46 -04:00
|
|
|
fake->failOn.insert("bad");
|
|
|
|
|
|
2026-05-19 16:20:32 -04:00
|
|
|
int result = logos_core_load_module("bad", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
EXPECT_EQ(result, 0);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, LoadModule_DoesNotCallSendTokenOnLoadFailure) {
|
2026-04-23 09:20:45 -04:00
|
|
|
registerModule("bad");
|
2026-04-21 15:15:46 -04:00
|
|
|
fake->failOn.insert("bad");
|
|
|
|
|
|
2026-05-19 16:20:32 -04:00
|
|
|
logos_core_load_module("bad", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
|
|
|
|
|
EXPECT_TRUE(fake->sendTokenCalls.empty());
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, LoadModule_DoesNotMarkAsLoadedOnFailure) {
|
2026-04-23 09:20:45 -04:00
|
|
|
registerModule("bad");
|
2026-04-21 15:15:46 -04:00
|
|
|
fake->failOn.insert("bad");
|
|
|
|
|
|
2026-05-19 16:20:32 -04:00
|
|
|
logos_core_load_module("bad", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
|
2026-04-23 09:20:45 -04:00
|
|
|
EXPECT_EQ(logos_core_is_module_loaded("bad"), 0);
|
2026-04-21 15:15:46 -04:00
|
|
|
}
|
|
|
|
|
|
2026-06-16 17:56:06 -04:00
|
|
|
TEST_F(ModuleLoaderAbstractionTest, LoadModule_ReturnsFalseForUnknownModule) {
|
2026-05-19 16:20:32 -04:00
|
|
|
int result = logos_core_load_module("not_registered", false);
|
2026-04-21 15:15:46 -04:00
|
|
|
EXPECT_EQ(result, 0);
|
|
|
|
|
EXPECT_TRUE(fake->loadCalls.empty());
|
|
|
|
|
}
|