Skip to content

Commit

Permalink
Addon Manager: Fix GUI unit tests
Browse files Browse the repository at this point in the history
Eliminate all calls to terminate() when dealing with QThread.
  • Loading branch information
chennes committed Jan 13, 2023
1 parent b933d46 commit d6b3efc
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 30 deletions.
4 changes: 2 additions & 2 deletions src/Mod/AddonManager/AddonManagerTest/gui/gui_mocks.py
Expand Up @@ -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()
Expand All @@ -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):
Expand Down
Expand Up @@ -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)

Expand Down
19 changes: 16 additions & 3 deletions src/Mod/AddonManager/AddonManagerTest/gui/test_update_all_gui.py
Expand Up @@ -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(
Expand Down Expand Up @@ -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()

Expand All @@ -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(
Expand Down
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-

# ***************************************************************************
# * Copyright (c) 2022 FreeCAD Project Association *
# * *
Expand Down Expand Up @@ -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(
Expand Down
Expand Up @@ -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"
Expand Down
22 changes: 12 additions & 10 deletions src/Mod/AddonManager/addonmanager_installer_gui.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down
33 changes: 20 additions & 13 deletions src/Mod/AddonManager/addonmanager_update_all_gui.py
Expand Up @@ -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"""
Expand Down Expand Up @@ -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"""
Expand Down

0 comments on commit d6b3efc

Please sign in to comment.