Files
rdm-card-reader/tests/test_wheels.py
T
ske087 23cd8f2df5 Ship the reader wheels inside the package for offline install
A device with no internet could not set itself up: the auto-install needed
PyPI. The rdm6300 and pyserial wheels now ship inside the package, so first
start works with no network at all.

Both wheels are pure Python (py3-none-any / py2.py3-none-any), which is what
makes this possible - the same files work on armv7l, aarch64 and x86_64. This
is the difference from the workstation app's Files/repository cache, which
held aarch64 manylinux binaries and so could not be committed; that cache only
ever existed on the Pi devices.

- bundled_wheel_dir() locates them; ensure_dependencies() prefers them
  automatically, so the service needs no --wheel-dir.
- verify_wheels() checks SHA256SUMS before installing. A wheel that fails its
  checksum is refused rather than installed, then PyPI is tried, and if that is
  unreachable it reports exit 2 instead of proceeding unverified.
- local_wheel_missing() falls back to PyPI when the bundle does not cover a
  requirement, instead of attempting a --no-index install that must fail.
- An explicit --wheel-dir that is incomplete or unverified is called out, not
  silently swapped for PyPI.

Verified end to end: built wheel and sdist both contain the wheels; a venv
with no rdm6300 self-provisions with a dead proxy and no PIP_NO_INDEX; and a
tampered wheel is refused with nothing installed.
2026-10-01 15:48:34 +03:00

230 lines
7.5 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):
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):
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):
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):
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)