diff options
Diffstat (limited to 'tests/installer-steps/test_dotfiles_dependency_packages.py')
| -rw-r--r-- | tests/installer-steps/test_dotfiles_dependency_packages.py | 119 |
1 files changed, 119 insertions, 0 deletions
diff --git a/tests/installer-steps/test_dotfiles_dependency_packages.py b/tests/installer-steps/test_dotfiles_dependency_packages.py new file mode 100644 index 0000000..58b7275 --- /dev/null +++ b/tests/installer-steps/test_dotfiles_dependency_packages.py @@ -0,0 +1,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() |
