23cd8f2df5
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.
230 lines
7.5 KiB
Python
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)
|