每个测试批次有严格超时,发生异常时隔离失败模块并留存原始日志。任何超时均阻止上线判定。
| 批次 | 状态 | 通过用例 | 耗时(秒) | 日志 |
|---|
#!/usr/bin/env python3 """V8.21 repeatable integration regression with per-batch deadlines and isolation. Runs pytest against only the local repository. Timeout cleanup kills the whole process group on POSIX rather than leaving orphan pytest/Worker children. Nonzero, flaky and timed-out runs always fail the quality gate. """ from __future__ import annotations import argparse import datetime as dt import html import json import os import re import signal import subprocess import sys import time from dataclasses import asdict, dataclass from pathlib import Path ROOT=Path(__file__).resolve().parent.parent @dataclass class Run: label: str modules: list[str] status: str seconds: float exit_code: int | None passed: int log: str def run_pytest(label: str, modules: list[str], seconds: int, reports: Path, env: dict[str,str]) -> Run: args=[sys.executable,'-m','pytest','-vv','--tb=short','--disable-warnings','--no-header',*modules] filename=re.sub(r'[^a-zA-Z0-9_-]+','-',label)+'.log' path=reports/filename began=time.monotonic() flags={'start_new_session':True} if os.name!='nt' else {'creationflags':subprocess.CREATE_NEW_PROCESS_GROUP} expired=False with path.open('wb') as out: out.write(('$ '+' '.join(args)+'\n').encode('utf-8')) out.flush() process=subprocess.Popen(args,cwd=ROOT,env=env,stdout=out,stderr=subprocess.STDOUT,**flags) try: process.wait(timeout=seconds) except subprocess.TimeoutExpired: expired=True if os.name!='nt': try: os.killpg(process.pid,signal.SIGTERM) except ProcessLookupError: pass else: process.terminate() try: process.wait(timeout=4) except subprocess.TimeoutExpired: if os.name!='nt': try: os.killpg(process.pid,signal.SIGKILL) except ProcessLookupError: pass else: process.kill() process.wait(timeout=5) out.write(f'\nTIMEOUT: deadline={seconds}s; process group terminated\n'.encode()) content=path.read_text(encoding='utf-8',errors='replace') m=re.findall(r'(?m)(\d+) passed(?:,| in |\s*\()',content) passed=int(m[-1]) if m else 0 status='TIMEOUT' if expired else ('PASS' if process.returncode==0 and passed>0 else 'FAIL') return Run(label,modules,status,round(time.monotonic()-began,2),process.returncode if not expired else None,passed,str(path)) def write_reports(runs: list[Run],prefix: Path,modules_count: int,repeat: int): statuses={x:sum(r.status==x for r in runs) for x in ('PASS','FAIL','TIMEOUT')} batches=[r for r in runs if re.fullmatch(r'round\d+-batch\d+',r.label)] bad=[r for r in batches if r.status!='PASS'] result={'version':'8.21.0-dev.1','date':dt.datetime.now(dt.timezone.utc).isoformat(), 'modules':modules_count,'repeats':repeat,'status':'PASS' if not bad else 'FAIL', 'tests_last_complete_round':sum(r.passed for r in batches if r.label.startswith(f'round{repeat}-')), 'counts':statuses,'failures':[asdict(x) for x in bad], 'runs':[asdict(x) for x in runs], 'note':'Timed out, failing, or recovered-on-retry tests are not considered passing.'} prefix.parent.mkdir(parents=True,exist_ok=True) prefix.with_suffix('.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8') lines=['# SiteForge V8.21 集成回归','',f"状态:**{result['status']}**;模块:{modules_count};重复轮次:{repeat}",'', '| 批次 | 状态 | 通过用例 | 秒 | 日志 |','|---|---|---:|---:|---|'] for r in runs: lines.append(f'| {r.label} | {r.status} | {r.passed} | {r.seconds} | {Path(r.log).name} |') lines+=['','失败、超时或重跑后通过均视为验收未通过;详见日志。',''] prefix.with_suffix('.md').write_text('\n'.join(lines),encoding='utf-8') rows=''.join(f'
结果:{result['status']} · {modules_count} 模块 · {repeat} 轮
每个测试批次有严格超时,发生异常时隔离失败模块并留存原始日志。任何超时均阻止上线判定。
| 批次 | 状态 | 通过用例 | 耗时(秒) | 日志 |
|---|