aboutsummaryrefslogtreecommitdiff
path: root/tests/installer-steps/test_clone_user_repos.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/installer-steps/test_clone_user_repos.py')
-rw-r--r--tests/installer-steps/test_clone_user_repos.py155
1 files changed, 155 insertions, 0 deletions
diff --git a/tests/installer-steps/test_clone_user_repos.py b/tests/installer-steps/test_clone_user_repos.py
new file mode 100644
index 0000000..51d8434
--- /dev/null
+++ b/tests/installer-steps/test_clone_user_repos.py
@@ -0,0 +1,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()