An audit rather than a feature, prompted by wanting this fit to hand to somebody else. Five options had no command-line flag written down in the table the manual is generated from -- location, hold, schedules, tile_url and speed_unit -- so five flags that exist were missing from the manual. Four of them did exist under other names and are now recorded; hold had no flag at all and has one. Four switches could be turned off from the command line and not back on: --no-lookup, --no-basemap, --no-airports and --no-labels had no positive halves, so an option turned off in the saved settings could not be turned on again for one run. All four now have both. And adsb, which opens the window and draws a map when it stops, could not be given any of the settings that decide what those look like: no --at, no --radius, no --tiles, no --map-brightness, no --width, --fps, --trail, --fade, --stale, --airports or --labels. It takes all of them now. The manual had no list of the aircraft options at all -- the ADS-B sections were hand-written prose -- so five of them appeared nowhere in it. It now generates an AIRCRAFT OPTIONS section from the same table the menu and the flags come from, and the readme carries a table of all thirty-four with their flags and defaults. Three tests hold the three of them together: one that every option records its flag, one that every flag the table claims actually exists on a command, and one that the readme names them all. The installing instructions now list every dependency rather than only the optional ones: the four Python packages with their names in Debian, Fedora and Arch, and librtlsdr, which is a C library and therefore the one thing pip cannot bring and a virtual environment cannot supply. What reaches a network is written down too -- which host, when, and which file it is cached in -- since somebody installing this on a metered or air-gapped machine has to be able to see that nothing is fetched behind their back. build-repo.sh needs dpkg-dev and apt-utils, which a minimal system does not have, and now says so. Verified rather than asserted: a clean virtual environment, pip install from this tree, and a real log read back through the installed command. It pulls seven wheels rather than the four the page claimed, the other three being what Rich brings with it. Also: matplotlib is gone from the readme's dependency table, nothing having imported it; ffmpeg and Qt are in it, both having been missing; ffmpeg is a Suggests on the package; and resume.sh is ignored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
245 lines
9.5 KiB
Python
245 lines
9.5 KiB
Python
"""The licence, and the instructions for installing under it.
|
|
|
|
Both are files a person reads rather than code that runs, which is exactly why
|
|
they are worth a test: nothing else notices when a licence file goes missing
|
|
from a package, when the version notice stops naming the terms, or when the
|
|
install instructions describe a command that no longer exists.
|
|
"""
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import bandsaunter
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
LICENCE = ROOT / "LICENSE"
|
|
INSTALL = ROOT / "INSTALL.md"
|
|
README = ROOT / "README.md"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The licence itself
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_the_licence_is_there_and_is_the_gnu_gpl():
|
|
assert LICENCE.is_file(), "no LICENSE file"
|
|
text = LICENCE.read_text()
|
|
assert "GNU GENERAL PUBLIC LICENSE" in text
|
|
assert "Version 3, 29 June 2007" in text
|
|
|
|
|
|
def test_the_licence_is_the_whole_of_it():
|
|
"""A truncated licence is not a licence. The GPL ends with the notice it
|
|
tells you to attach to a program, so if that is present, so is the rest."""
|
|
text = LICENCE.read_text()
|
|
assert "TERMS AND CONDITIONS" in text
|
|
assert "How to Apply These Terms to Your New Programs" in text
|
|
assert len(text.splitlines()) > 600
|
|
|
|
|
|
def test_the_packaging_metadata_says_the_same_licence():
|
|
body = (ROOT / "pyproject.toml").read_text()
|
|
assert 'license = {text = "GPL-3.0-or-later"}' in body
|
|
assert "GNU General Public License v3 or later" in body
|
|
|
|
|
|
def test_the_readme_says_which_licence_and_points_at_the_file():
|
|
body = README.read_text()
|
|
assert "GNU General Public License" in body
|
|
assert "[LICENSE](LICENSE)" in body
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# What the programs say for themselves
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("program", ["bandsaunter", "saunterbrowse"])
|
|
def test_the_version_notice_names_the_version_and_the_terms(program):
|
|
notice = bandsaunter.version_notice(program)
|
|
lines = notice.splitlines()
|
|
assert lines[0] == f"{program} {bandsaunter.__version__}"
|
|
assert "Copyright (C)" in notice
|
|
assert "GNU GPL version 3 or later" in notice
|
|
assert "NO WARRANTY" in notice
|
|
|
|
|
|
def test_the_command_prints_it():
|
|
out = subprocess.run([sys.executable, "-m", "bandsaunter", "--version"],
|
|
capture_output=True, text=True, timeout=60)
|
|
assert bandsaunter.__version__ in out.stdout
|
|
assert "GPLv3+" in out.stdout
|
|
# Four lines under the version, not reflowed into a paragraph by argparse.
|
|
assert len(out.stdout.strip().splitlines()) == 5
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The manuals
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("page", ["bandsaunter.1", "saunterbrowse.1"])
|
|
def test_both_manuals_have_a_copying_section(page):
|
|
body = (ROOT / "packaging" / page).read_text()
|
|
assert ".SH COPYING" in body
|
|
assert "General\nPublic License" in body or "General Public License" in body
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The Debian package carries the licence where policy says it must
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_the_package_ships_the_licence_at_the_path_policy_requires():
|
|
script = (ROOT / "packaging" / "build-deb.sh").read_text()
|
|
assert "usr/share/doc/bandsaunter/copyright" in script
|
|
assert '"$here/LICENSE"' in script
|
|
|
|
|
|
def test_the_package_ships_the_install_instructions_too():
|
|
assert 'INSTALL.md' in (ROOT / "packaging" / "build-deb.sh").read_text()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The install instructions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_the_install_file_exists_and_is_pointed_at_from_the_readme():
|
|
assert INSTALL.is_file(), "no INSTALL.md"
|
|
assert "[INSTALL.md](INSTALL.md)" in README.read_text()
|
|
|
|
|
|
def test_the_install_file_covers_both_ways_of_installing():
|
|
body = INSTALL.read_text()
|
|
assert "./packaging/build-deb.sh" in body
|
|
assert "python3 -m venv" in body
|
|
assert "pip install ." in body
|
|
|
|
|
|
def test_it_warns_about_the_thing_every_debian_user_hits_first():
|
|
"""pip refuses to install outside a virtual environment on Debian and
|
|
Fedora. Someone who has not met PEP 668 reads that error as a broken
|
|
program."""
|
|
body = INSTALL.read_text()
|
|
assert "externally-managed-environment" in body
|
|
assert "PEP 668" in body
|
|
|
|
|
|
def test_it_lists_the_optional_dependencies_and_what_each_one_buys():
|
|
body = INSTALL.read_text()
|
|
for optional in ("espeak-ng", "ffmpeg", "faster-whisper", "vosk",
|
|
"rtl-sdr", "pyqt6", "openai-whisper", "pocketsphinx"):
|
|
assert optional in body, optional
|
|
# What each one buys, and what is lost without it: the two columns are
|
|
# the point of the table, not the list of names.
|
|
assert "Gives you" in body and "Without it" in body
|
|
assert "Optional." in body
|
|
|
|
|
|
def test_it_lists_the_required_dependencies_and_who_needs_them():
|
|
"""Somebody installing from scratch has to be told the whole of it,
|
|
including the one piece pip cannot bring."""
|
|
body = INSTALL.read_text()
|
|
for required in ("python3-numpy", "python3-scipy", "python3-rich",
|
|
"python3-yaml", "librtlsdr0"):
|
|
assert required in body, required
|
|
assert "Fedora" in body and "Arch" in body
|
|
|
|
|
|
def test_it_says_which_dependency_pip_cannot_install():
|
|
"""librtlsdr is a C library, so no virtual environment brings it and it
|
|
is the one thing that has to come from the distribution by hand."""
|
|
body = INSTALL.read_text()
|
|
assert "librtlsdr" in body
|
|
assert "pip` cannot install it" in body or "cannot come from pip" in body
|
|
assert "dnf install rtl-sdr" in body
|
|
assert "pacman -S rtl-sdr" in body
|
|
assert "brew install librtlsdr" in body
|
|
|
|
|
|
def test_it_says_what_reaches_a_network_and_where_it_is_cached():
|
|
"""Nothing is fetched behind anybody's back, and somebody installing
|
|
this on a metered or air-gapped machine has to be able to see that."""
|
|
body = INSTALL.read_text()
|
|
for source in ("api.adsbdb.com", "hexdb.io", "tile.openstreetmap.org",
|
|
"overpass-api.de"):
|
|
assert source in body, source
|
|
assert "~/.cache/bandsaunter/tiles" in body
|
|
|
|
|
|
def test_it_says_how_to_install_the_recogniser_and_the_model():
|
|
"""The one part with a real download in it, and the part that has to be
|
|
put in the same environment bandsaunter runs from."""
|
|
body = INSTALL.read_text()
|
|
assert "pip install faster-whisper" in body
|
|
assert "--user --break-system-packages" in body # the ~/.local layout
|
|
assert "bandsaunter transcribe --engines" in body # how to check it took
|
|
assert "Systran/faster-whisper-base.en" in body # what gets downloaded
|
|
assert "~/.cache/huggingface" in body # and where it lands
|
|
assert "WhisperModel('base.en'" in body # fetching it on purpose
|
|
|
|
|
|
def test_the_model_the_instructions_name_is_the_one_the_program_asks_for():
|
|
"""The instructions download base.en because that is the default; if the
|
|
default moves, they are downloading the wrong thing."""
|
|
from bandsaunter.config import ScanConfig
|
|
|
|
assert ScanConfig().transcribe_model in INSTALL.read_text()
|
|
|
|
|
|
def test_it_names_the_ways_a_model_can_be_supplied_without_a_network():
|
|
body = INSTALL.read_text()
|
|
assert "BANDSAUNTER_MODEL_DIR" in body
|
|
assert "/usr/share/bandsaunter/models" in body
|
|
assert "build-repo.sh" in body
|
|
|
|
|
|
def test_it_says_where_the_engine_output_switch_is():
|
|
"""A silent minute on the first transcription reads as a hang."""
|
|
assert "BANDSAUNTER_ENGINE_OUTPUT=1" in INSTALL.read_text()
|
|
|
|
|
|
def test_the_environment_variables_it_names_are_the_ones_the_code_reads():
|
|
from bandsaunter import transcribe as tr
|
|
|
|
body = INSTALL.read_text()
|
|
for variable in ("BANDSAUNTER_MODEL_DIR", "BANDSAUNTER_ENGINE_OUTPUT"):
|
|
assert variable in body
|
|
assert str(tr.MODEL_DIR) in body or "BANDSAUNTER_MODEL_DIR" in body
|
|
|
|
|
|
def test_the_settings_it_tells_you_to_change_exist():
|
|
from bandsaunter.config import ScanConfig
|
|
|
|
fields = set(ScanConfig().__dict__)
|
|
told = set(re.findall(r"bandsaunter config ([a-z_]+)=", INSTALL.read_text()))
|
|
assert told, "the instructions change no settings at all"
|
|
assert told <= fields, told - fields
|
|
|
|
|
|
def test_it_says_what_to_do_when_the_dongle_is_not_found():
|
|
body = INSTALL.read_text()
|
|
assert "dvb_usb_rtl28xxu" in body
|
|
assert "librtlsdr" in body
|
|
|
|
|
|
def test_every_bandsaunter_command_it_tells_you_to_run_exists():
|
|
"""An instruction to run a command that was renamed is worse than none."""
|
|
from bandsaunter.cli import build_parser
|
|
|
|
known = set()
|
|
for action in build_parser()._subparsers._group_actions:
|
|
known.update(action.choices)
|
|
told = set(re.findall(r"^\s*(?:\$ )?bandsaunter ([a-z]+)",
|
|
INSTALL.read_text(), re.MULTILINE))
|
|
assert told, "the instructions run no commands at all"
|
|
assert told <= known | {"--version"}, told - known
|
|
|
|
|
|
def test_the_flags_it_tells_you_to_use_are_real():
|
|
from bandsaunter.cli import build_parser
|
|
|
|
parser = build_parser()
|
|
for line in ("scan -b 2m --simulate", "adsb --simulate --seconds 30",
|
|
"devices --test", "transcribe --engines"):
|
|
parser.parse_args(line.split()) # raises SystemExit if not
|