aboutsummaryrefslogtreecommitdiff
path: root/tests/installer-steps/test_pacman_install.py
blob: 28c9a7fdfb07616ffa24b165ab452f8cf16f2d0a (plain)
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
"""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()