aboutsummaryrefslogtreecommitdiff
path: root/libs/plugin_python/tests/runner
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/plugin_python/tests/runner
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/plugin_python/tests/runner')
-rw-r--r--libs/plugin_python/tests/runner/CMakeLists.txt65
-rw-r--r--libs/plugin_python/tests/runner/plugins/dummy-diagnose.py37
-rw-r--r--libs/plugin_python/tests/runner/plugins/dummy-filemapper.py32
-rw-r--r--libs/plugin_python/tests/runner/plugins/dummy-game.py45
-rw-r--r--libs/plugin_python/tests/runner/plugins/dummy-installer.py46
-rw-r--r--libs/plugin_python/tests/runner/plugins/dummy-iplugin.py29
-rw-r--r--libs/plugin_python/tests/runner/test_diagnose.cpp61
-rw-r--r--libs/plugin_python/tests/runner/test_filemapper.cpp64
-rw-r--r--libs/plugin_python/tests/runner/test_game.cpp28
-rw-r--r--libs/plugin_python/tests/runner/test_installer.cpp112
-rw-r--r--libs/plugin_python/tests/runner/test_iplugin.cpp43
-rw-r--r--libs/plugin_python/tests/runner/test_lifetime.cpp47
12 files changed, 609 insertions, 0 deletions
diff --git a/libs/plugin_python/tests/runner/CMakeLists.txt b/libs/plugin_python/tests/runner/CMakeLists.txt
new file mode 100644
index 0000000..ea17438
--- /dev/null
+++ b/libs/plugin_python/tests/runner/CMakeLists.txt
@@ -0,0 +1,65 @@
+cmake_minimum_required(VERSION 3.22)
+
+# setting-up the tests for the runner is a bit complex because we need a tons of
+# things
+
+# first we configure the tests as with other tests
+add_executable(runner-tests EXCLUDE_FROM_ALL)
+mo2_default_source_group()
+mo2_target_sources(runner-tests
+ FOLDER src
+ PRIVATE
+ test_diagnose.cpp
+ test_filemapper.cpp
+ test_game.cpp
+ test_installer.cpp
+ test_iplugin.cpp
+ test_lifetime.cpp
+)
+mo2_target_sources(runner-tests
+ FOLDER src/mocks
+ PRIVATE
+ ../mocks/DummyFileTree.h
+ ../mocks/MockOrganizer.h
+)
+mo2_target_sources(runner-tests
+ FOLDER src/plugins
+ PRIVATE
+ plugins/dummy-diagnose.py
+ plugins/dummy-filemapper.py
+ plugins/dummy-game.py
+ plugins/dummy-installer.py
+ plugins/dummy-iplugin.py
+)
+mo2_configure_tests(runner-tests NO_SOURCES WARNINGS 4)
+
+set_target_properties(runner-tests PROPERTIES FOLDER tests/runner)
+
+# link to runner
+target_link_libraries(runner-tests PUBLIC runner)
+
+# linking to Python - this is not required to get proper linking but required so that
+# CMake generator variables will lookup appropriate DLLs for Python and update PATH
+# accordingly thanks to mo2_configure_tests
+target_link_libraries(runner-tests PUBLIC Python::Python)
+
+# add mocks
+target_include_directories(runner-tests
+ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../mocks)
+
+set(PYLIB_DIR ${CMAKE_CURRENT_BINARY_DIR}/pylibs)
+mo2_python_pip_install(runner-tests
+ DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pylibs
+ PACKAGES
+ pytest
+ PyQt${MO2_QT_VERSION_MAJOR}==${MO2_PYQT_VERSION}
+ PyQt${MO2_QT_VERSION_MAJOR}-Qt${MO2_QT_VERSION_MAJOR}==${MO2_QT_VERSION})
+
+add_dependencies(runner-tests mobase)
+
+set(PYTHONPATH "${PYLIB_DIR}\\;$<TARGET_FILE_DIR:mobase>\\;${Python_DLL_DIR}\\;${Python_LIB_DIR}")
+
+set_tests_properties(${runner-tests_gtests}
+ PROPERTIES
+ ENVIRONMENT "PLUGIN_DIR=${CMAKE_CURRENT_SOURCE_DIR}/plugins;PYTHONPATH=${PYTHONPATH}"
+)
diff --git a/libs/plugin_python/tests/runner/plugins/dummy-diagnose.py b/libs/plugin_python/tests/runner/plugins/dummy-diagnose.py
new file mode 100644
index 0000000..6f25ef0
--- /dev/null
+++ b/libs/plugin_python/tests/runner/plugins/dummy-diagnose.py
@@ -0,0 +1,37 @@
+import mobase
+
+
+class DummyDiagnose(mobase.IPluginDiagnose):
+ def activeProblems(self) -> list[int]:
+ return [1, 2]
+
+ def shortDescription(self, key: int) -> str:
+ return f"short-{key}"
+
+ def fullDescription(self, key: int) -> str:
+ return f"long-{key}"
+
+ def hasGuidedFix(self, key: int) -> bool:
+ return key == 1
+
+
+class DummyDiagnoseAndGame(mobase.IPluginDiagnose, mobase.IPluginGame):
+ def __init__(self):
+ mobase.IPluginDiagnose.__init__(self)
+ mobase.IPluginGame.__init__(self)
+
+ def activeProblems(self) -> list[int]:
+ return [5, 7]
+
+ def shortDescription(self, key: int) -> str:
+ return f"short-{key}"
+
+ def fullDescription(self, key: int) -> str:
+ return f"long-{key}"
+
+ def hasGuidedFix(self, key: int) -> bool:
+ return key == 7
+
+
+def createPlugins() -> list[mobase.IPlugin]:
+ return [DummyDiagnose(), DummyDiagnoseAndGame()] # type: ignore
diff --git a/libs/plugin_python/tests/runner/plugins/dummy-filemapper.py b/libs/plugin_python/tests/runner/plugins/dummy-filemapper.py
new file mode 100644
index 0000000..1993b55
--- /dev/null
+++ b/libs/plugin_python/tests/runner/plugins/dummy-filemapper.py
@@ -0,0 +1,32 @@
+import mobase
+
+
+class DummyFileMapper(mobase.IPluginFileMapper):
+ def mappings(self) -> list[mobase.Mapping]:
+ return [
+ mobase.Mapping(
+ source="the source", destination="the destination", is_directory=False
+ ),
+ mobase.Mapping(
+ source="the other source",
+ destination="the other destination",
+ is_directory=True,
+ ),
+ ]
+
+
+class DummyFileMapperAndGame(mobase.IPluginFileMapper, mobase.IPluginGame):
+ def __init__(self):
+ mobase.IPluginFileMapper.__init__(self)
+ mobase.IPluginGame.__init__(self)
+
+ def mappings(self) -> list[mobase.Mapping]:
+ return [
+ mobase.Mapping(
+ source="the source", destination="the destination", is_directory=False
+ ),
+ ]
+
+
+def createPlugins() -> list[mobase.IPlugin]:
+ return [DummyFileMapper(), DummyFileMapperAndGame()] # type: ignore
diff --git a/libs/plugin_python/tests/runner/plugins/dummy-game.py b/libs/plugin_python/tests/runner/plugins/dummy-game.py
new file mode 100644
index 0000000..e17c9b8
--- /dev/null
+++ b/libs/plugin_python/tests/runner/plugins/dummy-game.py
@@ -0,0 +1,45 @@
+from collections.abc import Sequence
+
+from PyQt6.QtWidgets import QWidget
+
+import mobase
+
+
+class DummyModDataChecker(mobase.ModDataChecker):
+ def dataLooksValid(
+ self, filetree: mobase.IFileTree
+ ) -> mobase.ModDataChecker.CheckReturn:
+ return mobase.ModDataChecker.VALID
+
+ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree:
+ return filetree
+
+
+class DummySaveGameInfo(mobase.SaveGameInfo):
+ def getMissingAssets(self, save: mobase.ISaveGame) -> dict[str, Sequence[str]]:
+ return {}
+
+ def getSaveGameWidget(self, parent: QWidget) -> mobase.ISaveGameInfoWidget | None:
+ return None
+
+
+# we do not implement everything since this will do fine if we do not call anything
+# from the C++ side
+#
+class DummyGame(mobase.IPluginGame):
+ _features: dict[type, object]
+
+ def __init__(self):
+ super().__init__()
+
+ self._features = {
+ mobase.ModDataChecker: DummyModDataChecker(),
+ mobase.SaveGameInfo: DummySaveGameInfo(),
+ }
+
+ def _featureList(self) -> dict[type, object]:
+ return self._features
+
+
+def createPlugin() -> mobase.IPlugin:
+ return DummyGame() # type: ignore
diff --git a/libs/plugin_python/tests/runner/plugins/dummy-installer.py b/libs/plugin_python/tests/runner/plugins/dummy-installer.py
new file mode 100644
index 0000000..3c4e67e
--- /dev/null
+++ b/libs/plugin_python/tests/runner/plugins/dummy-installer.py
@@ -0,0 +1,46 @@
+# -*- encoding: utf-8 -*-
+
+from typing import Union, cast
+
+import mobase
+
+
+# we do not implement everything since this will do fine if we do not call anything
+# from the C++ side
+#
+class DummyInstaller(mobase.IPluginInstallerSimple):
+ def isManualInstaller(self) -> bool:
+ return False
+
+ def priority(self) -> int:
+ return 10
+
+ def isArchiveSupported(self, tree: mobase.IFileTree) -> bool:
+ return tree.find("needed-file.txt") is not None
+
+ def install(
+ self,
+ name: mobase.GuessedString,
+ tree: mobase.IFileTree,
+ version: str,
+ nexus_id: int,
+ ) -> Union[
+ mobase.InstallResult,
+ mobase.IFileTree,
+ tuple[mobase.InstallResult, mobase.IFileTree, str, int],
+ ]:
+ if tree.find("needed-file.txt") is None:
+ return mobase.InstallResult.FAILED
+
+ if tree.find("extra-file.txt"):
+ name.update("new name")
+ new_tree = tree.createOrphanTree()
+ new_tree.move(tree, "subtree")
+ cast(mobase.IFileTree, new_tree.find("subtree")).remove("extra-file.txt")
+ return new_tree
+
+ return (mobase.InstallResult.NOT_ATTEMPTED, tree, "2.4.5", 33)
+
+
+def createPlugin() -> mobase.IPlugin:
+ return DummyInstaller() # type: ignore
diff --git a/libs/plugin_python/tests/runner/plugins/dummy-iplugin.py b/libs/plugin_python/tests/runner/plugins/dummy-iplugin.py
new file mode 100644
index 0000000..eaaf6c0
--- /dev/null
+++ b/libs/plugin_python/tests/runner/plugins/dummy-iplugin.py
@@ -0,0 +1,29 @@
+import mobase
+
+
+class DummyPlugin(mobase.IPlugin):
+ def init(self, organizer: mobase.IOrganizer) -> bool:
+ return True
+
+ def author(self) -> str:
+ return "The Author"
+
+ def name(self) -> str:
+ return "The Name"
+
+ def description(self) -> str:
+ return "The Description"
+
+ def version(self) -> mobase.VersionInfo:
+ return mobase.VersionInfo("1.3.0")
+
+ def settings(self) -> list[mobase.PluginSetting]:
+ return [
+ mobase.PluginSetting(
+ "a setting", "the setting description", default_value=12
+ )
+ ]
+
+
+def createPlugin() -> mobase.IPlugin:
+ return DummyPlugin()
diff --git a/libs/plugin_python/tests/runner/test_diagnose.cpp b/libs/plugin_python/tests/runner/test_diagnose.cpp
new file mode 100644
index 0000000..467c9cb
--- /dev/null
+++ b/libs/plugin_python/tests/runner/test_diagnose.cpp
@@ -0,0 +1,61 @@
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "pythonrunner.h"
+
+#include <QCoreApplication>
+
+#include <uibase/iplugindiagnose.h>
+#include <uibase/iplugingame.h>
+
+#include "MockOrganizer.h"
+
+using namespace MOBase;
+
+using ::testing::ElementsAre;
+
+TEST(IPluginDiagnose, Simple)
+{
+ const auto plugins_folder = QString(std::getenv("PLUGIN_DIR"));
+
+ auto runner = mo2::python::createPythonRunner();
+ runner->initialize();
+
+ // load objects
+ const auto objects = runner->load(plugins_folder + "/dummy-diagnose.py");
+ EXPECT_EQ(objects.size(), 3);
+
+ // load the first IPluginDiagnose
+ {
+ IPluginDiagnose* plugin = qobject_cast<IPluginDiagnose*>(objects[0]);
+ EXPECT_NE(plugin, nullptr);
+
+ ASSERT_THAT(plugin->activeProblems(), ElementsAre(1, 2));
+ EXPECT_EQ(plugin->shortDescription(1), "short-1");
+ EXPECT_EQ(plugin->fullDescription(1), "long-1");
+ EXPECT_TRUE(plugin->hasGuidedFix(1));
+ EXPECT_EQ(plugin->shortDescription(2), "short-2");
+ EXPECT_EQ(plugin->fullDescription(2), "long-2");
+ EXPECT_FALSE(plugin->hasGuidedFix(2));
+ }
+
+ // load the second one (this is cast before IPluginGame so should be before)
+ {
+ IPluginDiagnose* plugin = qobject_cast<IPluginDiagnose*>(objects[1]);
+ EXPECT_NE(plugin, nullptr);
+
+ ASSERT_THAT(plugin->activeProblems(), ElementsAre(5, 7));
+ EXPECT_EQ(plugin->shortDescription(5), "short-5");
+ EXPECT_EQ(plugin->fullDescription(5), "long-5");
+ EXPECT_FALSE(plugin->hasGuidedFix(5));
+ EXPECT_EQ(plugin->shortDescription(7), "short-7");
+ EXPECT_EQ(plugin->fullDescription(7), "long-7");
+ EXPECT_TRUE(plugin->hasGuidedFix(7));
+ }
+
+ // load the game plugin
+ {
+ IPluginGame* plugin = qobject_cast<IPluginGame*>(objects[2]);
+ EXPECT_NE(plugin, nullptr);
+ }
+}
diff --git a/libs/plugin_python/tests/runner/test_filemapper.cpp b/libs/plugin_python/tests/runner/test_filemapper.cpp
new file mode 100644
index 0000000..5044283
--- /dev/null
+++ b/libs/plugin_python/tests/runner/test_filemapper.cpp
@@ -0,0 +1,64 @@
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "pythonrunner.h"
+
+#include <QCoreApplication>
+
+#include <uibase/ipluginfilemapper.h>
+#include <uibase/iplugingame.h>
+
+#include "MockOrganizer.h"
+
+using namespace MOBase;
+
+TEST(IPluginFileMapper, Simple)
+{
+ const auto plugins_folder = QString(std::getenv("PLUGIN_DIR"));
+
+ auto runner = mo2::python::createPythonRunner();
+ runner->initialize();
+
+ // load objects
+ const auto objects = runner->load(plugins_folder + "/dummy-filemapper.py");
+ EXPECT_EQ(objects.size(), 3);
+
+ // load the first IPluginFileMapper
+ {
+ IPluginFileMapper* plugin = qobject_cast<IPluginFileMapper*>(objects[0]);
+ EXPECT_NE(plugin, nullptr);
+
+ const auto m = plugin->mappings();
+ EXPECT_EQ(m.size(), 2);
+
+ EXPECT_EQ(m[0].source, "the source");
+ EXPECT_EQ(m[0].destination, "the destination");
+ EXPECT_EQ(m[0].isDirectory, false);
+ EXPECT_EQ(m[0].createTarget, false);
+
+ EXPECT_EQ(m[1].source, "the other source");
+ EXPECT_EQ(m[1].destination, "the other destination");
+ EXPECT_EQ(m[1].isDirectory, true);
+ EXPECT_EQ(m[1].createTarget, false);
+ }
+
+ // load the second one (this is cast before IPluginGame so should be before)
+ {
+ IPluginFileMapper* plugin = qobject_cast<IPluginFileMapper*>(objects[1]);
+ EXPECT_NE(plugin, nullptr);
+
+ const auto m = plugin->mappings();
+ EXPECT_EQ(m.size(), 1);
+
+ EXPECT_EQ(m[0].source, "the source");
+ EXPECT_EQ(m[0].destination, "the destination");
+ EXPECT_EQ(m[0].isDirectory, false);
+ EXPECT_EQ(m[0].createTarget, false);
+ }
+
+ // load the game plugin
+ {
+ IPluginGame* plugin = qobject_cast<IPluginGame*>(objects[2]);
+ EXPECT_NE(plugin, nullptr);
+ }
+}
diff --git a/libs/plugin_python/tests/runner/test_game.cpp b/libs/plugin_python/tests/runner/test_game.cpp
new file mode 100644
index 0000000..0d1d718
--- /dev/null
+++ b/libs/plugin_python/tests/runner/test_game.cpp
@@ -0,0 +1,28 @@
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "pythonrunner.h"
+
+#include <QCoreApplication>
+
+#include <uibase/iplugingame.h>
+
+#include "MockOrganizer.h"
+
+using namespace MOBase;
+
+TEST(IPluginGame, Simple)
+{
+ const auto plugins_folder = QString(std::getenv("PLUGIN_DIR"));
+
+ auto runner = mo2::python::createPythonRunner();
+ runner->initialize();
+
+ // load objects
+ const auto objects = runner->load(plugins_folder + "/dummy-game.py");
+ EXPECT_EQ(objects.size(), 1);
+
+ // load the IPlugin
+ IPluginGame* plugin = qobject_cast<IPluginGame*>(objects[0]);
+ EXPECT_NE(plugin, nullptr);
+}
diff --git a/libs/plugin_python/tests/runner/test_installer.cpp b/libs/plugin_python/tests/runner/test_installer.cpp
new file mode 100644
index 0000000..349c904
--- /dev/null
+++ b/libs/plugin_python/tests/runner/test_installer.cpp
@@ -0,0 +1,112 @@
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include <uibase/iplugininstallersimple.h>
+
+#include "MockOrganizer.h"
+#include "pythonrunner.h"
+
+#include <QCoreApplication>
+
+#include "DummyFileTree.h"
+
+using namespace MOBase;
+
+TEST(IPluginInstaller, Simple)
+{
+ const auto plugins_folder = QString(std::getenv("PLUGIN_DIR"));
+
+ auto runner = mo2::python::createPythonRunner();
+ runner->initialize();
+
+ // load objects
+ const auto objects = runner->load(plugins_folder + "/dummy-installer.py");
+ EXPECT_EQ(objects.size(), 1);
+
+ // load the IPlugin
+ IPluginInstallerSimple* plugin = qobject_cast<IPluginInstallerSimple*>(objects[0]);
+ EXPECT_NE(plugin, nullptr);
+
+ // basic tests
+ EXPECT_EQ(plugin->priority(), 10);
+ EXPECT_EQ(plugin->isManualInstaller(), false);
+
+ GuessedValue<QString> name{"default name"};
+ QString version = "1.0.0";
+ int nexus_id = 12;
+
+ // invalid tree
+ {
+ std::shared_ptr<IFileTree> invalid_tree =
+ std::make_shared<DummyFileTree>(nullptr, "root");
+ invalid_tree->addFile("some file");
+ invalid_tree->addDirectory("some directory")->addFile("some other file");
+ EXPECT_EQ(plugin->isArchiveSupported(invalid_tree), false);
+ EXPECT_EQ(plugin->install(name, invalid_tree, version, nexus_id),
+ IPluginInstaller::RESULT_FAILED);
+
+ // extra arguments stay the same
+ EXPECT_EQ(name, "default name");
+ EXPECT_EQ(version, "1.0.0");
+ EXPECT_EQ(nexus_id, 12);
+ }
+
+ // valid simple tree
+ {
+ std::shared_ptr<IFileTree> valid_tree =
+ std::make_shared<DummyFileTree>(nullptr, "root");
+ const auto initial_tree = valid_tree;
+ valid_tree->addFile("needed-file.txt");
+ valid_tree->addFile("some file");
+ valid_tree->addDirectory("some directory")->addFile("some other file");
+
+ // valid tree
+ EXPECT_EQ(plugin->isArchiveSupported(valid_tree), true);
+ EXPECT_EQ(plugin->install(name, valid_tree, version, nexus_id),
+ IPluginInstaller::RESULT_NOTATTEMPTED);
+
+ // name is not modified
+ EXPECT_EQ(name, "default name");
+
+ // tree is not modified
+ EXPECT_EQ(valid_tree, initial_tree);
+
+ // version and ID are modified
+ EXPECT_EQ(version, "2.4.5");
+ EXPECT_EQ(nexus_id, 33);
+ }
+
+ // "complex" tree
+ {
+ std::shared_ptr<IFileTree> complex_tree =
+ std::make_shared<DummyFileTree>(nullptr, "root");
+ const auto initial_tree = complex_tree;
+ const auto needed_file = complex_tree->addFile("needed-file.txt");
+ const auto extra_file = complex_tree->addFile("extra-file.txt");
+ const auto some_file = complex_tree->addFile("some file");
+ const auto some_directory = complex_tree->addDirectory("some directory");
+ some_directory->addFile("some other file");
+
+ // valid tree
+ EXPECT_EQ(plugin->isArchiveSupported(complex_tree), true);
+ EXPECT_EQ(plugin->install(name, complex_tree, version, nexus_id),
+ IPluginInstaller::RESULT_SUCCESS);
+
+ // name is modified
+ EXPECT_EQ(name, "new name");
+
+ // tree is modified
+ EXPECT_EQ(complex_tree->findDirectory("subtree")->find("needed-file.txt"),
+ needed_file);
+ EXPECT_EQ(
+ complex_tree->findDirectory("subtree")->findDirectory("extra-file.txt"),
+ nullptr);
+ EXPECT_EQ(
+ complex_tree->findDirectory("subtree")->findDirectory("some directory"),
+ some_directory);
+
+ // version and ID are not modified (from previous call)
+ EXPECT_EQ(version, "2.4.5");
+ EXPECT_EQ(nexus_id, 33);
+ }
+}
diff --git a/libs/plugin_python/tests/runner/test_iplugin.cpp b/libs/plugin_python/tests/runner/test_iplugin.cpp
new file mode 100644
index 0000000..27c4829
--- /dev/null
+++ b/libs/plugin_python/tests/runner/test_iplugin.cpp
@@ -0,0 +1,43 @@
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <uibase/iplugin.h>
+
+#include "MockOrganizer.h"
+#include "pythonrunner.h"
+
+#include <QCoreApplication>
+
+using namespace MOBase;
+
+TEST(IPlugin, Basic)
+{
+ const auto plugins_folder = QString(std::getenv("PLUGIN_DIR"));
+
+ auto runner = mo2::python::createPythonRunner();
+ runner->initialize();
+
+ // load objects
+ const auto objects = runner->load(plugins_folder + "/dummy-iplugin.py");
+ EXPECT_EQ(objects.size(), 1);
+
+ // load the IPlugin
+ const IPlugin* plugin = qobject_cast<IPlugin*>(objects[0]);
+ EXPECT_NE(plugin, nullptr);
+
+ EXPECT_EQ(plugin->author(), "The Author");
+ EXPECT_EQ(plugin->name(), "The Name");
+ EXPECT_EQ(plugin->version(), VersionInfo(1, 3, 0));
+ EXPECT_EQ(plugin->description(), "The Description");
+
+ // settings
+ const auto settings = plugin->settings();
+ EXPECT_EQ(settings.size(), 1);
+ EXPECT_EQ(settings[0].key, "a setting");
+ EXPECT_EQ(settings[0].description, "the setting description");
+ EXPECT_EQ(settings[0].defaultValue.userType(), QMetaType::Type::Int);
+ EXPECT_EQ(settings[0].defaultValue.toInt(), 12);
+
+ // no translation, no custom implementation -> name()
+ EXPECT_EQ(plugin->localizedName(), "The Name");
+}
diff --git a/libs/plugin_python/tests/runner/test_lifetime.cpp b/libs/plugin_python/tests/runner/test_lifetime.cpp
new file mode 100644
index 0000000..bf057d3
--- /dev/null
+++ b/libs/plugin_python/tests/runner/test_lifetime.cpp
@@ -0,0 +1,47 @@
+#include "gtest/gtest.h"
+
+#include "MockOrganizer.h"
+#include "pythonrunner.h"
+
+#include <QCoreApplication>
+
+TEST(Lifetime, Plugins)
+{
+ const auto plugins_folder = QString(std::getenv("PLUGIN_DIR"));
+
+ auto runner = mo2::python::createPythonRunner();
+ runner->initialize();
+
+ {
+ const auto objects = runner->load(plugins_folder + "/dummy-iplugin.py");
+
+ // we found one plugin
+ EXPECT_EQ(objects.size(), 1);
+
+ // check that deleting the object actually destroys it
+ bool destroyed = false;
+ QObject::connect(objects[0], &QObject::destroyed, [&destroyed]() {
+ destroyed = true;
+ });
+ delete objects[0];
+ EXPECT_EQ(destroyed, true);
+ }
+
+ // same things but with a parent
+ {
+ QObject* dummy_parent = new QObject();
+ const auto objects = runner->load(plugins_folder + "/dummy-iplugin.py");
+
+ // we found one plugin
+ EXPECT_EQ(objects.size(), 1);
+ objects[0]->setParent(dummy_parent);
+
+ // check that deleting the object actually destroys it
+ bool destroyed = false;
+ QObject::connect(objects[0], &QObject::destroyed, [&destroyed]() {
+ destroyed = true;
+ });
+ delete dummy_parent;
+ EXPECT_EQ(destroyed, true);
+ }
+}