add tests using the module testing framework

This commit is contained in:
Iuri Matias
2026-04-09 14:53:26 -04:00
parent 5f32acea50
commit fb941b5b87
9 changed files with 459 additions and 42 deletions
+1 -9
View File
@@ -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()
Generated
+6 -6
View File
@@ -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": {
+5 -15
View File
@@ -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" ];
};
};
}
+1
View File
@@ -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": [],
+66 -12
View File
@@ -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()
+97
View File
@@ -0,0 +1,97 @@
// Mock PackageManagerLib for package_manager_module unit tests (link-time substitution).
#include <logos_clib_mock.h>
#include <package_manager_lib.h>
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<std::string> 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"};
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <string>
#include <vector>
// 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<std::string> platformVariantsToTry();
};
+211
View File
@@ -0,0 +1,211 @@
// Unit tests for PackageManagerImpl — PackageManagerLib is mocked (mock_package_manager_lib.cpp).
#include <logos_test.h>
#include "package_manager_impl.h"
#include <QString>
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);
}
@@ -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 <logos_test.h>
#include "package_manager_impl.h"
#include <QDir>
#include <QTemporaryDir>
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());
}