"""The in-application menus, driven by scripted answers.""" import pytest from rich.console import Console from bandsaunter import settings as st, tui from bandsaunter.config import ScanConfig @pytest.fixture def console(): return Console(width=100, file=open("/dev/null", "w"), force_terminal=False) def drive(monkeypatch, answers): """Feed the menus a fixed list of answers, then stop.""" script = list(answers) def fake_ask(console, prompt, default=""): if not script: raise _Done() return script.pop(0) monkeypatch.setattr(tui, "_ask", fake_ask) monkeypatch.setattr(tui.Confirm, "ask", lambda *a, **k: True) return script class _Done(Exception): """Raised to break out when the script runs out.""" def run(monkeypatch, console, func, answers, *args): drive(monkeypatch, answers) try: return func(console, *args) except _Done: return None def test_settings_menu_edits_a_value(monkeypatch, console): cfg = ScanConfig() run(monkeypatch, console, tui.settings_menu, ["1", "2", "6", "", ""], cfg) assert cfg.hang_seconds == 6.0 def test_settings_menu_finds_a_setting_by_search(monkeypatch, console): cfg = ScanConfig() run(monkeypatch, console, tui.settings_menu, ["voice score", "0.3", ""], cfg) assert cfg.min_voice_score == 0.3 def test_a_bad_value_is_refused_and_the_old_one_kept(monkeypatch, console): cfg = ScanConfig() before = cfg.hang_seconds run(monkeypatch, console, tui.settings_menu, ["1", "2", "banana", "", "", ""], cfg) assert cfg.hang_seconds == before def test_a_value_failing_validation_is_rolled_back(monkeypatch, console): """min_record longer than record would mean nothing is ever kept.""" cfg = ScanConfig() cfg.record_seconds = 5.0 run(monkeypatch, console, tui.settings_menu, ["1", "4", "9", "", "", ""], cfg) assert cfg.min_record_seconds != 9.0 assert not [e for e in cfg.validate() if "ranges" not in e] def test_every_group_can_be_opened_and_left(monkeypatch, console): cfg = ScanConfig() for i in range(1, len(st.GROUPS) + 1): run(monkeypatch, console, tui.settings_menu, [str(i), "", ""], cfg) def test_every_setting_renders_its_help(console): """Built-in help must exist and render for every single setting.""" cfg = ScanConfig() for s in st.SETTINGS: tui.setting_help(console, s, cfg) def test_reset_a_group_restores_defaults(monkeypatch, console): cfg = ScanConfig() cfg.hang_seconds = 99.0 cfg.record_seconds = 99.0 run(monkeypatch, console, tui.settings_menu, ["1", "d", "", ""], cfg) assert cfg.hang_seconds == ScanConfig().hang_seconds assert cfg.record_seconds == ScanConfig().record_seconds def test_help_screen_shows_every_topic(monkeypatch, console): for topic in tui._TOPICS: run(monkeypatch, console, tui.help_screen, [topic, ""]) def test_help_screen_looks_up_settings(monkeypatch, console): run(monkeypatch, console, tui.help_screen, ["hang", ""]) def test_ranges_can_be_added_from_the_band_plan(monkeypatch, console): cfg = ScanConfig() run(monkeypatch, console, tui.choose_presets, ["gmrs", "1", ""], cfg) assert cfg.ranges, "no range was added" def test_ranges_can_be_typed_in(monkeypatch, console): cfg = ScanConfig() run(monkeypatch, console, tui.add_manual_ranges, ["144M", "148M", "nfm", ""], cfg) assert len(cfg.ranges) == 1 assert cfg.ranges[0].start == 144e6 and cfg.ranges[0].stop == 148e6 assert cfg.ranges[0].mode == "nfm" def test_a_bad_frequency_is_refused(monkeypatch, console): cfg = ScanConfig() run(monkeypatch, console, tui.add_manual_ranges, ["banana", ""], cfg) assert not cfg.ranges def test_main_menu_can_start_a_scan(monkeypatch, console): cfg = ScanConfig() from bandsaunter.ranges import parse_range_list cfg.ranges = parse_range_list("144M-148M") out = run(monkeypatch, console, tui.run_tui, ["s"], cfg) assert out is cfg def test_main_menu_refuses_to_start_without_ranges(monkeypatch, console): cfg = ScanConfig() out = run(monkeypatch, console, tui.run_tui, ["s", "q"], cfg) assert out is None, "started a scan with nothing to scan" def test_main_menu_quits(monkeypatch, console): assert run(monkeypatch, console, tui.run_tui, ["q"], ScanConfig()) is None def test_closed_input_unwinds_instead_of_looping(monkeypatch, console): """With no input left, the menus must give up rather than spin forever.""" def eof(*a, **k): raise EOFError monkeypatch.setattr(tui.Prompt, "ask", eof) assert tui.run_tui(console, ScanConfig()) is None with pytest.raises(tui.TUIAbort): tui.settings_menu(console, ScanConfig()) def test_interrupt_unwinds_too(monkeypatch, console): def interrupt(*a, **k): raise KeyboardInterrupt monkeypatch.setattr(tui.Prompt, "ask", interrupt) assert tui.run_tui(console, ScanConfig()) is None # --------------------------------------------------------------------------- # First run # --------------------------------------------------------------------------- def test_first_run_is_detected_and_then_not(tmp_path): from bandsaunter.config import ScanConfig, is_first_run, save_default assert is_first_run(tmp_path) save_default(ScanConfig(), tmp_path) assert not is_first_run(tmp_path) def test_first_run_asks_where_to_save_and_remembers(monkeypatch, console, tmp_path): from bandsaunter import tui from bandsaunter.config import load_default wanted = tmp_path / "my recordings" monkeypatch.setattr(tui, "_ask", lambda c, p, d="": str(wanted)) monkeypatch.setattr(tui, "save_default", lambda cfg: __import__("bandsaunter.config", fromlist=["x"]).save_default( cfg, tmp_path)) cfg = ScanConfig() assert tui.first_run_setup(console, cfg) assert cfg.output_dir == str(wanted) assert wanted.is_dir(), "the directory should be created" back, _ = load_default(tmp_path) assert back.output_dir == str(wanted) def test_first_run_accepts_the_suggested_default(monkeypatch, console, tmp_path): from bandsaunter import tui monkeypatch.setattr(tui, "_ask", lambda c, p, d="": "") monkeypatch.setattr(tui, "save_default", lambda cfg: tmp_path / "x.yaml") monkeypatch.setattr(tui, "DEFAULT_OUTPUT_DIR", str(tmp_path / "default")) cfg = ScanConfig() tui.first_run_setup(console, cfg) assert cfg.output_dir == str(tmp_path / "default") def test_first_run_rejects_a_directory_it_cannot_write(monkeypatch, console, tmp_path): """Better to say so now than to fail on the first recording.""" from bandsaunter import tui answers = iter(["/proc/nonsense/cannot-create", str(tmp_path / "ok")]) monkeypatch.setattr(tui, "_ask", lambda c, p, d="": next(answers)) monkeypatch.setattr(tui, "save_default", lambda cfg: tmp_path / "x.yaml") cfg = ScanConfig() tui.first_run_setup(console, cfg) assert cfg.output_dir == str(tmp_path / "ok")