#!/usr/bin/env python3 """ViperC 回归测试脚本(全集版;可用 --escape/--only 跑子集) 自动遍历 Test/ 目录下所有含 project.json 的项目目录并逐个编译。 编译命令默认走 TPC 种子(TransPyC/Projectrans.py);给 --exe 则直推自举产物 (如 viperc.exe),与 SequentialRegressionTest.py 的 --exe 语义一致。 所有项目编译完成后串行执行编译结果,超时 30 秒强制结束。 退出码非 0 表示有失败。 """ import os import sys import json import time import subprocess import re import argparse # 强制 stdout/stderr 使用 UTF-8 编码,避免 Windows GBK 编码在打印含替换字符的输出时崩溃 try: sys.stdout.reconfigure(encoding='utf-8', errors='replace') sys.stderr.reconfigure(encoding='utf-8', errors='replace') except Exception: pass # ============================================================ # 二阶段皆通过的项目清单 (2026-06-26 验证) # 编译阶段: 35/35 通过 运行阶段: 35/35 通过 # ------------------------------------------------------------ # BuddyTest BuiltinDecoratorTest CPythonTest # ClosureTest ClosureTest2 DecoratorMarkerTest # DecoratorTest DictTest DocTest # EdgeCaseTest EqTest FakeDuckTest # ForAndLoopTest FormatStringTest FuncTest # GenericTest GenericTest2 IterTest # JsonTest ListTest LoopTest # PointerTest ShortCircuitTest SimdTest # StringIterTest StringTest StructTest # TestFBProject TestProject TestProject2 # TestProject3 TypeCastTest VRandomTest # VariadicTest ZlibTest # ============================================================ # 测试目录 TEST_DIR = os.path.dirname(os.path.abspath(__file__)) # 仓库根目录(含 TransPyC/、ViperC/、Test/ 等同级目录) PROJECT_ROOT = os.path.dirname(TEST_DIR) # Projectrans.py 路径:编译器入口在 /TransPyC/Projectrans.py # (旧值 /Projectrans.py 已不存在 → 76 个工程全部 "can't open file" 编译失败; # 与 MCP/transpyc_mcp/server.py:33-35 记录的"入口已迁移"一致) PROJECTRANS = os.path.join(PROJECT_ROOT, "TransPyC", "Projectrans.py") # 运行超时(秒) RUN_TIMEOUT = 30 # 编译超时(秒) COMPILE_TIMEOUT = 300 # stdout 中匹配这些关键词则视为运行失败 _FAIL_PATTERNS = re.compile( r'(?i)\b(failed|error|traceback|exception|aborted|segmentation fault|core dumped)\b' ) # 排除 "0 failed" 这类成功摘要行,以及测试输出中的 "error" 误报 _PASS_PATTERNS = re.compile( r'\b0\s+failed\b' r'|\bError\s+(handling|code|message|check|report|status|info|detail|test)\b' r'|\+--\s*Error\s' # section headers like "+-- Error handling --+" r'|\bclear\s+error\b' # "[PASS] clear error clears code" r'|\berror\s+(code|message)\b' # "error code is set", "error message set" r'|\[PASS\].*\berror\b' # "[PASS] ... error ..." test assertions r'|\[FAIL\].*\berror\b', # "[FAIL] ... error ..." test assertions re.IGNORECASE ) def find_test_projects(): """查找所有含 project.json 的测试项目目录""" projects = [] for name in sorted(os.listdir(TEST_DIR)): proj_dir = os.path.join(TEST_DIR, name) if not os.path.isdir(proj_dir): continue proj_json = os.path.join(proj_dir, "project.json") if os.path.isfile(proj_json): projects.append((name, proj_dir, proj_json)) return projects # ============================================================ # 单独运行支持:--escape / --only # ============================================================ # escape 相关项目(生命期/逃逸检查 + M2.4 new 描述符 + M3 owned slife RAII + §3.6 析构 + # P2 willFree + A2 @life(ret(...))): # Test/ 下:NewTest、NewLifetimeTest、NewLifetimeWillFreeOkTest、NewLifetimeRetOkTest(正向)、 # NewLifetimeFreeTest / NewLifetimeAsyncTest / NewLifetimeWillFreeTest / # NewLifetimeUseAfterFreeTest / NewLifetimeRetEscapeTest / NewLifetimeRetXmodTest(负向); # 仓库根下:EscapeTest/EscapeOkTest/EscapeMethodTest/EscapeXmodTest ESCAPE_PROJECTS = ["EscapeTest", "EscapeOkTest", "EscapeMethodTest", "EscapeXmodTest", "NewTest", "NewLifetimeTest", "NewLifetimeWillFreeOkTest", "NewLifetimeRetOkTest", "NewLifetimeFreeTest", "NewLifetimeAsyncTest", "NewLifetimeWillFreeTest", "NewLifetimeUseAfterFreeTest", "NewLifetimeWillFreeKwargTest", "NewLifetimeWillFreeMethodTest", "NewLifetimeWillFreeAddrTest", "NewLifetimeFieldEscapeTest", "NewLifetimeAnnFieldEscapeTest", "NewLifetimeRetEscapeTest", "NewLifetimeRetXmodTest"] # 负向项目(**期望编译失败**,见 SequentialRegressionTest.NEGATIVE_PROJECTS): # 本脚本按"编译必须成功 + 能跑"判定,纳入会误报,故 --escape 时剔除; # 负向断言请用 SequentialRegressionTest(反向判定)或 run_lifetime_negative_tests.py。 NEGATIVE_PROJECT_NAMES = {"EscapeTest", "EscapeMethodTest", "EscapeXmodTest", "NewLifetimeFreeTest", "NewLifetimeAsyncTest", "NewLifetimeWillFreeTest", "NewLifetimeUseAfterFreeTest", "NewLifetimeWillFreeKwargTest", "NewLifetimeWillFreeMethodTest", "NewLifetimeWillFreeAddrTest", "NewLifetimeFieldEscapeTest", "NewLifetimeAnnFieldEscapeTest", "NewLifetimeRetEscapeTest", "NewLifetimeRetXmodTest"} def find_project_file(proj_dir): """项目目录下找 project.json(Test/ 约定)或 project.vpj(仓库根约定)""" for fn in ("project.json", "project.vpj"): p = os.path.join(proj_dir, fn) if os.path.isfile(p): return p return None def select_named_projects(names): """按名字解析项目:优先 Test/,其次仓库根 (根级 escape 测试)""" sel = [] for n in names: d = os.path.join(TEST_DIR, n) if not os.path.isdir(d): d = os.path.join(PROJECT_ROOT, n) pj = find_project_file(d) if pj is None: print(f" 跳过 {n}: 找不到 project.json / project.vpj") continue sel.append((n, d, pj)) return sel def build_compile_cmd(args): """根据 --exe/--compiler 构建编译命令模板({project} 为项目 json 路径占位) --exe 指定编译器可执行文件(如 viperc.exe),优先于 --compiler; 两者都未指定时使用 TPC 种子(python TransPyC/Projectrans.py)。 --rebuild-includes 时追加 --rebuild-includes(全量重编 includes)。 """ suffix = " --rebuild-includes" if args.rebuild_includes else "" if args.exe: # 必须绝对化:每个项目都在自己的目录里执行(cwd=项目目录),相对 --exe 会按各自层级解析错位 exe_abs = os.path.abspath(args.exe) return f'"{exe_abs}" --project {{project}} --clean{suffix}' if args.compiler: return args.compiler return f'D:\\Python312\\python.exe -u "{PROJECTRANS}" --project {{project}} --clean{suffix}' def compile_project(name, proj_dir, proj_json_path, compile_cmd): """编译单个测试项目,返回 (success, elapsed, output)""" cmd = compile_cmd.format(project=proj_json_path) start = time.time() result = subprocess.run( cmd, shell=True, capture_output=True, text=True, encoding='utf-8', errors='replace', cwd=proj_dir, timeout=COMPILE_TIMEOUT, ) elapsed = time.time() - start stdout = result.stdout or "" stderr = result.stderr or "" combined = stdout + stderr success = result.returncode == 0 return (success, elapsed, combined) def find_executables(output_dir): """在 output 目录中查找可执行文件""" exes = [] if not os.path.isdir(output_dir): return exes for f in os.listdir(output_dir): fpath = os.path.join(output_dir, f) if os.path.isfile(fpath) and (f.endswith('.exe') or f.endswith('.bin')): exes.append(fpath) return exes def run_executable(exe_path): """运行单个可执行文件,返回 (returncode, stdout, stderr, timed_out) 超时 RUN_TIMEOUT 秒强制结束。 使用 CREATE_NO_WINDOW 避免弹出 cmd 窗口,通过管道捕获 stdout/stderr。 """ try: CREATE_NO_WINDOW = 0x08000000 result = subprocess.run( [exe_path], cwd=os.path.dirname(exe_path), capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=RUN_TIMEOUT, creationflags=CREATE_NO_WINDOW, ) return (result.returncode, result.stdout or "", result.stderr or "", False) except subprocess.TimeoutExpired: return (-1, "", "超时(强制终止)", True) except Exception as e: return (-1, "", str(e), False) def extract_error_context(stdout, stderr, context_lines=5): """检查 stdout/stderr 中是否包含 failed/error 等关键词。 返回 (matched_line, context_str) 或 None。 context_str 包含匹配行及上下 context_lines 行。 """ combined = (stdout + "\n" + stderr).strip() if not combined: return None lines = combined.split('\n') for i, line in enumerate(lines): if _FAIL_PATTERNS.search(line): # 跳过 "0 failed" 这类成功摘要行 if _PASS_PATTERNS.search(line): continue # 提取上下 context_lines 行 start = max(0, i - context_lines) end = min(len(lines), i + context_lines + 1) context_parts = [] for j in range(start, end): marker = " >>>" if j == i else " " context_parts.append(f"{marker} {lines[j]}") return (line.strip(), '\n'.join(context_parts)) return None def main(): ap = argparse.ArgumentParser(description="ViperC 回归测试(可用 --escape/--only 单独跑子集)") ap.add_argument("--escape", action="store_true", help="只跑 escape 相关项目(生命期/逃逸检查 + new 描述符)") ap.add_argument("--only", default=None, help="只跑指定项目(逗号分隔,名字即目录名)") ap.add_argument("--exe", default=None, help="编译器可执行文件(如 viperc.exe),用它编译测试项目(替代 TPC 种子)") ap.add_argument("--compiler", default=None, help="自定义编译命令模板,用 {project} 表示项目 json 路径;默认 TPC python") ap.add_argument("--rebuild-includes", action="store_true", help="每次编译追加 --rebuild-includes:清空 includes.binary 全量重编") args = ap.parse_args() if args.exe and not os.path.isfile(os.path.abspath(args.exe)): print(f"错误: --exe 不存在: {os.path.abspath(args.exe)}") return 1 compile_cmd = build_compile_cmd(args) print("=" * 70) print("ViperC 回归测试(默认 TPC 种子;--exe 可直推 viperc.exe)") print("=" * 70) print(f"编译命令: {compile_cmd}") print() if args.escape: projects = [p for p in select_named_projects(ESCAPE_PROJECTS) if p[0] not in NEGATIVE_PROJECT_NAMES] print("(负向项目已剔除;负向断言请用 SequentialRegressionTest 或 run_lifetime_negative_tests.py)") elif args.only: projects = select_named_projects([n.strip() for n in args.only.split(",") if n.strip()]) else: projects = find_test_projects() total = len(projects) print(f"发现 {total} 个测试项目\n") # ========== 阶段一:编译所有项目 ========== print("=" * 70) print("阶段一:编译所有项目") print("=" * 70) compiled = [] # (name, proj_dir, proj_json, compile_ok, elapsed, output) compile_passed = 0 compile_failed = 0 for idx, (name, proj_dir, proj_json) in enumerate(projects, 1): print(f"[{idx}/{total}] 编译 {name} ...", end=" ", flush=True) try: compile_ok, elapsed, output = compile_project(name, proj_dir, proj_json, compile_cmd) except subprocess.TimeoutExpired: print("编译超时") compiled.append((name, proj_dir, proj_json, False, COMPILE_TIMEOUT, "编译超时(5分钟)")) compile_failed += 1 continue except Exception as e: print(f"异常: {e}") compiled.append((name, proj_dir, proj_json, False, 0.0, str(e))) compile_failed += 1 continue status = "通过" if compile_ok else "失败" print(f"{status} ({elapsed:.1f}s)") compiled.append((name, proj_dir, proj_json, compile_ok, elapsed, output)) if compile_ok: compile_passed += 1 else: compile_failed += 1 print(f"\n编译阶段: 通过 {compile_passed}, 失败 {compile_failed}\n") # ========== 阶段二:运行所有编译成功的可执行文件 ========== print("=" * 70) print("阶段二:运行所有可执行文件") print("=" * 70) # 收集所有需要运行的可执行文件 run_tasks = [] # (project_name, exe_path) for name, proj_dir, proj_json, compile_ok, elapsed, output in compiled: if not compile_ok: continue try: with open(proj_json, 'r', encoding='utf-8') as f: proj_config = json.load(f) except Exception: continue # 候选输出目录:TPC 与 VPC 布局一致(OutputDir = {BuildDir}/output) od = proj_config.get('output_dir') or 'output' bd = proj_config.get('build_dir') or '__vpcbuild__' base_dirs = [ os.path.join(proj_dir, od), os.path.join(proj_dir, bd, od), os.path.join(proj_dir, bd, 'output'), ] exes = [] for d in [os.path.abspath(x) for x in base_dirs]: exes = find_executables(d) if exes: break for exe in exes: run_tasks.append((name, exe)) print(f"找到 {len(run_tasks)} 个可执行文件\n") run_results = {} # project_name -> list of (exe_name, returncode, stdout, stderr, timed_out) run_passed = 0 run_failed = 0 total_runs = len(run_tasks) if run_tasks: # 串行运行,避免并发导致的资源竞争(控制台输出、堆分配器等) for idx, (proj_name, exe_path) in enumerate(run_tasks, 1): exe_name = os.path.basename(exe_path) print(f"[{idx}/{total_runs}] 运行 {proj_name} ... ", end=" ", flush=True) try: returncode, stdout, stderr, timed_out = run_executable(exe_path) except Exception as e: returncode, stdout, stderr, timed_out = -1, "", str(e), False if timed_out: status = "超时" elif returncode == 0: status = "通过" else: status = f"失败(退出码 {returncode})" print(status, flush=True) if proj_name not in run_results: run_results[proj_name] = [] run_results[proj_name].append((exe_name, returncode, stdout, stderr, timed_out)) # 汇总运行结果 for name, proj_dir, proj_json, compile_ok, elapsed, output in compiled: if not compile_ok: continue exe_results = run_results.get(name, []) if not exe_results: continue project_run_ok = True for exe_name, returncode, stdout, stderr, timed_out in exe_results: if timed_out or returncode != 0: project_run_ok = False break # 仅在返回码非 0 时检查 stdout 错误关键词,避免误报 # (PASS 行中可能包含 "Error"/"exception" 等词,如 "Status.Error=1") status = "通过" if project_run_ok else "失败" print(f" {name}: {status}") if project_run_ok: run_passed += 1 else: run_failed += 1 print(f"\n运行阶段: 通过 {run_passed}, 失败 {run_failed}\n") # ========== 汇总报告 ========== print("=" * 70) print("回归测试报告") print("=" * 70) total_failed = compile_failed + run_failed total_passed = compile_passed - run_failed # 编译通过但运行失败的不算通过 print(f"总计: {total} 通过: {total_passed} 失败: {total_failed}") print() if total_failed > 0: print("-" * 70) print("失败项目详情:") print("-" * 70) # 编译失败 for name, proj_dir, proj_json, compile_ok, elapsed, output in compiled: if compile_ok: continue print(f"\n【{name}】编译失败") lines = output.strip().split("\n") if output else [] if len(lines) > 300: print(f" ... (省略前 {len(lines) - 300} 行) ...") lines = lines[-300:] for line in lines: print(f" {line}") # 运行失败 for name, proj_dir, proj_json, compile_ok, elapsed, output in compiled: if not compile_ok: continue exe_results = run_results.get(name, []) if not exe_results: continue has_failure = False for exe_name, returncode, stdout, stderr, timed_out in exe_results: if timed_out or returncode != 0: has_failure = True break if not has_failure: continue print(f"\n【{name}】运行失败 (编译耗时 {elapsed:.1f}s)") for exe_name, returncode, stdout, stderr, timed_out in exe_results: if timed_out: print(f" [运行] {exe_name}: 超时(5秒强制终止)") continue if returncode != 0: print(f" [运行] {exe_name}: 退出码 {returncode}") if stderr.strip(): print(f" stderr: {stderr.strip()[:300]}") continue # 检查错误关键词,展示匹配行及上下 5 行 error_ctx = extract_error_context(stdout, stderr) if error_ctx: matched_line, context_str = error_ctx print(f" [运行] {exe_name}: 输出含错误关键词") print(f" 匹配行: {matched_line}") print(f" 上下文:") for ctx_line in context_str.split('\n'): print(f" {ctx_line}") print() print("=" * 70) if total_failed == 0: print("所有测试通过!") else: print(f"有 {total_failed} 个测试失败,请检查上方详情。") print("=" * 70) return 1 if total_failed > 0 else 0 if __name__ == "__main__": sys.exit(main())