""" Tests for thor_tray's thread lifecycle. Regression guard for the bug where saving settings — which restarts the watcher thread via ``_restart_watcher`` — also killed the auto-updater thread, because both shared a single ``stop_event``. The updater must key off a separate lifetime event (``_app_stop``) that a watcher restart never disturbs; it should only stop when the app actually exits. """ import sys import time import threading import unittest from unittest import mock # Thor's tray pulls in Windows-only GUI deps at import time; stub them so the # module imports on a headless dev/CI box. for _name in ("pystray", "PIL", "PIL.Image", "PIL.ImageDraw"): sys.modules.setdefault(_name, mock.MagicMock()) import thor_tray # noqa: E402 class TrayThreadLifecycle(unittest.TestCase): def setUp(self): # Keep _start_watcher cheap: a no-op watcher loop so restarting the # watcher doesn't spin up the real THORDATA scanner / network heartbeat. patcher = mock.patch.object( thor_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 = thor_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 = thor_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 = thor_tray.WatcherTray() app._restart_watcher() self.assertFalse(app.stop_event.is_set()) def test_updater_survives_watcher_restart_then_stops_on_exit(self): app = thor_tray.WatcherTray() app._icon = None with mock.patch.object(thor_tray, "check_for_update", return_value=(None, None)), \ mock.patch.object(thor_tray, "_update_log"), \ 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()