Files
STM32F4-Base/docs/门禁说明/host-unit-tests.md
T
super_agent_bot 72b4b0943c docs: clear task and layer shorthand IDs
Replace task/batch shorthand IDs and residual layer labels in
human-facing docs with semantic phrases. Add the new cleanup
policy to docs/编号规范.md and update the entry docs.

Machine keys (TC/CODE-RULE/MENU-STATE/TRAP/FR), hardware
designators/pins/MAC, protocol/version numbers and frozen
historical evidence are intentionally kept.

Docs only; no code behavior change. Verified with
check_doc_links.py --all and git diff --check.
2026-11-02 03:33:51 +08:00

143 lines
13 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.
# 主机侧单测(22 个 `*-unit-test` 钩子)— 同一机制的 22 个实例
> 一句话:把**不依赖硬件的逻辑层**拿到 PC 上编译并跑断言(每个模块一个 `test/host/<模块>/run_unit_test.py`)。
> 它是"回归最快的两层之一"(静态层 + 逻辑层),但**不等于真机正确**。
- 22 个钩子各有一个 `files:` 白名单(改动被测模块或测试目录才跑)、`pass_filenames: false`;
远端 CI 只跑 静态门禁 + 主机侧纯逻辑单测(见 `docs/CI/README.md`)。
## 1. 它在查什么(22 个实例)
| 钩子 | 被测对象 |
|---|---|
| `watchdog-unit-test` | 看门狗判定状态机 `App/util/task_watchdog.c` |
| `sys-clock-unit-test` | 系统时钟换算 `App/util/sys_clock.c` |
| `cfg-file-unit-test` | 配置引擎 `App/cfg/cfg_file.c` |
| `phasor-unit-test` | 相量层 `App/algo/phasor.c` + `sample_ring.c` |
| `adc-input-unit-test` | ADC 输入层 `App/algo/adc_input.c` |
| `vol-lock-unit-test` | 卷锁 `App/fs/vol_lock.c` |
| `time-sync-unit-test` | 时间同步 `App/sync/time_sync.c` |
| `adc-health-unit-test` | ADC 有效性校验 `App/algo/adc_health.c` |
| `prot-set-unit-test` | 保护定值模型 `App/algo/prot_set.c` |
| `prot-elm-unit-test` | 保护元件框架 `App/algo/prot_elm.c` |
| `freq-unit-test` | 测频 `App/algo/freq.c` |
| `sha256-unit-test` | SHA-256 `App/util/sha256.c` |
| `ftp-auth-unit-test` | FTP 鉴权状态机 `Drivers/BSP/NET/lftpd/lftpd_auth.c` |
| `iec104-unit-test` | IEC104 协议栈(ASDU / 链路 / 端口表) |
| `soe-unit-test` | SOE 动作记录编解码 `App/log/soe_rec.c`、`soe_store.c` |
| `nand-port-unit-test` | dhara NAND 移植层 `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` |
| `adc-cal-unit-test` | 通道标定层 `App/algo/adc_cal.c`(2026-10-25 挂闸,下同) |
| `prot-dim-unit-test` | 量纲回归:标定 → 相量 → 定值 → 判据链的单位与倍率一致性 |
| `meas-unit-test` | 测频与遥测快照层 `App/algo/meas.c`(200 ms 双区,104 与 UI 共用上游) |
| `run-par-unit-test` | 运行参数段 `App/algo/run_par.c`(遥测死区与系数) |
| `vol-idle-sync-unit-test` | 卷空闲同步 `App/fs/vol_idle_sync.c`(空闲窗口内的 f_sync 语义) |
| `ui-data-unit-test` | UI 数据快照层 `Drivers/BSP/UI/ui_data.c` 与各页写入方 |
共同机制:`run_unit_test.py` 在宿主编译器上编译被测 `.c` + 该目录的 `stub/` 替身,跑断言并汇总
用例数(各模块的 `TC-HOST-<模块码>` 号段见 `docs/编号规范.md`)。
各套件的来历与牙齿(迁移自原手册;2026-10-11 至 10-15 分批挂闸):
| 套件(登记出处) | 规模 | 为什么非要有 |
|---|---|---|
| `sha256`(FTP 鉴权) | 13 例 | FIPS 180-4 标准向量(空串 / `abc` / 56 B 补位跨界 / 1 MB)+ `hex_eq` 非法输入。哈希算错是"正确口令被拒 / 错误口令放行"的静默语义错误,板端只见 `530`/`230` 看不出根因 |
| `ftp-auth`(FTP 鉴权) | 36 例 | 连续 5 败锁定 / 60 s 解锁 / 成功清零 / tick 32 位回绕。板端每验一条要真等 60 s、回绕要跑满 49.7 天;纯函数(tick 由参数注入)就是为了让这些分支能毫秒级穷举 |
| `iec104`(IEC 104 上行) | 82 例 | CP56 往返 / APCI 半帧粘帧坏帧 / STARTDT-STOPDT / 总召分片 / COV 死区 / k=12 窗满 / w=8 触发 S / 三个超时定时器 / N(R) 倒退。板端复现要真主站 + 真等待(t1=15 s、窗满要发满 12 帧) |
| `freq`(测频与遥测快照) | 46 断言 | 扫频 45~55 Hz 误差 ≤0.02 Hz(实测 0.0101)/ 1% 噪声 ≤0.10 Hz / 启动收敛 ≤40 ms / 阶跃 ≤120 ms。板端无标准源照不出来;定长窗的负序泄漏纹波(0.317 Hz)只有合成扫频能暴露 |
| `soe`(SOE 记录) | 32 断言 | 64 B 定长记录编解码往返 / 截断与坏校验和拒绝 / IV 位 / seq 连续性 / 类型枚举。记录格式是**磁盘上的字节级契约**,错了要 FTP 下载再解码才看得见 |
| `nand-port`(dhara 移植层两条硬约束) | 149 断言 | 整页扫描的空闲判定(页首 64 B 全 0xFF 而第 1000 字节有数据等形态)+ **与旧算法对照**(旧实现只看页首,同一页答"空闲")。这类缺陷真机极难复现,发作后只表现为"某次复位后丢数据" |
- 运行器统一用 MSVC `cl.exe`(远端 CI 的 主机侧纯逻辑单测层);公共骨架是
`test/host/common/unit_test_harness.py` —— **改动这个骨架会同时触发 22 个钩子**
(每个钩子的 `files:` 都把它写进触发面,这是有意的)。
## 2. 为什么要它
- 状态机、编解码、换算这类逻辑,在真机上验证成本极高(要么构造故障、要么等复现);
在 PC 上毫秒级就能重跑,是"改完敢不敢提交"的第一层信心。
- 它们**已经在真实收口上起过作用**:`nand-port-unit-test` 与 `sys-clock-unit-test` 的桩里就
注入过 RTC 读失败等错误路径,用来证明失败分支的行为(本次 BSP 错误码契约收编时同步改过这两个 runner
的 include 目录,见 §6 第 4 条)。
## 3. 触发面
- 只有**改动该被测模块或该测试目录**时对应钩子才跑(22 个钩子各管一块,改日志不会触发 22 份编译)。
- 单独跑:`& '.venv\Scripts\python.exe' test/host/<模块>/run_unit_test.py`。
## 4. 报错怎么读
常见两类,处置完全不同:
1. **断言失败** → 逻辑真的变了/错了:先判断是被测代码错还是期望值该更新;
2. **编译失败**(找不到头文件)→ 多为**测试环境滞后**:`run_unit_test.py` 的 `include_dirs`
是手写清单。今天真实发生:新加 `Drivers/BSP/bsp_err.h` 后,两个 runner 报"无法打开包括文件
`bsp_err.h`",要在两个 runner 里补一行 `os.path.join(ROOT, "Drivers", "BSP")`。
## 5. 好处
- 快(单套件数百毫秒级)、可在无硬件机器上跑、可在远端 CI 上跑(CI 不跑 Keil 构建,单测是唯一
能跑的代码级验证);
- 错误路径可注入:真机上很难制造的"RTC 读失败""扇区坏"在这里是一行赋值。
## 6. 限制与盲区
1. **宿主编译器 ≠ ARMCC**:ARMCC 的警告(`#870-D` 多字节字符、`#1-D` 之类)、结构体对齐、
`enum` 取型、MicroLIB 行为差异,本层一概照不到 → 必须 `/keil-build`。
2. **桩就是第二份真相**:`stub/` 与被测代码依赖的真实头文件不同步时,会出现"单测全绿、真机
行为不同"。桩签名改了而测试没跟着改,编译器未必报错(尤其桩返回宽类型)。
3. `include_dirs` 与源文件清单是**手写**的:新依赖一个目录就会假失败(见 §4 第 2 类),反过来,
漏了某个真实依赖可能让测试**编过却少编了一个翻译单元**(少测而不自知)。
4. **专属钩子不是唯一防线,而「没有专属钩子」也不等于「没被跑」**。2026-10-26 按 git 历史与实测重新
清点(2026-10-28 复点、2026-10-29 录波三套入库后再复点:下列数字已按复点值更新;上一版写「16 个模块有专属钩子、另有 12 套、总闸跑 28 套」——三个数都随 2026-10-25 的
六根新钩子与后续新增套件过期了,正是本目录反复在防的"文档与机器面不同码"):
- **22** 根专属钩子 = 有独立 `files:` 触发面、需要**秒级归因**的那个子集;
- `test/host/` 下带 `run_unit_test.py` 的套件实测 **47** 套(48 个目录,除 `common` 与
`__pycache__`;两个数分别来自 `check_test_coverage.py --list`(47 行)与 `ls -d test/host/*/`(48 个目录),2026-10-30 复点)
⇒ **没有专属钩子的是 25 套**:`boot_stats`、`cfg_file_store_mcu`、`console_log`、
`ftp_cfg`、`iec104_serv`、`lftpd`、`lftpd_inet`、`lftpd_io`、`net_cfg`、`net_events`、
`net_listener`、`net_pending`、`net_select`、`net_socket`、`prot_set_store`、`sb_led_map`、`sb_link`、`sb_proto`、`soe_store`、
`sys_log_mcu`、`task_prof`、`ui_sink`、`wave_ring`、`wave_store`、`wave_trig`(末三套 2026-10-29 随
故障录波入库,屏板三套(`sb_proto`/`sb_link` 2026-10-29、`sb_led_map` 2026-10-30)随屏板链路入库,同一条 Lead 裁定不补专属钩子;25 = 47 套 − 22 根专属钩子,逐点数与两处机器源互证);
- 这 22 套每次 CI 与 `scripts/ci_local.py` 都由 `test-coverage` **总闸**真跑(该钩子带 `stages: [manual]`:
`git commit` 阶段不跑它,这一根与 `cppcheck` / `test-cppcheck` 同属 CI 与 `ci_local.py` 的 manual 段);
发现规则是
`HOST_TEST_DIR.rglob("run_unit_test.py")`(与 `scripts/ci_local.py` 的主机侧纯逻辑单测同一口径),
退出码语义写死「0 = 全部单测通过且覆盖率相对基线没回退 / 1 = 有单测失败或覆盖率回退」⇒
**失败照样拦(CI 与 `ci_local.py` 的 manual 段)**。差的只是归因粒度(要在总闸输出里认出是哪套)与耗时(47 套全量比单根慢得多);
- 要不要给某套补第 23 根,判据现成:pre-commit 4.x 没有 `always: true`,**该模块有没有自己的
`files:` 触发面**决定补了会不会真跑;只为凑数加一根,等于把「快归因面」和「全量把关」两件事混成
一件,还要让钩子计数再改一次;
- **裁定(2026-10-27,Lead):这批无专属钩子套件(裁定当时 18 套,2026-10-28 为 19 套,2026-10-29 现值 22 套;含 `test/host/ui_sink` 与新增的 `wave_ring` / `wave_store` / `wave_trig`)不补专属钩子,差异按设计说明记录。**
理由不是省事:22 根专属钩子存在的唯一意义是**秒级归因**;而 `ui_sink` 改动面小,它的失败面全部
落在 `test-coverage` 总闸的必跑路径里,坏了照样在 CI / `ci_local.py` 拦下。再加一根只是多一个必须「声明数 / 文档数 /
实跑数」三处同源同步的计数点——本仓 2026-10-26 到 27 一天之内就为 40 / 50 / 51 这三个数不同源
返工过两轮。**要读「这套跑没跑」,读总闸输出,不要来数钩子。** 两个方向的空头账一起堵住:
① 别把没接线的套件算进钩子账(原写「23 个 / 含 ui-sink」正是这么错的;2026-10-27 复核的准数是
22 根 / 40 套 / 18 套无专属钩子(三个数都是当时值)——第 41 套 `boot_stats` 当日 11:29 才入库,此后两数当时是 41 / 19,现值 47 / 25 以本条上面的实测行为准);② 也别把总闸的绿读成「每套都被单独归因过」——它只保证
「全跑过 + 覆盖率没回退」。将来确实要给某套补专属钩子,先过上一条判据,并在**同一笔**里改
AGENTS 与门禁索引的计数(口径 = YAML 解析条目数,见 `docs/门禁说明/README.md` §2 首条)。
- 总闸的触发面是 `App/`、`Drivers/BSP/`、`test/host/`、覆盖率脚本与基线,**外加门禁自身**
(`.pre-commit-config.yaml`)——不带上自身就等于谁改坏这根闸的 `files:` 模式它就再也不跑
(同 `lv-conf-sync` / `bsp-err` 的既有做法)。纯 UI/文档改动不触发它,那时确实没有可回归的行为。
5. 时序与并发不在范围:中断抢占、DMA 完成回调、临界区、真机 tick 抖动都照不到(历史上最贵的
几个坑正是这类,见 `docs/问题记录/*_Trap_Records.md`)。
6. 断言写的是"作者当时想到的分支":**行 / 函数覆盖有度量了**(`test-coverage` 总闸出
Cobertura 并守基线台账),但**分支覆盖仍无**——按用户裁定 2026-10-23 记为**已接受缺口**,
口径与理由见 `docs/测试说明/单元测试/主机单测覆盖率口径.md` §2.1,不是待办。
## 7. 豁免与基线
- 没有基线:新增或改动的模块必须自身通过。
- 用例号段:`TC-HOST-<模块码>` 以 `test/host/<目录>/*` 为属主登记处(`docs/编号规范.md` §4.1);
**该号段 2026-10-27 起停发新号**(用户裁定「批次三,不坐」):新增用例**不配号**,写语义化测试函数名,
现存号只读、不清、不复用。追溯器只双向比对已有集合,**不会**因为新用例没号而报错——别把它当"要带号"。
**当时的机器口径**(2026-10-27 落地,如今只剩留痕):`scripts/archive/id_gate/id_registry.json` 37 个 TC 类族登记 `frozen: true`、34 个子族游标冻结在当日存量最高号,
当年在号段上再加新号 = `id-check` 校验 5 错误级;无专属子族的模块码落在主族 `TC`(游标仍为空)。**两道编号门禁已退役(2026-10-28 / 29),这些校验都不再跑**,那一面如今只由规范与评审兜住
→ `docs/编号规范.md` §4.1 末框与 `scripts/archive/id_gate/编号治理历史.md` §9.1 末段。
## 8. 改这套门禁本身(新增一个模块的宿主单测)
- **新增模块的宿主单测 = 复制一个 runner 模板 + 在 `.pre-commit-config.yaml` 加一个钩子 +
登记 TC 号段**,三件事缺一件就会出现"测试存在但没人跑"或"跑了但无号可引"。
- 改 `include_dirs`/源文件清单时,务必确认失败是环境问题而不是逻辑回归(先看是编译错还是断言错)。