Initializare dupa curatare folder
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
"""Tests for the deploy artifacts.
|
||||
|
||||
These are the files that only ever run on the target device, so a mistake in
|
||||
them is found late and painfully. The checks here are the ones that would have
|
||||
caught real bugs: the install must not depend on the network, and the service
|
||||
must not reference something the installer does not create.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PROJECT = os.path.dirname(HERE)
|
||||
DEPLOY = os.path.join(PROJECT, "deploy")
|
||||
|
||||
INSTALL_SH = os.path.join(DEPLOY, "install.sh")
|
||||
UNIT = os.path.join(DEPLOY, "rdm-card-reader.service")
|
||||
RULES = os.path.join(DEPLOY, "99-rdm-reader.rules")
|
||||
|
||||
|
||||
def read(path):
|
||||
with open(path, "r") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
# -- install.sh -------------------------------------------------------------
|
||||
|
||||
def test_installer_exists_and_is_executable():
|
||||
assert os.path.exists(INSTALL_SH)
|
||||
mode = os.stat(INSTALL_SH).st_mode
|
||||
assert mode & stat.S_IXUSR, "install.sh should be executable"
|
||||
|
||||
|
||||
def test_installer_does_not_pip_install_the_project():
|
||||
"""`pip install .` needs setuptools from the network, so it breaks offline.
|
||||
|
||||
This is the bug that made installation fail on an air-gapped Pi with
|
||||
"Could not find a version that satisfies the requirement setuptools>=68".
|
||||
"""
|
||||
text = read(INSTALL_SH)
|
||||
code = "\n".join(
|
||||
line for line in text.splitlines() if not line.strip().startswith("#")
|
||||
)
|
||||
assert not re.search(r'pip"?\s+install[^\n]*"\$INSTALL_DIR"', code), (
|
||||
"install.sh must not 'pip install' the project directory: it requires "
|
||||
"downloading setuptools and fails with no network"
|
||||
)
|
||||
assert not re.search(r"pip\s+install\s+\.\s*$", code, re.M)
|
||||
|
||||
|
||||
def test_installer_installs_runtime_deps_from_bundled_wheels():
|
||||
text = read(INSTALL_SH)
|
||||
assert "--no-index" in text
|
||||
assert "--find-links" in text
|
||||
assert "wheels" in text
|
||||
|
||||
|
||||
def test_installer_adds_src_to_path():
|
||||
"""Without this the package is not importable at all."""
|
||||
text = read(INSTALL_SH)
|
||||
assert ".pth" in text
|
||||
|
||||
|
||||
def test_installer_network_steps_are_not_fatal():
|
||||
"""A device with no internet must still get a working install."""
|
||||
text = read(INSTALL_SH)
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("apt-get"):
|
||||
assert "|| " in line or line.rstrip().endswith("\\"), (
|
||||
"network-dependent step should not abort the install: " + stripped
|
||||
)
|
||||
# The pip self-upgrade is best-effort too. The fallback may sit on the
|
||||
# following line (line continuation), so join continuations first.
|
||||
joined = text.replace("\\\n", " ")
|
||||
assert re.search(r"upgrade pip[^\n]*\|\|", joined), (
|
||||
"'pip install --upgrade pip' needs the network, so it must not abort "
|
||||
"the install; add '|| ...' to it"
|
||||
)
|
||||
|
||||
|
||||
def test_installer_copies_the_wheels_with_the_source():
|
||||
"""The bundled wheels live under src/, so src/ must be copied."""
|
||||
text = read(INSTALL_SH)
|
||||
match = re.search(r"for item in ([^\n;]+);", text)
|
||||
assert match, "could not find the copy list in install.sh"
|
||||
assert "src" in match.group(1)
|
||||
|
||||
|
||||
def test_installer_checks_venv_creation_can_fail_clearly():
|
||||
text = read(INSTALL_SH)
|
||||
assert "python3-venv" in text, "should mention the usual cause of venv failure"
|
||||
|
||||
|
||||
def test_installer_verifies_the_install_before_enabling_service():
|
||||
text = read(INSTALL_SH)
|
||||
assert "--version" in text, "installer should sanity-check the install"
|
||||
|
||||
|
||||
def test_installer_copies_launch_doc():
|
||||
text = read(INSTALL_SH)
|
||||
assert "LAUNCH.md" in text
|
||||
|
||||
|
||||
# -- service unit -----------------------------------------------------------
|
||||
|
||||
def test_unit_exists():
|
||||
assert os.path.exists(UNIT)
|
||||
|
||||
|
||||
def test_unit_restarts_on_failure():
|
||||
text = read(UNIT)
|
||||
assert "Restart=always" in text
|
||||
assert "RestartSec=" in text
|
||||
|
||||
|
||||
def test_unit_runs_as_a_non_root_user_in_dialout():
|
||||
text = read(UNIT)
|
||||
assert re.search(r"^User=", text, re.M)
|
||||
assert re.search(r"^Group=dialout", text, re.M)
|
||||
|
||||
|
||||
def _sections(text):
|
||||
"""Split an INI-style unit into {section: body}.
|
||||
|
||||
Splitting on the literal string "[Service]" is wrong: the file mentions it
|
||||
in a comment above the real section header.
|
||||
"""
|
||||
sections = {}
|
||||
current = None
|
||||
for line in text.splitlines():
|
||||
header = re.match(r"^\[([^\]]+)\]\s*$", line)
|
||||
if header:
|
||||
current = header.group(1)
|
||||
sections[current] = []
|
||||
elif current is not None:
|
||||
sections[current].append(line)
|
||||
return {name: "\n".join(body) for name, body in sections.items()}
|
||||
|
||||
|
||||
def test_unit_start_limits_are_in_the_unit_section():
|
||||
"""systemd silently ignores these in [Service]."""
|
||||
sections = _sections(read(UNIT))
|
||||
assert "Unit" in sections and "Service" in sections
|
||||
for key in ("StartLimitIntervalSec", "StartLimitBurst"):
|
||||
assert key not in sections["Service"], (
|
||||
"{} belongs in [Unit]; systemd ignores it in [Service]".format(key)
|
||||
)
|
||||
assert "StartLimitBurst" in sections["Unit"]
|
||||
|
||||
|
||||
def test_unit_has_the_expected_sections():
|
||||
sections = _sections(read(UNIT))
|
||||
assert set(sections) >= {"Unit", "Service", "Install"}
|
||||
|
||||
|
||||
def test_unit_uses_loopback_by_default():
|
||||
text = read(UNIT)
|
||||
# No --host flag means the CLI default (127.0.0.1) applies.
|
||||
assert "--host" not in text
|
||||
|
||||
|
||||
def test_unit_serves_in_login_mode():
|
||||
text = read(UNIT)
|
||||
assert "--serve" in text
|
||||
assert "--mode login" in text
|
||||
|
||||
|
||||
def test_unit_points_at_the_stable_device_alias():
|
||||
text = read(UNIT)
|
||||
assert "/dev/rdm-reader" in text
|
||||
|
||||
|
||||
# -- udev rule --------------------------------------------------------------
|
||||
|
||||
def test_udev_rule_creates_the_alias_the_unit_expects():
|
||||
text = read(RULES)
|
||||
assert 'SYMLINK+="rdm-reader"' in text
|
||||
unit = read(UNIT)
|
||||
assert "/dev/rdm-reader" in unit, "unit and rule must agree on the alias name"
|
||||
|
||||
|
||||
def test_udev_rule_sets_group_and_mode():
|
||||
text = read(RULES)
|
||||
assert 'GROUP="dialout"' in text
|
||||
assert 'MODE="0660"' in text
|
||||
|
||||
|
||||
def test_udev_rule_only_has_one_active_match():
|
||||
"""Multiple active rules would fight over the same alias."""
|
||||
active = [
|
||||
line
|
||||
for line in read(RULES).splitlines()
|
||||
if line.strip().startswith('SUBSYSTEM=="tty"')
|
||||
]
|
||||
assert len(active) == 1, "exactly one rule should be active, found {}".format(
|
||||
len(active)
|
||||
)
|
||||
Reference in New Issue
Block a user