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>
72 lines
2.9 KiB
Python
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
|