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>
This commit is contained in:
parent
44c98b11e3
commit
ba6c925351
8 changed files with 1711 additions and 4 deletions
72
tests/test_manpage.py
Normal file
72
tests/test_manpage.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue