diff --git a/src/Mod/AddonManager/AddonManagerTest/gui/gui_mocks.py b/src/Mod/AddonManager/AddonManagerTest/gui/gui_mocks.py index 7d4883accd33..f6f7ec70184e 100644 --- a/src/Mod/AddonManager/AddonManagerTest/gui/gui_mocks.py +++ b/src/Mod/AddonManager/AddonManagerTest/gui/gui_mocks.py @@ -63,7 +63,7 @@ def run(self): self.dialog_found = True self.timer.stop() - if not self.dialog_found: + if self.execution_counter > 25 and not self.dialog_found: # OK, it wasn't the active modal widget... was it some other window, and never became # active? That's an error, but we should get it closed anyway. windows = QtWidgets.QApplication.topLevelWidgets() @@ -80,7 +80,7 @@ def run(self): self.has_run = True self.execution_counter += 1 if self.execution_counter > 100: - print("Stopper timer after 100 iterations") + print("Stopped timer after 100 iterations") self.timer.stop() def click_button(self, widget): diff --git a/src/Mod/AddonManager/AddonManagerTest/gui/test_installer_gui.py b/src/Mod/AddonManager/AddonManagerTest/gui/test_installer_gui.py index 6deb9562743b..863b5646ff30 100644 --- a/src/Mod/AddonManager/AddonManagerTest/gui/test_installer_gui.py +++ b/src/Mod/AddonManager/AddonManagerTest/gui/test_installer_gui.py @@ -552,6 +552,9 @@ def test_ask_to_install_toolbar_button_enabled_no(self): translate("toolbar_button", "Add button?"), QtWidgets.QDialogButtonBox.No, ) + # Note: that dialog does not use a QButtonBox, so we can really only test its + # reject() signal, which is triggered by the DialogWatcher when it cannot find + # the button. In this case, failure to find that button is NOT an error. self.installer._ask_to_install_toolbar_button() # Blocks until killed by watcher self.assertTrue(dialog_watcher.dialog_found) diff --git a/src/Mod/AddonManager/AddonManagerTest/gui/test_update_all_gui.py b/src/Mod/AddonManager/AddonManagerTest/gui/test_update_all_gui.py index af40eb30a5ca..89ddff7b8c82 100644 --- a/src/Mod/AddonManager/AddonManagerTest/gui/test_update_all_gui.py +++ b/src/Mod/AddonManager/AddonManagerTest/gui/test_update_all_gui.py @@ -116,7 +116,20 @@ def test_setup_dialog(self): self.assertEqual(self.test_object.dialog.tableWidget.rowCount(), 3) def test_cancelling_installation(self): - self.factory.work_function = lambda: sleep(0.1) + class Worker: + def __init__(self): + self.counter = 0 + self.LIMIT = 100 + self.limit_reached = False + def run(self): + while self.counter < self.LIMIT: + if QtCore.QThread.currentThread().isInterruptionRequested(): + return + self.counter += 1 + sleep(0.01) + self.limit_reached = True + worker = Worker() + self.factory.work_function = worker.run self.test_object.run() cancel_timer = QtCore.QTimer() cancel_timer.timeout.connect( @@ -217,7 +230,7 @@ def test_update_finished(self): 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.test_object.worker_thread.quit() self.assertTrue(call_interceptor.called) self.test_object.worker_thread.wait() @@ -227,7 +240,7 @@ def test_finalize(self): 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.quit() self.test_object.worker_thread.wait() self.assertFalse(self.test_object.running) self.assertIsNotNone( diff --git a/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_startup.py b/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_startup.py index 41d6aefdbb92..8973482bc652 100644 --- a/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_startup.py +++ b/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_startup.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # *************************************************************************** # * Copyright (c) 2022 FreeCAD Project Association * # * * @@ -39,11 +37,14 @@ LoadMacrosFromCacheWorker, ) +run_slow_tests = False + class TestWorkersStartup(unittest.TestCase): MODULE = "test_workers_startup" # file name without extension + @unittest.skipUnless(run_slow_tests, "This integration test is slow and uses the network") def setUp(self): """Set up the test""" self.test_dir = os.path.join( diff --git a/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_utility.py b/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_utility.py index 91da00039376..82a97c3ddfad 100644 --- a/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_utility.py +++ b/src/Mod/AddonManager/AddonManagerTest/gui/test_workers_utility.py @@ -35,6 +35,7 @@ class TestWorkersUtility(unittest.TestCase): MODULE = "test_workers_utility" # file name without extension + @unittest.skip("Test is slow and uses the network: refactor!") def setUp(self): self.test_dir = os.path.join( FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data" diff --git a/src/Mod/AddonManager/addonmanager_installer_gui.py b/src/Mod/AddonManager/addonmanager_installer_gui.py index a75303b29fd5..eae53bb2c92f 100644 --- a/src/Mod/AddonManager/addonmanager_installer_gui.py +++ b/src/Mod/AddonManager/addonmanager_installer_gui.py @@ -79,6 +79,7 @@ def __del__(self): self.worker_thread.quit() self.worker_thread.wait(500) if self.worker_thread.isRunning(): + FreeCAD.Console.PrintError("INTERNAL ERROR: Thread did not quit() cleanly, using terminate()\n") self.worker_thread.terminate() def run(self): @@ -164,15 +165,6 @@ def _handle_disallowed_python(self, python_requires: List[str]) -> bool: message, QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, ) - FreeCAD.Console.PrintMessage( - translate( - "AddonsInstaller", - "The following Python packages are allowed to be automatically installed", - ) - + ":\n" - ) - for package in self.installer.allowed_packages: - FreeCAD.Console.PrintMessage(f" * {package}\n") if r == QtWidgets.QMessageBox.Ok: # Force the installation to proceed @@ -335,9 +327,11 @@ def _run_dependency_installer(self, addons, python_requires, python_optional): self.dependency_worker_thread.start() def _cleanup_dependency_worker(self) -> None: + return self.dependency_worker_thread.quit() self.dependency_worker_thread.wait(500) if self.dependency_worker_thread.isRunning(): + FreeCAD.Console.PrintError("INTERNAL ERROR: Thread did not quit() cleanly, using terminate()\n") self.dependency_worker_thread.terminate() def _report_no_python_exe(self) -> None: @@ -441,7 +435,6 @@ def install(self) -> None: self.installer.moveToThread(self.worker_thread) self.installer.finished.connect(self.worker_thread.quit) self.worker_thread.started.connect(self.installer.run) - self.worker_thread.start() # Returns immediately self.installing_dialog = QtWidgets.QMessageBox( QtWidgets.QMessageBox.NoIcon, @@ -455,6 +448,7 @@ def install(self) -> None: self.installing_dialog.rejected.connect(self._cancel_addon_installation) self.installer.finished.connect(self.installing_dialog.hide) self.installing_dialog.show() + self.worker_thread.start() # Returns immediately def _cancel_addon_installation(self): dlg = QtWidgets.QMessageBox( @@ -535,6 +529,14 @@ def __init__(self, addon: object): "User parameter:BaseApp/Workbench/Global/Toolbar" ) self.macro_dir = FreeCAD.getUserMacroDir(True) + + def __del__(self): + if self.worker_thread and hasattr(self.worker_thread, "quit"): + self.worker_thread.quit() + self.worker_thread.wait(500) + if self.worker_thread.isRunning(): + FreeCAD.Console.PrintError("INTERNAL ERROR: Thread did not quit() cleanly, using terminate()\n") + self.worker_thread.terminate() def run(self): """Perform the installation, including any necessary user interaction via modal dialog diff --git a/src/Mod/AddonManager/addonmanager_update_all_gui.py b/src/Mod/AddonManager/addonmanager_update_all_gui.py index b82bddca8103..2f83b9ad417f 100644 --- a/src/Mod/AddonManager/addonmanager_update_all_gui.py +++ b/src/Mod/AddonManager/addonmanager_update_all_gui.py @@ -119,12 +119,8 @@ def _setup_dialog(self): def _cancel_installation(self): self.cancelled = True - self.worker_thread.requestInterruption() - self.worker_thread.wait(100) - if self.worker_thread.isRunning(): - self.worker_thread.terminate() - self.worker_thread.wait() - self.running = False + if self.worker_thread and self.worker_thread.isRunning(): + self.worker_thread.requestInterruption() def _add_addon_to_table(self, addon: Addon): """Add the given addon to the list, with no icon in the first column""" @@ -180,23 +176,34 @@ def _update_failed(self, addon): def _update_finished(self): """Callback for updater that has finished all its work""" - self.worker_thread.terminate() - self.worker_thread.wait() + if self.worker_thread is not None and self.worker_thread.isRunning(): + self.worker_thread.quit() + self.worker_thread.wait() self.addon_updated.emit(self.active_installer.addon_to_install) if not self.cancelled: self._process_next_update() + else: + self._setup_cancelled_state() def _finalize(self): """No more updates, clean up and shut down""" if self.worker_thread is not None and self.worker_thread.isRunning(): - self.worker_thread.terminate() + self.worker_thread.quit() self.worker_thread.wait() + text = translate("Addons installer", "Finished updating the following addons") + self._set_dialog_to_final_state(text) + self.running = False + + def _setup_cancelled_state(self): + text1 = translate("AddonsInstaller", "Update was cancelled") + text2 = translate("AddonsInstaller", "some addons may have been updated") + self._set_dialog_to_final_state(text1 + ": " + text2) + self.running = False + + def _set_dialog_to_final_state(self,new_content): self.dialog.buttonBox.clear() self.dialog.buttonBox.addButton(QtWidgets.QDialogButtonBox.Close) - self.dialog.label.setText( - translate("Addons installer", "Finished updating the following addons") - ) - self.running = False + self.dialog.label.setText(new_content) def is_running(self): """True if the thread is running, and False if not"""