#!/usr/bin/env python3 # -*- coding: utf-8 -*- """run_all_suites.py - 多套件回归编排(纯标准库)。 做什么(2026-10-01 测试架构评审 T-5): 套件互斥(`test_config.h` 的 `#error`)+ 编译期开关 → 跑 4 个套件的回归 本来要手工循环 4 轮(改宏 → 构建 → 烧录 → 采集 → 判读)。本脚本把它编排成一条命令: 对每个套件: 1. 改 test_config.h 启用该套件(其它全注释) 2. @build(0 错 0 警,否则跳过该套件) 3. 烧录(MDK-ARM/download.bat,Reset & Run) 4. @test 采集 + 判读(verify_log.py,含套件汇总行校验 T-2) 结束后: 5. **无条件还原 test_config.h**(try/finally,含异常/KeyboardInterrupt 路径) 为什么要还原(这是本脚本存在的核心理由): 跑完一轮忘了还原 = **测试固件进库**(与 T-1 同源风险)。T-1 是"提交期门禁"兜底, 本脚本是"流程自动化"——两条防线各自独立。 ⚠️ 代价提醒: 每轮要真机烧录 + 采集(8s 静默 + boot),4 套件 ≈ 数分钟起;破坏性套件还会改设备 持久状态。**仅回归窗口用,绝不挂 pre-commit。** 退出码: 0 = 全部套件 PASS 1 = 有套件 FAIL(构建失败 / 采集错误 / 标记判定失败) 2 = 环境/用法错误(缺文件、无法还原配置等) 用法(仓库根目录): & '.venv\\Scripts\\python.exe' test/run_all_suites.py & '.venv\\Scripts\\python.exe' test/run_all_suites.py --suites STORAGE,GD5F & '.venv\\Scripts\\python.exe' test/run_all_suites.py --dry-run # 只打印计划 & '.venv\\Scripts\\python.exe' test/run_all_suites.py --keep-config # 不还原(调试用,慎) 设计依据:docs/待处理内容.md T-5;docs/测试说明/测试开发标准流程.md §3.1 创建日期:2026-10-01 """ import argparse import re import subprocess import sys import time from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent CONFIG = REPO_ROOT / "test" / "board" / "test_config.h" BUILD_BAT = REPO_ROOT / "MDK-ARM" / "build.bat" DOWNLOAD_BAT = REPO_ROOT / "MDK-ARM" / "download.bat" RUN_BOOT = REPO_ROOT / "test" / "host" / "run_boot_test.py" # 套件 -> (宏名, verify_log 需要的汇总行名称, 该套件的 Required 标记) # 汇总行名称与套件 `XXX_TEST_REPORT("...")` 里的字符串一致(见各模块 Test_Guide) SUITES = { "CH395F": ("TEST_SUITE_CH395F", None, ""), "STORAGE": ("TEST_SUITE_STORAGE", "Storage Tests", "=== Storage Tests:"), "GD5F": ("TEST_SUITE_GD5F", "GD5F2GQ5UE Tests", "=== GD5F2GQ5UE Tests:"), "RTC": ("TEST_SUITE_RTC", "RTC Tests", "=== RTC Tests:"), } # 套件名 -> 汇总行里的名称可能因模块而异,故上面的 summary 值仅为默认;实际以 # `--required` 给出的前缀做 Required 匹配,`--require-suite-summary` 做严格解析。 _ALL_MACROS = [m for m, _, _ in SUITES.values()] def _rewrite_config(enable_macro): """把 test_config.h 里所有 TEST_SUITE_* 开关设为只启用 enable_macro(None = 全注释)。 只动"选择开关"行(`#define TEST_SUITE_` / `//#define TEST_SUITE_`), 不碰派生的 TEST_SUITE_SELECTED / TEST_SUITE_NAME。 按原换行符与 BOM 状态回写(仓库编码铁律)。 """ raw = CONFIG.read_bytes() bom = raw.startswith(b"\xef\xbb\xbf") text = raw.decode("utf-8-sig") if bom else raw.decode("utf-8") newline = "\r\n" if "\r\n" in text else "\n" lines = text.split(newline) switch_re = re.compile(r"^\s*(//)?\s*#\s*define\s+(TEST_SUITE_\w+)\s*$") out = [] for ln in lines: m = switch_re.match(ln) if m and m.group(2) in _ALL_MACROS: name = m.group(2) if enable_macro is not None and name == enable_macro: out.append("#define {}".format(name)) else: out.append("//#define {}".format(name)) else: out.append(ln) data = newline.join(out).encode("utf-8") if bom: data = b"\xef\xbb\xbf" + data CONFIG.write_bytes(data) def _run(cmd, timeout=None): """跑子进程,返回 (rc, 输出文本)。输出按字节捕获后 UTF-8 兜底解码。""" try: cp = subprocess.run(cmd, capture_output=True, timeout=timeout, cwd=str(REPO_ROOT), shell=isinstance(cmd, str)) except subprocess.TimeoutExpired as e: blob = (e.stdout or b"") + (e.stderr or b"") return -1, blob.decode("utf-8", errors="replace") blob = (cp.stdout or b"") + (cp.stderr or b"") return cp.returncode, blob.decode("utf-8", errors="replace") def main(argv=None): for stream in (sys.stdout, sys.stderr): try: stream.reconfigure(encoding="utf-8", errors="replace") except Exception: pass ap = argparse.ArgumentParser(description="多套件回归编排(T-5,纯标准库)") ap.add_argument("--suites", default=",".join(SUITES.keys()), help="要跑的套件,逗号分隔(默认全部:{})".format(",".join(SUITES))) ap.add_argument("--build-timeout", type=int, default=300) ap.add_argument("--flash-timeout", type=int, default=300) ap.add_argument("--dry-run", action="store_true", help="只打印计划,不改配置不构建不烧录") ap.add_argument("--keep-config", action="store_true", help="结束后不还原 test_config.h(调试用;⚠️ 会留下测试固件配置)") args = ap.parse_args(argv) py = sys.executable picked = [s.strip().upper() for s in args.suites.split(",") if s.strip()] bad = [s for s in picked if s not in SUITES] if bad: print("[ERROR] 未知套件:{}(可选:{})".format( ",".join(bad), ",".join(SUITES))) return 2 for p in (CONFIG, BUILD_BAT, DOWNLOAD_BAT, RUN_BOOT): if not p.is_file(): print("[ERROR] 缺文件:{}".format(p)) return 2 print("=== 多套件回归编排 ===") print("套件:{}".format(", ".join(picked))) print("构建:{}".format(BUILD_BAT)) print("烧录:{}".format(DOWNLOAD_BAT)) print("判读:{}".format(RUN_BOOT)) if args.dry_run: print("[DRY-RUN] 仅打印计划,未改动任何文件。") return 0 results = {} # 套件 -> "PASS"/"FAIL"/"BUILD_FAIL" restored = False try: # 先确保从产品构建(干净起点)开始,避免上一轮的残留干扰 _rewrite_config(None) baseline = CONFIG.read_bytes() for suite in picked: macro, summary_name, required = SUITES[suite] print("") print("################ {} ################".format(suite)) _rewrite_config(macro) # 1) 构建 rc, out = _run([str(BUILD_BAT)], timeout=args.build_timeout) tail = "\n".join(out.strip().splitlines()[-6:]) print("[BUILD] rc={}".format(rc)) if tail: print(tail) if rc != 0: print("[{}] BUILD_FAIL -> 跳过本套件".format(suite)) results[suite] = "BUILD_FAIL" continue # 2) 烧录 t0 = time.monotonic() frc, fout = _run(["cmd", "/c", str(DOWNLOAD_BAT)], timeout=args.flash_timeout) print("[FLASH] rc={} elapsed={:.1f}s".format(frc, time.monotonic() - t0)) if frc != 0: print("\n".join(fout.strip().splitlines()[-6:])) # 烧录失败不直接判死:板子可能已复位,交由采集+判读决定(与 @test 一致) print("[WARN] 烧录 rc={}(继续:板子可能已复位)".format(frc)) # 3) 采集 + 判读(含套件汇总行校验 T-2) cmd = [py, str(RUN_BOOT)] if required: cmd += ["--required", required] if summary_name: cmd += ["--require-suite-summary", summary_name] # 套件构建预期可能出现 [ERR](守卫负路径),放宽 Forbidden 至硬错误 cmd += ["--forbidden", "HardFault;assert;[ERR]"] rc, out = _run(cmd, timeout=args.flash_timeout + 120) print("\n".join(out.strip().splitlines()[-8:])) results[suite] = "PASS" if rc == 0 else "FAIL" print("[{}] -> {}".format(suite, results[suite])) finally: # 4) 无条件还原(本脚本存在的核心理由) if args.keep_config: print("") print("[WARN] --keep-config:未还原 test_config.h(⚠️ 可能留下测试固件配置)") else: try: _rewrite_config(None) # 与进入时的基线一致性核对(防止还原逻辑本身出错) now = CONFIG.read_bytes() if now != baseline: print("[WARN] 还原后的配置与进入时基线不一致,请人工核对 {}".format(CONFIG)) else: restored = True print("") print("[OK] 已还原 test_config.h 为产品构建(全部 TEST_SUITE_* 注释)") except Exception as e: # noqa: BLE001 - 还原失败必须显式报出 print("[ERROR] 还原 test_config.h 失败:{}".format(e), file=sys.stderr) # 汇总 print("") print("=== 编排结果 ===") for s in picked: print(" {:<8} {}".format(s, results.get(s, "SKIPPED"))) if not restored and not args.keep_config: return 2 failed = [s for s, r in results.items() if r != "PASS"] if failed: print("[FAIL] 未通过:{}".format(", ".join(failed))) return 1 print("[OK] 全部套件 PASS") return 0 if __name__ == "__main__": sys.exit(main())