Files
computer-playbook/tests/unit/python/scripts/system/worktree/test_down.py
T

170 lines
6.3 KiB
Python

"""Contract of ``make worktree-down``.
Two promises are pinned here because both were broken. A teardown must release
the branch even when the agent sandbox pins the metadata dir as a read-only bind
mount: ``git worktree remove`` then dies with EBUSY after it has already deleted
the checkout, which used to abort the whole target with rc 255 even though the
branch was in fact free. And a status probe that fails must never be read as
"clean": on a broken ``.git`` link ``git status`` exits non-zero with empty
output, which used to satisfy the guard, tear down the compose stack, and only
then die at ``git worktree remove`` with an unexplained rc 128.
"""
from __future__ import annotations
import os
import shutil
import stat
import subprocess
import tempfile
import unittest
from pathlib import Path
from utils.cache.files import PROJECT_ROOT
WORKTREE_SCRIPTS = PROJECT_ROOT / "scripts" / "system" / "worktree"
GIT_IDENTITY = {
"GIT_AUTHOR_NAME": "test",
"GIT_AUTHOR_EMAIL": "test@example.invalid",
"GIT_COMMITTER_NAME": "test",
"GIT_COMMITTER_EMAIL": "test@example.invalid",
}
GIT_REPO_SCOPE = (
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_COMMON_DIR",
"GIT_PREFIX",
"GIT_OBJECT_DIRECTORY",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
)
NEEDS_UNPRIVILEGED = "clearing the write bit does not restrain root"
class WorktreeDownFixture:
"""A throwaway repo with the worktree scripts copied in and branch ``feat``."""
def __init__(self, tmp: str) -> None:
self.root = Path(tmp)
self.repo = self.root / "repo"
self.base = self.root / "worktrees"
self.checkout = self.base / "feat"
inherited = {k: v for k, v in os.environ.items() if k not in GIT_REPO_SCOPE}
self.env = {**inherited, **GIT_IDENTITY}
scripts = self.repo / "scripts" / "system" / "worktree"
scripts.parent.mkdir(parents=True)
shutil.copytree(WORKTREE_SCRIPTS, scripts)
self.script = scripts / "down.sh"
self.git("init", "-b", "main")
(self.repo / "Makefile").write_text("compose-down:\n\t@true\n")
self.git("add", "-A")
self.git("commit", "-m", "init")
self.git("branch", "feat")
self.base.mkdir()
self.git("worktree", "add", str(self.checkout), "feat")
def git(self, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(
["git", *args],
cwd=self.repo,
env=self.env,
capture_output=True,
text=True,
check=True,
)
def meta_dir(self) -> Path:
return self.repo / ".git" / "worktrees" / "feat"
def down(self, force: str = "false") -> subprocess.CompletedProcess:
return subprocess.run(
["bash", str(self.script), "feat", str(self.base), force],
env=self.env,
capture_output=True,
text=True,
check=False,
)
def registered(self) -> bool:
listing = self.git("worktree", "list", "--porcelain").stdout
return str(self.checkout) in listing
class TestWorktreeDown(unittest.TestCase):
def test_clean_worktree_is_released(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
fixture = WorktreeDownFixture(tmp)
result = fixture.down()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(fixture.checkout.exists())
self.assertFalse(fixture.registered())
def test_uncommitted_changes_block_the_removal(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
fixture = WorktreeDownFixture(tmp)
(fixture.checkout / "scratch.txt").write_text("work in progress\n")
result = fixture.down()
self.assertNotEqual(result.returncode, 0)
self.assertIn("uncommitted changes", result.stderr)
self.assertTrue(fixture.checkout.exists())
def test_force_drops_uncommitted_changes(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
fixture = WorktreeDownFixture(tmp)
(fixture.checkout / "scratch.txt").write_text("work in progress\n")
result = fixture.down(force="true")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(fixture.checkout.exists())
def test_unreadable_status_refuses_instead_of_deleting(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
fixture = WorktreeDownFixture(tmp)
(fixture.checkout / ".git").write_text("gitdir: /nonexistent\n")
result = fixture.down()
self.assertNotEqual(result.returncode, 0)
self.assertIn("cannot read the git status", result.stderr)
self.assertTrue(fixture.checkout.exists())
@unittest.skipIf(os.geteuid() == 0, NEEDS_UNPRIVILEGED)
def test_pinned_metadata_dir_still_releases_the_branch(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
fixture = WorktreeDownFixture(tmp)
meta_parent = fixture.meta_dir().parent
mode = meta_parent.stat().st_mode
meta_parent.chmod(mode & ~stat.S_IWUSR)
try:
result = fixture.down()
finally:
meta_parent.chmod(mode)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(fixture.checkout.exists())
self.assertFalse(fixture.registered())
@unittest.skipIf(os.geteuid() == 0, NEEDS_UNPRIVILEGED)
def test_undeletable_pointers_fail_loudly(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
fixture = WorktreeDownFixture(tmp)
meta = fixture.meta_dir()
mode = meta.stat().st_mode
meta.chmod(mode & ~stat.S_IWUSR)
try:
result = fixture.down()
finally:
meta.chmod(mode)
self.assertNotEqual(result.returncode, 0)
self.assertIn("still registers branch", result.stderr)
def test_missing_worktree_is_an_error(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
fixture = WorktreeDownFixture(tmp)
shutil.rmtree(fixture.checkout)
result = fixture.down()
self.assertEqual(result.returncode, 1)
self.assertIn("no worktree at", result.stderr)
if __name__ == "__main__":
unittest.main()