From 961562616ed57b3325f8aa57ac9f2c8c4560243b Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Wed, 29 Jul 2026 16:23:57 +0200 Subject: [PATCH] fix(connector): register the exit handler only when it is wired to aboutToQuit --- bec_widgets/utils/bec_connector.py | 9 +++++++- tests/unit_tests/test_bec_connector.py | 30 +++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/bec_widgets/utils/bec_connector.py b/bec_widgets/utils/bec_connector.py index b504ea00..eb85b23d 100644 --- a/bec_widgets/utils/bec_connector.py +++ b/bec_widgets/utils/bec_connector.py @@ -154,9 +154,16 @@ class BECConnector: logger.info("Shutting down BEC Client", repr(client)) client.shutdown() - BECConnector.EXIT_HANDLERS[self.client] = terminate + # In our GUI processes a QApplication always exists by this point (and a + # QWidget-based connector could not even be constructed without one - Qt + # aborts first). The guard is defensive for QObject-only connectors, which + # Qt happily creates standalone, e.g. in headless tests or plugin code. + # Record the handler only once it is actually wired: otherwise the + # EXIT_HANDLERS entry would suppress the registration of a later connector + # created when an application does exist, silently losing client teardown. app = QApplication.instance() if app is not None: + BECConnector.EXIT_HANDLERS[self.client] = terminate app.aboutToQuit.connect(terminate) else: logger.warning( diff --git a/tests/unit_tests/test_bec_connector.py b/tests/unit_tests/test_bec_connector.py index dda3aedb..b21a726c 100644 --- a/tests/unit_tests/test_bec_connector.py +++ b/tests/unit_tests/test_bec_connector.py @@ -207,8 +207,9 @@ def test_bec_connector_export_settings(): def test_bec_connector_terminate_registration_no_qapp_instance(qtbot): - """Constructing a BECConnector before a QApplication exists must not raise; the exit - handler is still registered, only the aboutToQuit wiring is skipped.""" + """Constructing a BECConnector without a QApplication (possible for QObject-only + connectors) must not raise, and must leave nothing registered: there is no + aboutToQuit to wire, so the exit handler is not recorded either.""" import bec_widgets.utils.bec_connector as m fresh_client = mock.MagicMock(name="fresh_client") @@ -216,6 +217,29 @@ def test_bec_connector_terminate_registration_no_qapp_instance(qtbot): try: with mock.patch.object(m.QApplication, "instance", return_value=None): BECConnectorQObject(client=fresh_client) # must not raise - assert fresh_client in BECConnector.EXIT_HANDLERS + assert fresh_client not in BECConnector.EXIT_HANDLERS finally: BECConnector.EXIT_HANDLERS.pop(fresh_client, None) + + +def test_bec_connector_terminate_registered_once_qapp_exists(qtbot): + """A connector created without a QApplication must not block the registration of a + later connector for the same client: once an application exists, teardown is wired.""" + import bec_widgets.utils.bec_connector as m + + fresh_client = mock.MagicMock(name="late_app_client") + try: + with mock.patch.object(m.QApplication, "instance", return_value=None): + BECConnectorQObject(client=fresh_client) + + # now an application exists (qtbot guarantees one) -> handler gets wired + BECConnectorQObject(client=fresh_client) + assert fresh_client in BECConnector.EXIT_HANDLERS + + fresh_client.shutdown.reset_mock() + QApplication.instance().aboutToQuit.emit() + assert fresh_client.shutdown.called, "client teardown must run on aboutToQuit" + finally: + handler = BECConnector.EXIT_HANDLERS.pop(fresh_client, None) + if handler is not None: + QApplication.instance().aboutToQuit.disconnect(handler)