2055de0303
背景: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/开关切换/异常还原均实测通过。
234 lines
9.8 KiB
Python
234 lines
9.8 KiB
Python
#!/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())
|