aboutsummaryrefslogtreecommitdiff
path: root/tests/hypr-live-update-guard/test_hypr_live_update_guard.py
blob: 5ec5ce897a2ba82ca27909dc6056251561520bfd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
"""Tests for the hypr-live-update-guard pacman PreTransaction hook script.

The guard aborts a live pacman upgrade of GPU/compositor runtime libraries
(mesa, hyprland, wayland, GPU drivers) while a Hyprland session is running,
so the compositor doesn't SIGABRT when a now-"(deleted)" library is next
called. It reads the triggering package names on stdin (pacman NeedsTargets)
and exits non-zero to abort the transaction (AbortOnFail) before any package
is swapped. When Hyprland isn't running, or an override is set, it exits 0
and the upgrade proceeds.

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

Run from repo root:
    python3 -m unittest tests.hypr-live-update-guard.test_hypr_live_update_guard
"""

import os
import subprocess
import tempfile
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")


def run_guard(stdin="mesa\n", running="1", allow=None, sentinel=None):
    env = dict(os.environ)
    env["HYPR_GUARD_RUNNING"] = running
    if allow is not None:
        env["HYPR_ALLOW_LIVE_UPDATE"] = allow
    # 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"
    return subprocess.run(
        ["sh", GUARD],
        input=stdin, capture_output=True, text=True, timeout=10, env=env,
    )


class HyprLiveUpdateGuard(unittest.TestCase):
    # --- Normal cases ---------------------------------------------------

    def test_running_with_dangerous_pkg_aborts(self):
        r = run_guard(stdin="mesa\n", running="1")
        self.assertEqual(r.returncode, 1, r.stderr)

    def test_abort_message_names_the_package_and_tty_remedy(self):
        r = run_guard(stdin="mesa\n", running="1")
        self.assertIn("mesa", r.stderr)
        self.assertIn("TTY", r.stderr)

    def test_not_running_allows(self):
        r = run_guard(stdin="mesa\n", running="0")
        self.assertEqual(r.returncode, 0, r.stderr)

    def test_not_running_is_silent(self):
        r = run_guard(stdin="mesa\nhyprland\n", running="0")
        self.assertEqual(r.stderr.strip(), "")

    # --- Boundary cases -------------------------------------------------

    def test_multiple_packages_all_listed(self):
        r = run_guard(stdin="mesa\nhyprland\nvulkan-radeon\n", running="1")
        self.assertEqual(r.returncode, 1)
        for pkg in ("mesa", "hyprland", "vulkan-radeon"):
            self.assertIn(pkg, r.stderr)

    def test_running_with_empty_stdin_still_guards(self):
        # The hook only fires when dangerous targets exist, so an empty target
        # list shouldn't normally happen; if Hyprland is up, stay safe (abort).
        r = run_guard(stdin="", running="1")
        self.assertEqual(r.returncode, 1)

    # --- Override / error cases -----------------------------------------

    def test_env_override_proceeds_even_when_running(self):
        r = run_guard(stdin="mesa\n", running="1", allow="1")
        self.assertEqual(r.returncode, 0, r.stderr)

    def test_sentinel_file_override_proceeds(self):
        with tempfile.NamedTemporaryFile(prefix="guard-allow-") as f:
            r = run_guard(stdin="mesa\n", running="1", sentinel=f.name)
            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)


if __name__ == "__main__":
    unittest.main()