0f7b5a87fb
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号) 一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、 采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。 门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏 undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8 reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF; id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。 test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字, 是本批唯一改变构建产物的文件集合。 为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged 部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、 不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。 实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01% (基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 / 告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。 基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处 引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使 识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。 ⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二 根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有), 今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。 固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0; App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889 断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿; check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带 BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。 .cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法: Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。 同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%, RW_CCM 57,376 B / 87.9%,阈值 92%。
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 测试架构评审,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;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())
|