96 lines
4.3 KiB
Python
96 lines
4.3 KiB
Python
#!/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))
|