bandsaunter/tests/test_licence.py
The Dust Council 4239635f74 Say what the terms are, and how to install under them
The program had no licence file at all, and pyproject claimed MIT into
the void.  It is now the GNU General Public License, version 3 or later:
LICENSE holds the text verbatim, pyproject declares it with the OSI
classifier, both .deb builds write /usr/share/doc/<pkg>/copyright in the
machine-readable format Policy requires, both manuals carry a COPYING
section, and --version prints the GNU notice on both programs.

INSTALL.md is the step-by-step: what you need, the Debian package, the
virtual environment for everywhere else, how to check it worked, every
optional dependency with what it buys and what happens without it, and
the errors people actually hit first -- PEP 668 at the top, because on
Debian a plain "pip install ." refuses and reads as a broken program.

Speech transcription gets its own four steps, because it is the only
part with a real download in it: the recogniser into the environment
bandsaunter runs from, checking it took, the model (base.en, 148 MB,
from Hugging Face into ~/.cache/huggingface, fetched deliberately rather
than in the middle of a scan), then turning it on.  With the sizes of
every model, the offline routes, and what to do when --engines says no
although pip says yes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
2026-09-03 22:58:18 -07:00

211 lines
8 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"):
assert optional in body, optional
assert "Optional dependencies" 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