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