diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/package-inventory/test_package_inventory.py | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/tests/package-inventory/test_package_inventory.py b/tests/package-inventory/test_package_inventory.py new file mode 100644 index 0000000..b48e2ab --- /dev/null +++ b/tests/package-inventory/test_package_inventory.py @@ -0,0 +1,155 @@ +"""Tests for scripts/package-inventory. + +package-inventory compares the packages archsetup declares against what is +installed on the live system. It extracts archsetup's packages — direct +pacman_install/aur_install calls plus `for software in ...` loop lists, +skipping variable arguments — then diffs them against pacman's view of the +system in both directions: declared-but-not-installed, and +installed-but-not-declared (split into AUR/foreign vs official). + +Tests run the real script against a fixture installer snippet and a fake +pacman serving controlled -Qqe/-Qq/-Qqen/-Qqem output. No real pacman db, +no network. The seams are env overrides: + PKGINV_ARCHSETUP path to the installer script to extract from + PKGINV_PACMAN pacman binary to query the system with + +Run from repo root: + make test-unit + (or python3 -m unittest tests.package-inventory.test_package_inventory) +""" + +import os +import re +import shutil +import stat +import subprocess +import tempfile +import unittest + +ANSI = re.compile(r"\x1b\[[0-9;]*m") # summary colorizes counts; strip for matching + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +SCRIPT = os.path.join(REPO_ROOT, "scripts/package-inventory") + +# Installer fixture. Declared set after extraction (sorted, unique): +# pkg-common, pkg-missing, aur-common, loop-a, loop-b, loop-c +# The bare "$software" lines are loop plumbing, not packages, and drop out. +FIXTURE = """ +pacman_install pkg-common # installed -> in both +pacman_install pkg-missing # not installed anywhere +aur_install aur-common # installed (foreign) -> in both +for software in loop-a loop-b \\ + loop-c; do + pacman_install "$software" +done +aur_install "$software" # variable -> not a package +""" + +# Fake-system state, keyed by the pacman query flag the script uses. +# -Qqe explicitly installed -Qq all installed (incl deps) +# -Qqen native (official) -Qqem foreign (AUR/manual) +# loop-c and pkg-missing are absent from the system; extra-native and +# extra-foreign are installed but undeclared. +SYSTEM = { + "-Qqe": ["pkg-common", "aur-common", "loop-a", "loop-b", + "extra-native", "extra-foreign"], + "-Qq": ["pkg-common", "aur-common", "loop-a", "loop-b", + "extra-native", "extra-foreign", "dep-x"], + "-Qqen": ["pkg-common", "loop-a", "loop-b", "extra-native"], + "-Qqem": ["aur-common", "extra-foreign"], +} + + +class InventoryHarness(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="pkg-inv-test-") + self.fixture = os.path.join(self.tmp, "installer-snippet") + with open(self.fixture, "w") as f: + f.write(FIXTURE) + + # Fake pacman: dispatch on the query flag, print the fixed set. + cases = "".join( + f' {flag}) printf "%s\\n" {" ".join(pkgs)} ;;\n' + for flag, pkgs in SYSTEM.items() + ) + self.fake_pacman = os.path.join(self.tmp, "pacman") + self._stub(self.fake_pacman, f'case "$1" in\n{cases} *) exit 1 ;;\nesac\n') + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def _stub(self, path, body): + with open(path, "w") as f: + f.write("#!/bin/sh\n" + body) + os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR) + + def run_inv(self, *args): + env = os.environ.copy() + env["PKGINV_ARCHSETUP"] = self.fixture + env["PKGINV_PACMAN"] = self.fake_pacman + return subprocess.run( + [SCRIPT, *args], + env=env, capture_output=True, text=True, timeout=20, + ) + + +class TestSummary(InventoryHarness): + def test_summary_counts_extraction_and_both_diffs(self): + r = self.run_inv("--summary") + self.assertEqual(r.returncode, 0, msg=r.stderr) + out = ANSI.sub("", r.stdout) + # 6 declared proves the for-loop list parsed and "$software" dropped. + self.assertRegex(out, r"Archsetup declares:\s+6\b") + self.assertRegex(out, r"In common:\s+4\b") + self.assertRegex(out, r"In archsetup, not system:\s+2\b") + self.assertRegex(out, r"On system, not archsetup:\s+2\b") + + +class TestDiffLists(InventoryHarness): + def test_archsetup_only_lists_declared_but_uninstalled(self): + r = self.run_inv("--archsetup-only") + self.assertEqual(r.returncode, 0, msg=r.stderr) + got = set(r.stdout.split()) + # loop-c proves a loop-extracted package flows through the diff. + self.assertEqual(got, {"pkg-missing", "loop-c"}) + + def test_system_only_lists_installed_but_undeclared(self): + r = self.run_inv("--system-only") + self.assertEqual(r.returncode, 0, msg=r.stderr) + got = set(r.stdout.split()) + self.assertEqual(got, {"extra-native", "extra-foreign"}) + + def test_variable_arg_is_not_treated_as_a_package(self): + r = self.run_inv("--archsetup-only") + self.assertNotIn("software", r.stdout) + + +class TestFullReport(InventoryHarness): + def test_full_report_splits_undeclared_by_source(self): + r = self.run_inv() + self.assertEqual(r.returncode, 0, msg=r.stderr) + self.assertIn("In archsetup but NOT installed", r.stdout) + self.assertIn("pkg-missing", r.stdout) + self.assertIn("loop-c", r.stdout) + self.assertIn("On system but NOT in archsetup", r.stdout) + self.assertIn("AUR/foreign", r.stdout) + self.assertIn("extra-foreign", r.stdout) + self.assertIn("Official repos", r.stdout) + self.assertIn("extra-native", r.stdout) + + +class TestCli(InventoryHarness): + def test_help_describes_modes(self): + r = self.run_inv("--help") + self.assertEqual(r.returncode, 0, msg=r.stderr) + self.assertIn("Usage", r.stdout) + self.assertIn("--archsetup-only", r.stdout) + + def test_unknown_option_errors(self): + r = self.run_inv("--bogus") + self.assertNotEqual(r.returncode, 0) + self.assertIn("Unknown option", r.stderr) + + +if __name__ == "__main__": + unittest.main() |
