Files
STM32F4-Base/scripts/check_test_traceability.py
T
wandering1 301c3e5183 feat(门禁): 编号门禁 id-check 落地(36→37 钩子)+ 批 1 编号单词化改名
新增门禁本体
- scripts/check_ids.py:实现 docs/编号规范.md §8 八项校验;默认 warn-only,
  唯一硬拦项是「可判定比例跌破基线」(防回退的本体)
- scripts/id_registry.json:66 个前缀族,逐项来自 §4 总表 + 全库实测游标,
  偏差一律写进 cursor_note,不凭记忆编造
- scripts/id_aliases.json:旧号 → 新号 / 作废标记(照搬 section_aliases.json 范式)
- 顺带收编 check_test_traceability.py:正则补齐大写后缀与无模块码形态,
  比对范围扩到 test/host 与 7 篇「单元测试说明.md」,另加 --board-only 回退旧口径

批 1 改名(用户授权解禁「不改写存量号」)
- 代码规范条款 S1~S19 → CODE-RULE-nn:78 处 / 2 文件
- UI 像素回归状态 S0~S7 → MENU-STATE-nn:61 处 / 3 文件(冻结在 07)
- 两族属主登记处 = 脚本自身的规则表 / STATES 表,文档只作引用方
- 新增 scripts/rename_ids.py(§7.3 规划脚本):映射表驱动、显式 scope、默认干跑、
  自动写 scoped 别名,跳过 [示例]/[计划] 行与变更记录表,按原字节读写保 BOM 行尾
- 已应用映射表存档 scripts/rename_maps/batch1.json 作审计轨迹

有牙实测(证明门禁不是空转)
- 注入 Z-99 / 裸形 / 悬空 / 越游标 / 未紧邻 / 跨文档重复定义 六例,
  分别被校验 1/2/3/5/6/4 点名,--strict 退出码 1;临时档删除后工作树干净

验证与同步
- 可判定比例基线 82.3% → 82.7%(已 --update-baseline 入档);错误级 1312 → 1291
- ci_local.py 全绿(L0 + L0.5 共 1104 断言);ui-golden Passed;
  check_doc_links.py --all 无法判定 0
- 未改任何 C/H 源文件,故本次不需 @build
- 文档:AGENTS.md 与 pre-commit使用说明.md 计数 36→37 并新增 §2.25 详解;
  编号规范 §4.1 补两行、§8.1 落地记录、§8.2 规范与仓库偏差、§8.3 迁移批次记录
2026-10-21 09:33:54 +08:00

