utils/scripts/tests/test_gate_stamp.py
Krzysztof kuhy Rudnicki 8b7105e20c gate_stamp: split full into <60 s calls with full --list + step <id>
`full` ran every manual-stage hook in one pre-commit call, which takes
over 60 s in screen-locker and leetcode-guard. `step <hook-id>` runs one
manual hook on --all-files and records "<tree> <id>" in
$GIT_DIR/gate-steps (per-worktree); the step that completes the set
writes the stamp exactly as `write` does. Dirty trees and non-manual ids
are refused with exit 2. Hook ids come from scripts/gate_stamp_hooks.py
(PyYAML, /usr/bin/python3), which treats an absent default_stages as
"every stage", matching pre-commit, so the steps cover what `full` runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
2026-10-17 16:45:28 +02:00

202 lines
6.4 KiB
Python

"""gate_stamp.sh step / full --list: the heavy gate split into <60 s calls.
Each test builds a throwaway git repo whose manual-stage hooks are
`language: system` with `entry: true` / `entry: false`, so pre-commit runs
offline in well under a second and the only thing under test is the stamp
bookkeeping. Every hook is `always_run` -- a hook with no matching files is
"Skipped" with exit 0, which would let `entry: false` pass.
"""
from __future__ import annotations
import subprocess
from pathlib import Path
import pytest
SCRIPT = Path(__file__).resolve().parent.parent / "gate_stamp.sh"
CONFIG = """\
default_stages: [pre-commit]
repos:
- repo: local
hooks:
- id: ok-a
name: ok a
entry: "true"
language: system
pass_filenames: false
always_run: true
stages: [manual]
- id: ok-b
name: ok b
entry: "true"
language: system
pass_filenames: false
always_run: true
stages: [pre-push, manual]
- id: bad
name: bad
entry: "false"
language: system
pass_filenames: false
always_run: true
stages: [manual]
- id: commit-only
name: commit only
entry: "true"
language: system
pass_filenames: false
always_run: true
"""
def _git(repo: Path, *args: str) -> str:
return subprocess.run(
["git", *args], cwd=repo, check=True, capture_output=True, text=True
).stdout.strip()
def _gate(repo: Path, *args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["bash", str(SCRIPT), *args],
cwd=repo,
check=False,
capture_output=True,
text=True,
)
def _make_repo(path: Path, config: str) -> Path:
path.mkdir()
_git(path, "init", "-q", "-b", "main")
_git(path, "config", "user.name", "t")
_git(path, "config", "user.email", "t@example.invalid")
(path / ".pre-commit-config.yaml").write_text(config, encoding="utf-8")
(path / "f.txt").write_text("x\n", encoding="utf-8")
_git(path, "add", ".")
_git(path, "commit", "-q", "-m", "init")
return path
@pytest.fixture
def repo(tmp_path: Path) -> Path:
return _make_repo(tmp_path / "r", CONFIG)
def _git_dir(repo: Path) -> Path:
return (repo / _git(repo, "rev-parse", "--git-dir")).resolve()
def _tree(repo: Path) -> str:
return _git(repo, "rev-parse", "HEAD^{tree}")
def test_full_list_prints_manual_ids_only(repo: Path) -> None:
res = _gate(repo, "full", "--list")
assert res.returncode == 0, res.stderr
assert res.stdout.splitlines() == ["ok-a", "ok-b", "bad"]
def test_list_counts_unstaged_hooks_when_default_stages_is_absent(
tmp_path: Path,
) -> None:
# pre-commit treats a missing default_stages as "every stage", so a hook
# without `stages` runs under --hook-stage manual and must be a step too.
config = CONFIG.replace("default_stages: [pre-commit]\n", "")
r = _make_repo(tmp_path / "nodefault", config)
res = _gate(r, "full", "--list")
assert res.stdout.splitlines() == ["ok-a", "ok-b", "bad", "commit-only"]
def test_list_counts_unstaged_hooks_when_default_stages_has_manual(
tmp_path: Path,
) -> None:
config = CONFIG.replace("[pre-commit]", "[pre-commit, manual]")
r = _make_repo(tmp_path / "withmanual", config)
res = _gate(r, "full", "--list")
assert "commit-only" in res.stdout.splitlines()
def test_step_records_and_reports_missing_ids(repo: Path) -> None:
res = _gate(repo, "step", "ok-a")
assert res.returncode == 0, res.stdout + res.stderr
steps = (_git_dir(repo) / "gate-steps").read_text(encoding="utf-8")
assert f"{_tree(repo)} ok-a" in steps.splitlines()
assert "ok-b" in res.stdout
assert "bad" in res.stdout
assert not (_git_dir(repo) / "gate-stamp").exists()
def test_rerunning_a_step_does_not_duplicate_its_record(repo: Path) -> None:
_gate(repo, "step", "ok-a")
_gate(repo, "step", "ok-a")
steps = (_git_dir(repo) / "gate-steps").read_text(encoding="utf-8")
assert steps.splitlines().count(f"{_tree(repo)} ok-a") == 1
def test_a_failing_step_records_nothing(repo: Path) -> None:
res = _gate(repo, "step", "bad")
assert res.returncode != 0
steps = _git_dir(repo) / "gate-steps"
assert not steps.exists() or "bad" not in steps.read_text(encoding="utf-8")
assert not (_git_dir(repo) / "gate-stamp").exists()
def test_stamp_appears_only_after_every_manual_id_passes(tmp_path: Path) -> None:
config = CONFIG.replace('entry: "false"', 'entry: "true"')
r = _make_repo(tmp_path / "allgreen", config)
stamp = _git_dir(r) / "gate-stamp"
for hook in ("ok-a", "ok-b"):
assert _gate(r, "step", hook).returncode == 0
assert not stamp.exists()
res = _gate(r, "step", "bad")
assert res.returncode == 0, res.stdout + res.stderr
assert stamp.read_text(encoding="utf-8").split()[0] == _tree(r)
assert "recorded tree" in res.stdout
assert _gate(r, "check").returncode == 0
def test_step_on_a_dirty_tree_is_refused(repo: Path) -> None:
(repo / "f.txt").write_text("changed\n", encoding="utf-8")
res = _gate(repo, "step", "ok-a")
assert res.returncode == 2
assert "differs from HEAD" in res.stderr
assert not (_git_dir(repo) / "gate-steps").exists()
def test_step_with_an_unknown_id_is_refused(repo: Path) -> None:
for hook in ("nope", "commit-only"):
res = _gate(repo, "step", hook)
assert res.returncode == 2
assert "not a manual-stage hook" in res.stderr
assert not (_git_dir(repo) / "gate-steps").exists()
def test_step_without_an_id_is_refused(repo: Path) -> None:
res = _gate(repo, "step")
assert res.returncode == 2
def test_step_in_a_linked_worktree_uses_its_own_git_dir(repo: Path) -> None:
wt = repo.parent / "wt"
_git(repo, "worktree", "add", "-q", str(wt))
res = _gate(wt, "step", "ok-a")
assert res.returncode == 0, res.stdout + res.stderr
assert (repo / ".git/worktrees/wt/gate-steps").exists()
assert not (repo / ".git/gate-steps").exists()
def test_full_hints_at_step(repo: Path) -> None:
res = _gate(repo, "full")
assert "step" in res.stdout.splitlines()[0]
assert res.returncode != 0 # `bad` fails, so no stamp
assert not (_git_dir(repo) / "gate-stamp").exists()
def test_check_failure_mentions_list_and_step(repo: Path) -> None:
res = _gate(repo, "check")
assert res.returncode == 1
assert "full --list" in res.stderr
assert "step <id>" in res.stderr