94 lines
3.6 KiB
Python
94 lines
3.6 KiB
Python
|
|
# 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())
|