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/开关切换/异常还原均实测通过。
196 lines
7.5 KiB
Python
196 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""check_test_suite.py - 测试套件开关门禁:防止测试固件被静默提交(纯标准库)。
|
||
|
||
背景(2026-10-01 测试架构评审 T-1,P0):
|
||
`test/board/test_config.h` 的 `TEST_SUITE_*` 宏决定固件里编入哪个测试套件;
|
||
**全部注释 = 产品构建**。但该开关此前**没有任何门禁检查**——实测已提交的
|
||
HEAD 里 `TEST_SUITE_CH395F` 就是开启状态,`.map` 含 34 处 test 代码,
|
||
即仓库里躺在分支上的是**测试固件**而非产品固件。纯靠人记得改回去。
|
||
|
||
本脚本做什么:
|
||
扫描 `test/board/test_config.h`,判定 `TEST_SUITE_*` 是否处于启用状态:
|
||
|
||
- 全部注释(产品构建) -> PASS
|
||
- 启用了一个或多个(测试构建) -> FAIL(列出套件名 + 修复方法)
|
||
- 启用了多个(违反互斥) -> FAIL(编译期另有 `#error` 兜底)
|
||
|
||
测试构建本身是**合法**的(跑套件回归时就是要开),所以放行通道是
|
||
**显式声明**而非偷偷绕过:
|
||
|
||
python scripts/check_test_suite.py --allow-test-build
|
||
或 设置环境变量 ALLOW_TEST_BUILD=1
|
||
|
||
显式放行后仍然会打印醒目的 `[WARN]` 横幅,提醒"此提交是测试固件"。
|
||
|
||
判定实现:
|
||
只看**有效代码行**——先剥掉块注释与行注释,再匹配 `#define TEST_SUITE_<X>`。
|
||
这样注释掉的开关(`//#define TEST_SUITE_GD5F`)不会误判为启用。
|
||
|
||
退出码:
|
||
0 = PASS(产品构建,或已显式放行测试构建)
|
||
1 = FAIL(检测到未放行的测试套件开关)
|
||
2 = 环境错误(文件不存在 / 无法读取)
|
||
|
||
用法(仓库根目录):
|
||
& '.venv\\Scripts\\python.exe' scripts/check_test_suite.py
|
||
& '.venv\\Scripts\\python.exe' scripts/check_test_suite.py --allow-test-build
|
||
& '.venv\\Scripts\\python.exe' scripts/check_test_suite.py --file <路径>
|
||
|
||
设计依据:docs/待处理内容.md T-1;docs/测试说明/测试开发标准流程.md §3.1
|
||
创建日期:2026-10-01
|
||
"""
|
||
|
||
import argparse
|
||
import os
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||
DEFAULT_TARGET = REPO_ROOT / "test" / "board" / "test_config.h"
|
||
|
||
# 已知套件宏(顺序 = 报告顺序)。不写死也可用通用正则,但写死可给出
|
||
# "哪个套件被启用"的可读名称,并在新增套件时强制有人来更新本表(有意为之)。
|
||
SUITE_MACROS = [
|
||
("TEST_SUITE_CH395F", "CH395F 网络套件(硬件层 1~5 / NET 层 6~11)"),
|
||
("TEST_SUITE_STORAGE", "STORAGE 存储套件(FTL + FatFS)"),
|
||
("TEST_SUITE_GD5F", "GD5F NAND 驱动套件(10 阶段)"),
|
||
("TEST_SUITE_RTC", "RTC 套件(SD2506)"),
|
||
]
|
||
|
||
# 只关注"套件选择开关"本身。同文件里还有派生的 TEST_SUITE_SELECTED /
|
||
# TEST_SUITE_NAME(由开关推导,非用户可选项),必须排除——否则会把派生宏
|
||
# 当成"启用的套件"。故用**白名单精确匹配**,不做 Test 前缀泛匹配。
|
||
_SUITE_NAMES = {name for name, _ in SUITE_MACROS}
|
||
_DEFINE_RE = re.compile(r"^\s*#\s*define\s+(\w+)\b")
|
||
|
||
|
||
def _strip_comments(text):
|
||
"""剥掉 C 注释,返回等行数的"代码行"列表(保持行号以便定位)。
|
||
|
||
逐字符扫描而非正则——注释标记出现在字符串字面量里的情况在本文件不存在,
|
||
但逐字符实现能顺带处理 `/* ... */` 跨行,避免"整段注释被当代码"。
|
||
"""
|
||
out = []
|
||
in_block = False
|
||
for line in text.splitlines():
|
||
buf = []
|
||
i = 0
|
||
while i < len(line):
|
||
two = line[i:i + 2]
|
||
if in_block:
|
||
if two == "*/":
|
||
in_block = False
|
||
i += 2
|
||
continue
|
||
i += 1
|
||
continue
|
||
if two == "/*":
|
||
in_block = True
|
||
i += 2
|
||
continue
|
||
if two == "//":
|
||
break
|
||
buf.append(line[i])
|
||
i += 1
|
||
out.append("".join(buf))
|
||
return out
|
||
|
||
|
||
def scan(file_path):
|
||
"""返回 [(宏名, 行号)]——处于启用状态的测试套件宏,按出现顺序。"""
|
||
p = Path(file_path)
|
||
raw = p.read_bytes()
|
||
# 本仓库 C 源为 UTF-8(多数无 BOM);容错回退,避免个别文件编码异常直接崩
|
||
for enc in ("utf-8-sig", "utf-8", "gbk"):
|
||
try:
|
||
text = raw.decode(enc)
|
||
break
|
||
except UnicodeDecodeError:
|
||
continue
|
||
else:
|
||
text = raw.decode("utf-8", errors="replace")
|
||
|
||
active = []
|
||
for lineno, code in enumerate(_strip_comments(text), start=1):
|
||
m = _DEFINE_RE.match(code)
|
||
if not m:
|
||
continue
|
||
name = m.group(1)
|
||
if name not in _SUITE_NAMES:
|
||
continue
|
||
active.append((name, lineno))
|
||
return active
|
||
|
||
|
||
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="测试套件开关门禁:确保默认提交的是产品构建(纯标准库)")
|
||
ap.add_argument("--file", default=str(DEFAULT_TARGET),
|
||
help="测试套件配置文件(默认 test/board/test_config.h)")
|
||
ap.add_argument("--allow-test-build", action="store_true",
|
||
help="显式放行测试构建(跑套件回归时用;等价于 ALLOW_TEST_BUILD=1)")
|
||
ap.add_argument("--quiet", action="store_true", help="只在 FAIL 时输出")
|
||
args = ap.parse_args(argv)
|
||
|
||
allow = args.allow_test_build or os.environ.get("ALLOW_TEST_BUILD", "") not in ("", "0", "false", "False")
|
||
|
||
p = Path(args.file)
|
||
if not p.is_file():
|
||
print("[FAIL] 找不到测试套件配置文件:{}".format(p))
|
||
return 2
|
||
|
||
active = scan(p)
|
||
known = dict(SUITE_MACROS)
|
||
rel = p if not p.is_absolute() else p
|
||
try:
|
||
rel = p.resolve().relative_to(REPO_ROOT)
|
||
except ValueError:
|
||
pass
|
||
|
||
lines = []
|
||
lines.append("=== 测试套件开关检查: {} ===".format(rel))
|
||
|
||
if not active:
|
||
lines.append("[OK] 全部 TEST_SUITE_* 已注释 -> 产品构建 (product build)")
|
||
else:
|
||
lines.append("[WARN] 检测到 {} 个启用的测试套件开关:".format(len(active)))
|
||
for name, lineno in active:
|
||
desc = known.get(name, "")
|
||
lines.append(" L{:<4} {} {}".format(lineno, name, desc))
|
||
if len(active) > 1:
|
||
lines.append("[FAIL] 套件互斥被违反(一次构建最多启用一个;编译期另有 #error 兜底)")
|
||
|
||
if not args.quiet or active:
|
||
print("\n".join(lines))
|
||
|
||
if not active:
|
||
return 0
|
||
|
||
if allow:
|
||
print("")
|
||
print("[WARN] ============================================")
|
||
print("[WARN] 已显式放行测试构建(ALLOW_TEST_BUILD)")
|
||
print("[WARN] 本次提交的固件含测试代码,不是产品固件!")
|
||
print("[WARN] 提交后请尽快在 test/board/test_config.h 注释掉开关。")
|
||
print("[WARN] ============================================")
|
||
return 0
|
||
|
||
print("")
|
||
print("[FAIL] 默认要求产品构建:请注释掉 test/board/test_config.h 中的 TEST_SUITE_* 开关。")
|
||
print(" 正在跑套件回归、确需提交测试构建时,显式放行:")
|
||
print(" python scripts/check_test_suite.py --allow-test-build")
|
||
print(" 或: ALLOW_TEST_BUILD=1 git commit ...")
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|