"""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