031cf0f25a
The workstation app lit an LED on GPIO 23 with the card. This brings that
across, with three deliberate differences: it never blocks the reader, it
cannot be fatal, and its state is reported.
WMT toggled the LED directly inside card_inserted, on the reader thread, where
a slow write stalls card detection. Here every change is queued to a worker, so
insert() returns immediately (tested with a deliberately slow pin).
Two modes, because the light can mean two things:
error (default) lit while the reader runs, dark while reading, blinks on a
bad read - a health indicator
presence lit only while a card is on the reader
gpiozero raises BadPinFactory on a machine with no GPIO, and that subclasses
ImportError - so a single except ImportError reports 'gpiozero is not
installed' when the library is present but has no usable pin provider. The
library check and the pin construction are therefore separate.
Vendors gpiozero + colorzero (~180 KB) so the LED installs offline too, and
deliberately not setuptools: gpiozero declares it via colorzero, but it is only
a packaging requirement and is never imported at runtime. Installing to the
bundled wheels therefore uses --no-deps, saving 818 KB.
Verified end to end over a pseudo-terminal with a fake pin: a real RDM6300
frame drives the LED through the app's own CLI in both modes.
230 lines
7.6 KiB
Python
230 lines
7.6 KiB
Python
"""Tests for the vendored wheels and their verification.
|
|
|
|
The bundled wheels let a device with no internet set itself up, so the things
|
|
that matter are: they are actually present and pure-Python (portable), the
|
|
checksums match, and a tampered wheel is refused rather than installed.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import sys
|
|
|
|
SRC = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"
|
|
)
|
|
if SRC not in sys.path:
|
|
sys.path.insert(0, SRC)
|
|
|
|
import pytest # noqa: E402
|
|
|
|
from rdm_card_reader import bootstrap # noqa: E402
|
|
from rdm_card_reader.bootstrap import ( # noqa: E402
|
|
bundled_wheel_dir,
|
|
ensure_dependencies,
|
|
local_wheel_missing,
|
|
verify_wheels,
|
|
)
|
|
|
|
|
|
# -- the wheels that ship with the package ---------------------------------
|
|
|
|
def test_bundled_wheel_dir_exists():
|
|
folder = bundled_wheel_dir()
|
|
assert folder is not None
|
|
assert os.path.isdir(folder)
|
|
|
|
|
|
def test_both_required_wheels_are_present():
|
|
folder = bundled_wheel_dir()
|
|
names = os.listdir(folder)
|
|
assert any(n.startswith("rdm6300-") and n.endswith(".whl") for n in names)
|
|
assert any(n.startswith("pyserial-") and n.endswith(".whl") for n in names)
|
|
|
|
|
|
def test_bundled_wheels_are_pure_python():
|
|
"""A compiled wheel would only work on one architecture; these must not."""
|
|
folder = bundled_wheel_dir()
|
|
wheels = [n for n in os.listdir(folder) if n.endswith(".whl")]
|
|
assert wheels
|
|
for name in wheels:
|
|
assert name.endswith("-none-any.whl"), (
|
|
"{} is architecture-specific and cannot be shipped".format(name)
|
|
)
|
|
|
|
|
|
def test_bundled_wheels_verify_against_their_checksums():
|
|
folder = bundled_wheel_dir()
|
|
assert verify_wheels(folder) == []
|
|
|
|
|
|
def test_checksums_file_is_shipped():
|
|
folder = bundled_wheel_dir()
|
|
assert os.path.exists(os.path.join(folder, "SHA256SUMS"))
|
|
|
|
|
|
def test_total_vendored_size_is_reasonable():
|
|
"""Guard against someone dropping a huge binary into the repo."""
|
|
folder = bundled_wheel_dir()
|
|
total = sum(
|
|
os.path.getsize(os.path.join(folder, name))
|
|
for name in os.listdir(folder)
|
|
)
|
|
assert total < 2 * 1024 * 1024
|
|
|
|
|
|
# -- verification -----------------------------------------------------------
|
|
|
|
def test_tampered_wheel_is_detected(tmp_path):
|
|
folder = tmp_path / "wheels"
|
|
folder.mkdir()
|
|
wheel = folder / "rdm6300-0.1.1-py3-none-any.whl"
|
|
wheel.write_bytes(b"not the real wheel")
|
|
(folder / "SHA256SUMS").write_text(
|
|
"afb067da8b8d23a477138ca67d4ff1d198f12d157b64668c9dd3cbd7df54e74e"
|
|
" rdm6300-0.1.1-py3-none-any.whl\n"
|
|
)
|
|
problems = verify_wheels(str(folder))
|
|
assert problems
|
|
assert any("checksum mismatch" in p for p in problems)
|
|
|
|
|
|
def test_missing_wheel_is_reported(tmp_path):
|
|
folder = tmp_path / "wheels"
|
|
folder.mkdir()
|
|
(folder / "SHA256SUMS").write_text(
|
|
"abc123 rdm6300-0.1.1-py3-none-any.whl\n"
|
|
)
|
|
problems = verify_wheels(str(folder))
|
|
assert any("missing" in p for p in problems)
|
|
|
|
|
|
def test_no_sums_file_means_nothing_to_check(tmp_path):
|
|
folder = tmp_path / "wheels"
|
|
folder.mkdir()
|
|
(folder / "x-1.0-py3-none-any.whl").write_bytes(b"x")
|
|
assert verify_wheels(str(folder)) == []
|
|
|
|
|
|
def test_verification_passes_for_untouched_copy(tmp_path):
|
|
source = bundled_wheel_dir()
|
|
dest = tmp_path / "wheels"
|
|
shutil.copytree(source, dest)
|
|
assert verify_wheels(str(dest)) == []
|
|
|
|
|
|
# -- coverage check ---------------------------------------------------------
|
|
|
|
def test_local_wheel_missing_detects_absent_requirement(tmp_path):
|
|
folder = tmp_path / "wheels"
|
|
folder.mkdir()
|
|
assert local_wheel_missing(["rdm6300>=0.1.1"], str(folder)) == ["rdm6300>=0.1.1"]
|
|
|
|
|
|
def test_local_wheel_missing_accepts_present_requirement():
|
|
folder = bundled_wheel_dir()
|
|
assert local_wheel_missing(["rdm6300>=0.1.1", "pyserial>=3.5"], folder) == []
|
|
|
|
|
|
def test_local_wheel_missing_normalises_underscores(tmp_path):
|
|
folder = tmp_path / "wheels"
|
|
folder.mkdir()
|
|
(folder / "charset_normalizer-3.4.1-py3-none-any.whl").write_bytes(b"x")
|
|
assert local_wheel_missing(["charset-normalizer>=3"], str(folder)) == []
|
|
|
|
|
|
# -- integration into ensure_dependencies -----------------------------------
|
|
|
|
def test_bundled_wheels_used_automatically(monkeypatch):
|
|
"""No --wheel-dir given, so the bundled ones must be picked up."""
|
|
seen = {}
|
|
monkeypatch.setattr(
|
|
bootstrap, "missing_packages",
|
|
lambda packages=None: [] if seen.get("done") else [("rdm6300", "rdm6300>=0.1.1")],
|
|
)
|
|
|
|
def fake_install(requirements, wheel_dir=None, logger=None, timeout=300, no_deps=False):
|
|
seen["wheel_dir"] = wheel_dir
|
|
seen["done"] = True
|
|
return True
|
|
|
|
monkeypatch.setattr(bootstrap, "install", fake_install)
|
|
lines = []
|
|
assert ensure_dependencies(logger=lines.append) is True
|
|
assert seen["wheel_dir"] == bundled_wheel_dir()
|
|
assert any("bundled" in line for line in lines)
|
|
|
|
|
|
def test_tampered_bundle_falls_back_to_pypi(monkeypatch, tmp_path):
|
|
"""A bad checksum must not block installation - but must be reported."""
|
|
folder = tmp_path / "wheels"
|
|
folder.mkdir()
|
|
(folder / "rdm6300-0.1.1-py3-none-any.whl").write_bytes(b"tampered")
|
|
(folder / "SHA256SUMS").write_text("deadbeef rdm6300-0.1.1-py3-none-any.whl\n")
|
|
|
|
seen = {}
|
|
monkeypatch.setattr(bootstrap, "bundled_wheel_dir", lambda: str(folder))
|
|
monkeypatch.setattr(
|
|
bootstrap, "missing_packages",
|
|
lambda packages=None: [] if seen.get("done") else [("rdm6300", "rdm6300>=0.1.1")],
|
|
)
|
|
|
|
def fake_install(requirements, wheel_dir=None, logger=None, timeout=300, no_deps=False):
|
|
seen["wheel_dir"] = wheel_dir
|
|
seen["done"] = True
|
|
return True
|
|
|
|
monkeypatch.setattr(bootstrap, "install", fake_install)
|
|
lines = []
|
|
assert ensure_dependencies(logger=lines.append) is True
|
|
assert seen["wheel_dir"] is None # fell back to PyPI
|
|
assert any("checksum mismatch" in line for line in lines)
|
|
|
|
|
|
def test_explicit_wheel_dir_overrides_bundled(monkeypatch, tmp_path):
|
|
folder = tmp_path / "wheels"
|
|
folder.mkdir()
|
|
(folder / "rdm6300-0.1.1-py3-none-any.whl").write_bytes(b"x")
|
|
(folder / "pyserial-3.5-py2.py3-none-any.whl").write_bytes(b"x")
|
|
|
|
seen = {}
|
|
monkeypatch.setattr(
|
|
bootstrap, "missing_packages",
|
|
lambda packages=None: [] if seen.get("done") else [("rdm6300", "rdm6300>=0.1.1")],
|
|
)
|
|
|
|
def fake_install(requirements, wheel_dir=None, logger=None, timeout=300, no_deps=False):
|
|
seen["wheel_dir"] = wheel_dir
|
|
seen["done"] = True
|
|
return True
|
|
|
|
monkeypatch.setattr(bootstrap, "install", fake_install)
|
|
ensure_dependencies(wheel_dir=str(folder))
|
|
assert seen["wheel_dir"] == str(folder)
|
|
|
|
|
|
def test_bundle_not_covering_a_requirement_falls_back(monkeypatch, tmp_path):
|
|
"""A bundle missing one package must not cause a guaranteed pip failure."""
|
|
folder = tmp_path / "wheels"
|
|
folder.mkdir()
|
|
(folder / "rdm6300-0.1.1-py3-none-any.whl").write_bytes(b"x")
|
|
|
|
seen = {}
|
|
monkeypatch.setattr(bootstrap, "bundled_wheel_dir", lambda: str(folder))
|
|
monkeypatch.setattr(
|
|
bootstrap, "missing_packages",
|
|
lambda packages=None: [] if seen.get("done")
|
|
else [("rdm6300", "rdm6300>=0.1.1"), ("serial", "pyserial>=3.5")],
|
|
)
|
|
|
|
def fake_install(requirements, wheel_dir=None, logger=None, timeout=300, no_deps=False):
|
|
seen["wheel_dir"] = wheel_dir
|
|
seen["wheel_dir_at_call"] = wheel_dir
|
|
seen["done"] = True
|
|
return True
|
|
|
|
monkeypatch.setattr(bootstrap, "install", fake_install)
|
|
lines = []
|
|
ensure_dependencies(logger=lines.append)
|
|
assert seen["wheel_dir"] is None
|
|
assert any("do not cover" in line for line in lines)
|