"""GPIO startup failures are optional; queued presses cannot survive shutdown.""" import threading import os import unittest from unittest import mock from PyQt6.QtCore import QObject, pyqtSignal from PyQt6.QtWidgets import QApplication from python_app.gui.controllers.control_button_controller import ControlButtonController from python_app.models.run_config_schema import ControlButtonModel from python_app.tests.gui_test_support import isolated_window class _Watcher(QObject): pressed = pyqtSignal() failed = pyqtSignal(str) def __init__(self, *, parent: QObject, **_kwargs: object) -> None: super().__init__(parent) self.stopped = False def start(self) -> None: pass def stop(self) -> None: self.stopped = True self.pressed.emit() class ControlButtonControllerTest(unittest.TestCase): def setUp(self) -> None: self.app = QApplication.instance() or QApplication([]) self.config = ControlButtonModel(enabled=True, pin=17) self.capture = mock.Mock() self.parent = QObject() self.controller = ControlButtonController(self.parent, lambda: self.config, self.capture, mock.MagicMock()) self.addCleanup(self.controller.close) def test_close_rejects_already_queued_and_stop_time_presses(self) -> None: with mock.patch("python_app.gui.controllers.control_button_controller.ControlButtonWatcher", _Watcher): self.controller.start() watcher = self.controller._watcher assert isinstance(watcher, _Watcher) watcher.pressed.emit() self.capture.assert_called_once() worker = threading.Thread(target=watcher.pressed.emit) worker.start() worker.join(timeout=1) self.controller.close() self.app.processEvents() self.assertTrue(watcher.stopped) self.capture.assert_called_once() self.assertIsNone(self.controller._watcher) def test_disabled_invalid_and_unavailable_gpio_do_not_start_capture(self) -> None: with mock.patch("python_app.gui.controllers.control_button_controller.ControlButtonWatcher") as create: self.config.enabled = False self.controller.start() self.config.enabled, self.config.pin = True, -1 self.controller.start() create.assert_not_called() self.config.pin = 17 create.side_effect = OSError("GPIO unavailable") self.controller.start() self.assertIsNone(self.controller._watcher) self.capture.assert_not_called() def test_gpio_capture_failure_is_logged_without_a_modal_dialog(self) -> None: window = isolated_window(self.addCleanup) window._profile.state.config.control_button.enabled = True window._profile.state.config.control_button.pin = 17 window._control_button._capture_reference = lambda: window._log.show_error("GPIO capture failed") with mock.patch("python_app.gui.controllers.control_button_controller.ControlButtonWatcher", _Watcher), \ mock.patch.dict(os.environ, {"RADAR_SYSTEM_HEADLESS": "0"}), \ mock.patch("python_app.gui.controllers.log_controller.QMessageBox") as dialog, \ self.assertLogs("python_app.gui", level="ERROR") as logs: window._control_button.start() watcher = window._control_button._watcher assert isinstance(watcher, _Watcher) watcher.pressed.emit() dialog.assert_not_called() self.assertTrue(any("GPIO capture failed" in line for line in logs.output))