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
211 lines
8 KiB
Python
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
|