Files
STM32F4-Base/test/host/common/README.md
T
wandering1 0f7b5a87fb chore(编号): 存量编号全量转语义短语,门禁识别面与语料同批重打
登记簿、文档与代码注释里的存量字母加数字编号(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%。
2026-10-26 12:28:14 +08:00

120 lines
6.0 KiB
Markdown
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.
# test/host/common/ — 主机侧测试公共库
> L0.5 主机侧单元测试的**通用 harness**:被各模块的单测运行器共享,
> 集中承载工具链探测与编译/运行/判定的公共细节。
## 文件一览
| 文件 | 作用 |
|---|---|
| `unit_test_harness.py` | **通用单测 harness**(纯标准库):探测本机 MSVC → 拼 `INCLUDE`/`LIB` → 编译测试单元 → 运行 → 判定退出码 |
## 为什么要有这层(2026-10-01 抽取)
看门狗单测最早的运行器(`test/host/watchdog/run_unit_test.py`)把工具链探测、
`INCLUDE`/`LIB` 拼接、GBK 解码、`/wd4819` 抑制、退出码契约**全写死在模块目录内**。
第二个 L0.5 单测出现时须整段复制——工具链细节一旦修正(新增 VS 版本、SDK 布局
变化、编码处理调整)就会发散。抽取后各模块运行器只剩"声明式"配置。
> **边界**:harness **不认识任何模块**——源码路径、桩目录、额外 include、断言
> 汇总行特征全部由调用方传入。新单测只写一个 `TestBuild(...)` + 一行 `sys.exit(...)`。
## 用法(模块侧 `run_unit_test.py` 模板)
```python
import os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test
BUILD = TestBuild(
name="<模块名> Tests", # 人类可读名(打印用)
src=os.path.join(HERE, "<模块>_test.c"), # 测试单元(内部 #include 生产源)
include_dirs=[ # 顺序敏感:桩目录必须在生产目录之前
os.path.join(HERE, "stub"),
os.path.join(ROOT, "App", "util"),
HERE,
],
summary_marker="=== <模块名> Tests:", # 运行输出中必须出现的汇总行前缀
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
```
另一写法(不落 `run_unit_test.py`,直接内联):
```python
from unit_test_harness import TestBuild, run_host_unit_test
raise SystemExit(run_host_unit_test(TestBuild(name="...", src="...", ...)))
```
## 退出码契约(各模块运行器统一遵循)
| 码 | 含义 |
|---|---|
| `0` | PASS——编译 + 运行成功,且日志含 `summary_marker` |
| `1` | FAIL——有测试用例未通过 |
| `2` | 环境错误——找不到编译器 / 编译失败 / 未产生汇总行 |
## `TestBuild` 字段
| 字段 | 必填 | 说明 |
|---|---|---|
| `name` | ✓ | 打印用名称;同时用于派生 exe 名与 obj 目录名 |
| `src` | ✓ | 测试单元 `.c` 的绝对路径(内部 `#include` 生产源,保证测的是真代码) |
| `include_dirs` | | `/I` 目录列表;**桩目录需排在前面**才能覆盖真实头 |
| `summary_marker` | | 运行输出中必须出现的汇总行前缀;空 = 不校验 |
| `extra_flags` | | 追加的 `cl.exe` 开关 |
| `exe_name` / `obj_tag` | | 覆盖派生的产物名 / 中间目录名(默认由 `name` 派生;落点见下节"产物目录归属") |
## 环境要求
同 L0.5 通用要求(见 [`../../../docs/测试说明/测试开发标准流程.md`](../../../docs/测试说明/测试开发标准流程.md) §4.2):
MSVC `cl.exe`(装了 VS 即有)+ Windows SDK + 纯标准库 Python;**不调 `vcvars64.bat`**
(内部 `reg.exe` 受限),直接拼环境变量调 `cl.exe`。
## 产物目录归属(跨工作树并行,2026-10-25)
**obj / exe / pdb 每次运行独占一个子目录**,按工作树分家:
```
%TEMP%\dtu_host\<仓库根路径 sha1 前 10 位>\runs\<pid>-<纳秒时间戳>\
<suite>_obj\*.obj ← 中间目标文件
<suite>_tests.exe ← 本次编译产物(跑完随目录一起删)
<suite>_tests.exe.owner.json ← 归属凭据:suite / repo_root / pid / started
```
为什么这么放(两条都是实测出来的,不是洁癖):
- **旧写法把 obj/exe 摊在全机共用的 `%TEMP%`,名字由套件名派生**(
`%TEMP%\net_socket_tests.exe`)。多棵工作树并行跑同名套件就会互相覆盖,而最坏情况
不是编不过:A 树编译完,B 树把同名 exe 又盖一遍,A 树随后执行的是 **B 树编出来的
二进制**——两棵树的 `summary_marker` 一模一样,harness 分辨不出来,等于拿别人的
运行结果给自己下结论。2026-10-25 三条道并行时全量 `ci_local` 里 `test-cppcheck` /
`test-coverage` 偶发 "files were modified by this hook"、单跑即绿,就是这一类。
- **按工作树分家 + 每次跑独占**同时解决"跨树互踩"和"同树并发"(覆盖率钩子与手工 L0.5
同时跑同一棵树时也会撞)。不牺牲任何东西:每个测试单元就一个 `.c`,`cl` 每次整体
重编,"共享 obj 省编译"本来就是错觉。**没有加锁**——这里没有需要互斥的独占资源,
只是名字撞了;加锁等于把 L0.5 变成和 build/bench 一样的全局串行。
两个开关(都写在 harness 顶部注释里,判据以 `unit_test_harness.host_build_root()` 为准):
- `DTU_HOST_BUILD=<目录>`:改产物根(CI/容器要固定路径时用)。覆盖率侧同源——
`scripts/check_test_coverage.py` 复用同一个函数,不再用自己那套固定名
(旧 `%TEMP%\dtu_test_cov` 的共享名,正是 docs/测试说明/主机单测覆盖率口径.md 里
"并行期每人只跑 `--module` 自己模块"那条规避的由来;分家之后该规避不再必要)。
- `DTU_HOST_BUILD_KEEP=1`:保留本次独占目录不删(要单步调试那个 exe 时用,会打印路径)。
想知道当前树算到哪个目录:
```powershell
& '.venv\Scripts\python.exe' -c "import sys;sys.path.insert(0,'test/host/common');import unit_test_harness as U;print(U.host_build_root())"
```
## ⚠️ 注意
- 本目录在 pre-commit 的 cppcheck / 代码规范**排除范围**(`test/` 豁免)
- 改动本 harness 会触发 `watchdog-unit-test` 钩子(`files` 正则已含本文件)