diff --git a/CMakeLists.txt b/CMakeLists.txt index c206fd2..4f9f4a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ else() message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.") endif() -# Define the module (uses LogosProviderBase / LOGOS_METHOD API) +# LogosProviderBase / LOGOS_METHOD — dispatch is generated under generated_code/ logos_module( NAME package_manager SOURCES @@ -19,11 +19,3 @@ logos_module( package_manager_lib lgx ) - -# Provider dispatch file (generated by logos-cpp-generator --provider-header -# during preConfigure). Added separately without GENERATED property to avoid -# an AUTOMOC dependency cycle with ninja. -set(_DISPATCH "${CMAKE_CURRENT_SOURCE_DIR}/generated_code/logos_provider_dispatch.cpp") -if(EXISTS "${_DISPATCH}") - target_sources(package_manager_module_plugin PRIVATE "${_DISPATCH}") -endif() diff --git a/flake.lock b/flake.lock index 1115611..1fcabce 100644 --- a/flake.lock +++ b/flake.lock @@ -256,11 +256,11 @@ ] }, "locked": { - "lastModified": 1775676249, - "narHash": "sha256-ipOQ5e9Xs4JGROrt1dH5DteXPbU91IpBipRppxG5dsE=", + "lastModified": 1775684846, + "narHash": "sha256-Dz2EnyIe5STjXevvmhTJz29VR5cGanwQwjHvvgi9iJg=", "owner": "logos-co", "repo": "logos-module-builder", - "rev": "cea2766106dbe1b25b25b5ba16f4f93ef1107aa0", + "rev": "4f53bc34b884c5f1fc23021a7ffe4c381574994d", "type": "github" }, "original": { @@ -1539,11 +1539,11 @@ ] }, "locked": { - "lastModified": 1775163659, - "narHash": "sha256-6gk164iURa+emkY30WA9sIUCEA9ByavuBEveC5sfa08=", + "lastModified": 1775684803, + "narHash": "sha256-BnnrAjYJHW994WYAhd6e6/T7igLqJm4utjhqx1a6kLw=", "owner": "logos-co", "repo": "logos-test-framework", - "rev": "a1bcd3608847535975ba4a2da473c03257a82412", + "rev": "55b15075b5990b3c043030a3e404c7f11d57c32b", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 315ad11..5fc7e4a 100644 --- a/flake.nix +++ b/flake.nix @@ -11,9 +11,6 @@ src = ./.; configFile = ./metadata.json; flakeInputs = inputs; - # logos-package-manager is a pre-built flake providing - # libpackage_manager_lib and liblgx. Use structured format to map - # variants: #lib links dev libs, #lib-portable links portable libs. externalLibInputs = { logos_pm = { input = inputs.logos-package-manager; @@ -23,17 +20,10 @@ }; }; }; - # preConfigure as a function receives { externalLibs } with resolved - # store paths — no env vars or store globbing needed. - preConfigure = { externalLibs }: let pm = externalLibs.logos_pm; in '' - mkdir -p lib - cp ${pm}/lib/libpackage_manager_lib.* lib/ 2>/dev/null || true - cp ${pm}/lib/liblgx.* lib/ 2>/dev/null || true - cp ${pm}/include/*.h lib/ 2>/dev/null || true - - echo "Generating provider dispatch for package_manager..." - logos-cpp-generator --provider-header "$(pwd)/src/package_manager_impl.h" \ - --output-dir ./generated_code - ''; + tests = { + dir = ./tests; + # Same key as nix.external_libraries[].name — documents intent; go_static filtering is N/A here. + mockCLibs = [ "logos_pm" ]; + }; }; } diff --git a/metadata.json b/metadata.json index 2e09268..f5a50bf 100644 --- a/metadata.json +++ b/metadata.json @@ -4,6 +4,7 @@ "description": "Plugin manager for the Logos system", "author": "Logos Core Team", "type": "core", + "interface": "provider", "category": "management", "main": "package_manager_plugin", "dependencies": [], diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b40de75..5f5f69c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,27 +3,81 @@ project(PackageManagerModuleTests LANGUAGES CXX) include(LogosTest) -# Find the external libraries (copied to lib/ by preConfigure) -find_library(PM_LIB package_manager_lib PATHS "${CMAKE_CURRENT_SOURCE_DIR}/../lib" NO_DEFAULT_PATH REQUIRED) -find_library(LGX_LIB lgx PATHS "${CMAKE_CURRENT_SOURCE_DIR}/../lib" NO_DEFAULT_PATH REQUIRED) - +# Unit tests (mocked PackageManagerLib) logos_test( NAME package_manager_module_tests MODULE_SOURCES ../src/package_manager_impl.cpp TEST_SOURCES main.cpp - test_directory_config.cpp - test_scanning.cpp - test_signature.cpp - test_keyring.cpp + test_package_manager.cpp + MOCK_C_SOURCES + mocks/mock_package_manager_lib.cpp EXTRA_INCLUDES - ../lib + stubs GENERATED_SOURCES ../generated_code/logos_provider_dispatch.cpp GENERATED_DIR ../generated_code - EXTRA_LINK_LIBS - ${PM_LIB} - ${LGX_LIB} ) + +# Integration tests (real PackageManagerLib + lgx) +find_library(LIBPM_PATH + NAMES libpackage_manager_lib.so libpackage_manager_lib.dylib + package_manager_lib + PATHS ${CMAKE_CURRENT_SOURCE_DIR}/../lib + NO_DEFAULT_PATH) + +find_library(LIBLGX_PATH + NAMES liblgx.so liblgx.dylib lgx + PATHS ${CMAKE_CURRENT_SOURCE_DIR}/../lib + NO_DEFAULT_PATH) + +if(LIBPM_PATH) + set(_pm_integration_libs "${LIBPM_PATH}") + if(LIBLGX_PATH) + list(APPEND _pm_integration_libs "${LIBLGX_PATH}") + endif() + + message(STATUS "[PackageManagerTests] package manager lib found: ${LIBPM_PATH} — building integration tests") + if(LIBLGX_PATH) + message(STATUS "[PackageManagerTests] lgx found: ${LIBLGX_PATH}") + else() + message(STATUS "[PackageManagerTests] lgx not found in ../lib — linking package_manager_lib only") + endif() + + logos_test( + NAME package_manager_module_integration_tests + MODULE_SOURCES + ../src/package_manager_impl.cpp + TEST_SOURCES + main.cpp + test_package_manager_integration.cpp + test_directory_config.cpp + test_scanning.cpp + test_signature.cpp + test_keyring.cpp + EXTRA_INCLUDES + ../lib + EXTRA_LINK_LIBS + ${_pm_integration_libs} + GENERATED_SOURCES + ../generated_code/logos_provider_dispatch.cpp + GENERATED_DIR + ../generated_code + ) + + set(_rpath_dirs "") + get_filename_component(_pm_dir "${LIBPM_PATH}" DIRECTORY) + list(APPEND _rpath_dirs "${_pm_dir}") + if(LIBLGX_PATH) + get_filename_component(_lgx_dir "${LIBLGX_PATH}" DIRECTORY) + list(APPEND _rpath_dirs "${_lgx_dir}") + endif() + list(REMOVE_DUPLICATES _rpath_dirs) + string(REPLACE ";" ":" _rpath_joined "${_rpath_dirs}") + set_target_properties(package_manager_module_integration_tests PROPERTIES + BUILD_RPATH "${_rpath_joined}") +else() + message(STATUS "[PackageManagerTests] package_manager_lib not found in ../lib — skipping integration tests") +endif() diff --git a/tests/mocks/mock_package_manager_lib.cpp b/tests/mocks/mock_package_manager_lib.cpp new file mode 100644 index 0000000..a733007 --- /dev/null +++ b/tests/mocks/mock_package_manager_lib.cpp @@ -0,0 +1,97 @@ +// Mock PackageManagerLib for package_manager_module unit tests (link-time substitution). + +#include +#include + +static std::string mockDupCStr(const char* key, const char* fallback) { + const char* ret = LOGOS_CMOCK_RETURN_STRING(key); + if (ret && ret[0]) + return std::string(ret); + return fallback ? std::string(fallback) : std::string(); +} + +PackageManagerLib::PackageManagerLib() { + LOGOS_CMOCK_RECORD("PackageManagerLib_ctor"); +} + +PackageManagerLib::~PackageManagerLib() { + LOGOS_CMOCK_RECORD("PackageManagerLib_dtor"); +} + +void PackageManagerLib::setEmbeddedModulesDirectory(const std::string& dir) { + LOGOS_CMOCK_RECORD("setEmbeddedModulesDirectory"); + (void)dir; +} + +void PackageManagerLib::addEmbeddedModulesDirectory(const std::string& dir) { + LOGOS_CMOCK_RECORD("addEmbeddedModulesDirectory"); + (void)dir; +} + +void PackageManagerLib::setEmbeddedUiPluginsDirectory(const std::string& dir) { + LOGOS_CMOCK_RECORD("setEmbeddedUiPluginsDirectory"); + (void)dir; +} + +void PackageManagerLib::addEmbeddedUiPluginsDirectory(const std::string& dir) { + LOGOS_CMOCK_RECORD("addEmbeddedUiPluginsDirectory"); + (void)dir; +} + +void PackageManagerLib::setUserModulesDirectory(const std::string& dir) { + LOGOS_CMOCK_RECORD("setUserModulesDirectory"); + (void)dir; +} + +void PackageManagerLib::setUserUiPluginsDirectory(const std::string& dir) { + LOGOS_CMOCK_RECORD("setUserUiPluginsDirectory"); + (void)dir; +} + +std::string PackageManagerLib::installPluginFile(const std::string& pluginPath, std::string& errorMsg, + bool skipIfNotNewerVersion, + std::string* installedPluginPath, + bool* isCoreModule) { + LOGOS_CMOCK_RECORD("installPluginFile"); + if (skipIfNotNewerVersion) { + LOGOS_CMOCK_RECORD("installPluginFile_skipIfNotNewer_true"); + } else { + LOGOS_CMOCK_RECORD("installPluginFile_skipIfNotNewer_false"); + } + (void)pluginPath; + + errorMsg = mockDupCStr("installPluginFile_error", ""); + + if (installedPluginPath) { + *installedPluginPath = mockDupCStr("installPluginFile_installedPath", ""); + } + if (isCoreModule) { + *isCoreModule = LOGOS_CMOCK_RETURN(bool, "installPluginFile_isCore"); + } + + return mockDupCStr("installPluginFile_result", ""); +} + +std::string PackageManagerLib::getInstalledPackages() { + LOGOS_CMOCK_RECORD("getInstalledPackages"); + return mockDupCStr("getInstalledPackages", "[]"); +} + +std::string PackageManagerLib::getInstalledModules() { + LOGOS_CMOCK_RECORD("getInstalledModules"); + return mockDupCStr("getInstalledModules", "[]"); +} + +std::string PackageManagerLib::getInstalledUiPlugins() { + LOGOS_CMOCK_RECORD("getInstalledUiPlugins"); + return mockDupCStr("getInstalledUiPlugins", "[]"); +} + +std::vector PackageManagerLib::platformVariantsToTry() { + LOGOS_CMOCK_RECORD("platformVariantsToTry"); + const char* v = LOGOS_CMOCK_RETURN_STRING("platformVariantsToTry_first"); + if (v && v[0]) { + return {std::string(v)}; + } + return {"mock-variant"}; +} diff --git a/tests/stubs/package_manager_lib.h b/tests/stubs/package_manager_lib.h new file mode 100644 index 0000000..8fab4d1 --- /dev/null +++ b/tests/stubs/package_manager_lib.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +// Stub header for unit tests — matches the subset of PackageManagerLib used by +// PackageManagerImpl. The real header is installed under lib/ from logos-package-manager. + +class PackageManagerLib { +public: + PackageManagerLib(); + ~PackageManagerLib(); + + void setEmbeddedModulesDirectory(const std::string& dir); + void addEmbeddedModulesDirectory(const std::string& dir); + void setEmbeddedUiPluginsDirectory(const std::string& dir); + void addEmbeddedUiPluginsDirectory(const std::string& dir); + void setUserModulesDirectory(const std::string& dir); + void setUserUiPluginsDirectory(const std::string& dir); + + std::string installPluginFile(const std::string& pluginPath, std::string& errorMsg, + bool skipIfNotNewerVersion = false, + std::string* installedPluginPath = nullptr, + bool* isCoreModule = nullptr); + + std::string getInstalledPackages(); + std::string getInstalledModules(); + std::string getInstalledUiPlugins(); + + static std::vector platformVariantsToTry(); +}; diff --git a/tests/test_package_manager.cpp b/tests/test_package_manager.cpp new file mode 100644 index 0000000..9994037 --- /dev/null +++ b/tests/test_package_manager.cpp @@ -0,0 +1,211 @@ +// Unit tests for PackageManagerImpl — PackageManagerLib is mocked (mock_package_manager_lib.cpp). + +#include +#include "package_manager_impl.h" + +#include + +LOGOS_TEST(onInit_does_not_throw) { + auto t = LogosTestContext("package_manager"); + PackageManagerImpl impl; + t.init(&impl); + LOGOS_ASSERT_FALSE(t.moduleCalled("any_module", "any_method")); +} + +LOGOS_TEST(installPlugin_success_core_emits_core_event) { + auto t = LogosTestContext("package_manager"); + t.captureEvents(); + t.mockCFunction("installPluginFile_result").returns("/installed/core.dylib"); + t.mockCFunction("installPluginFile_installedPath").returns("/installed/core.dylib"); + t.mockCFunction("installPluginFile_error").returns(""); + t.mockCFunction("installPluginFile_isCore").returns(true); + + PackageManagerImpl impl; + t.init(&impl); + + QVariantMap m = impl.installPlugin(QStringLiteral("/path/to/foo.lgx"), false); + LOGOS_ASSERT_EQ(m[QStringLiteral("path")].toString(), QStringLiteral("/installed/core.dylib")); + LOGOS_ASSERT_TRUE(m[QStringLiteral("isCoreModule")].toBool()); + LOGOS_ASSERT_FALSE(m.contains(QStringLiteral("error"))); + LOGOS_ASSERT_EQ(m[QStringLiteral("name")].toString(), QStringLiteral("foo")); + LOGOS_ASSERT_TRUE(t.eventEmitted("corePluginFileInstalled")); + LOGOS_ASSERT_FALSE(t.eventEmitted("uiPluginFileInstalled")); + LOGOS_ASSERT_TRUE(t.cFunctionCalled("installPluginFile")); +} + +LOGOS_TEST(installPlugin_success_ui_emits_ui_event) { + auto t = LogosTestContext("package_manager"); + t.captureEvents(); + t.mockCFunction("installPluginFile_result").returns("/ui/plugin.qml"); + t.mockCFunction("installPluginFile_installedPath").returns("/ui/plugin.qml"); + t.mockCFunction("installPluginFile_error").returns(""); + t.mockCFunction("installPluginFile_isCore").returns(false); + + PackageManagerImpl impl; + t.init(&impl); + + QVariantMap m = impl.installPlugin(QStringLiteral("/path/bar.lgx"), false); + LOGOS_ASSERT_FALSE(m[QStringLiteral("isCoreModule")].toBool()); + LOGOS_ASSERT_TRUE(t.eventEmitted("uiPluginFileInstalled")); + LOGOS_ASSERT_FALSE(t.eventEmitted("corePluginFileInstalled")); +} + +LOGOS_TEST(installPlugin_failure_sets_error_no_event) { + auto t = LogosTestContext("package_manager"); + t.captureEvents(); + t.mockCFunction("installPluginFile_result").returns(""); + t.mockCFunction("installPluginFile_error").returns("invalid lgx"); + t.mockCFunction("installPluginFile_installedPath").returns(""); + + PackageManagerImpl impl; + t.init(&impl); + + QVariantMap m = impl.installPlugin(QStringLiteral("/bad.lgx"), false); + LOGOS_ASSERT_TRUE(m[QStringLiteral("path")].toString().isEmpty()); + LOGOS_ASSERT_EQ(m[QStringLiteral("error")].toString(), QStringLiteral("invalid lgx")); + LOGOS_ASSERT_FALSE(t.eventEmitted("corePluginFileInstalled")); + LOGOS_ASSERT_FALSE(t.eventEmitted("uiPluginFileInstalled")); +} + +LOGOS_TEST(installPlugin_skipIfNotNewerVersion_passed_to_mock) { + auto t = LogosTestContext("package_manager"); + t.mockCFunction("installPluginFile_result").returns("/ok"); + t.mockCFunction("installPluginFile_installedPath").returns("/ok"); + t.mockCFunction("installPluginFile_error").returns(""); + + PackageManagerImpl impl; + t.init(&impl); + + impl.installPlugin(QStringLiteral("/x.lgx"), true); + LOGOS_ASSERT_TRUE(t.cFunctionCalled("installPluginFile_skipIfNotNewer_true")); + + impl.installPlugin(QStringLiteral("/y.lgx"), false); + LOGOS_ASSERT_TRUE(t.cFunctionCalled("installPluginFile_skipIfNotNewer_false")); +} + +LOGOS_TEST(setEmbeddedModulesDirectory_forwards_to_lib) { + auto t = LogosTestContext("package_manager"); + PackageManagerImpl impl; + t.init(&impl); + impl.setEmbeddedModulesDirectory(QStringLiteral("/emb/mod")); + LOGOS_ASSERT_TRUE(t.cFunctionCalled("setEmbeddedModulesDirectory")); +} + +LOGOS_TEST(addEmbeddedModulesDirectory_forwards_to_lib) { + auto t = LogosTestContext("package_manager"); + PackageManagerImpl impl; + t.init(&impl); + impl.addEmbeddedModulesDirectory(QStringLiteral("/emb/m2")); + LOGOS_ASSERT_TRUE(t.cFunctionCalled("addEmbeddedModulesDirectory")); +} + +LOGOS_TEST(setEmbeddedUiPluginsDirectory_forwards_to_lib) { + auto t = LogosTestContext("package_manager"); + PackageManagerImpl impl; + t.init(&impl); + impl.setEmbeddedUiPluginsDirectory(QStringLiteral("/emb/ui")); + LOGOS_ASSERT_TRUE(t.cFunctionCalled("setEmbeddedUiPluginsDirectory")); +} + +LOGOS_TEST(addEmbeddedUiPluginsDirectory_forwards_to_lib) { + auto t = LogosTestContext("package_manager"); + PackageManagerImpl impl; + t.init(&impl); + impl.addEmbeddedUiPluginsDirectory(QStringLiteral("/emb/ui2")); + LOGOS_ASSERT_TRUE(t.cFunctionCalled("addEmbeddedUiPluginsDirectory")); +} + +LOGOS_TEST(setUserModulesDirectory_forwards_to_lib) { + auto t = LogosTestContext("package_manager"); + PackageManagerImpl impl; + t.init(&impl); + impl.setUserModulesDirectory(QStringLiteral("/user/mod")); + LOGOS_ASSERT_TRUE(t.cFunctionCalled("setUserModulesDirectory")); +} + +LOGOS_TEST(setUserUiPluginsDirectory_forwards_to_lib) { + auto t = LogosTestContext("package_manager"); + PackageManagerImpl impl; + t.init(&impl); + impl.setUserUiPluginsDirectory(QStringLiteral("/user/ui")); + LOGOS_ASSERT_TRUE(t.cFunctionCalled("setUserUiPluginsDirectory")); +} + +LOGOS_TEST(getInstalledPackages_parses_json_array) { + auto t = LogosTestContext("package_manager"); + t.mockCFunction("getInstalledPackages").returns(R"([{"name":"pkg1","version":"1.0.0"}])"); + + PackageManagerImpl impl; + t.init(&impl); + + QVariantList list = impl.getInstalledPackages(); + LOGOS_ASSERT_EQ(list.size(), 1); + QVariantMap row = list[0].toMap(); + LOGOS_ASSERT_EQ(row[QStringLiteral("name")].toString(), QStringLiteral("pkg1")); + LOGOS_ASSERT_TRUE(t.cFunctionCalled("getInstalledPackages")); +} + +LOGOS_TEST(getInstalledModules_parses_json) { + auto t = LogosTestContext("package_manager"); + t.mockCFunction("getInstalledModules").returns(R"([{"name":"mod_a"}])"); + + PackageManagerImpl impl; + t.init(&impl); + + QVariantList list = impl.getInstalledModules(); + LOGOS_ASSERT_EQ(list.size(), 1); + LOGOS_ASSERT_EQ(list[0].toMap()[QStringLiteral("name")].toString(), QStringLiteral("mod_a")); +} + +LOGOS_TEST(getInstalledUiPlugins_parses_json) { + auto t = LogosTestContext("package_manager"); + t.mockCFunction("getInstalledUiPlugins").returns(R"([{"name":"ui_z"}])"); + + PackageManagerImpl impl; + t.init(&impl); + + QVariantList list = impl.getInstalledUiPlugins(); + LOGOS_ASSERT_EQ(list.size(), 1); + LOGOS_ASSERT_EQ(list[0].toMap()[QStringLiteral("name")].toString(), QStringLiteral("ui_z")); +} + +LOGOS_TEST(getInstalledPackages_empty_json_array) { + auto t = LogosTestContext("package_manager"); + // Default mock returns "[]" when getInstalledPackages string not set; force explicit + t.mockCFunction("getInstalledPackages").returns("[]"); + + PackageManagerImpl impl; + t.init(&impl); + + LOGOS_ASSERT_TRUE(impl.getInstalledPackages().isEmpty()); +} + +LOGOS_TEST(getValidVariants_uses_platformVariantsToTry) { + auto t = LogosTestContext("package_manager"); + t.mockCFunction("platformVariantsToTry_first").returns("custom-variant"); + + PackageManagerImpl impl; + t.init(&impl); + + QStringList v = impl.getValidVariants(); + LOGOS_ASSERT_EQ(v.size(), 1); + LOGOS_ASSERT_EQ(v[0], QStringLiteral("custom-variant")); + LOGOS_ASSERT_TRUE(t.cFunctionCalled("platformVariantsToTry")); +} + +LOGOS_TEST(getValidVariants_default_mock_variant) { + auto t = LogosTestContext("package_manager"); + PackageManagerImpl impl; + t.init(&impl); + + QStringList v = impl.getValidVariants(); + LOGOS_ASSERT_FALSE(v.isEmpty()); + LOGOS_ASSERT_EQ(v[0], QStringLiteral("mock-variant")); +} + +LOGOS_TEST(no_cross_module_calls_by_default) { + auto t = LogosTestContext("package_manager"); + PackageManagerImpl impl; + t.init(&impl); + LOGOS_ASSERT_EQ(t.moduleCallCount("capability_module", "requestModule"), 0); +} diff --git a/tests/test_package_manager_integration.cpp b/tests/test_package_manager_integration.cpp new file mode 100644 index 0000000..bc0cece --- /dev/null +++ b/tests/test_package_manager_integration.cpp @@ -0,0 +1,41 @@ +// Integration tests — real PackageManagerLib from ../lib (built by Nix / workspace). +// Skipped at configure time when the library is not present. + +#include +#include "package_manager_impl.h" + +#include +#include + +LOGOS_TEST(integration_getValidVariants_non_empty) { + PackageManagerImpl impl; + QStringList v = impl.getValidVariants(); + LOGOS_ASSERT_FALSE(v.isEmpty()); + for (const QString& s : v) { + LOGOS_ASSERT_FALSE(s.isEmpty()); + } +} + +LOGOS_TEST(integration_empty_user_modules_yields_empty_lists) { + QTemporaryDir dir; + LOGOS_ASSERT_TRUE(dir.isValid()); + + PackageManagerImpl impl; + impl.setUserModulesDirectory(dir.path()); + impl.setUserUiPluginsDirectory(dir.path()); + + LOGOS_ASSERT_TRUE(impl.getInstalledModules().isEmpty()); + LOGOS_ASSERT_TRUE(impl.getInstalledUiPlugins().isEmpty()); + LOGOS_ASSERT_TRUE(impl.getInstalledPackages().isEmpty()); +} + +LOGOS_TEST(integration_embedded_directory_scan_empty) { + QTemporaryDir emb; + LOGOS_ASSERT_TRUE(emb.isValid()); + + PackageManagerImpl impl; + impl.addEmbeddedModulesDirectory(emb.path()); + + // No packages under an empty embedded dir + LOGOS_ASSERT_TRUE(impl.getInstalledModules().isEmpty()); +}