The terminal board says what is overhead. This says where: a real map with the aircraft moving on it as the frames arrive, and beside each one a box carrying everything known about the flight -- type and registration, who operates it, where it came from and where it is going, height with a rate of climb, speed and heading, how far away and on what bearing, its position, how many frames it has sent and how long since the last one. Qt is asked for and not required. Four bindings are tried, the module imports on a machine with none of them, and asking for the window without one gets the instructions rather than a traceback -- before the receiver is opened, since nothing is gained by taking the dongle for a window that cannot be drawn. In the menu, "listen now" is now "passive capture" with a realtime display beside it. Closing the window leaves exactly the files pressing control-C leaves, because listen and watch share one read loop and one finishing step; the receiver runs on its own thread, so a slow repaint cannot cost a frame and a slow tile fetch cannot stall the picture. The animation's labels grew to match: flight level and speed, type and registration, and both ends of the route, each with a small flag of the country its airport is in. The flags are a table rather than a network -- twelve pixels by eight, where a flag is the arrangement that makes one recognisable rather than a rendering of the real thing -- and a country not in the table is named by its two letters, since a flag that is nearly another country's is worse than none. Where a route arrives as bare codes the country comes from the ICAO prefix. Four things found on the way. The window ignored --seconds, so "listen for ten minutes" meant something different with a window open; it closes itself now. The register was being asked twice per aircraft, once for labels and once for airport positions. Cached routes had no country in them, so the first real redraw drew no flags at all -- routes are versioned now. And past fourteen aircraft on one frame the labels go back to the callsign, the height and the speed, because five lines beside each of three hundred aircraft is a page of overlapping text with a map somewhere behind it. Long names are folded rather than allowed to stretch a box, breaking at the arrow of a route so the two ends stay whole; and the animation's label placement gained the same ring search the window uses, having only ever tried four spots. 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", "pyqt6"):
|
|
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
|