diff --git a/CMakeLists.txt b/CMakeLists.txt index 9354b95..8d7eda0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,8 @@ add_subdirectory(src/proxy) # force plugin_python to build mobase add_dependencies(plugin_python mobase) +set(PLUGIN_PYTHON_TESTS ${PLUGIN_PYTHON_TESTS} CACHE BOOL "build tests for plugin_python") + if (PLUGIN_PYTHON_TESTS) add_subdirectory(tests) endif() diff --git a/src/mobase/wrappers/basic_classes.cpp b/src/mobase/wrappers/basic_classes.cpp index 6016988..4db5848 100644 --- a/src/mobase/wrappers/basic_classes.cpp +++ b/src/mobase/wrappers/basic_classes.cpp @@ -520,7 +520,11 @@ namespace mo2::python { DWORD returnCode; bool result = o->waitForApplication((HANDLE)handle, refresh, &returnCode); - return std::make_tuple(result, returnCode); + + // we force signed return code because it's probably what's expected + // in Python + return std::make_tuple( + result, static_cast>(returnCode)); }, "handle"_a, "refresh"_a = true) diff --git a/src/mobase/wrappers/pyplugins.cpp b/src/mobase/wrappers/pyplugins.cpp index 9d2103b..36f11ba 100644 --- a/src/mobase/wrappers/pyplugins.cpp +++ b/src/mobase/wrappers/pyplugins.cpp @@ -89,7 +89,8 @@ namespace mo2::python { .def("nexusGameID", &IPluginGame::nexusGameID) .def("looksValid", &IPluginGame::looksValid, "directory"_a) .def("gameVersion", &IPluginGame::gameVersion) - .def("getLauncherName", &IPluginGame::getLauncherName); + .def("getLauncherName", &IPluginGame::getLauncherName) + .def("getSupportURL", &IPluginGame::getSupportURL); } // multiple installers diff --git a/src/mobase/wrappers/pyplugins.h b/src/mobase/wrappers/pyplugins.h index c1df5f5..0610f16 100644 --- a/src/mobase/wrappers/pyplugins.h +++ b/src/mobase/wrappers/pyplugins.h @@ -479,6 +479,10 @@ namespace mo2::python { { PYBIND11_OVERRIDE_PURE(QString, IPluginGame, getLauncherName, ); } + QString getSupportURL() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, getSupportURL, ); + } protected: std::map featureList() const override; diff --git a/src/pybind11-qt/pybind11_qt_basic.cpp b/src/pybind11-qt/pybind11_qt_basic.cpp index 488f1c2..fbd3ac2 100644 --- a/src/pybind11-qt/pybind11_qt_basic.cpp +++ b/src/pybind11-qt/pybind11_qt_basic.cpp @@ -139,9 +139,8 @@ namespace pybind11::detail { return type_caster::cast(var.toStringList(), policy, parent); case QVariant::List: return type_caster::cast(var.toList(), policy, parent); - case QVariant::Map: // return - // type_caster::cast(var.toList(), - // policy, parent); + case QVariant::Map: + return type_caster::cast(var.toMap(), policy, parent); default: { PyErr_Format(PyExc_TypeError, "type unsupported: %d", var.type()); throw pybind11::error_already_set(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c8bb5f8..0141184 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,46 +1,4 @@ cmake_minimum_required(VERSION 3.16) -set(PYLIB_DIR ${CMAKE_CURRENT_BINARY_DIR}/pylibs) - -if (TARGET uibase) - set(UIBASE_PATH $) -else() - set(UIBASE_PATH "${MO2_INSTALL_PATH}/bin/uibase.dll") -endif() - -add_custom_target(pytest - ${CMAKE_COMMAND} -E env - PYTHONPATH="${PYLIB_DIR}\\;$" - "UIBASE_PATH=${UIBASE_PATH}" "QT_ROOT=${QT_ROOT}" - ${CMAKE_CURRENT_BINARY_DIR}/pylibs/bin/pytest.exe ${CMAKE_CURRENT_SOURCE_DIR}/../tests -s - WORKING_DIRECTORY ${MO2_INSTALL_PATH} - -) -mo2_python_pip_install(pytest - DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pylibs - PACKAGES pytest PyQt6==6.3.0) -add_dependencies(pytest mobase) - - -file(GLOB test_files CONFIGURE_DEPENDS "test_*.cpp") -foreach (test_file ${test_files}) - get_filename_component(target ${test_file} NAME_WLE) - string(REPLACE "test_" "" pymodule ${target}) - pybind11_add_module(${target} THIN_LTO ${test_file}) - set_target_properties(${target} - PROPERTIES - OUTPUT_NAME ${pymodule} - LIBRARY_OUTPUT_DIRECTORY "${PYLIB_DIR}/mobase_tests") - - if(DEFINED CMAKE_CONFIGURATION_TYPES) - foreach(config ${CMAKE_CONFIGURATION_TYPES}) - string(TOUPPER ${config} config) - set_target_properties(${target} PROPERTIES - LIBRARY_OUTPUT_DIRECTORY_${config} "${PYLIB_DIR}/mobase_tests") - endforeach() - endif() - - mo2_add_dependencies(${target} PRIVATE uibase Qt::Core Qt::Widgets) - target_link_libraries(${target} PRIVATE pybind11::qt) - add_dependencies(pytest ${target}) -endforeach() +add_subdirectory(python) +add_subdirectory(runner) diff --git a/tests/mocks/MockOrganizer.h b/tests/mocks/MockOrganizer.h new file mode 100644 index 0000000..316ba9e --- /dev/null +++ b/tests/mocks/MockOrganizer.h @@ -0,0 +1,56 @@ +#include "imoinfo.h" +#include + +using namespace MOBase; + +class MockOrganizer : public IOrganizer { +public: + // clang-format off + MOCK_METHOD(IModRepositoryBridge*, createNexusBridge, (), (const, override)); + MOCK_METHOD(QString, profileName, (), (const, override)); + MOCK_METHOD(QString, profilePath, (), (const, override)); + MOCK_METHOD(QString, downloadsPath, (), (const, override)); + MOCK_METHOD(QString, overwritePath, (), (const, override)); + MOCK_METHOD(QString, basePath, (), (const, override)); + MOCK_METHOD(QString, modsPath, (), (const, override)); + MOCK_METHOD(VersionInfo, appVersion, (), (const, override)); + MOCK_METHOD(IModInterface*, createMod, (GuessedValue &name), (override)); + MOCK_METHOD(IPluginGame*, getGame, (const QString &gameName), (const, override)); + MOCK_METHOD(void, modDataChanged, (IModInterface *mod), (override)); + MOCK_METHOD(QVariant, pluginSetting, (const QString &pluginName, const QString &key), (const, override)); + MOCK_METHOD(void, setPluginSetting, (const QString &pluginName, const QString &key, const QVariant &value), (override)); + MOCK_METHOD(bool, isPluginEnabled, (const QString& pluginName), (const, override)); + MOCK_METHOD(bool, isPluginEnabled, (IPlugin *plugin), (const, override)); + MOCK_METHOD(QVariant, persistent, (const QString &pluginName, const QString &key, const QVariant &def), (const, override)); + MOCK_METHOD(void, setPersistent, (const QString &pluginName, const QString &key, const QVariant &value, bool sync), (override)); + MOCK_METHOD(QString, pluginDataPath, (), (const, override)); + MOCK_METHOD(IModInterface*, installMod, (const QString &fileName, const QString &nameSuggestion), (override)); + MOCK_METHOD(QString, resolvePath, (const QString &fileName), (const, override)); + MOCK_METHOD(QStringList, listDirectories, (const QString &directoryName), (const, override)); + MOCK_METHOD(QStringList, findFiles, (const QString &path, const std::function &filter), (const, override)); + MOCK_METHOD(QStringList, findFiles, (const QString &path, const QStringList &filter), (const, override)); + MOCK_METHOD(QStringList, getFileOrigins, (const QString &fileName) ,(const, override)); + MOCK_METHOD(QList, findFileInfos, (const QString &path, const std::function &filter), (const, override)); + MOCK_METHOD(std::shared_ptr, virtualFileTree, (), (const, override)); + MOCK_METHOD(MOBase::IDownloadManager*, downloadManager, (), (const, override)); + MOCK_METHOD(MOBase::IPluginList*, pluginList, (), (const, override)); + MOCK_METHOD(MOBase::IModList*, modList, (), (const, override)); + MOCK_METHOD(MOBase::IProfile*, profile, (), (const, override)); + MOCK_METHOD(HANDLE, startApplication, (const QString &executable, const QStringList &args, const QString &cwd, const QString &profile, const QString &forcedCustomOverwrite, bool ignoreCustomOverwrite), (override)); + MOCK_METHOD(bool, waitForApplication, (HANDLE handle, bool refresh, LPDWORD exitCode), (const, override)); + MOCK_METHOD(bool, onAboutToRun, (const std::function &func), (override)); + MOCK_METHOD(bool, onFinishedRun, (const std::function &func), (override)); + MOCK_METHOD(void, refresh, (bool saveChanges), (override)); + MOCK_METHOD(MOBase::IPluginGame const *, managedGame, (), (const, override)); + MOCK_METHOD(bool, onUserInterfaceInitialized, (const std::function &), (override)); + MOCK_METHOD(bool, onProfileCreated, (const std::function&), (override)); + MOCK_METHOD(bool, onProfileRemoved, (const std::function&), (override)); + MOCK_METHOD(bool, onProfileRenamed, (const std::function&), (override)); + MOCK_METHOD(bool, onProfileChanged, (const std::function &), (override)); + MOCK_METHOD(bool, onPluginSettingChanged, (const std::function &), (override)); + MOCK_METHOD(bool, onPluginEnabled, (const std::function&), (override)); + MOCK_METHOD(bool, onPluginEnabled, (const QString&, const std::function&), (override)); + MOCK_METHOD(bool, onPluginDisabled, (const std::function&), (override)); + MOCK_METHOD(bool, onPluginDisabled, (const QString&, const std::function&), (override)); + // clang-format on +}; diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt new file mode 100644 index 0000000..15b24d1 --- /dev/null +++ b/tests/python/CMakeLists.txt @@ -0,0 +1,54 @@ +cmake_minimum_required(VERSION 3.16) + +# pytest +find_package(GTest REQUIRED) + +set(PYLIB_DIR ${CMAKE_CURRENT_BINARY_DIR}/pylibs) + +if (TARGET uibase) + set(UIBASE_PATH $) +else() + set(UIBASE_PATH "${MO2_INSTALL_PATH}/bin/uibase.dll") +endif() + +add_custom_target(pytest + ${CMAKE_COMMAND} -E env + PYTHONPATH="${PYLIB_DIR}\\;$" + "UIBASE_PATH=${UIBASE_PATH}" "QT_ROOT=${QT_ROOT}" + ${CMAKE_CURRENT_BINARY_DIR}/pylibs/bin/pytest.exe ${CMAKE_CURRENT_SOURCE_DIR} -s + WORKING_DIRECTORY ${MO2_INSTALL_PATH} + +) +mo2_python_pip_install(pytest + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pylibs + PACKAGES pytest PyQt6==6.3.0) +add_dependencies(pytest mobase) + + +file(GLOB test_files CONFIGURE_DEPENDS "test_*.cpp") +foreach (test_file ${test_files}) + get_filename_component(target ${test_file} NAME_WLE) + + string(REPLACE "test_" "" pymodule ${target}) + pybind11_add_module(${target} THIN_LTO ${test_file}) + set_target_properties(${target} + PROPERTIES + OUTPUT_NAME ${pymodule} + LIBRARY_OUTPUT_DIRECTORY "${PYLIB_DIR}/mobase_tests") + + if(DEFINED CMAKE_CONFIGURATION_TYPES) + foreach(config ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER ${config} config) + set_target_properties(${target} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY_${config} "${PYLIB_DIR}/mobase_tests") + endforeach() + endif() + + mo2_add_dependencies(${target} PRIVATE uibase Qt::Core Qt::Widgets) + # set_property(TARGET ${target} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") + target_link_libraries(${target} PRIVATE pybind11::qt GTest::gmock) + + target_include_directories(${target} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../mocks) + add_dependencies(pytest ${target}) +endforeach() diff --git a/tests/conftest.py b/tests/python/conftest.py similarity index 100% rename from tests/conftest.py rename to tests/python/conftest.py diff --git a/tests/test_guessed_string.cpp b/tests/python/test_guessed_string.cpp similarity index 100% rename from tests/test_guessed_string.cpp rename to tests/python/test_guessed_string.cpp diff --git a/tests/test_guessed_string.py b/tests/python/test_guessed_string.py similarity index 100% rename from tests/test_guessed_string.py rename to tests/python/test_guessed_string.py diff --git a/tests/python/test_organizer.cpp b/tests/python/test_organizer.cpp new file mode 100644 index 0000000..55b6e9b --- /dev/null +++ b/tests/python/test_organizer.cpp @@ -0,0 +1,43 @@ +#include "pybind11_qt/pybind11_qt.h" + +#include + +#include +#include + +#include "MockOrganizer.h" + +namespace py = pybind11; +using namespace pybind11::literals; +using ::testing::NiceMock; + +PYBIND11_MODULE(organizer, m) +{ + m.def( + "organizer", + []() -> IOrganizer* { + MockOrganizer* mock = new NiceMock(); + ON_CALL(*mock, profileName).WillByDefault([&mock]() { + return "profile"; + }); + const auto handle = (HANDLE)std::uintptr_t{4654}; + ON_CALL(*mock, startApplication).WillByDefault([&mock, handle](auto&&...) { + return handle; + }); + ON_CALL(*mock, waitForApplication) + .WillByDefault([&mock, original_handle = handle]( + HANDLE handle, bool refresh, LPDWORD exitCode) { + if (handle == original_handle) { + *exitCode = 0; + return true; + } + else { + *exitCode = -1; + return false; + } + }); + + return mock; + }, + py::return_value_policy::take_ownership); +} diff --git a/tests/python/test_organizer.py b/tests/python/test_organizer.py new file mode 100644 index 0000000..9fcdcc9 --- /dev/null +++ b/tests/python/test_organizer.py @@ -0,0 +1,12 @@ +import pytest +from PyQt6.QtWidgets import QWidget + +m = pytest.importorskip("mobase_tests.organizer") + + +def test_getters(): + o = m.organizer() + assert o.profileName() == "profile" + assert o.startApplication("") == 4654 + assert o.waitForApplication(42) == (False, -1) + assert o.waitForApplication(4654) == (True, 0) diff --git a/tests/test_qt.cpp b/tests/python/test_qt.cpp similarity index 100% rename from tests/test_qt.cpp rename to tests/python/test_qt.cpp diff --git a/tests/test_qt.py b/tests/python/test_qt.py similarity index 100% rename from tests/test_qt.py rename to tests/python/test_qt.py diff --git a/tests/test_qt_widgets.cpp b/tests/python/test_qt_widgets.cpp similarity index 99% rename from tests/test_qt_widgets.cpp rename to tests/python/test_qt_widgets.cpp index 245b3f2..30c39a3 100644 --- a/tests/test_qt_widgets.cpp +++ b/tests/python/test_qt_widgets.cpp @@ -5,8 +5,6 @@ #include #include -#include - namespace py = pybind11; using namespace pybind11::literals; diff --git a/tests/test_qt_widgets.py b/tests/python/test_qt_widgets.py similarity index 100% rename from tests/test_qt_widgets.py rename to tests/python/test_qt_widgets.py diff --git a/tests/runner/CMakeLists.txt b/tests/runner/CMakeLists.txt new file mode 100644 index 0000000..feb510a --- /dev/null +++ b/tests/runner/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) + +add_executable(pythonrunner-tests) +mo2_configure_tests(pythonrunner-tests + WARNINGS OFF) +mo2_add_dependencies(pythonrunner-tests PUBLIC uibase) +target_include_directories(pythonrunner-tests + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../mocks) diff --git a/tests/runner/test_organizer.cpp b/tests/runner/test_organizer.cpp new file mode 100644 index 0000000..ddd5cae --- /dev/null +++ b/tests/runner/test_organizer.cpp @@ -0,0 +1,13 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "MockOrganizer.h" + +using ::testing::Eq; +using ::testing::NaggyMock; +using ::testing::Return; + +TEST(Organizer, Basic) +{ + MockOrganizer mock; +}