Files
STM32F4-Base/docs/门禁说明/host-unit-tests.md
T
wandering1 2d7c4fd40b ci(gates): test-coverage 的触发面补上门禁自身,否则改坏 files: 就没人跑这根闸
上一提交落地后立刻暴露这个口子:那次提交里新钩子显示 Skipped (no files to check),
因为它自己的 files: 不含 .pre-commit-config.yaml —— 也就是说,任何人改坏这一行
正则(哪怕只是打错一个字符),这根闸就永久静默停用,而提交照样全绿。这正是
本仓反对的"会静默失效的门禁",且已有先例处置:lv-conf-sync 与 bsp-err 都把
"门禁自身"写进触发面。

验证方式(不是只测绿的一半):
- 改前:pre-commit run test-coverage --files .pre-commit-config.yaml → Skipped
- 改后:同一命令 → 实跑并 Passed(约 45 s,28 套)
代价:以后改门禁配置要多等 45 秒——这种提交本来就是低频操作,值。

顺带修两处上一批留下的表述:
- 口径 §8 的 YAML 块补同一个 files: 模式(保持与落库那份一致)。
- 该节抬头那句我上次写坏了:JS 里把续行写成独立表达式,导致"…README.md 里只挂
  一行索引指向本文"这半句被丢掉,句子断在文档名上。已补全。
- 门禁说明 host-unit-tests.md 的触发面描述同步为"含门禁自身",并写明理由。

自查:doc-links --all --sections-strict 通过;改动文件 U+FFFD = 0、无 CRLF;
零代码改动故未跑 @build。未 push。
2026-10-24 07:53:40 +08:00

113 lines
8.1 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.
# 主机侧单测(16 个 `*-unit-test` 钩子)— 同一机制的 16 个实例
> 一句话:把**不依赖硬件的逻辑层**拿到 PC 上编译并跑断言(每个模块一个 `test/host/<模块>/run_unit_test.py`)。
> 它是"回归最快的两层之一"(静态层 + 逻辑层),但**不等于真机正确**。
- 16 个钩子各有一个 `files:` 白名单(改动被测模块或测试目录才跑)、`pass_filenames: false`;
远端 CI 只跑 L0 静态门禁 + L0.5 主机侧单测(见 `docs/CI/README.md`)。
## 1. 它在查什么(16 个实例)
| 钩子 | 被测对象 |
|---|---|
| `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` |
共同机制:`run_unit_test.py` 在宿主编译器上编译被测 `.c` + 该目录的 `stub/` 替身,跑断言并汇总
用例数(各模块的 `TC-HOST-<模块码>` 号段见 `docs/编号规范.md`)。
各套件的来历与牙齿(迁移自原手册;2026-10-11 至 10-15 分批挂闸):
| 套件(登记出处) | 规模 | 为什么非要有 |
|---|---|---|
| `sha256`(N-19) | 13 例 | FIPS 180-4 标准向量(空串 / `abc` / 56 B 补位跨界 / 1 MB)+ `hex_eq` 非法输入。哈希算错是"正确口令被拒 / 错误口令放行"的静默语义错误,板端只见 `530`/`230` 看不出根因 |
| `ftp-auth`(N-19) | 36 例 | 连续 5 败锁定 / 60 s 解锁 / 成功清零 / tick 32 位回绕。板端每验一条要真等 60 s、回绕要跑满 49.7 天;纯函数(tick 由参数注入)就是为了让这些分支能毫秒级穷举 |
| `iec104`(B4) | 82 例 | CP56 往返 / APCI 半帧粘帧坏帧 / STARTDT-STOPDT / 总召分片 / COV 死区 / k=12 窗满 / w=8 触发 S / 三个超时定时器 / N(R) 倒退。板端复现要真主站 + 真等待(t1=15 s、窗满要发满 12 帧) |
| `freq`(B8) | 46 断言 | 扫频 45~55 Hz 误差 ≤0.02 Hz(实测 0.0101)/ 1% 噪声 ≤0.10 Hz / 启动收敛 ≤40 ms / 阶跃 ≤120 ms。板端无标准源照不出来;定长窗的负序泄漏纹波(0.317 Hz)只有合成扫频能暴露 |
| `soe`(B9) | 32 断言 | 64 B 定长记录编解码往返 / 截断与坏校验和拒绝 / IV 位 / seq 连续性 / 类型枚举。记录格式是**磁盘上的字节级契约**,错了要 FTP 下载再解码才看得见 |
| `nand-port`(N-21) | 149 断言 | 整页扫描的空闲判定(页首 64 B 全 0xFF 而第 1000 字节有数据等形态)+ **与旧算法对照**(旧实现只看页首,同一页答"空闲")。这类缺陷真机极难复现,发作后只表现为"某次复位后丢数据" |
- 运行器统一用 MSVC `cl.exe`(远端 CI 的 L0.5 层);公共骨架是
`test/host/common/unit_test_harness.py` —— **改动这个骨架会同时触发 16 个钩子**
(每个钩子的 `files:` 都把它写进触发面,这是有意的)。
## 2. 为什么要它
- 状态机、编解码、换算这类逻辑,在真机上验证成本极高(要么构造故障、要么等复现);
在 PC 上毫秒级就能重跑,是"改完敢不敢提交"的第一层信心。
- 它们**已经在真实收口上起过作用**:`nand-port-unit-test` 与 `sys-clock-unit-test` 的桩里就
注入过 RTC 读失败等错误路径,用来证明失败分支的行为(本次 S-5 收编时同步改过这两个 runner
的 include 目录,见 §6 第 4 条)。
## 3. 触发面
- 只有**改动该被测模块或该测试目录**时对应钩子才跑(16 个钩子各管一块,改日志不会触发 16 份编译)。
- 单独跑:`& '.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 行为差异,本层一概照不到 → 必须 `@build`。
2. **桩就是第二份真相**:`stub/` 与被测代码依赖的真实头文件不同步时,会出现"单测全绿、真机
行为不同"。桩签名改了而测试没跟着改,编译器未必报错(尤其桩返回宽类型)。
3. `include_dirs` 与源文件清单是**手写**的:新依赖一个目录就会假失败(见 §4 第 2 类),反过来,
漏了某个真实依赖可能让测试**编过却少编了一个翻译单元**(少测而不自知)。
4. **专属钩子不是唯一防线**(2026-10-24 更正,原写"没有对应钩子的模块 = 零回归",如今不成立):上表 16
个模块各有专属钩子;仓库另有 12 套(P1 新增)没有专属钩子,改坏了由 `test-coverage` **总闸**兜
住——它跑全部 28 套,并守"已覆盖行只增不减"。总闸的触发面是 `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`);
新增用例要带号,否则 `id-check` 会按形态/悬空规则处理。
## 8. 改这套门禁本身(新增一个模块的宿主单测)
- **新增模块的宿主单测 = 复制一个 runner 模板 + 在 `.pre-commit-config.yaml` 加一个钩子 +
登记 TC 号段**,三件事缺一件就会出现"测试存在但没人跑"或"跑了但无号可引"。
- 改 `include_dirs`/源文件清单时,务必确认失败是环境问题而不是逻辑回归(先看是编译错还是断言错)。
## 9. 变更记录
| 日期 | 变更 |
|---|---|
| 2026-10-23 | 首篇逐项文档(16 个实例清单 + 今天两个 runner 需补 include 目录的实测) |