// ============================================================================= // 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 #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 #include #include #include #include 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, 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 loadCalls; std::vector> sendTokenCalls; std::vector terminateCalls; int terminateAllCount = 0; // Modules to fail on load std::unordered_set failOn; // Modules currently "running" std::unordered_set activeModules; }; } // anonymous namespace // --------------------------------------------------------------------------- // Test fixture: installs FakeRuntime, cleans up registry after each test. // --------------------------------------------------------------------------- class ModuleRuntimeAbstractionTest : public ::testing::Test { protected: std::shared_ptr fake; void SetUp() override { logos_core_terminate_all(); logos_core_clear(); SubprocessManager::clearAll(); fake = std::make_shared(); 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()); } void registerPlugin(const std::string& name, const std::vector& deps = {}) { std::string path = "/fake/" + name + "_plugin.so"; logos_core_register_plugin(name.c_str(), path.c_str()); std::vector 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(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()); }