diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/hypr-live-update-guard/test_hypr_live_update_guard.py | 72 | ||||
| -rw-r--r-- | tests/installer-steps/test_pacman_install.py | 95 |
2 files changed, 166 insertions, 1 deletions
diff --git a/tests/hypr-live-update-guard/test_hypr_live_update_guard.py b/tests/hypr-live-update-guard/test_hypr_live_update_guard.py index 5ec5ce8..a6c6f68 100644 --- a/tests/hypr-live-update-guard/test_hypr_live_update_guard.py +++ b/tests/hypr-live-update-guard/test_hypr_live_update_guard.py @@ -12,6 +12,10 @@ Test seams (env vars the production script honors): HYPR_GUARD_RUNNING 1/0 forces the Hyprland-running check (default: pgrep) HYPR_ALLOW_LIVE_UPDATE 1 overrides the guard (proceed anyway) HYPR_GUARD_SENTINEL path whose existence also overrides the guard + HYPR_GUARD_VERSIONS "pkg installed candidate" lines replacing the + pacman -Q / expac -S version lookups; when set, + a package absent from the map reads as unknown + (conservative block) Run from repo root: python3 -m unittest tests.hypr-live-update-guard.test_hypr_live_update_guard @@ -26,8 +30,18 @@ import unittest REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) GUARD = os.path.join(REPO_ROOT, "scripts", "hypr-live-update-guard") +# Every package the legacy tests feed, mapped to a version CHANGE — those +# tests describe real upgrades, and the map keeps them hermetic (no +# pacman/expac calls against the test host). +CHANGING_VERSIONS = "\n".join(( + "mesa 25.1.0-1 26.0.0-1", + "hyprland 0.55.3-1 0.55.4-1", + "vulkan-radeon 25.1.0-1 26.0.0-1", +)) -def run_guard(stdin="mesa\n", running="1", allow=None, sentinel=None): + +def run_guard(stdin="mesa\n", running="1", allow=None, sentinel=None, + versions=CHANGING_VERSIONS): env = dict(os.environ) env["HYPR_GUARD_RUNNING"] = running if allow is not None: @@ -35,6 +49,7 @@ def run_guard(stdin="mesa\n", running="1", allow=None, sentinel=None): # Point the sentinel at a path that does not exist unless a test sets one, # so the host's real /run state can't leak into the result. env["HYPR_GUARD_SENTINEL"] = sentinel if sentinel else "/nonexistent/guard-sentinel" + env["HYPR_GUARD_VERSIONS"] = versions return subprocess.run( ["sh", GUARD], input=stdin, capture_output=True, text=True, timeout=10, env=env, @@ -75,6 +90,42 @@ class HyprLiveUpdateGuard(unittest.TestCase): r = run_guard(stdin="", running="1") self.assertEqual(r.returncode, 1) + # --- Version awareness ------------------------------------------------ + + def test_same_version_reinstall_allows(self): + # a pure reinstall replaces identical bytes with identical bytes — + # no live-swap hazard, the guard must let it through + r = run_guard(stdin="hyprland\n", running="1", + versions="hyprland 0.55.4-1 0.55.4-1") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertEqual(r.stderr.strip(), "") + + def test_all_same_version_multi_pkg_allows(self): + versions = "\n".join(("mesa 26.1.4-1 26.1.4-1", + "hyprland 0.55.4-1 0.55.4-1", + "vulkan-radeon 26.1.4-1 26.1.4-1")) + r = run_guard(stdin="mesa\nhyprland\nvulkan-radeon\n", running="1", + versions=versions) + self.assertEqual(r.returncode, 0, r.stderr) + + def test_mixed_blocks_naming_only_the_version_changing(self): + versions = "\n".join(("mesa 25.1.0-1 26.0.0-1", + "hyprland 0.55.4-1 0.55.4-1")) + r = run_guard(stdin="mesa\nhyprland\n", running="1", + versions=versions) + self.assertEqual(r.returncode, 1) + self.assertIn("- mesa", r.stderr) + # the prose mentions the Hyprland session, so assert on the + # package-list line format, not the bare word + self.assertNotIn("- hyprland", r.stderr) + + def test_unknown_versions_block_conservatively(self): + # seam set but package absent from the map = the lookup failed + # (AUR target, -U transaction, expac missing) — stay safe + r = run_guard(stdin="mesa\n", running="1", versions="") + self.assertEqual(r.returncode, 1) + self.assertIn("- mesa", r.stderr) + # --- Override / error cases ----------------------------------------- def test_env_override_proceeds_even_when_running(self): @@ -86,6 +137,25 @@ class HyprLiveUpdateGuard(unittest.TestCase): r = run_guard(stdin="mesa\n", running="1", sentinel=f.name) self.assertEqual(r.returncode, 0, r.stderr) + def test_sentinel_is_consumed_on_use(self): + # one touch = one transaction: if the override's cleanup never runs + # (a crashed caller), a leftover sentinel must not keep the guard + # disarmed until reboot — the hook deletes it as it honors it + fd, path = tempfile.mkstemp(prefix="guard-allow-") + os.close(fd) + try: + r = run_guard(stdin="mesa\n", running="1", sentinel=path) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertFalse(os.path.exists(path)) + finally: + if os.path.exists(path): + os.unlink(path) + + def test_env_override_consumes_nothing(self): + # the env override isn't a file; nothing to consume, still proceeds + r = run_guard(stdin="mesa\n", running="1", allow="1") + self.assertEqual(r.returncode, 0, r.stderr) + def test_override_env_zero_does_not_bypass(self): r = run_guard(stdin="mesa\n", running="1", allow="0") self.assertEqual(r.returncode, 1, r.stderr) diff --git a/tests/installer-steps/test_pacman_install.py b/tests/installer-steps/test_pacman_install.py new file mode 100644 index 0000000..28c9a7f --- /dev/null +++ b/tests/installer-steps/test_pacman_install.py @@ -0,0 +1,95 @@ +"""Characterization tests for pacman_install's install-reason handling. + +pacman --needed skips a package that is already present as a dependency and +leaves its install reason alone. A declared package can then sit as asdeps +on an existing system, show up as an orphan once its accidental dependent +leaves, and get swept away by an orphan cleanup (expac and lm_sensors nearly +went this way on 2026-07-08). pacman_install therefore marks every declared +package explicit after a successful install. + +Method mirrors test_orchestrators: sed-extract the real functions from +`archsetup`, source them with `display`/`error_warn` silenced and `pacman` +replaced by a recorder, run, and assert the recorded calls. + +Run from repo root: + python3 -m unittest tests.installer-steps.test_pacman_install +""" + +import os +import subprocess +import textwrap +import unittest + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +ARCHSETUP = os.path.join(REPO_ROOT, "archsetup") + + +def run_pacman_install(pkg, pacman_s_rc=0, pacman_d_rc=0): + """Extract retry_install + pacman_install, run against a fake pacman. + + Returns (exit_code, recorded pacman calls as a list of strings). + """ + script = textwrap.dedent(""" + set -u + MAX_INSTALL_RETRIES=3 + logfile=/dev/null + display() { :; } + error_warn() { return 1; } + pacman() { + echo "pacman $*" >> "$CALLS" + case "$1" in + --noconfirm) return "$PACMAN_S_RC" ;; + -D) return "$PACMAN_D_RC" ;; + esac + } + %(functions)s + pacman_install "%(pkg)s" + """) + extract = subprocess.run( + ["sed", "-n", + "/^retry_install()/,/^}/p;/^pacman_install()/,/^}/p", ARCHSETUP], + capture_output=True, text=True, check=True) + calls_file = os.path.join(os.environ.get("TMPDIR", "/tmp"), + f"pacman-install-calls-{os.getpid()}") + if os.path.exists(calls_file): + os.unlink(calls_file) + open(calls_file, "w").close() + env = dict(os.environ, CALLS=calls_file, + PACMAN_S_RC=str(pacman_s_rc), PACMAN_D_RC=str(pacman_d_rc)) + proc = subprocess.run( + ["bash", "-c", script % {"functions": extract.stdout, "pkg": pkg}], + capture_output=True, text=True, env=env) + with open(calls_file) as f: + calls = [line.strip() for line in f if line.strip()] + os.unlink(calls_file) + return proc.returncode, calls + + +class PacmanInstallTests(unittest.TestCase): + def test_success_marks_package_explicit(self): + rc, calls = run_pacman_install("expac") + self.assertEqual(rc, 0) + self.assertIn("pacman --noconfirm --needed -S expac", calls) + self.assertIn("pacman -D --asexplicit expac", calls) + # the mark comes after the install, never before + self.assertGreater(calls.index("pacman -D --asexplicit expac"), + calls.index("pacman --noconfirm --needed -S expac")) + + def test_failed_install_never_marks(self): + rc, calls = run_pacman_install("expac", pacman_s_rc=1) + self.assertNotEqual(rc, 0) + self.assertNotIn("pacman -D --asexplicit expac", calls) + # all three retry attempts happened + self.assertEqual( + calls.count("pacman --noconfirm --needed -S expac"), 3) + + def test_mark_failure_does_not_fail_the_install(self): + # -D can fail in odd corners (readonly db mid-transaction); the + # install itself succeeded and must report success + rc, calls = run_pacman_install("expac", pacman_d_rc=1) + self.assertEqual(rc, 0) + self.assertIn("pacman -D --asexplicit expac", calls) + + +if __name__ == "__main__": + unittest.main() |
