测试:落地测试架构改进 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/开关切换/异常还原均实测通过。
This commit is contained in:
2026-10-01 07:28:57 +08:00
parent 6b90fa0581
commit 2055de0303
24 changed files with 1135 additions and 261 deletions
+3
View File
@@ -14,6 +14,7 @@
| `check_doc_links.py` | `doc-links` | **文档链接存在性** + 命名规范(D-3/D-7) |
| `gen_task_table.py` | `task-table` | FreeRTOS 任务表以 `Src/freertos.c` 为**唯一事实源**生成/校验(D-2/D-7) |
| `check_mem_numbers.py` | `mem-numbers` | 内存/Flash 数字以 `.map` 为唯一事实源校验(D-7 体积档) |
| `check_test_suite.py` | `test-suite-switch` | 测试套件开关=产品构建(防测试固件静默提交) |
### 报告生成(非门禁)
@@ -32,6 +33,8 @@
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all
& '.venv\Scripts\python.exe' scripts/gen_task_table.py --check # 或 --write / --diff
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --check
& '.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/ram_report.py
& '.venv\Scripts\python.exe' scripts/stack_report.py -s tools\serial_tool\state\serial.log
```
+195
View File
@@ -0,0 +1,195 @@
#!/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())