aboutsummaryrefslogtreecommitdiff
path: root/tests/installer-steps/test_dotfiles_dependency_packages.py
blob: 58b7275ec615dbb317db51bcb9e911ce9ad8b554 (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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
"""Pin the packages the dotfiles assume are present.

Three packages are not optional extras: something the dotfiles ship depends
on each one, and when the package is missing the dependent silently does the
wrong thing rather than failing.

- libreoffice-fresh :: common/.config/mimeapps.list maps presentations,
  documents and spreadsheets to libreoffice-impress/-writer/-calc. With the
  package absent those .desktop files don't exist, so xdg-mime falls through
  to the next application claiming the type — on a machine with the winvm
  dotfiles that is powerpoint.desktop, which boots a Windows VM to open a
  deck (2026-09-18).
- imv :: gui-open --image execs imv, and without it every agent-side image
  render fails with "required application is unavailable" (2026-09-18).
- git-lfs :: a repo tracking globs in LFS fails every checkout and merge
  with "smudge filter lfs failed" (2026-09-20).

All three were installed before the 2026-08-13 rebuild and absent after it,
which is the regression this pins: they are dependencies of shipped defaults,
so the installer has to declare them rather than leave them to whatever a
machine happens to carry.

Method mirrors test_required_software: sed-extract supplemental_software from
the real `archsetup`, stub pacman_install as a recorder, run it, and assert
against what it actually invoked. Running the function beats matching its
source text, because the property under test is "every machine installs this"
and only a run resolves the conditionals that could make that false. The
function is run once per desktop environment for the same reason.

Run from repo root:
    python3 -m unittest tests.installer-steps.test_dotfiles_dependency_packages
"""

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

DEPENDENCY_PACKAGES = ("libreoffice-fresh", "imv", "git-lfs")

# Every desktop environment the installer branches on inside this function.
DESKTOP_ENVS = ("dwm", "hyprland")


def declared_packages(desktop_env):
    """Return (exit_code, [pacman package, ...]) for one desktop environment.

    Everything the function calls besides pacman_install is stubbed to a no-op,
    so the run records package declarations and nothing else. aur_install is
    deliberately separate: these three are pacman packages, and folding the two
    recorders together would let an AUR declaration satisfy the pin.
    """
    script = textwrap.dedent(f"""\
        desktop_env={desktop_env}
        display() {{ :; }}
        aur_install() {{ :; }}
        mask_fwupd_passim() {{ :; }}
        run_task() {{ :; }}
        error_warn() {{ :; }}
        pacman_install() {{ echo "$1"; }}
        source <(sed -n '/^supplemental_software() {{/,/^}}/p' "{ARCHSETUP}")
        supplemental_software
    """)
    result = subprocess.run(
        ["bash", "-c", script], capture_output=True, text=True, timeout=30,
    )
    return result.returncode, result.stdout.split()


class DotfilesDependencyPackages(unittest.TestCase):
    # ------------------------------------------------------------ normal ----
    def test_each_dependency_package_is_installed(self):
        rc, pkgs = declared_packages("hyprland")
        self.assertEqual(rc, 0)
        for package in DEPENDENCY_PACKAGES:
            with self.subTest(package=package):
                self.assertIn(
                    package, pkgs,
                    f"{package} is a dependency of a shipped dotfiles default "
                    "and must be declared",
                )

    # ---------------------------------------------------------- boundary ----
    def test_each_is_declared_exactly_once(self):
        # A second declaration is dead weight and drifts out of sync with the
        # first when one of them is edited.
        rc, pkgs = declared_packages("hyprland")
        self.assertEqual(rc, 0)
        for package in DEPENDENCY_PACKAGES:
            with self.subTest(package=package):
                self.assertEqual(pkgs.count(package), 1)

    # ------------------------------------------------------------- error ----
    def test_none_is_gated_behind_a_desktop_environment(self):
        # The dotfiles defaults that need these apply on every DE, so a
        # declaration reachable under only one of them would leave the same
        # hole on the other.
        for env in DESKTOP_ENVS:
            rc, pkgs = declared_packages(env)
            self.assertEqual(rc, 0)
            for package in DEPENDENCY_PACKAGES:
                with self.subTest(desktop_env=env, package=package):
                    self.assertIn(package, pkgs)

    def test_harness_observes_desktop_environment_gating(self):
        # The control for the test above: ranger IS gated to dwm on purpose, so
        # if this run can't see that, the DE-independence assertion is vacuous
        # and would pass against a gated package too.
        _, dwm = declared_packages("dwm")
        _, hyprland = declared_packages("hyprland")
        self.assertIn("ranger", dwm)
        self.assertNotIn("ranger", hyprland)


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