diff --git a/src/logos_core/plugin_manager.cpp b/src/logos_core/plugin_manager.cpp index 95fcd22..6e45de9 100644 --- a/src/logos_core/plugin_manager.cpp +++ b/src/logos_core/plugin_manager.cpp @@ -415,6 +415,50 @@ namespace PluginManager { loadPlugin(pluginName); } + QString currentPlatformVariant() { +#if defined(Q_OS_MAC) + #if defined(Q_PROCESSOR_ARM) + return "darwin-arm64"; + #else + return "darwin-x86_64"; + #endif +#elif defined(Q_OS_LINUX) + #if defined(Q_PROCESSOR_X86_64) + return "linux-x86_64"; + #elif defined(Q_PROCESSOR_ARM_64) + return "linux-arm64"; + #else + return "linux-x86"; + #endif +#elif defined(Q_OS_WIN) + #if defined(Q_PROCESSOR_X86_64) + return "windows-x86_64"; + #else + return "windows-x86"; + #endif +#else + return "unknown"; +#endif + } + + QStringList platformVariantsToTry() { + QString primary = currentPlatformVariant(); + QStringList variants; + variants << primary; + + if (primary == "linux-x86_64") { + variants << "linux-amd64"; + } else if (primary == "linux-amd64") { + variants << "linux-x86_64"; + } else if (primary == "linux-arm64") { + variants << "linux-aarch64"; + } else if (primary == "linux-aarch64") { + variants << "linux-arm64"; + } + + return variants; + } + QStringList findPlugins(const QString &pluginsDir) { QDir dir(pluginsDir); QStringList plugins; @@ -426,27 +470,62 @@ namespace PluginManager { return plugins; } - // Get all files in the directory - QStringList entries = dir.entryList(QDir::Files); - qDebug() << "Files found:" << entries; + QStringList subdirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + qDebug() << "Subdirectories found:" << subdirs; - // Filter for plugin files based on platform - QStringList nameFilters; - #ifdef Q_OS_WIN - nameFilters << "*.dll"; - #elif defined(Q_OS_MAC) - nameFilters << "*.dylib"; - #else - nameFilters << "*.so"; - #endif - - dir.setNameFilters(nameFilters); - QStringList pluginFiles = dir.entryList(QDir::Files); - - for (const QString &fileName : pluginFiles) { - QString filePath = dir.absoluteFilePath(fileName); - plugins.append(filePath); - qDebug() << "Found plugin:" << filePath; + for (const QString &subdir : subdirs) { + QDir pluginDir(dir.absoluteFilePath(subdir)); + QString manifestPath = pluginDir.absoluteFilePath("manifest.json"); + + if (!QFile::exists(manifestPath)) { + qDebug() << "No manifest.json in" << pluginDir.absolutePath() << ", skipping"; + continue; + } + + QFile manifestFile(manifestPath); + if (!manifestFile.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open manifest:" << manifestPath; + continue; + } + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(manifestFile.readAll(), &parseError); + manifestFile.close(); + + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Failed to parse manifest:" << manifestPath << parseError.errorString(); + continue; + } + + QJsonObject manifest = doc.object(); + QJsonValue mainValue = manifest.value("main"); + + if (!mainValue.isObject()) { + qWarning() << "Manifest 'main' field is not an object:" << manifestPath; + continue; + } + + QJsonObject mainObj = mainValue.toObject(); + QStringList variants = platformVariantsToTry(); + QString mainLib; + for (const QString &variant : variants) { + mainLib = mainObj.value(variant).toString(); + if (!mainLib.isEmpty()) break; + } + + if (mainLib.isEmpty()) { + qDebug() << "No entry for platform variants" << variants << "in manifest:" << manifestPath; + continue; + } + + QString libPath = pluginDir.absoluteFilePath(mainLib); + if (!QFile::exists(libPath)) { + qWarning() << "Main library not found:" << libPath; + continue; + } + + plugins.append(libPath); + qDebug() << "Found plugin:" << libPath; } return plugins; diff --git a/tests/test_plugin_manager.cpp b/tests/test_plugin_manager.cpp index 6669bbf..db1374c 100644 --- a/tests/test_plugin_manager.cpp +++ b/tests/test_plugin_manager.cpp @@ -8,8 +8,42 @@ #include #include #include +#include +#include #include +namespace { +QString testPlatformVariant() { +#if defined(Q_OS_MAC) + #if defined(Q_PROCESSOR_ARM) + return "darwin-arm64"; + #else + return "darwin-x86_64"; + #endif +#elif defined(Q_OS_LINUX) + #if defined(Q_PROCESSOR_X86_64) + return "linux-x86_64"; + #elif defined(Q_PROCESSOR_ARM_64) + return "linux-arm64"; + #else + return "linux-x86"; + #endif +#elif defined(Q_OS_WIN) + #if defined(Q_PROCESSOR_X86_64) + return "windows-x86_64"; + #else + return "windows-x86"; + #endif +#else + return "unknown"; +#endif +} + +QJsonObject mainFieldForPlatform(const QString &libName) { + return QJsonObject{{testPlatformVariant(), libName}}; +} +} + // Test fixture for plugin manager tests class PluginManagerTest : public ::testing::Test { protected: @@ -208,41 +242,141 @@ TEST_F(PluginManagerTest, FindPlugins_ReturnsEmptyForEmptyDir) { EXPECT_TRUE(plugins.isEmpty()); } -// Verifies that findPlugins() correctly filters plugin files by platform-specific extensions -// (.dylib on macOS, .so on Linux, .dll on Windows) and ignores other file types -TEST_F(PluginManagerTest, FindPlugins_FiltersByPlatformExtension) { +// Verifies that findPlugins() discovers plugins from subdirectories containing a manifest.json +// with a "main" field pointing to an existing library file +TEST_F(PluginManagerTest, FindPlugins_DiscoversPluginFromManifest) { QTemporaryDir tempDir; ASSERT_TRUE(tempDir.isValid()); - // Create test files with various extensions - QString correctExt; -#ifdef Q_OS_WIN - correctExt = ".dll"; -#elif defined(Q_OS_MAC) - correctExt = ".dylib"; -#else - correctExt = ".so"; -#endif + QDir rootDir(tempDir.path()); - // Create a file with the correct extension - QFile correctFile(tempDir.filePath("plugin" + correctExt)); - ASSERT_TRUE(correctFile.open(QIODevice::WriteOnly)); - correctFile.close(); + // Create a valid plugin subdirectory with manifest and library + ASSERT_TRUE(rootDir.mkpath("my_plugin")); + QDir pluginDir(rootDir.filePath("my_plugin")); - // Create files with wrong extensions - QFile txtFile(tempDir.filePath("plugin.txt")); - ASSERT_TRUE(txtFile.open(QIODevice::WriteOnly)); - txtFile.close(); + QString libName = "my_plugin.dylib"; + QFile libFile(pluginDir.filePath(libName)); + ASSERT_TRUE(libFile.open(QIODevice::WriteOnly)); + libFile.close(); - QFile noExtFile(tempDir.filePath("plugin")); - ASSERT_TRUE(noExtFile.open(QIODevice::WriteOnly)); - noExtFile.close(); + QFile manifest(pluginDir.filePath("manifest.json")); + ASSERT_TRUE(manifest.open(QIODevice::WriteOnly)); + manifest.write(QJsonDocument(QJsonObject{{"main", mainFieldForPlatform(libName)}}).toJson()); + manifest.close(); QStringList plugins = PluginManager::findPlugins(tempDir.path()); - // Should only find the file with the correct extension ASSERT_EQ(plugins.size(), 1); - EXPECT_TRUE(plugins[0].endsWith(correctExt)); + EXPECT_TRUE(plugins[0].endsWith(libName)); +} + +// Verifies that findPlugins() skips subdirectories that have no manifest.json +TEST_F(PluginManagerTest, FindPlugins_SkipsSubdirWithoutManifest) { + QTemporaryDir tempDir; + ASSERT_TRUE(tempDir.isValid()); + + QDir rootDir(tempDir.path()); + ASSERT_TRUE(rootDir.mkpath("no_manifest_plugin")); + + // Create a library file but no manifest + QFile libFile(rootDir.filePath("no_manifest_plugin/plugin.dylib")); + ASSERT_TRUE(libFile.open(QIODevice::WriteOnly)); + libFile.close(); + + QStringList plugins = PluginManager::findPlugins(tempDir.path()); + EXPECT_TRUE(plugins.isEmpty()); +} + +// Verifies that findPlugins() skips manifests with a missing "main" field +TEST_F(PluginManagerTest, FindPlugins_SkipsManifestWithoutMainField) { + QTemporaryDir tempDir; + ASSERT_TRUE(tempDir.isValid()); + + QDir rootDir(tempDir.path()); + ASSERT_TRUE(rootDir.mkpath("bad_manifest_plugin")); + + QFile manifest(rootDir.filePath("bad_manifest_plugin/manifest.json")); + ASSERT_TRUE(manifest.open(QIODevice::WriteOnly)); + manifest.write(QJsonDocument(QJsonObject{{"version", "1.0"}}).toJson()); + manifest.close(); + + QStringList plugins = PluginManager::findPlugins(tempDir.path()); + EXPECT_TRUE(plugins.isEmpty()); +} + +// Verifies that findPlugins() skips manifests where "main" has no entry for the current platform +TEST_F(PluginManagerTest, FindPlugins_SkipsManifestWithWrongPlatform) { + QTemporaryDir tempDir; + ASSERT_TRUE(tempDir.isValid()); + + QDir rootDir(tempDir.path()); + ASSERT_TRUE(rootDir.mkpath("wrong_platform_plugin")); + QDir pluginDir(rootDir.filePath("wrong_platform_plugin")); + + QFile libFile(pluginDir.filePath("plugin.dll")); + ASSERT_TRUE(libFile.open(QIODevice::WriteOnly)); + libFile.close(); + + QJsonObject mainObj{{"fake-platform-999", "plugin.dll"}}; + QFile manifest(pluginDir.filePath("manifest.json")); + ASSERT_TRUE(manifest.open(QIODevice::WriteOnly)); + manifest.write(QJsonDocument(QJsonObject{{"main", mainObj}}).toJson()); + manifest.close(); + + QStringList plugins = PluginManager::findPlugins(tempDir.path()); + EXPECT_TRUE(plugins.isEmpty()); +} + +// Verifies that findPlugins() skips manifests where "main" points to a nonexistent file +TEST_F(PluginManagerTest, FindPlugins_SkipsManifestWithMissingLibrary) { + QTemporaryDir tempDir; + ASSERT_TRUE(tempDir.isValid()); + + QDir rootDir(tempDir.path()); + ASSERT_TRUE(rootDir.mkpath("missing_lib_plugin")); + + QFile manifest(rootDir.filePath("missing_lib_plugin/manifest.json")); + ASSERT_TRUE(manifest.open(QIODevice::WriteOnly)); + manifest.write(QJsonDocument(QJsonObject{{"main", mainFieldForPlatform("nonexistent.dylib")}}).toJson()); + manifest.close(); + + QStringList plugins = PluginManager::findPlugins(tempDir.path()); + EXPECT_TRUE(plugins.isEmpty()); +} + +// Verifies that findPlugins() discovers multiple valid plugins and ignores invalid ones +TEST_F(PluginManagerTest, FindPlugins_DiscoversMultiplePlugins) { + QTemporaryDir tempDir; + ASSERT_TRUE(tempDir.isValid()); + + QDir rootDir(tempDir.path()); + + // Valid plugin A + ASSERT_TRUE(rootDir.mkpath("plugin_a")); + QFile libA(rootDir.filePath("plugin_a/liba.dylib")); + ASSERT_TRUE(libA.open(QIODevice::WriteOnly)); + libA.close(); + QFile manifestA(rootDir.filePath("plugin_a/manifest.json")); + ASSERT_TRUE(manifestA.open(QIODevice::WriteOnly)); + manifestA.write(QJsonDocument(QJsonObject{{"main", mainFieldForPlatform("liba.dylib")}}).toJson()); + manifestA.close(); + + // Valid plugin B + ASSERT_TRUE(rootDir.mkpath("plugin_b")); + QFile libB(rootDir.filePath("plugin_b/libb.so")); + ASSERT_TRUE(libB.open(QIODevice::WriteOnly)); + libB.close(); + QFile manifestB(rootDir.filePath("plugin_b/manifest.json")); + ASSERT_TRUE(manifestB.open(QIODevice::WriteOnly)); + manifestB.write(QJsonDocument(QJsonObject{{"main", mainFieldForPlatform("libb.so")}}).toJson()); + manifestB.close(); + + // Invalid: no manifest + ASSERT_TRUE(rootDir.mkpath("plugin_c")); + + QStringList plugins = PluginManager::findPlugins(tempDir.path()); + + ASSERT_EQ(plugins.size(), 2); } // =============================================================================