118 lines
5.4 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>]
Корпус — сегменты: строка с ключом layout ({home, layout}) открывает сегмент, следующие строки — его записи.
Для каждого сегмента строится своя песочница; префикс home записей заменяется на её home, HOME хуков — туда же.
Bash идёт через guard-main, Edit/Write — через guard-main и require-plan (блок, если блокирует любой).
Запись с known — известная ошибка хуков: расхождение — KNOWN (не ошибка прогона), совпадение — KNOWN-FIXED (ошибка:
снять known). Вывод: строка «id<TAB>why<TAB>ожидалось<TAB>получено<TAB>статус<TAB>команда» и итог «total N/M»;
с --summary — только MISMATCH и KNOWN-FIXED и «корпус хуков: N/M[, известных ошибок K]». Код 0 — нет ошибок.
"""
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 segments(lines):
"""[(шапка, [записи])]: строка с ключом layout открывает сегмент."""
segs = []
for obj in lines:
if "layout" in obj:
segs.append((obj, []))
elif segs:
segs[-1][1].append(obj)
return segs
def status(case, got):
if got == case["expected"]:
return "KNOWN-FIXED" if case.get("known") else "ok"
return "KNOWN" if case.get("known") else "MISMATCH"
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()]
env0 = {k: v for k, v in os.environ.items() if k not in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE")}
tmp = tempfile.mkdtemp(prefix="hook-corpus-")
try:
total = good = known = 0
for n, (head, cases) in enumerate(segments(lines)):
home = os.path.join(tmp, "seg-%d" % n, "home")
try:
build(home, head["layout"])
except Exception as e:
print("корпус хуков: песочница сегмента %d не собрана (%s)" % (n + 1, e))
return 1
env = dict(env0, HOME=home)
for c in cases:
got = verdict(hooks, c, head["home"], home, env)
st = status(c, got)
total += 1
good += st in ("ok", "KNOWN")
known += st == "KNOWN"
body = (c.get("command") or c.get("file_path") or "").replace(head["home"], "~")
row = "\t".join([c["id"], c["why"], c["expected"], got, st, " ".join(body.split())[:70]])
if not summary:
print(row)
elif st in ("MISMATCH", "KNOWN-FIXED"):
print(st + " " + row)
tail = ", известных ошибок %d" % known if known else ""
print(("корпус хуков: %d/%d%s" if summary else "total %d/%d%s") % (good, total, tail))
return 0 if good == total else 1
finally:
shutil.rmtree(tmp, ignore_errors=True)
if __name__ == "__main__":
sys.exit(main(sys.argv))