#!/usr/bin/env python3 # -*- coding: utf-8 -*- """check_test_suite.py - 测试套件开关门禁:防止测试固件被静默提交(纯标准库)。 背景(2026-10-01 测试架构评审,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_`。 这样注释掉的开关(`//#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;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())