From 480c32ba56bd6ea23b105088861314aabb013ffb Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Sat, 21 Mar 2026 21:13:53 -0700 Subject: [PATCH] try fix shutdown --- contact/settings.py | 21 ++++++++---- tests/test_settings.py | 75 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 tests/test_settings.py diff --git a/contact/settings.py b/contact/settings.py index 41b7d00..61c68c3 100644 --- a/contact/settings.py +++ b/contact/settings.py @@ -6,19 +6,26 @@ import sys import traceback import contact.ui.default_config as config -from contact.utilities.input_handlers import get_list_input -from contact.utilities.i18n import t -from contact.ui.dialog import dialog -from contact.utilities.i18n import t from contact.ui.colors import setup_colors -from contact.ui.splash import draw_splash from contact.ui.control_ui import set_region, settings_menu +from contact.ui.dialog import dialog +from contact.ui.splash import draw_splash from contact.utilities.arg_parser import setup_parser +from contact.utilities.i18n import t +from contact.utilities.input_handlers import get_list_input from contact.utilities.interfaces import initialize_interface, reconnect_interface +def close_interface(interface: object) -> None: + if interface is None: + return + with contextlib.suppress(Exception): + interface.close() + + def main(stdscr: curses.window) -> None: output_capture = io.StringIO() + interface = None try: with contextlib.redirect_stdout(output_capture), contextlib.redirect_stderr(output_capture): setup_colors() @@ -39,7 +46,7 @@ def main(stdscr: curses.window) -> None: ) if confirmation == "Yes": set_region(interface) - interface.close() + close_interface(interface) draw_splash(stdscr) interface = reconnect_interface(args) stdscr.clear() @@ -52,6 +59,8 @@ def main(stdscr: curses.window) -> None: logging.error("Traceback: %s", traceback.format_exc()) logging.error("Console output before crash:\n%s", console_output) raise + finally: + close_interface(interface) def ensure_min_rows(stdscr: curses.window, min_rows: int = 11) -> None: diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..56dee58 --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,75 @@ +from argparse import Namespace +from types import SimpleNamespace +import unittest +from unittest import mock + +import contact.settings as settings + + +class SettingsRuntimeTests(unittest.TestCase): + def test_main_closes_interface_after_normal_settings_exit(self) -> None: + stdscr = mock.Mock() + args = Namespace() + interface = mock.Mock() + interface.localNode = SimpleNamespace(localConfig=SimpleNamespace(lora=SimpleNamespace(region=1))) + + with mock.patch.object(settings, "setup_colors"): + with mock.patch.object(settings, "ensure_min_rows"): + with mock.patch.object(settings, "draw_splash"): + with mock.patch.object(settings.curses, "curs_set"): + with mock.patch.object(settings, "setup_parser") as setup_parser: + with mock.patch.object(settings, "initialize_interface", return_value=interface): + with mock.patch.object(settings, "settings_menu") as settings_menu: + setup_parser.return_value.parse_args.return_value = args + settings.main(stdscr) + + settings_menu.assert_called_once_with(stdscr, interface) + interface.close.assert_called_once_with() + + def test_main_closes_reconnected_interface_after_region_reset(self) -> None: + stdscr = mock.Mock() + args = Namespace() + old_interface = mock.Mock() + old_interface.localNode = SimpleNamespace(localConfig=SimpleNamespace(lora=SimpleNamespace(region=0))) + new_interface = mock.Mock() + new_interface.localNode = SimpleNamespace(localConfig=SimpleNamespace(lora=SimpleNamespace(region=1))) + + with mock.patch.object(settings, "setup_colors"): + with mock.patch.object(settings, "ensure_min_rows"): + with mock.patch.object(settings, "draw_splash"): + with mock.patch.object(settings.curses, "curs_set"): + with mock.patch.object(settings, "setup_parser") as setup_parser: + with mock.patch.object(settings, "initialize_interface", return_value=old_interface): + with mock.patch.object(settings, "get_list_input", return_value="Yes"): + with mock.patch.object(settings, "set_region") as set_region: + with mock.patch.object( + settings, "reconnect_interface", return_value=new_interface + ) as reconnect_interface: + with mock.patch.object(settings, "settings_menu") as settings_menu: + setup_parser.return_value.parse_args.return_value = args + settings.main(stdscr) + + set_region.assert_called_once_with(old_interface) + reconnect_interface.assert_called_once_with(args) + settings_menu.assert_called_once_with(stdscr, new_interface) + old_interface.close.assert_called_once_with() + new_interface.close.assert_called_once_with() + + def test_main_closes_interface_when_settings_menu_raises(self) -> None: + stdscr = mock.Mock() + args = Namespace() + interface = mock.Mock() + interface.localNode = SimpleNamespace(localConfig=SimpleNamespace(lora=SimpleNamespace(region=1))) + + with mock.patch.object(settings, "setup_colors"): + with mock.patch.object(settings, "ensure_min_rows"): + with mock.patch.object(settings, "draw_splash"): + with mock.patch.object(settings.curses, "curs_set"): + with mock.patch.object(settings, "setup_parser") as setup_parser: + with mock.patch.object(settings, "initialize_interface", return_value=interface): + with mock.patch.object(settings, "settings_menu", side_effect=RuntimeError("boom")): + setup_parser.return_value.parse_args.return_value = args + with self.assertRaises(RuntimeError): + settings.main(stdscr) + + interface.close.assert_called_once_with()