# Agent-N 全量回归可续跑驱动器:逐文件跑 pytest,结果追加 JSONL。 # 用法:.venv/Scripts/python.exe poc/pi-fallback/regression_driver.py <清单文件> <结果.jsonl> [预算秒] [单文件超时秒] # 已记录在结果文件中的条目自动跳过;单文件超时记录为 TIMEOUT 后交由人工拆分。 import json import re import subprocess import sys import time from pathlib import Path REPO = Path(__file__).resolve().parents[2] PY = REPO / ".venv" / "Scripts" / "python.exe" SUMMARY_RE = re.compile( r"(?:(\d+) passed)?[,\s]*(?:(\d+) failed)?[,\s]*(?:(\d+) error[s]?)?[,\s]*" r"(?:(\d+) skipped)?[,\s]*(?:(\d+) deselected)?") def parse_summary(out: str) -> dict: res = {"passed": 0, "failed": 0, "errors": 0, "skipped": 0} for line in reversed(out.splitlines()): line = line.strip() if "passed" in line or "failed" in line or "error" in line: m = SUMMARY_RE.search(line) if m and ("in " in line and "s" in line): res["passed"] = int(m.group(1) or 0) res["failed"] = int(m.group(2) or 0) res["errors"] = int(m.group(3) or 0) res["skipped"] = int(m.group(4) or 0) return res return res def failed_names(out: str) -> list[str]: names = [] for line in out.splitlines(): s = line.strip() if s.startswith(("FAILED ", "ERROR ")): names.append(s[:300]) return names def main() -> int: list_file, results_file = Path(sys.argv[1]), Path(sys.argv[2]) budget = float(sys.argv[3]) if len(sys.argv) > 3 else 250.0 per_file_timeout = float(sys.argv[4]) if len(sys.argv) > 4 else 200.0 files = [x.strip() for x in list_file.read_text(encoding="utf-8").splitlines() if x.strip()] done = set() if results_file.is_file(): for line in results_file.read_text(encoding="utf-8").splitlines(): if line.strip(): done.add(json.loads(line)["file"]) t0 = time.monotonic() ran = 0 with open(results_file, "a", encoding="utf-8") as out: for f in files: if f in done: continue if time.monotonic() - t0 > budget: print(f"[budget] 预算耗尽,停止(本轮完成 {ran} 个文件)") break ft0 = time.monotonic() rec = {"file": f} try: proc = subprocess.run( [str(PY), "-m", "pytest", f, "-q", "--tb=no", "-rfE", "-p", "no:cacheprovider"], cwd=str(REPO), capture_output=True, text=True, timeout=per_file_timeout, check=False) tail = proc.stdout + proc.stderr s = parse_summary(tail) rec.update(s) rec["rc"] = proc.returncode fails = failed_names(tail) if fails: rec["failedNames"] = fails except subprocess.TimeoutExpired: rec.update({"timeout": True, "passed": 0, "failed": 0, "errors": 0, "skipped": 0}) rec["sec"] = round(time.monotonic() - ft0, 1) out.write(json.dumps(rec, ensure_ascii=False) + "\n") out.flush() ran += 1 status = ("TIMEOUT" if rec.get("timeout") else f"passed={rec['passed']} failed={rec['failed']} " f"errors={rec['errors']}") print(f"[{status}] {f} ({rec['sec']}s)") print(f"[done] 本轮 {ran} 个文件,累计 {len(done) + ran}/{len(files)}") return 0 if __name__ == "__main__": sys.exit(main())