152 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""check_test_traceability.py - S4 用例追溯校验(默认仅告警,不阻断提交)。
背景(docs/测试说明/实机测试实施指南.md §8.2;原复审项 S4,已闭环):
文档声称 91 个 TC、代码有 222 个 TEST_CHECK(断言点数 > 用例数正常),
但**双向是否一一对应无人校验**——某个 TC 可能"文档写了、代码没实现",
或"代码实现了、文档没登记"。纯靠人肉比对。
本脚本做什么:
提取两侧的 TC 编号集合,输出双向差异:
① 代码侧:`test/board/*.c`(含 `*_test_task.c` 与 `net_latency_test.c`)
里出现的 `TC-<PFX>-<NNN>`(注释、DBG_INFO 串皆计入);
② 文档侧:`docs/测试说明/*_Test_Guide.md` 里出现的 `TC-<PFX>-<NNN>`。
[DOC-ONLY] 文档有、代码无 -> 未实现(或代码未标注)
[CODE-ONLY] 代码有、文档无 -> 未登记(或文档漏写)
作用域**只取 `*_Test_Guide.md`**:TC-CFG / TC-CLK / TC-WD 属主机侧单元
测试文档(`配置文件单元测试说明.md` 等),不参与板级追溯,天然被排除。
退出码:
0 = 无差异;或有差异但**默认仅告警**(与 doc-links 演进路径一致)
1 = 有差异且显式 `--strict`
2 = 环境错误(目录/文件缺失)
用法(仓库根目录):
& '.venv\\Scripts\\python.exe' scripts/check_test_traceability.py
& '.venv\\Scripts\\python.exe' scripts/check_test_traceability.py --strict
& '.venv\\Scripts\\python.exe' scripts/check_test_traceability.py --verbose
设计依据:docs/测试说明/实机测试实施指南.md §8.2(S4 用例追溯,原复审项已闭环)
创建日期:2026-10-03
"""
import argparse
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
CODE_DIR = REPO_ROOT / "test" / "board"
DOC_DIR = REPO_ROOT / "docs" / "测试说明"
# TC 编号:TC-<PFX>-<NNN>。<PFX> 允许字母数字(NET/GD5F/STO/RTC…)。
# 2026-10-20 收编进 id-check 门禁时补的三条(docs/编号规范.md §8「顺带收编」):
# ① 规范 §4.1 的合规形态 `TC-[A-Z]{1,4}-\d{3,4}[a-z]?`(带小写后缀,TC-NET-1502b);
# ② 现存的大写后缀形态(TC-AI-01A [示例],§7.1 待统一小写)也要能抓出来,否则整条漏扫;
# ③ 无模块码形态(iec104 的 TC-014/TC-104,§7.2 列为孤儿)纳入比对而不是静默忽略。
TC_RE = re.compile(r"TC-(?:[A-Z][A-Z0-9]{0,4}-\d{1,4}[A-Za-z]?|\d{2,4}[a-z]?)")
# 规范合规形态(用于 --verbose 里区分"形态待迁移"与"纯追溯断链")
TC_CANON_RE = re.compile(r"^TC-[A-Z]{1,4}-\d{3,4}[a-z]?$")
# 占位符前缀(文档模板里的 `TC-XXX-…`)不参与比对
PLACEHOLDER_PREFIXES = {"XXX"}
def _prefix(tc: str) -> str:
return tc.rsplit("-", 1)[0]
def collect(paths, label):
"""返回 (ids_set, per_prefix_counts)。逐文件扫描,缺文件不致命。"""
ids = set()
for p in paths:
try:
text = p.read_text(encoding="utf-8", errors="replace")
except OSError as e: # pragma: no cover - 环境错误
print(f"[ERR] 无法读取 {p}: {e}", file=sys.stderr)
return None, None
for m in TC_RE.findall(text):
if _prefix(m) in PLACEHOLDER_PREFIXES:
continue
ids.add(m)
counts = {}
for i in sorted(ids):
counts[_prefix(i)] = counts.get(_prefix(i), 0) + 1
return ids, counts
def main():
ap = argparse.ArgumentParser(description="S4 用例追溯校验(TC 编号双向比对)")
ap.add_argument("--strict", action="store_true",
help="有差异时返回退出码 1(默认仅告警,返回 0)")
ap.add_argument("--verbose", action="store_true",
help="额外打印各前缀的 TC 计数")
ap.add_argument("--code-dir", default=str(CODE_DIR))
ap.add_argument("--doc-dir", default=str(DOC_DIR))
ap.add_argument("--board-only", action="store_true",
help="只比板级(test/board/*.c ↔ *_Test_Guide.md),"
"不含主机侧单测一侧(2026-10-20 起默认纳入 7 篇「单元测试说明.md」)")
args = ap.parse_args()
code_dir = Path(args.code_dir)
doc_dir = Path(args.doc_dir)
host_dir = REPO_ROOT / "test" / "host"
code_files = sorted(code_dir.glob("*.c")) if code_dir.is_dir() else []
doc_files = sorted(doc_dir.glob("*_Test_Guide.md")) if doc_dir.is_dir() else []
if not args.board_only:
# 主机侧单测一侧:7 篇「单元测试说明.md」+ test/host 下的测试代码(*.c / *_test.py /
# run_unit_test.py)——TC 编号在两侧都是**追溯链的锚点**,缺一侧就是断链。
doc_files += sorted(doc_dir.glob("*单元测试说明.md"))
if host_dir.is_dir():
code_files += sorted(host_dir.rglob("*.c"))
code_files += sorted(host_dir.rglob("*.py"))
if not code_files:
print(f"[ERR] 未找到代码侧文件:{code_dir}/*.c", file=sys.stderr)
return 2
if not doc_files:
print(f"[ERR] 未找到文档侧文件:{doc_dir}/*_Test_Guide.md", file=sys.stderr)
return 2
code_ids, code_counts = collect(code_files, "code")
doc_ids, doc_counts = collect(doc_files, "doc")
if code_ids is None or doc_ids is None:
return 2
doc_only = sorted(doc_ids - code_ids)
code_only = sorted(code_ids - doc_ids)
print("=== S4 用例追溯校验(TC 编号双向比对)===")
print(f"代码侧:{len(code_files)} 个文件,{len(code_ids)} 个 TC")
print(f"文档侧:{len(doc_files)} 个文件,{len(doc_ids)} 个 TC")
print(f"交集:{len(code_ids & doc_ids)}")
if args.verbose:
print("--- 各前缀 TC 计数(代码 / 文档)---")
for pfx in sorted(set(code_counts) | set(doc_counts)):
print(f" {pfx}: code={code_counts.get(pfx, 0)} doc={doc_counts.get(pfx, 0)}")
print()
print(f"[DOC-ONLY] 文档有、代码无(未实现/代码未标注):{len(doc_only)}")
for i in doc_only:
print(f" {i}")
print(f"[CODE-ONLY] 代码有、文档无(未登记/文档漏写):{len(code_only)}")
for i in code_only:
print(f" {i}")
if not doc_only and not code_only:
print("\n[OK] 双向一一对应,无差异")
return 0
print(f"\n[WARN] 共 {len(doc_only) + len(code_only)} 处差异"
f"(默认仅告警,不阻断提交;收敛后再考虑升为门禁)")
return 1 if args.strict else 0
if __name__ == "__main__":
sys.exit(main())