aboutsummaryrefslogtreecommitdiff
path: root/tests/installer-steps/test_pacman_hook_order.py
blob: 45058dd7e4331e64d9491c9e6704bf1366772aa0 (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
"""Regression tests for the pacman hook ordering safety invariant.

pacman runs hooks in filename sort order, so the safety hooks archsetup
installs (the pre-pacman ZFS snapshot and the live-update guard, both
PreTransaction) must carry names that sort before mkinitcpio's stock
60-mkinitcpio-remove.hook. Otherwise a blocked transaction can remove the
current initramfs before the guard aborts, or after the snapshot window.

The earlier version of this test asserted the ordering by comparing two string
literals to each other, which is constant-true and never looked at the source.
These tests extract the hook filenames the installer actually writes and
compare those, so a rename in the source flows into the assertion.
"""

import os
import re
import unittest


REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
ARCHSETUP = os.path.join(REPO_ROOT, "archsetup")

MKINITCPIO_REMOVE = "60-mkinitcpio-remove.hook"  # stock mkinitcpio hook name

WRITE_RE = re.compile(r">\s*/etc/pacman\.d/hooks/(\S+\.hook)\b")


def written_hooks(text):
    """Hook filenames the source writes (redirections into the hooks dir)."""
    names = []
    for line in text.splitlines():
        if "rm -f" in line:
            continue
        m = WRITE_RE.search(line)
        if m:
            names.append(m.group(1))
    return names


class PacmanHookOrderTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        with open(ARCHSETUP, encoding="utf-8") as source:
            cls.text = source.read()
        cls.hooks = written_hooks(cls.text)

    def hook_named(self, suffix):
        matches = [h for h in self.hooks if h.endswith(suffix)]
        self.assertEqual(
            len(matches), 1,
            f"expected exactly one written hook ending {suffix}, got {matches}")
        return matches[0]

    def test_safety_hooks_sort_before_mkinitcpio_removal(self):
        # The invariant pacman actually enforces: filename sort order.
        for suffix in ("zfs-snapshot.hook", "hypr-live-update-guard.hook"):
            name = self.hook_named(suffix)
            self.assertLess(
                name, MKINITCPIO_REMOVE,
                f"{name} must sort before {MKINITCPIO_REMOVE} or the guard "
                "runs after the initramfs removal")

    def test_every_written_hook_has_an_ordering_prefix(self):
        # Ordering is deliberate; a hook without a numeric prefix sorts by
        # accident of its first letter.
        self.assertTrue(self.hooks, "extraction found no written hooks")
        for name in self.hooks:
            self.assertRegex(name, r"^\d{2}-",
                             f"{name} lacks the two-digit ordering prefix")

    def test_stale_unprefixed_hooks_are_cleaned_up(self):
        # Migration from installs made before the numeric prefixes.
        self.assertIn("rm -f /etc/pacman.d/hooks/zfs-snapshot.hook", self.text)
        self.assertIn(
            "rm -f /etc/pacman.d/hooks/hypr-live-update-guard.hook", self.text)


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