diff options
Diffstat (limited to 'libs/plugin_python/tests/runner')
| -rw-r--r-- | libs/plugin_python/tests/runner/CMakeLists.txt | 65 | ||||
| -rw-r--r-- | libs/plugin_python/tests/runner/plugins/dummy-diagnose.py | 37 | ||||
| -rw-r--r-- | libs/plugin_python/tests/runner/plugins/dummy-filemapper.py | 32 | ||||
| -rw-r--r-- | libs/plugin_python/tests/runner/plugins/dummy-game.py | 45 | ||||
| -rw-r--r-- | libs/plugin_python/tests/runner/plugins/dummy-installer.py | 46 | ||||
| -rw-r--r-- | libs/plugin_python/tests/runner/plugins/dummy-iplugin.py | 29 | ||||
| -rw-r--r-- | libs/plugin_python/tests/runner/test_diagnose.cpp | 61 | ||||
| -rw-r--r-- | libs/plugin_python/tests/runner/test_filemapper.cpp | 64 | ||||
| -rw-r--r-- | libs/plugin_python/tests/runner/test_game.cpp | 28 | ||||
| -rw-r--r-- | libs/plugin_python/tests/runner/test_installer.cpp | 112 | ||||
| -rw-r--r-- | libs/plugin_python/tests/runner/test_iplugin.cpp | 43 | ||||
| -rw-r--r-- | libs/plugin_python/tests/runner/test_lifetime.cpp | 47 |
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); + } +} |
