Files
STM32F4-Base/test/run_all_suites.py
T
wandering1 2055de0303 测试:落地测试架构改进 T-1~T-5(套件开关门禁/汇总行校验/通用 harness/公共断言宏/多套件编排)
背景:L0.5 主机侧单测落地后复审测试架构,给出 6 条改进意见(P0~P3),
登记进 docs/待处理内容.md「测试与验证基础设施」后逐条实施。

- T-1(P0) 测试套件开关门禁:新增 scripts/check_test_suite.py + pre-commit 钩子
  test-suite-switch,默认要求产品构建(全部 TEST_SUITE_* 注释),跑套件回归用
  --allow-test-build / ALLOW_TEST_BUILD=1 显式放行。顺带把 test_config.h 中长期
  开启的 TEST_SUITE_CH395F 改回注释(仓库此前躺着的是测试固件)。
- T-2(P1) verify_log.py 增加 --require-suite-summary:要求
  "=== <名称>: N/M PASSED (failed=0) ===" 且 N>0,堵住"套件没跑到汇总行/
  断言数为 0 仍判 PASS"的盲区;run_boot_test.py 同名参数透传。不传参向后兼容。
- T-3(P1) 抽取通用 harness test/host/common/unit_test_harness.py(工具链探测/
  INCLUDE-LIB 拼接/wd4819/GBK 解码/退出码契约);watchdog 运行器 170→55 行,只声明 TestBuild。
- T-4(P2) 新增 test/board/test_common.h 集中断言内核(TEST_CHECK_IMPL/TEST_REPORT_IMPL),
  4 套件各用一行宏绑定自身统计变量(保持 g_xxx_stats 独立、防跨套件串数)。
  刻意不 include dbg_log.h——否则冻结 DBG_TAG 默认值触发 #47-D 重定义警告。
- T-5(P2) 新增 test/run_all_suites.py 多套件回归编排,try/finally 无条件还原套件开关。

验证:@build 产品构建 0 错 0 警;4 套件逐一构建均 0/0;看门狗单测 39/39 PASS;
pre-commit 14 钩子全通过;编排脚本 dry-run/开关切换/异常还原均实测通过。
2026-10-01 07:28:57 +08:00

234 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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_<NAME>` / `//#define TEST_SUITE_<NAME>`),
不碰派生的 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())