96 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Регрессионный корпус хуков (B2): прогон fixtures/hook-cases.jsonl через guard-main и require-plan в песочнице.
Запуск: python3 hook_corpus.py <каталог хуков> [--summary] [--fixtures <jsonl>]
Песочница строится во временном каталоге по раскладке из первой строки корпуса; префикс домашнего каталога записей
заменяется на её home, HOME хуков подменяется туда же. Bash идёт через guard-main, Edit/Write — через guard-main и
require-plan (блок, если блокирует любой).
Вывод: строка на запись «id<TAB>why<TAB>ожидалось<TAB>получено<TAB>команда» и итог «total N/M»;
с --summary — только расхождения и «корпус хуков: N/M». Код выхода 0 — всё совпало, 1 — нет.
"""
import json
import os
import shutil
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
def _git(*args):
subprocess.run(["git", "-c", "user.name=corpus", "-c", "user.email=corpus@example.invalid"] + list(args),
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def build(home, layout):
for e in layout:
p = os.path.join(home, e["path"])
if "worktree_of" in e:
_git("-C", os.path.join(home, e["worktree_of"]), "worktree", "add", "-q", p, "-b", e["branch"])
elif "branch" in e:
os.makedirs(p, exist_ok=True)
_git("init", "-q", p)
_git("-C", p, "symbolic-ref", "HEAD", "refs/heads/" + e["branch"])
_git("-C", p, "commit", "-q", "--allow-empty", "-m", "init")
else:
os.makedirs(p, exist_ok=True)
def verdict(hooks, case, prefix, home, env):
swap = lambda s: s.replace(prefix, home) if isinstance(s, str) else s
tool = case["tool"]
ti = {"command": swap(case.get("command"))} if tool == "Bash" else {"file_path": swap(case.get("file_path"))}
payload = json.dumps({"cwd": swap(case.get("cwd")), "tool_name": tool, "tool_input": ti})
for name in ["guard-main"] + ([] if tool == "Bash" else ["require-plan"]):
r = subprocess.run(["bash", os.path.join(hooks, name + ".sh")], input=payload, universal_newlines=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, timeout=30)
if r.returncode == 2:
return "block"
if r.returncode != 0:
return "error"
return "allow"
def main(argv):
args = argv[1:]
summary = "--summary" in args
fx = os.path.join(HERE, "..", "fixtures", "hook-cases.jsonl")
if "--fixtures" in args:
fx = args[args.index("--fixtures") + 1]
pos = [a for k, a in enumerate(args) if not a.startswith("--") and (k == 0 or args[k - 1] != "--fixtures")]
if not pos:
print(__doc__.strip().splitlines()[2])
return 1
hooks = os.path.abspath(pos[0])
lines = [json.loads(l) for l in open(fx, encoding="utf-8") if l.strip()]
head, cases = lines[0], lines[1:]
tmp = tempfile.mkdtemp(prefix="hook-corpus-")
try:
home = os.path.join(tmp, "home")
try:
build(home, head["layout"])
except Exception as e:
print("корпус хуков: песочница не собрана (%s)" % e)
return 1
env = {k: v for k, v in os.environ.items() if k not in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE")}
env["HOME"] = home
ok = 0
for c in cases:
got = verdict(hooks, c, head["home"], home, env)
ok += got == c["expected"]
body = (c.get("command") or c.get("file_path") or "").replace(head["home"], "~")
row = "\t".join([c["id"], c["why"], c["expected"], got, " ".join(body.split())[:70]])
if not summary:
print(row)
elif got != c["expected"]:
print("MISMATCH " + row)
print(("корпус хуков: %d/%d" if summary else "total %d/%d") % (ok, len(cases)))
return 0 if ok == len(cases) else 1
finally:
shutil.rmtree(tmp, ignore_errors=True)
if __name__ == "__main__":
sys.exit(main(sys.argv))