"""The manual page, which is generated from the settings table.""" import shutil import subprocess import sys from pathlib import Path import pytest from bandsaunter import settings as st GENERATOR = Path(__file__).resolve().parent.parent / "packaging" / "make-man.py" @pytest.fixture(scope="module") def page(tmp_path_factory): out = tmp_path_factory.mktemp("man") / "bandsaunter.1" subprocess.run([sys.executable, str(GENERATOR), str(out)], check=True, capture_output=True) return out.read_text() def test_every_setting_is_documented(page): """A setting the manual does not mention is one nobody can look up.""" missing = [s.key for s in st.SETTINGS if s.key not in page] assert not missing, f"settings missing from the manual: {missing}" def test_every_flag_is_documented(page): missing = [f for s in st.SETTINGS for f in s.flags + s.off_flags if f.replace("-", "\\-") not in page and f not in page] assert not missing, f"flags missing from the manual: {missing}" def test_every_setting_explains_itself_in_plain_words(page): """The guidance is the point of the manual: what it is, when to change it.""" for s in st.SETTINGS: assert s.guidance, f"{s.key} has no plain-language guidance" assert len(s.guidance) > 80, f"{s.key}'s guidance says too little" # The first sentence has to stand on its own for someone skimming. assert s.guidance.rstrip().endswith("."), s.key def test_the_commands_and_the_keys_are_documented(page): for word in ("scan", "bands", "config", "transcribe", "devices", "profiles", "analyze"): assert f".B {word}\n" in page, f"command {word} undocumented" for section in ("SYNOPSIS", "DESCRIPTION", "COMMANDS", "OPTIONS", "SETTINGS", "FILES", "ENVIRONMENT", "EXAMPLES"): assert f".SH {section}" in page @pytest.mark.skipif(not shutil.which("groff"), reason="groff not installed") def test_it_renders_without_complaint(page, tmp_path): """Troff is unforgiving: an unescaped leading dot silently eats a line.""" src = tmp_path / "bandsaunter.1" src.write_text(page) proc = subprocess.run(["groff", "-man", "-Tutf8", "-ww", "-z", str(src)], capture_output=True, text=True) assert proc.returncode == 0, proc.stderr assert not proc.stderr.strip(), proc.stderr @pytest.mark.skipif(not shutil.which("groff"), reason="groff not installed") def test_the_guidance_survives_into_the_rendered_page(page, tmp_path): src = tmp_path / "bandsaunter.1" src.write_text(page) rendered = subprocess.run(["groff", "-man", "-Tutf8", str(src)], capture_output=True, text=True).stdout flat = " ".join(rendered.replace("\b", "").split()) # A sentence from one setting's guidance, chosen because it is the one a # newcomer most needs: what the squelch actually is. assert "This is the squelch knob." in flat # -- the browser's page ------------------------------------------------------ BROWSE_GENERATOR = (Path(__file__).resolve().parent.parent / "packaging" / "make-browse-man.py") @pytest.fixture(scope="module") def browse_page(tmp_path_factory): out = tmp_path_factory.mktemp("man") / "saunterbrowse.1" subprocess.run([sys.executable, str(BROWSE_GENERATOR), str(out)], check=True, capture_output=True) return out.read_text() def test_the_browser_has_a_page_of_its_own(browse_page): assert ".TH SAUNTERBROWSE 1" in browse_page assert "saunterbrowse \\- read and listen" in browse_page def test_every_browser_flag_is_documented(browse_page): from bandsaunter.browse import build_parser # --help is argparse's own and needs no prose of its own. flags = [o for a in build_parser()._actions for o in a.option_strings if o not in ("-h", "--help")] missing = [f for f in flags if f.replace("-", "\\-") not in browse_page and f not in browse_page] assert not missing, f"undocumented flags: {missing}" def test_every_browser_key_is_documented(browse_page): """A key that does something the manual does not mention is a key nobody will press.""" from bandsaunter.browse import FILING for key in ("Enter", "Space", "PgUp", "Home", "/", "s", "r", "o", "q", "t", "u", "d", "m"): assert f".B {key}\n" in browse_page or f'.B "{key}' in browse_page, key for _key, name, _why in FILING: assert name in browse_page, name assert '.B "' + " ".join(k for k, _, _ in FILING) in browse_page def test_the_browser_page_names_the_players_it_looks_for(browse_page): from bandsaunter.browse import PLAYERS for name, _ in PLAYERS: assert name in browse_page, name def test_the_two_pages_point_at_each_other(page, browse_page): assert "saunterbrowse (1)" in page or "saunterbrowse" in page assert "bandsaunter (1)" in browse_page def test_the_browser_page_renders_without_complaint(browse_page, tmp_path): groff = shutil.which("groff") if groff is None: pytest.skip("groff is not installed") src = tmp_path / "saunterbrowse.1" src.write_text(browse_page) done = subprocess.run([groff, "-man", "-ww", "-z", str(src)], capture_output=True, text=True) assert done.returncode == 0, done.stderr assert not done.stderr.strip(), done.stderr def test_the_manual_lists_every_aircraft_option(page): """It is generated from the same table the menu and the flags are, so an option added to the program cannot quietly fail to be documented.""" from bandsaunter import aircraft as air for option in air.OPTIONS: flags = tuple(option.flags) + tuple(option.off_flags) assert any(flag in page for flag in flags), \ f"{option.key} ({', '.join(flags)}) is not in the manual" assert option.key in page, f"{option.key} is not named in the manual"