""" Tests for series3_tray's thread lifecycle. Regression guard: saving the Settings dialog restarts the watcher thread via ``_restart_watcher``, which sets and replaces the shared ``stop_event``. The auto-updater must key off its own lifetime event (``_app_stop``) so a watcher restart never kills it; it should only stop when the app actually exits. """ import sys import time import threading import unittest from unittest import mock # Series3's tray pulls in Windows-only GUI deps at import time; stub them. for _name in ("pystray", "PIL", "PIL.Image", "PIL.ImageDraw"): sys.modules.setdefault(_name, mock.MagicMock()) import series3_tray # noqa: E402 class TrayThreadLifecycle(unittest.TestCase): def setUp(self): # No-op watcher loop so restarting the watcher doesn't spin up the real # Blastware scanner / network heartbeat. patcher = mock.patch.object( series3_tray.watcher, "run_watcher", lambda state, stop_event: None ) patcher.start() self.addCleanup(patcher.stop) def test_app_stop_is_separate_lifetime_event(self): app = series3_tray.WatcherTray() self.assertIsInstance(app._app_stop, threading.Event) self.assertIsNot(app._app_stop, app.stop_event) self.assertFalse(app._app_stop.is_set()) def test_exit_signals_app_stop(self): app = series3_tray.WatcherTray() icon = mock.MagicMock() app._exit(icon, None) self.assertTrue(app._app_stop.is_set()) icon.stop.assert_called_once() def test_restart_watcher_leaves_watcher_stop_unset(self): app = series3_tray.WatcherTray() app._restart_watcher() self.assertFalse(app.stop_event.is_set()) def test_updater_survives_watcher_restart_then_stops_on_exit(self): app = series3_tray.WatcherTray() app._icon = None with mock.patch.object(series3_tray, "check_for_update", return_value=(None, None)), \ mock.patch.object(app, "_tray_status", return_value="ok"): t = threading.Thread( target=app._icon_updater, daemon=True, name="test-updater" ) t.start() time.sleep(0.1) self.assertTrue(t.is_alive(), "updater failed to start") # A settings save restarts the watcher — it must NOT kill the updater. app._restart_watcher() time.sleep(0.2) self.assertTrue( t.is_alive(), "updater thread died on watcher restart (the bug)" ) # Real app exit DOES stop the updater. app._app_stop.set() t.join(timeout=2) self.assertFalse( t.is_alive(), "updater thread did not stop on app exit" ) if __name__ == "__main__": unittest.main()