"""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) )