bandsaunter/tests/test_manpage.py
The Dust Council ba6c925351 Add a manual page, and explain every setting in plain words
Every setting now carries a paragraph saying what it is in everyday terms
and why someone who does not already speak radio would turn it up, down,
on or off: what the squelch knob actually is, why automatic gain is a bad
idea for scanning, why a bias tee can damage equipment, why setting the
transcription language matters on noisy audio. The menus and
`config --describe` show it alongside the existing technical detail.

packaging/make-man.py generates bandsaunter(1) from that same table, so the
manual cannot document a setting the program lacks or miss one it has --
tests check both, that the page renders through groff without a single
warning, and that the guidance survives into the rendered output. Around
it are hand-written sections on the commands, entering frequencies, the
band plan, lock-outs, the keys during a scan, HF, single sideband, files,
environment variables and worked examples.

The .deb regenerates and installs it rather than shipping a copy, so an
installed manual always matches the installed program.

The README picks up what the last few commits added: the plain display as
a saved setting, what the settings tests now guarantee, and where to read
the manual before installing.

Version is the day's build: 2026-08-22_01.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 00:01:00 -07:00

72 lines
2.9 KiB
Python

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