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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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()
|