"""Characterization tests for pacman_install's install-reason handling. pacman --needed skips a package that is already present as a dependency and leaves its install reason alone. A declared package can then sit as asdeps on an existing system, show up as an orphan once its accidental dependent leaves, and get swept away by an orphan cleanup (expac and lm_sensors nearly went this way on 2026-07-08). pacman_install therefore marks every declared package explicit after a successful install. Method mirrors test_orchestrators: sed-extract the real functions from `archsetup`, source them with `display`/`error_warn` silenced and `pacman` replaced by a recorder, run, and assert the recorded calls. Run from repo root: python3 -m unittest tests.installer-steps.test_pacman_install """ 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") def run_pacman_install(pkg, pacman_s_rc=0, pacman_d_rc=0): """Extract retry_install + pacman_install, run against a fake pacman. Returns (exit_code, recorded pacman calls as a list of strings). """ script = textwrap.dedent(""" set -u MAX_INSTALL_RETRIES=3 logfile=/dev/null display() { :; } error_warn() { return 1; } pacman() { echo "pacman $*" >> "$CALLS" case "$1" in --noconfirm) return "$PACMAN_S_RC" ;; -D) return "$PACMAN_D_RC" ;; esac } %(functions)s pacman_install "%(pkg)s" """) extract = subprocess.run( ["sed", "-n", "/^retry_install()/,/^}/p;/^pacman_install()/,/^}/p", ARCHSETUP], capture_output=True, text=True, check=True) calls_file = os.path.join(os.environ.get("TMPDIR", "/tmp"), f"pacman-install-calls-{os.getpid()}") if os.path.exists(calls_file): os.unlink(calls_file) open(calls_file, "w").close() env = dict(os.environ, CALLS=calls_file, PACMAN_S_RC=str(pacman_s_rc), PACMAN_D_RC=str(pacman_d_rc)) proc = subprocess.run( ["bash", "-c", script % {"functions": extract.stdout, "pkg": pkg}], capture_output=True, text=True, env=env) with open(calls_file) as f: calls = [line.strip() for line in f if line.strip()] os.unlink(calls_file) return proc.returncode, calls class PacmanInstallTests(unittest.TestCase): def test_success_marks_package_explicit(self): rc, calls = run_pacman_install("expac") self.assertEqual(rc, 0) self.assertIn("pacman --noconfirm --needed -S expac", calls) self.assertIn("pacman -D --asexplicit expac", calls) # the mark comes after the install, never before self.assertGreater(calls.index("pacman -D --asexplicit expac"), calls.index("pacman --noconfirm --needed -S expac")) def test_failed_install_never_marks(self): rc, calls = run_pacman_install("expac", pacman_s_rc=1) self.assertNotEqual(rc, 0) self.assertNotIn("pacman -D --asexplicit expac", calls) # all three retry attempts happened self.assertEqual( calls.count("pacman --noconfirm --needed -S expac"), 3) def test_mark_failure_does_not_fail_the_install(self): # -D can fail in odd corners (readonly db mid-transaction); the # install itself succeeded and must report success rc, calls = run_pacman_install("expac", pacman_d_rc=1) self.assertEqual(rc, 0) self.assertIn("pacman -D --asexplicit expac", calls) if __name__ == "__main__": unittest.main()