d3107f374f
- rename_ref.py 搬迁:git mv + 全库引用改写 33 文件 82 处 - README.md 保留 §0 编号空间 / §1 未闭环一览 / §2 推进顺序权威 / §3 维护约定 - 拆出 6 文件:W1-采集保护收尾(任务卡五字段)、排期详述、暂缓-RS485、 登记簿、测试与架构专项、基线与待确认 - 外部引用重定位 ~50 处:裸「登记簿 §X」、§5.1→基线与待确认 §2.1、 §2.4→暂缓-RS485、§6→基线与待确认 §3、脚本注释 T-x/N-x→登记簿 - 新增并行写纪律:任务分支不改本文件夹,状态列由 Lead 主工作树更新 - 验证:doc-links 141 文档全绿;mem-numbers 一致;refgraph 重生成; @build 0 错误 0 警告;新文件无 BOM / LF / 零乱码
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())
|