aboutsummaryrefslogtreecommitdiff
path: root/tests/installer-steps/test_clone_user_repos.py
blob: 51d8434442ebe9d7e2c53f1866608c80a8230c4a (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
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
"""Test clone_user_repos: the two user repos are cloned with full history.

archsetup and dotfiles are not build directories. They are the two repos I
actively develop in on every machine this installer builds, so a shallow clone
is wrong for both. Velox came back from its 2026-08-13 rebuild with 7 commits
of history in each instead of 851, and nothing about the tree said so.

The quiet failure is what makes this worth a test rather than a one-line fix.
`git log -- <path>` against a shallow clone does not error; it answers "no
commits". So a credential-history check run on that machine reported five
sensitive files absent from history and exited clean, when the real answer was
that the clone could not see the history they live in. A security question came
back falsely reassuring. Everything else it breaks — blame, bisect, any
archaeology past the graft point — is merely annoying by comparison.

The AUR build clones are a different case and stay shallow: they are throwaway
build trees, cloned to run `make install` and then discarded, where history has
no value and the download cost is real. So this suite asserts both halves —
full history for the two user repos, and depth still pinned on the AUR path —
because a fix applied with too broad a brush would regress the build clones
without failing any test that only looked at the user repos.

Method: sed-extract clone_user_repos from the real `archsetup`, fake git /
mkdir / chown / display / error_warn / error_fatal, and read back the git
command lines the function issued.

Run from repo root:
    python3 -m unittest tests.installer-steps.test_clone_user_repos
"""

import os
import re
import subprocess
import tempfile
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(clone_fails=False, make_git_dir=True):
    """Drive clone_user_repos with every side effect faked.

    dotfiles_dir is pre-created with a .git so the function's "is this a real
    checkout?" guard passes on the happy path; make_git_dir=False exercises the
    guard itself.
    """
    with tempfile.TemporaryDirectory() as d:
        dotfiles_dir = os.path.join(d, "dotfiles")
        os.makedirs(dotfiles_dir)
        if make_git_dir:
            os.makedirs(os.path.join(dotfiles_dir, ".git"))
        clone_rc = 1 if clone_fails else 0
        script = textwrap.dedent(f"""\
            logfile=/dev/null
            action=""
            username=testuser
            archsetup_repo="https://example.invalid/archsetup.git"
            dotfiles_repo="https://example.invalid/dotfiles.git"
            dotfiles_branch=main
            dotfiles_dir="{dotfiles_dir}"
            display() {{ :; }}
            mkdir() {{ echo "MKDIR: $*" >> "{d}/calls.log"; return 0; }}
            chown() {{ echo "CHOWN: $*" >> "{d}/calls.log"; return 0; }}
            git() {{
                echo "GIT: $*" >> "{d}/calls.log"
                case "$1" in
                    clone) return {clone_rc} ;;
                    *) return 0 ;;
                esac
            }}
            error_warn() {{ echo "WARN: $1" >> "{d}/calls.log"; return 1; }}
            error_fatal() {{ echo "FATAL: $1" >> "{d}/calls.log"; exit 1; }}
            source <(sed -n '/^clone_user_repos() {{/,/^}}/p' "{ARCHSETUP}")
            clone_user_repos
            echo "RC=$?" >> "{d}/calls.log"
            exit 0
        """)
        subprocess.run(
            ["bash", "-c", script], capture_output=True, text=True, timeout=10,
        )
        with open(os.path.join(d, "calls.log")) as fh:
            return fh.read()


def clone_lines(log):
    return [ln for ln in log.splitlines() if ln.startswith("GIT: clone")]


class CloneUserRepos(unittest.TestCase):
    # ------------------------------------------------------------ normal ----
    def test_both_user_repos_are_cloned(self):
        lines = clone_lines(run())
        self.assertEqual(len(lines), 2,
                         f"expected an archsetup clone and a dotfiles clone, got: {lines}")
        self.assertTrue(any("archsetup.git" in ln for ln in lines))
        self.assertTrue(any("dotfiles.git" in ln for ln in lines))

    def test_archsetup_clone_carries_full_history(self):
        """A shallow archsetup clone answers history questions wrongly."""
        line = next(ln for ln in clone_lines(run()) if "archsetup.git" in ln)
        self.assertNotIn("--depth", line,
                         "archsetup is a working repo, not a build tree — a shallow "
                         "clone makes `git log -- <path>` answer 'no commits' instead "
                         "of failing, which is how a credential-history check came "
                         "back falsely clean on velox")

    def test_dotfiles_clone_carries_full_history(self):
        line = next(ln for ln in clone_lines(run()) if "dotfiles.git" in ln)
        self.assertNotIn("--depth", line,
                         "dotfiles is a working repo, not a build tree")

    def test_dotfiles_clone_still_pins_the_branch(self):
        """Dropping --depth must not disturb the --branch argument beside it."""
        line = next(ln for ln in clone_lines(run()) if "dotfiles.git" in ln)
        self.assertIn("--branch main", line)

    # ---------------------------------------------------------- boundary ----
    def test_no_user_repo_clone_is_shallow_by_any_spelling(self):
        """--depth, --depth=N and -depth are all shallow; catch the lot."""
        for line in clone_lines(run()):
            self.assertNotRegex(line, r"(^|\s)-{1,2}depth(\s|=)",
                                f"user-repo clone must be full: {line}")

    def test_aur_build_clones_stay_shallow(self):
        """The fix must not over-apply — build trees are throwaway.

        Read against the real file rather than the extracted function, because
        these clones live in a different function entirely and the risk being
        guarded is a careless repo-wide sed.
        """
        with open(ARCHSETUP) as fh:
            source = fh.read()
        build_clones = re.findall(r"^.*git clone.*build_dir.*$", source, re.M)
        self.assertTrue(build_clones, "expected AUR build clones to exist")
        for line in build_clones:
            self.assertIn("--depth 1", line,
                          f"AUR build clone should stay shallow: {line.strip()}")

    # ------------------------------------------------------------- error ----
    def test_clone_failure_is_reported_not_swallowed(self):
        log = run(clone_fails=True)
        self.assertIn("WARN:", log,
                      "a failed clone must surface through error_warn")

    def test_dotfiles_clone_producing_no_checkout_is_fatal(self):
        """The stow/restore steps downstream need a real checkout."""
        log = run(make_git_dir=False)
        self.assertIn("FATAL:", log)
        self.assertNotIn("RC=", log, "error_fatal must halt, not fall through")


if __name__ == "__main__":
    unittest.main()