mirror of
https://github.com/logos-co/logos-liblogos.git
synced 2026-08-27 12:51:10 +00:00
267 lines
8.8 KiB
C++
267 lines
8.8 KiB
C++
// =============================================================================
|
|
// Tests for the ModuleRuntime abstraction seam.
|
|
//
|
|
// Installs a FakeRuntime into PluginManager's RuntimeRegistry and drives the
|
|
// full PluginManager load/unload/terminateAll path. Proves that:
|
|
// - load(), sendToken(), terminate(), terminateAll() are routed through the
|
|
// runtime abstraction (not directly to a subprocess or Qt mechanism).
|
|
// - 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"
|
|
#include "plugin_manager.h"
|
|
#include "plugin_registry.h"
|
|
#include "runtime_registry.h"
|
|
#include "module_runtime.h"
|
|
#include "qt_subprocess/qt_subprocess_runtime.h"
|
|
#include <string>
|
|
#include <vector>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <memory>
|
|
|
|
using namespace LogosCore;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// FakeRuntime: records all calls; configurable per-module load result.
|
|
// Placed in an anonymous namespace to avoid ODR conflicts with the FakeRuntime
|
|
// stub in test_runtime_registry.cpp (same binary, different definition).
|
|
// ---------------------------------------------------------------------------
|
|
namespace {
|
|
|
|
struct FakeRuntime : public ModuleRuntime {
|
|
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
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test fixture: installs FakeRuntime, cleans up registry after each test.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
class ModuleRuntimeAbstractionTest : public ::testing::Test {
|
|
protected:
|
|
std::shared_ptr<FakeRuntime> fake;
|
|
|
|
void SetUp() override {
|
|
logos_core_terminate_all();
|
|
logos_core_clear();
|
|
SubprocessManager::clearAll();
|
|
|
|
fake = std::make_shared<FakeRuntime>();
|
|
PluginManager::runtimes().clearForTests();
|
|
PluginManager::runtimes().registerRuntime(fake);
|
|
}
|
|
|
|
void TearDown() override {
|
|
logos_core_terminate_all();
|
|
logos_core_clear();
|
|
SubprocessManager::clearAll();
|
|
// Restore default runtime so other test suites aren't affected.
|
|
PluginManager::runtimes().clearForTests();
|
|
PluginManager::runtimes().registerRuntime(
|
|
std::make_shared<LogosCore::QtSubprocessRuntime>());
|
|
}
|
|
|
|
void registerPlugin(const std::string& name,
|
|
const std::vector<std::string>& deps = {}) {
|
|
std::string path = "/fake/" + name + "_plugin.so";
|
|
logos_core_register_plugin(name.c_str(), path.c_str());
|
|
std::vector<const char*> depPtrs;
|
|
for (const auto& d : deps) depPtrs.push_back(d.c_str());
|
|
logos_core_register_plugin_dependencies(
|
|
name.c_str(),
|
|
depPtrs.empty() ? nullptr : depPtrs.data(),
|
|
static_cast<int>(depPtrs.size()));
|
|
}
|
|
};
|
|
|
|
// =============================================================================
|
|
// Basic load/unload routing
|
|
// =============================================================================
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_CallsFakeRuntimeLoad) {
|
|
registerPlugin("foo");
|
|
|
|
int result = logos_core_load_plugin("foo");
|
|
ASSERT_EQ(result, 1);
|
|
|
|
ASSERT_EQ(fake->loadCalls.size(), 1u);
|
|
EXPECT_EQ(fake->loadCalls[0], "foo");
|
|
}
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_CallsSendTokenAfterLoad) {
|
|
registerPlugin("foo");
|
|
|
|
logos_core_load_plugin("foo");
|
|
|
|
ASSERT_EQ(fake->sendTokenCalls.size(), 1u);
|
|
EXPECT_EQ(fake->sendTokenCalls[0].first, "foo");
|
|
EXPECT_FALSE(fake->sendTokenCalls[0].second.empty());
|
|
}
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_MarksModuleAsLoaded) {
|
|
registerPlugin("foo");
|
|
|
|
logos_core_load_plugin("foo");
|
|
|
|
EXPECT_EQ(logos_core_is_plugin_loaded("foo"), 1);
|
|
}
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_StoresRuntimeInRegistry) {
|
|
registerPlugin("foo");
|
|
logos_core_load_plugin("foo");
|
|
|
|
auto rt = PluginManager::registry().runtimeFor("foo");
|
|
EXPECT_EQ(rt.get(), fake.get());
|
|
}
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, UnloadPlugin_CallsFakeRuntimeTerminate) {
|
|
registerPlugin("foo");
|
|
logos_core_load_plugin("foo");
|
|
|
|
int result = logos_core_unload_plugin("foo");
|
|
ASSERT_EQ(result, 1);
|
|
|
|
ASSERT_EQ(fake->terminateCalls.size(), 1u);
|
|
EXPECT_EQ(fake->terminateCalls[0], "foo");
|
|
}
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, UnloadPlugin_MarksModuleAsUnloaded) {
|
|
registerPlugin("foo");
|
|
logos_core_load_plugin("foo");
|
|
logos_core_unload_plugin("foo");
|
|
|
|
EXPECT_EQ(logos_core_is_plugin_loaded("foo"), 0);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Dependency-ordered loads
|
|
// =============================================================================
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, LoadWithDeps_LoadsInTopologicalOrder) {
|
|
// Chain: c depends on b, b depends on a.
|
|
// Expected load order: a, b, c.
|
|
registerPlugin("a");
|
|
registerPlugin("b", {"a"});
|
|
registerPlugin("c", {"b"});
|
|
|
|
int result = logos_core_load_plugin_with_dependencies("c");
|
|
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");
|
|
}
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, LoadWithDeps_SkipsAlreadyLoadedModules) {
|
|
registerPlugin("a");
|
|
registerPlugin("b", {"a"});
|
|
|
|
logos_core_load_plugin("a");
|
|
fake->loadCalls.clear();
|
|
|
|
logos_core_load_plugin_with_dependencies("b");
|
|
|
|
ASSERT_EQ(fake->loadCalls.size(), 1u);
|
|
EXPECT_EQ(fake->loadCalls[0], "b");
|
|
}
|
|
|
|
// =============================================================================
|
|
// terminateAll routing
|
|
// =============================================================================
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, TerminateAll_CallsFakeTerminateAll) {
|
|
registerPlugin("foo");
|
|
logos_core_load_plugin("foo");
|
|
|
|
logos_core_terminate_all();
|
|
|
|
EXPECT_EQ(fake->terminateAllCount, 1);
|
|
EXPECT_EQ(logos_core_is_plugin_loaded("foo"), 0);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Error paths
|
|
// =============================================================================
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_ReturnsFalseWhenRuntimeLoadFails) {
|
|
registerPlugin("bad");
|
|
fake->failOn.insert("bad");
|
|
|
|
int result = logos_core_load_plugin("bad");
|
|
EXPECT_EQ(result, 0);
|
|
}
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_DoesNotCallSendTokenOnLoadFailure) {
|
|
registerPlugin("bad");
|
|
fake->failOn.insert("bad");
|
|
|
|
logos_core_load_plugin("bad");
|
|
|
|
EXPECT_TRUE(fake->sendTokenCalls.empty());
|
|
}
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_DoesNotMarkAsLoadedOnFailure) {
|
|
registerPlugin("bad");
|
|
fake->failOn.insert("bad");
|
|
|
|
logos_core_load_plugin("bad");
|
|
|
|
EXPECT_EQ(logos_core_is_plugin_loaded("bad"), 0);
|
|
}
|
|
|
|
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_ReturnsFalseForUnknownPlugin) {
|
|
int result = logos_core_load_plugin("not_registered");
|
|
EXPECT_EQ(result, 0);
|
|
EXPECT_TRUE(fake->loadCalls.empty());
|
|
}
|