Files

354 lines
13 KiB
C++
Raw Permalink Normal View History

// Tests for the path installPluginFile() reports back through its
// `installedPluginPath` out-parameter.
//
// Regression context: a QML-only ui_qml package (no backend library, so its
// manifest carries "main": {}) installed correctly but left this out-param
// EMPTY. Callers use it as the "something was installed" signal —
// package_manager_module gated its uiPluginFileInstalled event on it and put
// it in response["path"], and logos-package-manager-ui treats an empty "path"
// as failure — so a perfectly good install was rendered as a red RETRY and the
// plugin only showed up after an app restart.
//
// The "main": {} shape cannot be produced through liblgx's C API: lgx_add_variant
// only accepts a main-less directory variant when the package manifest already
// declares type "ui_qml", and there is no lgx_set_type(). So the manifest-shape
// matrix is driven directly through PackageManagerLib::resolveInstalledPackagePath(),
// and the end-to-end .lgx install tests cover the paths liblgx can express.
#include <gtest/gtest.h>
#include "package_manager_lib.h"
#include "test_support.h"
#include <lgx.h>
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>
namespace fs = std::filesystem;
using json = nlohmann::json;
namespace {
std::string hostVariant() {
auto variants = PackageManagerLib::platformVariantsToTry();
return variants.empty() ? "unknown" : variants.front();
}
// The extension resolveInstalledPackagePath() appends when it synthesises a
// main file name from the module name. Windows is tested first deliberately —
// a platform chain that tests it last is how Windows ends up on the Unix branch.
#if defined(_WIN32)
constexpr const char* kLibExt = ".dll";
#elif defined(__APPLE__)
constexpr const char* kLibExt = ".dylib";
#else
constexpr const char* kLibExt = ".so";
#endif
void writeFile(const fs::path& p, const std::string& contents) {
fs::create_directories(p.parent_path());
std::ofstream f(p);
f << contents;
}
} // namespace
// =============================================================================
// resolveInstalledPackagePath() — manifest-shape matrix
// =============================================================================
class InstalledPathTest : public ::testing::Test {
protected:
fs::path root;
void SetUp() override {
root = fs::temp_directory_path() / ("lgpm_instpath_" + std::to_string(std::rand()));
fs::create_directories(root);
}
void TearDown() override {
std::error_code ec;
fs::remove_all(root, ec);
}
// Creates <root>/<name>/manifest.json with the given JSON body.
fs::path makeModuleDir(const std::string& name, const json& manifest) {
fs::path dir = root / name;
fs::create_directories(dir);
std::ofstream mf(dir / "manifest.json");
mf << manifest.dump(2);
return dir;
}
std::string resolve(const fs::path& dir) {
return PackageManagerLib::resolveInstalledPackagePath(
dir.string(), PackageManagerLib::platformVariantsToTry());
}
};
// THE regression: ui_qml with an empty "main" map must report the module
// directory, not an empty string.
TEST_F(InstalledPathTest, UiQmlWithEmptyMainReportsModuleDirectory) {
json m;
m["name"] = "hello_ui";
m["version"] = "1.0.0";
m["type"] = "ui_qml";
m["view"] = "Main.qml";
m["main"] = json::object();
fs::path dir = makeModuleDir("hello_ui", m);
writeFile(dir / "Main.qml", "import QtQuick\nItem {}\n");
writeFile(dir / "qmldir", "module hello_ui\n");
EXPECT_EQ(resolve(dir), dir.string());
}
// Same, with "main" absent from the manifest entirely.
TEST_F(InstalledPathTest, UiQmlWithNoMainKeyReportsModuleDirectory) {
json m;
m["name"] = "hello_ui";
m["version"] = "1.0.0";
m["type"] = "ui_qml";
m["view"] = "Main.qml";
fs::path dir = makeModuleDir("hello_ui", m);
writeFile(dir / "Main.qml", "import QtQuick\nItem {}\n");
EXPECT_EQ(resolve(dir), dir.string());
}
// A QML-only package must NOT have a main file guessed from its module name,
// even when a same-named library happens to sit in the directory.
TEST_F(InstalledPathTest, UiQmlDoesNotSynthesiseMainFromModuleName) {
json m;
m["name"] = "hello_ui";
m["version"] = "1.0.0";
m["type"] = "ui_qml";
m["main"] = json::object();
fs::path dir = makeModuleDir("hello_ui", m);
writeFile(dir / (std::string("hello_ui") + kLibExt), "not the entry point");
EXPECT_EQ(resolve(dir), dir.string());
}
// A ui_qml package that DOES ship a backend keeps reporting the backend file —
// this is the shape every pre-existing ui_qml package (wallet_ui, accounts_ui)
// has, and it must not regress to the directory.
TEST_F(InstalledPathTest, UiQmlWithBackendReportsMainFile) {
const std::string lib = std::string("hello_ui_plugin") + kLibExt;
json m;
m["name"] = "hello_ui";
m["version"] = "1.0.0";
m["type"] = "ui_qml";
m["main"] = { { hostVariant(), lib } };
fs::path dir = makeModuleDir("hello_ui", m);
writeFile(dir / lib, "fake backend");
EXPECT_EQ(resolve(dir), (dir / lib).string());
}
TEST_F(InstalledPathTest, CoreModuleReportsMainFile) {
const std::string lib = std::string("waku_module_plugin") + kLibExt;
json m;
m["name"] = "waku_module";
m["version"] = "1.0.0";
m["type"] = "core";
m["main"] = { { hostVariant(), lib } };
fs::path dir = makeModuleDir("waku_module", m);
writeFile(dir / lib, "fake plugin");
EXPECT_EQ(resolve(dir), (dir / lib).string());
}
// "main" as a plain string (legacy single-variant manifests).
TEST_F(InstalledPathTest, StringMainReportsMainFile) {
const std::string lib = std::string("legacy_plugin") + kLibExt;
json m;
m["name"] = "legacy";
m["version"] = "1.0.0";
m["type"] = "core";
m["main"] = lib;
fs::path dir = makeModuleDir("legacy", m);
writeFile(dir / lib, "fake plugin");
EXPECT_EQ(resolve(dir), (dir / lib).string());
}
// Non-ui_qml with no "main": the module name is used as the base name and the
// platform extension appended.
TEST_F(InstalledPathTest, CoreWithoutMainSynthesisesFromModuleName) {
json m;
m["name"] = "guessed";
m["version"] = "1.0.0";
m["type"] = "core";
fs::path dir = makeModuleDir("guessed", m);
const std::string lib = std::string("guessed") + kLibExt;
writeFile(dir / lib, "fake plugin");
EXPECT_EQ(resolve(dir), (dir / lib).string());
}
// A declared main that is not in the payload is a packaging defect, not an
// install failure: report the directory AND say so on stderr rather than
// silently handing back an empty path.
TEST_F(InstalledPathTest, DeclaredButMissingMainFallsBackToDirectoryAndWarns) {
json m;
m["name"] = "broken";
m["version"] = "1.0.0";
m["type"] = "core";
m["main"] = { { hostVariant(), std::string("ghost") + kLibExt } };
fs::path dir = makeModuleDir("broken", m);
std::string out;
{
CerrCapture cap;
out = resolve(dir);
EXPECT_NE(cap.str().find("has no main file at"), std::string::npos)
<< "stderr was: " << cap.str();
}
EXPECT_EQ(out, dir.string());
}
// A module directory with no manifest at all still resolves to the directory.
TEST_F(InstalledPathTest, NoManifestFallsBackToDirectory) {
fs::path dir = root / "bare";
fs::create_directories(dir);
CerrCapture cap; // suppress the packaging warning
EXPECT_EQ(resolve(dir), dir.string());
}
// Only a directory that does not exist yields an empty result.
TEST_F(InstalledPathTest, MissingDirectoryYieldsEmpty) {
CerrCapture cap;
EXPECT_TRUE(resolve(root / "does_not_exist").empty());
}
// =============================================================================
// End-to-end installPluginFile() — the out-param on a real .lgx
// =============================================================================
class InstallOutParamTest : public ::testing::Test {
protected:
fs::path tempDir;
fs::path modulesDir;
fs::path uiPluginsDir;
void SetUp() override {
tempDir = fs::temp_directory_path() / ("lgpm_outparam_" + std::to_string(std::rand()));
modulesDir = tempDir / "modules";
uiPluginsDir = tempDir / "ui_plugins";
fs::create_directories(modulesDir);
fs::create_directories(uiPluginsDir);
}
void TearDown() override {
std::error_code ec;
fs::remove_all(tempDir, ec);
}
// Builds a .lgx whose single (host) variant is `contentDir`, declaring
// `mainName` as its main file. `mainName` need not exist in contentDir —
// that is exactly the "declared but missing" case we want to exercise.
fs::path createPackage(const std::string& name,
const std::string& mainName,
bool includeMainFile,
const std::string& version = "1.0.0") {
fs::path lgxPath = tempDir / (name + ".lgx");
fs::path contentDir = tempDir / (name + "_content");
fs::create_directories(contentDir);
writeFile(contentDir / "Main.qml", "import QtQuick\nItem {}\n");
if (includeMainFile)
writeFile(contentDir / mainName, "fake library content");
lgx_result_t res = lgx_create(lgxPath.string().c_str(), name.c_str());
if (!res.success) return {};
lgx_package_t pkg = lgx_load(lgxPath.string().c_str());
if (!pkg) return {};
lgx_set_version(pkg, version.c_str());
res = lgx_add_variant(pkg, hostVariant().c_str(),
contentDir.string().c_str(), mainName.c_str());
if (!res.success) { lgx_free_package(pkg); return {}; }
res = lgx_save(pkg, lgxPath.string().c_str());
lgx_free_package(pkg);
if (!res.success) return {};
return lgxPath;
}
PackageManagerLib createPM() {
PackageManagerLib pm;
pm.setUserModulesDirectory(modulesDir.string());
pm.setUserUiPluginsDirectory(uiPluginsDir.string());
pm.setSignaturePolicy(SignaturePolicy::NONE);
return pm;
}
};
TEST_F(InstallOutParamTest, ReportsMainFileWhenPackageShipsOne) {
const std::string lib = std::string("with_main_plugin") + kLibExt;
auto lgxPath = createPackage("with_main", lib, /*includeMainFile=*/true);
ASSERT_FALSE(lgxPath.empty());
auto pm = createPM();
std::string errorMsg;
std::string installedPath;
bool isCore = true;
std::string result = pm.installPluginFile(lgxPath.string(), errorMsg, false,
&installedPath, &isCore);
ASSERT_FALSE(result.empty()) << errorMsg;
EXPECT_FALSE(isCore);
EXPECT_EQ(installedPath, (uiPluginsDir / "with_main" / lib).string());
EXPECT_TRUE(fs::exists(installedPath));
}
// The install succeeds and the out-param must still identify the package.
// Before the fix this came back empty, which every caller reads as failure.
TEST_F(InstallOutParamTest, ReportsModuleDirectoryWhenMainFileIsAbsent) {
const std::string lib = std::string("no_main_plugin") + kLibExt;
auto lgxPath = createPackage("no_main", lib, /*includeMainFile=*/false);
ASSERT_FALSE(lgxPath.empty());
auto pm = createPM();
std::string errorMsg;
std::string installedPath;
std::string result;
{
CerrCapture cap;
result = pm.installPluginFile(lgxPath.string(), errorMsg, false,
&installedPath, nullptr);
}
ASSERT_FALSE(result.empty()) << errorMsg;
EXPECT_FALSE(installedPath.empty())
<< "a successful install must never report an empty path";
EXPECT_EQ(installedPath, (uiPluginsDir / "no_main").string());
EXPECT_TRUE(fs::is_directory(installedPath));
}
// The skipIfNotNewerVersion early return used to hardcode isCoreModule=false,
// so skipping an already-current CORE module made package_manager_module emit
// uiPluginFileInstalled for it. The type now comes off the installed manifest.
TEST_F(InstallOutParamTest, SkippedInstallReportsTypeFromInstalledManifest) {
// Pre-seed a newer copy of a CORE module in the user modules directory.
fs::path existing = modulesDir / "already_here";
fs::create_directories(existing);
{
json m;
m["name"] = "already_here";
m["version"] = "2.0.0";
m["type"] = "core";
std::ofstream mf(existing / "manifest.json");
mf << m.dump(2);
}
const std::string lib = std::string("already_here_plugin") + kLibExt;
auto lgxPath = createPackage("already_here", lib, true, "1.0.0");
ASSERT_FALSE(lgxPath.empty());
auto pm = createPM();
std::string errorMsg;
std::string installedPath;
bool isCore = false;
std::string result = pm.installPluginFile(lgxPath.string(), errorMsg, /*skip=*/true,
&installedPath, &isCore);
EXPECT_EQ(result, existing.string());
EXPECT_EQ(installedPath, existing.string());
EXPECT_TRUE(isCore) << "a skipped core install must not report itself as a UI plugin";
}