Addon Manager: Refactor installation code
Improve testability of installation code by refactoring it to completely separate the GUI and non-GUI code, and to provide more robust support for non-GUI access to some type of Addon Manager activity.
This commit is contained in:
22
src/Mod/AddonManager/AddonManagerTest/gui/gui_mocks.py
Normal file
22
src/Mod/AddonManager/AddonManagerTest/gui/gui_mocks.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
750
src/Mod/AddonManager/AddonManagerTest/gui/test_installer_gui.py
Normal file
750
src/Mod/AddonManager/AddonManagerTest/gui/test_installer_gui.py
Normal file
@@ -0,0 +1,750 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
import FreeCAD
|
||||
|
||||
from PySide import QtCore, QtWidgets
|
||||
|
||||
from addonmanager_installer_gui import AddonInstallerGUI, MacroInstallerGUI
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class DialogWatcher(QtCore.QObject):
|
||||
def __init__(self, dialog_to_watch_for, button):
|
||||
super().__init__()
|
||||
self.dialog_found = False
|
||||
self.has_run = False
|
||||
self.dialog_to_watch_for = dialog_to_watch_for
|
||||
self.button = button
|
||||
|
||||
def run(self):
|
||||
widget = QtWidgets.QApplication.activeModalWidget()
|
||||
if widget:
|
||||
# Is this the widget we are looking for?
|
||||
if (
|
||||
hasattr(widget, "windowTitle")
|
||||
and callable(widget.windowTitle)
|
||||
and widget.windowTitle() == self.dialog_to_watch_for
|
||||
):
|
||||
# Found the dialog we are looking for: now try to "click" the appropriate button
|
||||
self.click_button(widget)
|
||||
self.dialog_found = True
|
||||
self.has_run = True
|
||||
|
||||
def click_button(self, widget):
|
||||
buttons = widget.findChildren(QtWidgets.QPushButton)
|
||||
for button in buttons:
|
||||
text = button.text().replace("&", "")
|
||||
if text == self.button:
|
||||
button.click()
|
||||
|
||||
|
||||
class DialogInteractor(DialogWatcher):
|
||||
def __init__(self, dialog_to_watch_for, interaction):
|
||||
"""Takes the title of the dialog, a button string, and a callable."""
|
||||
super().__init__(dialog_to_watch_for, None)
|
||||
self.interaction = interaction
|
||||
|
||||
def run(self):
|
||||
widget = QtWidgets.QApplication.activeModalWidget()
|
||||
if widget:
|
||||
# Is this the widget we are looking for?
|
||||
if (
|
||||
hasattr(widget, "windowTitle")
|
||||
and callable(widget.windowTitle)
|
||||
and widget.windowTitle() == self.dialog_to_watch_for
|
||||
):
|
||||
# Found the dialog we are looking for: now try to "click" the appropriate button
|
||||
self.dialog_found = True
|
||||
if self.dialog_found:
|
||||
self.has_run = True
|
||||
if self.interaction is not None and callable(self.interaction):
|
||||
self.interaction(widget)
|
||||
|
||||
|
||||
class TestInstallerGui(unittest.TestCase):
|
||||
|
||||
MODULE = "test_installer_gui" # file name without extension
|
||||
|
||||
class MockAddon:
|
||||
def __init__(self):
|
||||
test_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
self.name = "MockAddon"
|
||||
self.display_name = "Mock Addon"
|
||||
self.url = os.path.join(test_dir, "test_simple_repo.zip")
|
||||
self.branch = "main"
|
||||
|
||||
def setUp(self):
|
||||
self.addon_to_install = TestInstallerGui.MockAddon()
|
||||
self.installer_gui = AddonInstallerGUI(self.addon_to_install)
|
||||
self.finalized_thread = False
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_success_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to an OK click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Success"), translate("AddonsInstaller", "OK")
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._installation_succeeded()
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_failure_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to a Cancel click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Installation Failed"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._installation_failed(
|
||||
self.addon_to_install, "Test of installation failure"
|
||||
)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_no_python_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to a No click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Cannot execute Python"),
|
||||
translate("AddonsInstaller", "No"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._report_no_python_exe()
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_no_pip_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to a No click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Cannot execute pip"),
|
||||
translate("AddonsInstaller", "No"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._report_no_pip("pip not actually run, this was a test")
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_dependency_failure_dialog(self):
|
||||
# Pop the modal dialog and verify that it opens, and responds to a No click
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Package installation failed"),
|
||||
translate("AddonsInstaller", "No"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._report_dependency_failure(
|
||||
"Unit test", "Nothing really failed, this is a test of the dialog box"
|
||||
)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_install(self):
|
||||
# Run the installation code and make sure it puts the directory in place
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
self.installer_gui.installer.installation_path = temp_dir
|
||||
self.installer_gui.install() # This does not block
|
||||
self.installer_gui.installer.success.disconnect(
|
||||
self.installer_gui._installation_succeeded
|
||||
)
|
||||
self.installer_gui.installer.failure.disconnect(
|
||||
self.installer_gui._installation_failed
|
||||
)
|
||||
while not self.installer_gui.worker_thread.isFinished():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.assertTrue(
|
||||
os.path.exists(os.path.join(temp_dir, "MockAddon")),
|
||||
"Installed directory not found",
|
||||
)
|
||||
|
||||
def test_handle_disallowed_python(self):
|
||||
disallowed_packages = ["disallowed_package_name"]
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Missing Requirement"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._handle_disallowed_python(disallowed_packages)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_handle_disallowed_python_long_list(self):
|
||||
"""A separate test for when there are MANY packages, which takes a separate code path."""
|
||||
disallowed_packages = []
|
||||
for i in range(50):
|
||||
disallowed_packages.append(f"disallowed_package_name_{i}")
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Missing Requirement"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._handle_disallowed_python(disallowed_packages)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_report_missing_workbenches_single(self):
|
||||
"""Test only missing one workbench"""
|
||||
wbs = ["OneMissingWorkbench"]
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Missing Requirement"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._report_missing_workbenches(wbs)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_report_missing_workbenches_multiple(self):
|
||||
"""Test only missing one workbench"""
|
||||
wbs = ["FirstMissingWorkbench", "SecondMissingWorkbench"]
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Missing Requirement"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._report_missing_workbenches(wbs)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_resolve_dependencies_then_install(self):
|
||||
class MissingDependenciesMock:
|
||||
def __init__(self):
|
||||
self.external_addons = ["addon_1", "addon_2"]
|
||||
self.python_requires = ["py_req_1", "py_req_2"]
|
||||
self.python_optional = ["py_opt_1", "py_opt_2"]
|
||||
|
||||
missing = MissingDependenciesMock()
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Resolve Dependencies"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer_gui._resolve_dependencies_then_install(missing)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
|
||||
def test_check_python_version_bad(self):
|
||||
class MissingDependenciesMock:
|
||||
def __init__(self):
|
||||
self.python_min_version = {"major": 3, "minor": 9999}
|
||||
|
||||
missing = MissingDependenciesMock()
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Incompatible Python version"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
stop_installing = self.installer_gui._check_python_version(missing)
|
||||
self.assertTrue(
|
||||
dialog_watcher.dialog_found, "Failed to find the expected dialog box"
|
||||
)
|
||||
self.assertTrue(
|
||||
stop_installing, "Failed to halt installation on bad Python version"
|
||||
)
|
||||
|
||||
def test_check_python_version_good(self):
|
||||
class MissingDependenciesMock:
|
||||
def __init__(self):
|
||||
self.python_min_version = {"major": 3, "minor": 0}
|
||||
|
||||
missing = MissingDependenciesMock()
|
||||
stop_installing = self.installer_gui._check_python_version(missing)
|
||||
self.assertFalse(
|
||||
stop_installing, "Failed to continue installation on good Python version"
|
||||
)
|
||||
|
||||
def test_clean_up_optional(self):
|
||||
class MissingDependenciesMock:
|
||||
def __init__(self):
|
||||
self.python_optional = [
|
||||
"allowed_packages_1",
|
||||
"allowed_packages_2",
|
||||
"disallowed_package",
|
||||
]
|
||||
|
||||
allowed_packages = ["allowed_packages_1", "allowed_packages_2"]
|
||||
missing = MissingDependenciesMock()
|
||||
self.installer_gui.installer.allowed_packages = set(allowed_packages)
|
||||
self.installer_gui._clean_up_optional(missing)
|
||||
self.assertTrue("allowed_packages_1" in missing.python_optional)
|
||||
self.assertTrue("allowed_packages_2" in missing.python_optional)
|
||||
self.assertFalse("disallowed_package" in missing.python_optional)
|
||||
|
||||
def intercept_run_dependency_installer(
|
||||
self, addons, python_requires, python_optional
|
||||
):
|
||||
self.assertEqual(python_requires, ["py_req_1", "py_req_2"])
|
||||
self.assertEqual(python_optional, ["py_opt_1", "py_opt_2"])
|
||||
self.assertEqual(addons[0].name, "addon_1")
|
||||
self.assertEqual(addons[1].name, "addon_2")
|
||||
|
||||
def test_dependency_dialog_yes_clicked(self):
|
||||
class DialogMock:
|
||||
class ListWidgetMock:
|
||||
class ListWidgetItemMock:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def text(self):
|
||||
return self.name
|
||||
|
||||
def checkState(self):
|
||||
return QtCore.Qt.Checked
|
||||
|
||||
def __init__(self, items):
|
||||
self.list = []
|
||||
for item in items:
|
||||
self.list.append(
|
||||
DialogMock.ListWidgetMock.ListWidgetItemMock(item)
|
||||
)
|
||||
|
||||
def count(self):
|
||||
return len(self.list)
|
||||
|
||||
def item(self, i):
|
||||
return self.list[i]
|
||||
|
||||
def __init__(self):
|
||||
self.listWidgetAddons = DialogMock.ListWidgetMock(
|
||||
["addon_1", "addon_2"]
|
||||
)
|
||||
self.listWidgetPythonRequired = DialogMock.ListWidgetMock(
|
||||
["py_req_1", "py_req_2"]
|
||||
)
|
||||
self.listWidgetPythonOptional = DialogMock.ListWidgetMock(
|
||||
["py_opt_1", "py_opt_2"]
|
||||
)
|
||||
|
||||
class AddonMock:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
self.installer_gui.dependency_dialog = DialogMock()
|
||||
self.installer_gui.addons = [AddonMock("addon_1"), AddonMock("addon_2")]
|
||||
self.installer_gui._run_dependency_installer = (
|
||||
self.intercept_run_dependency_installer
|
||||
)
|
||||
self.installer_gui._dependency_dialog_yes_clicked()
|
||||
|
||||
|
||||
class TestMacroInstallerGui(unittest.TestCase):
|
||||
class MockMacroAddon:
|
||||
class MockMacro:
|
||||
def __init__(self):
|
||||
self.install_called = False
|
||||
self.install_result = (
|
||||
True # External code can change to False to test failed install
|
||||
)
|
||||
self.name = "MockMacro"
|
||||
self.filename = "mock_macro_no_real_file.FCMacro"
|
||||
self.comment = "This is a mock macro for unit testing"
|
||||
self.icon = None
|
||||
self.xpm = None
|
||||
|
||||
def install(self):
|
||||
self.install_called = True
|
||||
return self.install_result
|
||||
|
||||
def __init__(self):
|
||||
self.macro = TestMacroInstallerGui.MockMacroAddon.MockMacro()
|
||||
self.name = self.macro.name
|
||||
self.display_name = self.macro.name
|
||||
|
||||
class MockParameter:
|
||||
"""Mock the parameter group to allow simplified behavior and introspection."""
|
||||
|
||||
def __init__(self):
|
||||
self.params = {}
|
||||
self.groups = {}
|
||||
self.accessed_parameters = {} # Dict is param name: default value
|
||||
|
||||
types = ["Bool", "String", "Int", "UInt", "Float"]
|
||||
for t in types:
|
||||
setattr(self, f"Get{t}", self.get)
|
||||
setattr(self, f"Set{t}", self.set)
|
||||
setattr(self, f"Rem{t}", self.rem)
|
||||
|
||||
def get(self, p, default=None):
|
||||
self.accessed_parameters[p] = default
|
||||
if p in self.params:
|
||||
return self.params[p]
|
||||
else:
|
||||
return default
|
||||
|
||||
def set(self, p, value):
|
||||
self.params[p] = value
|
||||
|
||||
def rem(self, p):
|
||||
if p in self.params:
|
||||
self.params.erase(p)
|
||||
|
||||
def GetGroup(self, name):
|
||||
if name not in self.groups:
|
||||
self.groups[name] = TestMacroInstallerGui.MockParameter()
|
||||
return self.groups[name]
|
||||
|
||||
def GetGroups(self):
|
||||
return self.groups.keys()
|
||||
|
||||
class ToolbarIntercepter:
|
||||
def __init__(self):
|
||||
self.ask_for_toolbar_called = False
|
||||
self.install_macro_to_toolbar_called = False
|
||||
self.tb = None
|
||||
self.custom_group = TestMacroInstallerGui.MockParameter()
|
||||
self.custom_group.set("Name", "MockCustomToolbar")
|
||||
|
||||
def _ask_for_toolbar(self, _):
|
||||
self.ask_for_toolbar_called = True
|
||||
return self.custom_group
|
||||
|
||||
def _install_macro_to_toolbar(self, tb):
|
||||
self.install_macro_to_toolbar_called = True
|
||||
self.tb = tb
|
||||
|
||||
class InstallerInterceptor:
|
||||
def __init__(self):
|
||||
self.ccc_called = False
|
||||
|
||||
def _create_custom_command(
|
||||
self,
|
||||
toolbar,
|
||||
filename,
|
||||
menuText,
|
||||
tooltipText,
|
||||
whatsThisText,
|
||||
statustipText,
|
||||
pixmapText,
|
||||
):
|
||||
self.ccc_called = True
|
||||
self.toolbar = toolbar
|
||||
self.filename = filename
|
||||
self.menuText = menuText
|
||||
self.tooltipText = tooltipText
|
||||
self.whatsThisText = whatsThisText
|
||||
self.statustipText = statustipText
|
||||
self.pixmapText = pixmapText
|
||||
|
||||
def setUp(self):
|
||||
self.mock_macro = TestMacroInstallerGui.MockMacroAddon()
|
||||
self.installer = MacroInstallerGUI(self.mock_macro)
|
||||
self.installer.addon_params = TestMacroInstallerGui.MockParameter()
|
||||
self.installer.toolbar_params = TestMacroInstallerGui.MockParameter()
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_ask_for_toolbar_no_dialog_default_exists(self):
|
||||
self.installer.addon_params.set("alwaysAskForToolbar", False)
|
||||
self.installer.addon_params.set("CustomToolbarName", "UnitTestCustomToolbar")
|
||||
utct = self.installer.toolbar_params.GetGroup("UnitTestCustomToolbar")
|
||||
utct.set("Name", "UnitTestCustomToolbar")
|
||||
utct.set("Active", True)
|
||||
result = self.installer._ask_for_toolbar([])
|
||||
self.assertIsNotNone(result)
|
||||
self.assertTrue(hasattr(result, "get"))
|
||||
name = result.get("Name")
|
||||
self.assertEqual(name, "UnitTestCustomToolbar")
|
||||
|
||||
def test_ask_for_toolbar_with_dialog_cancelled(self):
|
||||
|
||||
# First test: the user cancels the dialog
|
||||
self.installer.addon_params.set("alwaysAskForToolbar", True)
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Select Toolbar"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
result = self.installer._ask_for_toolbar([])
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_ask_for_toolbar_with_dialog_defaults(self):
|
||||
|
||||
# Second test: the user leaves the dialog at all default values, so:
|
||||
# - The checkbox "Ask every time" is unchecked
|
||||
# - The selected toolbar option is "Create new toolbar", which triggers a search for
|
||||
# a new custom toolbar name by calling _create_new_custom_toolbar, which we mock.
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Select Toolbar"),
|
||||
translate("AddonsInstaller", "Cancel"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
fake_custom_toolbar_group = TestMacroInstallerGui.MockParameter()
|
||||
fake_custom_toolbar_group.set("Name", "UnitTestCustomToolbar")
|
||||
self.installer._create_new_custom_toolbar = lambda: fake_custom_toolbar_group
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Select Toolbar"),
|
||||
translate("AddonsInstaller", "OK"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
result = self.installer._ask_for_toolbar([])
|
||||
self.assertIsNotNone(result)
|
||||
self.assertTrue(hasattr(result, "get"))
|
||||
name = result.get("Name")
|
||||
self.assertEqual(name, "UnitTestCustomToolbar")
|
||||
self.assertIn("alwaysAskForToolbar", self.installer.addon_params.params)
|
||||
self.assertFalse(self.installer.addon_params.get("alwaysAskForToolbar", True))
|
||||
|
||||
def test_ask_for_toolbar_with_dialog_selection(self):
|
||||
|
||||
# Third test: the user selects a custom toolbar in the dialog, and checks the box to always
|
||||
# ask.
|
||||
dialog_interactor = DialogInteractor(
|
||||
translate("AddonsInstaller", "Select Toolbar"),
|
||||
self.interactor_selection_option_and_checkbox,
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_interactor.run)
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_2 = self.installer.toolbar_params.GetGroup("UT_TB_2")
|
||||
ut_tb_3 = self.installer.toolbar_params.GetGroup("UT_TB_3")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ut_tb_2.set("Name", "UT_TB_2")
|
||||
ut_tb_3.set("Name", "UT_TB_3")
|
||||
result = self.installer._ask_for_toolbar(["UT_TB_1", "UT_TB_2", "UT_TB_3"])
|
||||
self.assertIsNotNone(result)
|
||||
self.assertTrue(hasattr(result, "get"))
|
||||
name = result.get("Name")
|
||||
self.assertEqual(name, "UT_TB_3")
|
||||
self.assertIn("alwaysAskForToolbar", self.installer.addon_params.params)
|
||||
self.assertTrue(self.installer.addon_params.get("alwaysAskForToolbar", False))
|
||||
|
||||
def interactor_selection_option_and_checkbox(self, parent):
|
||||
|
||||
boxes = parent.findChildren(QtWidgets.QComboBox)
|
||||
self.assertEqual(len(boxes), 1) # Just to make sure...
|
||||
box = boxes[0]
|
||||
box.setCurrentIndex(box.count() - 2) # Select the last thing but one
|
||||
|
||||
checkboxes = parent.findChildren(QtWidgets.QCheckBox)
|
||||
self.assertEqual(len(checkboxes), 1) # Just to make sure...
|
||||
checkbox = checkboxes[0]
|
||||
checkbox.setChecked(True)
|
||||
|
||||
parent.accept()
|
||||
|
||||
def test_macro_button_exists_no_command(self):
|
||||
# Test 1: No command for this macro
|
||||
self.installer._find_custom_command = lambda _: None
|
||||
button_exists = self.installer._macro_button_exists()
|
||||
self.assertFalse(button_exists)
|
||||
|
||||
def test_macro_button_exists_true(self):
|
||||
# Test 2: Macro is in the list of buttons
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UnitTestCommand")
|
||||
ut_tb_1.set(
|
||||
"UnitTestCommand", "FreeCAD"
|
||||
) # This is what the real thing looks like...
|
||||
self.installer._find_custom_command = lambda _: "UnitTestCommand"
|
||||
self.assertTrue(self.installer._macro_button_exists())
|
||||
|
||||
def test_macro_button_exists_false(self):
|
||||
# Test 3: Macro is not in the list of buttons
|
||||
self.installer._find_custom_command = lambda _: "UnitTestCommand"
|
||||
self.assertFalse(self.installer._macro_button_exists())
|
||||
|
||||
def test_ask_to_install_toolbar_button_disabled(self):
|
||||
self.installer.addon_params.SetBool("dontShowAddMacroButtonDialog", True)
|
||||
self.installer._ask_to_install_toolbar_button()
|
||||
# This should NOT block when dontShowAddMacroButtonDialog is True
|
||||
|
||||
def test_ask_to_install_toolbar_button_enabled_no(self):
|
||||
self.installer.addon_params.SetBool("dontShowAddMacroButtonDialog", False)
|
||||
dialog_watcher = DialogWatcher(
|
||||
translate("AddonsInstaller", "Add button?"),
|
||||
translate("AddonsInstaller", "No"),
|
||||
)
|
||||
QtCore.QTimer.singleShot(10, dialog_watcher.run)
|
||||
self.installer._ask_to_install_toolbar_button() # Blocks until killed by watcher
|
||||
self.assertTrue(dialog_watcher.dialog_found)
|
||||
|
||||
def test_get_toolbar_with_name_found(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UnitTestToolbar")
|
||||
ut_tb_1.set("Name", "Unit Test Toolbar")
|
||||
ut_tb_1.set("UnitTestParam", True)
|
||||
tb = self.installer._get_toolbar_with_name("Unit Test Toolbar")
|
||||
self.assertIsNotNone(tb)
|
||||
self.assertTrue(tb.get("UnitTestParam", False))
|
||||
|
||||
def test_get_toolbar_with_name_not_found(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UnitTestToolbar")
|
||||
ut_tb_1.set("Name", "Not the Unit Test Toolbar")
|
||||
tb = self.installer._get_toolbar_with_name("Unit Test Toolbar")
|
||||
self.assertIsNone(tb)
|
||||
|
||||
def test_create_new_custom_toolbar_no_existing(self):
|
||||
tb = self.installer._create_new_custom_toolbar()
|
||||
self.assertEqual(tb.get("Name", ""), "Auto-Created Macro Toolbar")
|
||||
self.assertTrue(tb.get("Active", False), True)
|
||||
|
||||
def test_create_new_custom_toolbar_one_existing(self):
|
||||
_ = self.installer._create_new_custom_toolbar()
|
||||
tb = self.installer._create_new_custom_toolbar()
|
||||
self.assertEqual(tb.get("Name", ""), "Auto-Created Macro Toolbar (2)")
|
||||
self.assertTrue(tb.get("Active", False), True)
|
||||
|
||||
def test_check_for_toolbar_true(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
self.assertTrue(self.installer._check_for_toolbar("UT_TB_1"))
|
||||
|
||||
def test_check_for_toolbar_false(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
self.assertFalse(self.installer._check_for_toolbar("Not UT_TB_1"))
|
||||
|
||||
def test_install_toolbar_button_first_custom_toolbar(self):
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertFalse(tbi.ask_for_toolbar_called)
|
||||
self.assertTrue("Custom_1" in self.installer.toolbar_params.GetGroups())
|
||||
|
||||
def test_install_toolbar_button_existing_custom_toolbar_1(self):
|
||||
# There is an existing custom toolbar, and we should use it
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
self.installer.addon_params.set("CustomToolbarName", "UT_TB_1")
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertFalse(tbi.ask_for_toolbar_called)
|
||||
self.assertEqual(tbi.tb.get("Name", ""), "UT_TB_1")
|
||||
|
||||
def test_install_toolbar_button_existing_custom_toolbar_2(self):
|
||||
# There are multiple existing custom toolbars, and we should use one of them
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_2 = self.installer.toolbar_params.GetGroup("UT_TB_2")
|
||||
ut_tb_3 = self.installer.toolbar_params.GetGroup("UT_TB_3")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ut_tb_2.set("Name", "UT_TB_2")
|
||||
ut_tb_3.set("Name", "UT_TB_3")
|
||||
self.installer.addon_params.set("CustomToolbarName", "UT_TB_3")
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertFalse(tbi.ask_for_toolbar_called)
|
||||
self.assertEqual(tbi.tb.get("Name", ""), "UT_TB_3")
|
||||
|
||||
def test_install_toolbar_button_existing_custom_toolbar_3(self):
|
||||
# There are multiple existing custom toolbars, but none of them match
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_2 = self.installer.toolbar_params.GetGroup("UT_TB_2")
|
||||
ut_tb_3 = self.installer.toolbar_params.GetGroup("UT_TB_3")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ut_tb_2.set("Name", "UT_TB_2")
|
||||
ut_tb_3.set("Name", "UT_TB_3")
|
||||
self.installer.addon_params.set("CustomToolbarName", "UT_TB_4")
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertTrue(tbi.ask_for_toolbar_called)
|
||||
self.assertEqual(tbi.tb.get("Name", ""), "MockCustomToolbar")
|
||||
|
||||
def test_install_toolbar_button_existing_custom_toolbar_4(self):
|
||||
# There are multiple existing custom toolbars, one of them matches, but we have set
|
||||
# "alwaysAskForToolbar" to True
|
||||
tbi = TestMacroInstallerGui.ToolbarIntercepter()
|
||||
self.installer._ask_for_toolbar = tbi._ask_for_toolbar
|
||||
self.installer._install_macro_to_toolbar = tbi._install_macro_to_toolbar
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_2 = self.installer.toolbar_params.GetGroup("UT_TB_2")
|
||||
ut_tb_3 = self.installer.toolbar_params.GetGroup("UT_TB_3")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ut_tb_2.set("Name", "UT_TB_2")
|
||||
ut_tb_3.set("Name", "UT_TB_3")
|
||||
self.installer.addon_params.set("CustomToolbarName", "UT_TB_3")
|
||||
self.installer.addon_params.set("alwaysAskForToolbar", True)
|
||||
self.installer._install_toolbar_button()
|
||||
self.assertTrue(tbi.install_macro_to_toolbar_called)
|
||||
self.assertTrue(tbi.ask_for_toolbar_called)
|
||||
self.assertEqual(tbi.tb.get("Name", ""), "MockCustomToolbar")
|
||||
|
||||
def test_install_macro_to_toolbar_icon_abspath(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ii = TestMacroInstallerGui.InstallerInterceptor()
|
||||
self.installer._create_custom_command = ii._create_custom_command
|
||||
with tempfile.NamedTemporaryFile() as ntf:
|
||||
self.mock_macro.macro.icon = ntf.name
|
||||
self.installer._install_macro_to_toolbar(ut_tb_1)
|
||||
self.assertTrue(ii.ccc_called)
|
||||
self.assertEqual(ii.pixmapText, ntf.name)
|
||||
|
||||
def test_install_macro_to_toolbar_icon_relpath(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ii = TestMacroInstallerGui.InstallerInterceptor()
|
||||
self.installer._create_custom_command = ii._create_custom_command
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.installer.macro_dir = td
|
||||
self.mock_macro.macro.icon = "RelativeIconPath.png"
|
||||
self.installer._install_macro_to_toolbar(ut_tb_1)
|
||||
self.assertTrue(ii.ccc_called)
|
||||
self.assertEqual(ii.pixmapText, os.path.join(td, "RelativeIconPath.png"))
|
||||
|
||||
def test_install_macro_to_toolbar_xpm(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ii = TestMacroInstallerGui.InstallerInterceptor()
|
||||
self.installer._create_custom_command = ii._create_custom_command
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.installer.macro_dir = td
|
||||
self.mock_macro.macro.xpm = "Not really xpm data, don't try to use it!"
|
||||
self.installer._install_macro_to_toolbar(ut_tb_1)
|
||||
self.assertTrue(ii.ccc_called)
|
||||
self.assertEqual(ii.pixmapText, os.path.join(td, "MockMacro_icon.xpm"))
|
||||
self.assertTrue(os.path.exists(os.path.join(td, "MockMacro_icon.xpm")))
|
||||
|
||||
def test_install_macro_to_toolbar_no_icon(self):
|
||||
ut_tb_1 = self.installer.toolbar_params.GetGroup("UT_TB_1")
|
||||
ut_tb_1.set("Name", "UT_TB_1")
|
||||
ii = TestMacroInstallerGui.InstallerInterceptor()
|
||||
self.installer._create_custom_command = ii._create_custom_command
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
self.installer.macro_dir = td
|
||||
self.installer._install_macro_to_toolbar(ut_tb_1)
|
||||
self.assertTrue(ii.ccc_called)
|
||||
self.assertIsNone(ii.pixmapText)
|
||||
246
src/Mod/AddonManager/AddonManagerTest/gui/test_update_all_gui.py
Normal file
246
src/Mod/AddonManager/AddonManagerTest/gui/test_update_all_gui.py
Normal file
@@ -0,0 +1,246 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
from time import sleep
|
||||
import unittest
|
||||
import FreeCAD
|
||||
|
||||
from Addon import Addon
|
||||
|
||||
from PySide import QtCore, QtWidgets
|
||||
|
||||
from addonmanager_update_all_gui import UpdateAllGUI, AddonStatus
|
||||
|
||||
|
||||
class MockUpdater(QtCore.QObject):
|
||||
success = QtCore.Signal(object)
|
||||
failure = QtCore.Signal(object)
|
||||
finished = QtCore.Signal()
|
||||
|
||||
def __init__(self, addon, addons=[]):
|
||||
super().__init__()
|
||||
self.addon_to_install = addon
|
||||
self.addons = addons
|
||||
self.has_run = False
|
||||
self.emit_success = True
|
||||
self.work_function = (
|
||||
None # Set to some kind of callable to make this function take time
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.has_run = True
|
||||
if self.work_function is not None and callable(self.work_function):
|
||||
self.work_function()
|
||||
if self.emit_success:
|
||||
self.success.emit(self.addon_to_install)
|
||||
else:
|
||||
self.failure.emit(self.addon_to_install)
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class MockUpdaterFactory:
|
||||
def __init__(self, addons):
|
||||
self.addons = addons
|
||||
self.work_function = None
|
||||
self.updater = None
|
||||
|
||||
def get_updater(self, addon):
|
||||
self.updater = MockUpdater(addon, self.addons)
|
||||
self.updater.work_function = self.work_function
|
||||
return self.updater
|
||||
|
||||
|
||||
class MockAddon:
|
||||
def __init__(self, name):
|
||||
self.display_name = name
|
||||
self.name = name
|
||||
self.macro = None
|
||||
|
||||
def status(self):
|
||||
return Addon.Status.UPDATE_AVAILABLE
|
||||
|
||||
|
||||
class CallInterceptor:
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
self.args = None
|
||||
|
||||
def intercept(self, *args):
|
||||
self.called = True
|
||||
self.args = args
|
||||
|
||||
|
||||
class TestUpdateAllGui(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.addons = []
|
||||
for i in range(3):
|
||||
self.addons.append(MockAddon(f"Mock Addon {i}"))
|
||||
self.factory = MockUpdaterFactory(self.addons)
|
||||
self.test_object = UpdateAllGUI(self.addons)
|
||||
self.test_object.updater_factory = self.factory
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_run(self):
|
||||
self.factory.work_function = lambda: sleep(0.1)
|
||||
self.test_object.run()
|
||||
while self.test_object.is_running():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.test_object.dialog.accept()
|
||||
|
||||
def test_setup_dialog(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.assertIsNotNone(
|
||||
self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel)
|
||||
)
|
||||
self.assertEqual(self.test_object.dialog.tableWidget.rowCount(), 3)
|
||||
|
||||
def test_cancelling_installation(self):
|
||||
self.factory.work_function = lambda: sleep(0.1)
|
||||
self.test_object.run()
|
||||
cancel_timer = QtCore.QTimer()
|
||||
cancel_timer.timeout.connect(
|
||||
self.test_object.dialog.buttonBox.button(
|
||||
QtWidgets.QDialogButtonBox.Cancel
|
||||
).click
|
||||
)
|
||||
cancel_timer.start(90)
|
||||
while self.test_object.is_running():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 10)
|
||||
self.assertGreater(len(self.test_object.addons_with_update), 0)
|
||||
|
||||
def test_add_addon_to_table(self):
|
||||
mock_addon = MockAddon("MockAddon")
|
||||
self.test_object.dialog.tableWidget.clear()
|
||||
self.test_object._add_addon_to_table(mock_addon)
|
||||
self.assertEqual(self.test_object.dialog.tableWidget.rowCount(), 1)
|
||||
|
||||
def test_update_addon_status(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object._update_addon_status(0, AddonStatus.WAITING)
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.WAITING.ui_string(),
|
||||
)
|
||||
self.test_object._update_addon_status(0, AddonStatus.INSTALLING)
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.INSTALLING.ui_string(),
|
||||
)
|
||||
self.test_object._update_addon_status(0, AddonStatus.SUCCEEDED)
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.SUCCEEDED.ui_string(),
|
||||
)
|
||||
self.test_object._update_addon_status(0, AddonStatus.FAILED)
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.FAILED.ui_string(),
|
||||
)
|
||||
|
||||
def test_process_next_update(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object._launch_active_installer = lambda: None
|
||||
self.test_object._process_next_update()
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.INSTALLING.ui_string(),
|
||||
)
|
||||
|
||||
self.test_object._process_next_update()
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(1, 1).text(),
|
||||
AddonStatus.INSTALLING.ui_string(),
|
||||
)
|
||||
|
||||
self.test_object._process_next_update()
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(2, 1).text(),
|
||||
AddonStatus.INSTALLING.ui_string(),
|
||||
)
|
||||
|
||||
self.test_object._process_next_update()
|
||||
|
||||
def test_launch_active_installer(self):
|
||||
self.test_object.active_installer = self.factory.get_updater(self.addons[0])
|
||||
self.test_object._update_succeeded = lambda _: None
|
||||
self.test_object._update_failed = lambda _: None
|
||||
self.test_object.process_next_update = lambda: None
|
||||
self.test_object._launch_active_installer()
|
||||
# The above call does not block, so spin until it has completed (basically instantly in testing)
|
||||
while self.test_object.worker_thread.isRunning():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.test_object.dialog.accept()
|
||||
|
||||
def test_update_succeeded(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object._update_succeeded(self.addons[0])
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.SUCCEEDED.ui_string(),
|
||||
)
|
||||
|
||||
def test_update_failed(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object._update_failed(self.addons[0])
|
||||
self.assertEqual(
|
||||
self.test_object.dialog.tableWidget.item(0, 1).text(),
|
||||
AddonStatus.FAILED.ui_string(),
|
||||
)
|
||||
|
||||
def test_update_finished(self):
|
||||
self.test_object._setup_dialog()
|
||||
call_interceptor = CallInterceptor()
|
||||
self.test_object.worker_thread = QtCore.QThread()
|
||||
self.test_object.worker_thread.start()
|
||||
self.test_object._process_next_update = call_interceptor.intercept
|
||||
self.test_object.active_installer = self.factory.get_updater(self.addons[0])
|
||||
self.test_object._update_finished()
|
||||
self.assertFalse(self.test_object.worker_thread.isRunning())
|
||||
self.test_object.worker_thread.terminate()
|
||||
self.assertTrue(call_interceptor.called)
|
||||
self.test_object.worker_thread.wait()
|
||||
|
||||
def test_finalize(self):
|
||||
self.test_object._setup_dialog()
|
||||
self.test_object.worker_thread = QtCore.QThread()
|
||||
self.test_object.worker_thread.start()
|
||||
self.test_object._finalize()
|
||||
self.assertFalse(self.test_object.worker_thread.isRunning())
|
||||
self.test_object.worker_thread.terminate()
|
||||
self.test_object.worker_thread.wait()
|
||||
self.assertFalse(self.test_object.running)
|
||||
self.assertIsNotNone(
|
||||
self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Close)
|
||||
)
|
||||
self.assertIsNone(
|
||||
self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel)
|
||||
)
|
||||
|
||||
def test_is_running(self):
|
||||
self.assertFalse(self.test_object.is_running())
|
||||
self.test_object.run()
|
||||
self.assertTrue(self.test_object.is_running())
|
||||
while self.test_object.is_running():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
|
||||
self.test_object.dialog.accept()
|
||||
@@ -1,211 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This file is part of the FreeCAD CAx development system. *
|
||||
# * *
|
||||
# * This library is free software; you can redistribute it and/or *
|
||||
# * modify it under the terms of the GNU Lesser General Public *
|
||||
# * License as published by the Free Software Foundation; either *
|
||||
# * version 2.1 of the License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * This library is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with this library; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
|
||||
# * 02110-1301 USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
import unittest
|
||||
import FreeCAD
|
||||
from addonmanager_git import initialize_git
|
||||
|
||||
from PySide2 import QtCore
|
||||
|
||||
import NetworkManager
|
||||
from Addon import Addon
|
||||
from addonmanager_workers_startup import (
|
||||
CreateAddonListWorker,
|
||||
UpdateChecker,
|
||||
)
|
||||
from addonmanager_workers_installation import InstallWorkbenchWorker
|
||||
|
||||
|
||||
class TestWorkersInstallation(unittest.TestCase):
|
||||
|
||||
MODULE = "test_workers_installation" # file name without extension
|
||||
|
||||
addon_list = (
|
||||
[]
|
||||
) # Cache at the class level so only the first test has to download it
|
||||
|
||||
def setUp(self):
|
||||
"""Set up the test"""
|
||||
self.test_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
|
||||
self.saved_mod_directory = Addon.mod_directory
|
||||
self.saved_cache_directory = Addon.cache_directory
|
||||
Addon.mod_directory = os.path.join(
|
||||
tempfile.gettempdir(), "FreeCADTesting", "Mod"
|
||||
)
|
||||
Addon.cache_directory = os.path.join(
|
||||
tempfile.gettempdir(), "FreeCADTesting", "Cache"
|
||||
)
|
||||
|
||||
os.makedirs(Addon.mod_directory, mode=0o777, exist_ok=True)
|
||||
os.makedirs(Addon.cache_directory, mode=0o777, exist_ok=True)
|
||||
|
||||
url = "https://api.github.com/zen"
|
||||
NetworkManager.InitializeNetworkManager()
|
||||
result = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url)
|
||||
if result is None:
|
||||
self.skipTest("No active internet connection detected")
|
||||
|
||||
self.macro_counter = 0
|
||||
self.workbench_counter = 0
|
||||
self.prefpack_counter = 0
|
||||
self.addon_from_cache_counter = 0
|
||||
self.macro_from_cache_counter = 0
|
||||
|
||||
self.package_cache = {}
|
||||
self.macro_cache = []
|
||||
|
||||
self.package_cache_filename = os.path.join(
|
||||
Addon.cache_directory, "packages.json"
|
||||
)
|
||||
self.macro_cache_filename = os.path.join(Addon.cache_directory, "macros.json")
|
||||
|
||||
if not TestWorkersInstallation.addon_list:
|
||||
self._create_addon_list()
|
||||
|
||||
# Workbench: use the FreeCAD-Help workbench for testing purposes
|
||||
self.help_addon = None
|
||||
for addon in self.addon_list:
|
||||
if addon.name == "Help":
|
||||
self.help_addon = addon
|
||||
break
|
||||
if not self.help_addon:
|
||||
print("Unable to locate the FreeCAD-Help addon to test with")
|
||||
self.skipTest("No active internet connection detected")
|
||||
|
||||
# Store the user's preference for whether git is enabled or disabled
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
self.saved_git_disabled_status = pref.GetBool("disableGit", False)
|
||||
|
||||
def tearDown(self):
|
||||
mod_dir = os.path.join(tempfile.gettempdir(), "FreeCADTesting", "Mod")
|
||||
if os.path.exists(mod_dir):
|
||||
self._rmdir(mod_dir)
|
||||
macro_dir = os.path.join(tempfile.gettempdir(), "FreeCADTesting", "Mod")
|
||||
if os.path.exists(macro_dir):
|
||||
self._rmdir(macro_dir)
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
pref.SetBool("disableGit", self.saved_git_disabled_status)
|
||||
|
||||
def test_workbench_installation(self):
|
||||
addon_location = os.path.join(
|
||||
tempfile.gettempdir(), "FreeCADTesting", "Mod", self.help_addon.name
|
||||
)
|
||||
worker = InstallWorkbenchWorker(self.help_addon, addon_location)
|
||||
worker.run() # Synchronous call, blocks until complete
|
||||
self.assertTrue(os.path.exists(addon_location))
|
||||
self.assertTrue(os.path.exists(os.path.join(addon_location, "package.xml")))
|
||||
|
||||
def test_workbench_installation_git_disabled(self):
|
||||
"""If the testing user has git enabled, also test the addon manager with git disabled"""
|
||||
if self.saved_git_disabled_status:
|
||||
self.skipTest("Git is disabled, this test is redundant")
|
||||
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
pref.SetBool("disableGit", True)
|
||||
|
||||
self.test_workbench_installation()
|
||||
|
||||
pref.SetBool("disableGit", False)
|
||||
|
||||
def test_workbench_update_checker(self):
|
||||
|
||||
git_manager = initialize_git()
|
||||
|
||||
if not git_manager:
|
||||
return
|
||||
|
||||
# Workbench: use the FreeCAD-Help workbench for testing purposes
|
||||
help_addon = None
|
||||
for addon in self.addon_list:
|
||||
if addon.name == "Help":
|
||||
help_addon = addon
|
||||
break
|
||||
if not help_addon:
|
||||
print("Unable to locate the FreeCAD-Help addon to test with")
|
||||
return
|
||||
|
||||
addon_location = os.path.join(
|
||||
tempfile.gettempdir(), "FreeCADTesting", "Mod", self.help_addon.name
|
||||
)
|
||||
worker = InstallWorkbenchWorker(addon, addon_location)
|
||||
worker.run() # Synchronous call, blocks until complete
|
||||
self.assertEqual(help_addon.status(), Addon.Status.PENDING_RESTART)
|
||||
|
||||
# Back up one revision
|
||||
git_manager.reset(addon_location, ["--hard", "HEAD~1"])
|
||||
|
||||
# At this point the addon should be "out of date", checked out to one revision behind
|
||||
# the most recent.
|
||||
|
||||
worker = UpdateChecker()
|
||||
worker.override_mod_directory(
|
||||
os.path.join(tempfile.gettempdir(), "FreeCADTesting", "Mod")
|
||||
)
|
||||
worker.check_workbench(help_addon) # Synchronous call
|
||||
self.assertEqual(help_addon.status(), Addon.Status.UPDATE_AVAILABLE)
|
||||
|
||||
# Now try to "update" it (which is really done via the install worker)
|
||||
worker = InstallWorkbenchWorker(addon, addon_location)
|
||||
worker.run() # Synchronous call, blocks until complete
|
||||
self.assertEqual(help_addon.status(), Addon.Status.PENDING_RESTART)
|
||||
|
||||
def _rmdir(self, path):
|
||||
try:
|
||||
shutil.rmtree(path, onerror=self._remove_readonly)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def _remove_readonly(self, func, path, _) -> None:
|
||||
"""Remove a read-only file."""
|
||||
|
||||
os.chmod(path, stat.S_IWRITE)
|
||||
func(path)
|
||||
|
||||
def _create_addon_list(self):
|
||||
"""Create the list of addons"""
|
||||
worker = CreateAddonListWorker()
|
||||
worker.addon_repo.connect(self._addon_added)
|
||||
worker.start()
|
||||
while worker.isRunning():
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
|
||||
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
|
||||
|
||||
def _addon_added(self, addon: Addon):
|
||||
"""Callback for adding an Addon: tracks the list, and counts the various types"""
|
||||
print(f"Addon added: {addon.name}")
|
||||
TestWorkersInstallation.addon_list.append(addon)
|
||||
if addon.contains_workbench():
|
||||
self.workbench_counter += 1
|
||||
if addon.contains_macro():
|
||||
self.macro_counter += 1
|
||||
if addon.contains_preference_pack():
|
||||
self.prefpack_counter += 1
|
||||
@@ -27,8 +27,6 @@ import unittest
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from addonmanager_git import initialize_git
|
||||
|
||||
import FreeCAD
|
||||
|
||||
from PySide2 import QtCore
|
||||
@@ -39,11 +37,6 @@ from addonmanager_workers_startup import (
|
||||
CreateAddonListWorker,
|
||||
LoadPackagesFromCacheWorker,
|
||||
LoadMacrosFromCacheWorker,
|
||||
CheckSingleUpdateWorker,
|
||||
)
|
||||
|
||||
from addonmanager_workers_installation import (
|
||||
InstallWorkbenchWorker,
|
||||
)
|
||||
|
||||
|
||||
@@ -131,7 +124,6 @@ class TestWorkersStartup(unittest.TestCase):
|
||||
f.write(json.dumps(self.macro_cache, indent=" "))
|
||||
|
||||
original_macro_counter = self.macro_counter
|
||||
original_workbench_counter = self.workbench_counter
|
||||
original_addon_list = self.addon_list.copy()
|
||||
self.macro_counter = 0
|
||||
self.workbench_counter = 0
|
||||
|
||||
Reference in New Issue
Block a user