aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/hypr-live-update-guard/test_hypr_live_update_guard.py72
-rw-r--r--tests/installer-steps/test_pacman_install.py95
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()