mirror of
https://github.com/pdxlocations/contact.git
synced 2026-03-28 17:12:35 +01:00
254 lines
12 KiB
Python
254 lines
12 KiB
Python
from argparse import Namespace
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
import contact.__main__ as entrypoint
|
|
import contact.ui.default_config as config
|
|
from contact.utilities.singleton import interface_state, ui_state
|
|
|
|
from tests.test_support import reset_singletons, restore_config, snapshot_config
|
|
|
|
|
|
class MainRuntimeTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
reset_singletons()
|
|
self.saved_config = snapshot_config("single_pane_mode")
|
|
|
|
def tearDown(self) -> None:
|
|
restore_config(self.saved_config)
|
|
reset_singletons()
|
|
|
|
def test_initialize_runtime_interface_uses_demo_branch(self) -> None:
|
|
args = Namespace(demo_screenshot=True)
|
|
|
|
with mock.patch.object(entrypoint, "configure_demo_database") as configure_demo_database:
|
|
with mock.patch.object(entrypoint, "build_demo_interface", return_value="demo-interface") as build_demo:
|
|
with mock.patch.object(entrypoint, "initialize_interface") as initialize_interface:
|
|
result = entrypoint.initialize_runtime_interface(args)
|
|
|
|
self.assertEqual(result, "demo-interface")
|
|
configure_demo_database.assert_called_once_with()
|
|
build_demo.assert_called_once_with()
|
|
initialize_interface.assert_not_called()
|
|
|
|
def test_initialize_runtime_interface_uses_live_branch_without_demo_flag(self) -> None:
|
|
args = Namespace(demo_screenshot=False)
|
|
|
|
with mock.patch.object(entrypoint, "initialize_interface", return_value="live-interface") as initialize_interface:
|
|
result = entrypoint.initialize_runtime_interface(args)
|
|
|
|
self.assertEqual(result, "live-interface")
|
|
initialize_interface.assert_called_once_with(args)
|
|
|
|
def test_interface_is_ready_detects_missing_local_node(self) -> None:
|
|
self.assertFalse(entrypoint.interface_is_ready(object()))
|
|
self.assertTrue(entrypoint.interface_is_ready(SimpleNamespace(localNode=SimpleNamespace(localConfig=mock.Mock()))))
|
|
|
|
def test_initialize_runtime_interface_with_retry_retries_until_node_is_ready(self) -> None:
|
|
args = Namespace(demo_screenshot=False)
|
|
stdscr = mock.Mock()
|
|
bad_interface = mock.Mock(spec=["close"])
|
|
good_interface = SimpleNamespace(localNode=SimpleNamespace(localConfig=mock.Mock()))
|
|
|
|
with mock.patch.object(entrypoint, "initialize_runtime_interface", side_effect=[bad_interface, good_interface]):
|
|
with mock.patch.object(entrypoint, "get_list_input", return_value="Retry") as get_list_input:
|
|
with mock.patch.object(entrypoint, "draw_splash") as draw_splash:
|
|
result = entrypoint.initialize_runtime_interface_with_retry(stdscr, args)
|
|
|
|
self.assertIs(result, good_interface)
|
|
get_list_input.assert_called_once()
|
|
bad_interface.close.assert_called_once_with()
|
|
draw_splash.assert_called_once_with(stdscr)
|
|
|
|
def test_initialize_runtime_interface_with_retry_returns_none_when_user_closes(self) -> None:
|
|
args = Namespace(demo_screenshot=False)
|
|
stdscr = mock.Mock()
|
|
bad_interface = mock.Mock(spec=["close"])
|
|
|
|
with mock.patch.object(entrypoint, "initialize_runtime_interface", return_value=bad_interface):
|
|
with mock.patch.object(entrypoint, "get_list_input", return_value="Close") as get_list_input:
|
|
with mock.patch.object(entrypoint, "draw_splash") as draw_splash:
|
|
result = entrypoint.initialize_runtime_interface_with_retry(stdscr, args)
|
|
|
|
self.assertIsNone(result)
|
|
get_list_input.assert_called_once()
|
|
bad_interface.close.assert_called_once_with()
|
|
draw_splash.assert_not_called()
|
|
|
|
def test_prompt_region_if_unset_reinitializes_interface_after_confirmation(self) -> None:
|
|
args = Namespace()
|
|
old_interface = mock.Mock()
|
|
new_interface = mock.Mock()
|
|
stdscr = mock.Mock()
|
|
interface_state.interface = old_interface
|
|
|
|
with mock.patch.object(entrypoint, "get_list_input", return_value="Yes"):
|
|
with mock.patch.object(entrypoint, "set_region") as set_region:
|
|
with mock.patch.object(entrypoint, "draw_splash") as draw_splash:
|
|
with mock.patch.object(entrypoint, "reconnect_interface", return_value=new_interface) as reconnect:
|
|
entrypoint.prompt_region_if_unset(args, stdscr)
|
|
|
|
set_region.assert_called_once_with(old_interface)
|
|
old_interface.close.assert_called_once_with()
|
|
draw_splash.assert_called_once_with(stdscr)
|
|
reconnect.assert_called_once_with(args)
|
|
self.assertIs(interface_state.interface, new_interface)
|
|
|
|
def test_prompt_region_if_unset_leaves_interface_unchanged_when_declined(self) -> None:
|
|
args = Namespace()
|
|
interface = mock.Mock()
|
|
interface_state.interface = interface
|
|
|
|
with mock.patch.object(entrypoint, "get_list_input", return_value="No"):
|
|
with mock.patch.object(entrypoint, "set_region") as set_region:
|
|
with mock.patch.object(entrypoint, "reconnect_interface") as reconnect:
|
|
entrypoint.prompt_region_if_unset(args)
|
|
|
|
set_region.assert_not_called()
|
|
reconnect.assert_not_called()
|
|
interface.close.assert_not_called()
|
|
self.assertIs(interface_state.interface, interface)
|
|
|
|
def test_initialize_globals_resets_and_populates_runtime_state(self) -> None:
|
|
ui_state.channel_list = ["stale"]
|
|
ui_state.all_messages = {"stale": [("old", "message")]}
|
|
ui_state.notifications = [1]
|
|
ui_state.packet_buffer = ["packet"]
|
|
ui_state.node_list = [99]
|
|
ui_state.selected_channel = 3
|
|
ui_state.selected_message = 4
|
|
ui_state.selected_node = 5
|
|
ui_state.start_index = [9, 9, 9]
|
|
config.single_pane_mode = "True"
|
|
|
|
with mock.patch.object(entrypoint, "get_nodeNum", return_value=123):
|
|
with mock.patch.object(entrypoint, "get_channels", return_value=["Primary"]) as get_channels:
|
|
with mock.patch.object(entrypoint, "get_node_list", return_value=[123, 456]) as get_node_list:
|
|
with mock.patch.object(entrypoint.pub, "subscribe") as subscribe:
|
|
with mock.patch.object(entrypoint, "init_nodedb") as init_nodedb:
|
|
with mock.patch.object(entrypoint, "seed_demo_messages") as seed_demo_messages:
|
|
with mock.patch.object(entrypoint, "load_messages_from_db") as load_messages:
|
|
entrypoint.initialize_globals(seed_demo=True)
|
|
|
|
self.assertEqual(ui_state.channel_list, ["Primary"])
|
|
self.assertEqual(ui_state.all_messages, {})
|
|
self.assertEqual(ui_state.notifications, [])
|
|
self.assertEqual(ui_state.packet_buffer, [])
|
|
self.assertEqual(ui_state.node_list, [123, 456])
|
|
self.assertEqual(ui_state.selected_channel, 0)
|
|
self.assertEqual(ui_state.selected_message, 0)
|
|
self.assertEqual(ui_state.selected_node, 0)
|
|
self.assertEqual(ui_state.start_index, [0, 0, 0])
|
|
self.assertTrue(ui_state.single_pane_mode)
|
|
self.assertEqual(interface_state.myNodeNum, 123)
|
|
get_channels.assert_called_once_with()
|
|
get_node_list.assert_called_once_with()
|
|
subscribe.assert_called_once_with(entrypoint.on_receive, "meshtastic.receive")
|
|
init_nodedb.assert_called_once_with()
|
|
seed_demo_messages.assert_called_once_with()
|
|
load_messages.assert_called_once_with()
|
|
|
|
def test_ensure_min_rows_retries_until_terminal_is_large_enough(self) -> None:
|
|
stdscr = mock.Mock()
|
|
stdscr.getmaxyx.side_effect = [(10, 80), (11, 80)]
|
|
|
|
with mock.patch.object(entrypoint, "dialog") as dialog:
|
|
with mock.patch.object(entrypoint.curses, "update_lines_cols") as update_lines_cols:
|
|
entrypoint.ensure_min_rows(stdscr, min_rows=11)
|
|
|
|
dialog.assert_called_once()
|
|
update_lines_cols.assert_called_once_with()
|
|
stdscr.clear.assert_called_once_with()
|
|
stdscr.refresh.assert_called_once_with()
|
|
|
|
def test_start_prints_help_and_exits_zero(self) -> None:
|
|
parser = mock.Mock()
|
|
|
|
with mock.patch.object(entrypoint.sys, "argv", ["contact", "--help"]):
|
|
with mock.patch.object(entrypoint, "setup_parser", return_value=parser):
|
|
with mock.patch.object(entrypoint.sys, "exit", side_effect=SystemExit(0)) as exit_mock:
|
|
with self.assertRaises(SystemExit) as raised:
|
|
entrypoint.start()
|
|
|
|
self.assertEqual(raised.exception.code, 0)
|
|
parser.print_help.assert_called_once_with()
|
|
exit_mock.assert_called_once_with(0)
|
|
|
|
def test_start_runs_curses_wrapper_and_closes_interface(self) -> None:
|
|
interface = mock.Mock()
|
|
interface_state.interface = interface
|
|
|
|
with mock.patch.object(entrypoint.sys, "argv", ["contact"]):
|
|
with mock.patch.object(entrypoint.curses, "wrapper") as wrapper:
|
|
entrypoint.start()
|
|
|
|
wrapper.assert_called_once_with(entrypoint.main)
|
|
interface.close.assert_called_once_with()
|
|
|
|
def test_start_does_not_crash_when_wrapper_returns_without_interface(self) -> None:
|
|
interface_state.interface = None
|
|
|
|
with mock.patch.object(entrypoint.sys, "argv", ["contact"]):
|
|
with mock.patch.object(entrypoint.curses, "wrapper") as wrapper:
|
|
entrypoint.start()
|
|
|
|
wrapper.assert_called_once_with(entrypoint.main)
|
|
|
|
def test_main_returns_cleanly_when_user_closes_missing_node_dialog(self) -> None:
|
|
stdscr = mock.Mock()
|
|
args = Namespace(settings=False, demo_screenshot=False)
|
|
|
|
with mock.patch.object(entrypoint, "setup_colors"):
|
|
with mock.patch.object(entrypoint, "ensure_min_rows"):
|
|
with mock.patch.object(entrypoint, "draw_splash"):
|
|
with mock.patch.object(entrypoint, "setup_parser") as setup_parser:
|
|
with mock.patch.object(entrypoint, "initialize_runtime_interface_with_retry", return_value=None):
|
|
with mock.patch.object(entrypoint, "initialize_globals") as initialize_globals:
|
|
setup_parser.return_value.parse_args.return_value = args
|
|
entrypoint.main(stdscr)
|
|
|
|
initialize_globals.assert_not_called()
|
|
|
|
def test_start_handles_keyboard_interrupt(self) -> None:
|
|
interface = mock.Mock()
|
|
interface_state.interface = interface
|
|
|
|
with mock.patch.object(entrypoint.sys, "argv", ["contact"]):
|
|
with mock.patch.object(entrypoint.curses, "wrapper", side_effect=KeyboardInterrupt):
|
|
with mock.patch.object(entrypoint.sys, "exit", side_effect=SystemExit(0)) as exit_mock:
|
|
with self.assertRaises(SystemExit) as raised:
|
|
entrypoint.start()
|
|
|
|
self.assertEqual(raised.exception.code, 0)
|
|
interface.close.assert_called_once_with()
|
|
exit_mock.assert_called_once_with(0)
|
|
|
|
def test_start_handles_keyboard_interrupt_with_no_interface(self) -> None:
|
|
interface_state.interface = None
|
|
|
|
with mock.patch.object(entrypoint.sys, "argv", ["contact"]):
|
|
with mock.patch.object(entrypoint.curses, "wrapper", side_effect=KeyboardInterrupt):
|
|
with mock.patch.object(entrypoint.sys, "exit", side_effect=SystemExit(0)) as exit_mock:
|
|
with self.assertRaises(SystemExit) as raised:
|
|
entrypoint.start()
|
|
|
|
self.assertEqual(raised.exception.code, 0)
|
|
exit_mock.assert_called_once_with(0)
|
|
|
|
def test_start_handles_fatal_exception_and_exits_one(self) -> None:
|
|
with mock.patch.object(entrypoint.sys, "argv", ["contact"]):
|
|
with mock.patch.object(entrypoint.curses, "wrapper", side_effect=RuntimeError("boom")):
|
|
with mock.patch.object(entrypoint.curses, "endwin") as endwin:
|
|
with mock.patch.object(entrypoint.traceback, "print_exc") as print_exc:
|
|
with mock.patch("builtins.print") as print_mock:
|
|
with mock.patch.object(entrypoint.sys, "exit", side_effect=SystemExit(1)) as exit_mock:
|
|
with self.assertRaises(SystemExit) as raised:
|
|
entrypoint.start()
|
|
|
|
self.assertEqual(raised.exception.code, 1)
|
|
endwin.assert_called_once_with()
|
|
print_exc.assert_called_once_with()
|
|
print_mock.assert_any_call("Fatal error:", mock.ANY)
|
|
exit_mock.assert_called_once_with(1)
|