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%。
188 lines
12 KiB
Markdown
188 lines
12 KiB
Markdown
# 看门狗单元测试说明(主机侧)
|
||
|
||
> **模块**:Task Watchdog(IWDG + 任务心跳位图,`App/util/task_watchdog.c`)
|
||
> **测试类型**:**主机侧纯逻辑单元测试**(L0.5 层,见 §1.5)——在 PC 上离线验证
|
||
> 判定状态机,零硬件、零烧录、秒级、不占 MCU 的 RAM。
|
||
> **状态**:✅ 已落地(47 断言,`@build` 0/0,已挂 pre-commit 钩子)
|
||
> **日期**:2026-10-01(2026-10-25 v1.5 按位分账改造,见变更记录)
|
||
> **关联文档**:
|
||
> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md)(被测行为的设计依据)、
|
||
> [`测试开发标准流程.md`](测试开发标准流程.md)(测试分层与门禁总览)、
|
||
> [`测试文档模板规范.md`](测试文档模板规范.md)(本文命名规范)
|
||
|
||
---
|
||
|
||
## 1. 概述
|
||
|
||
### 1.1 为什么单独做主机侧单测
|
||
|
||
`task_watchdog.c` 分两半:
|
||
|
||
| 半边 | 内容 | 可测性 |
|
||
|---|---|---|
|
||
| **判定状态机**(被测) | 位图取走清零、必需掩码比较、`s_miss_bits[]` 按位分账(容忍 = 同一位 8 连击)、100 ms 抽样节流、tick 回绕安全 | 纯逻辑,与硬件无关 → **可在 PC 上位穷举** |
|
||
| **硬件副作用** | `IWDG->KR` 写(喂狗 / 启动)、`HAL_GetTick()` | 需真机或桩 |
|
||
|
||
板端套件(`test/board/`)只能"在真机上完整跑一遍",**无法穷举边界**——例如:
|
||
|
||
- 节流窗口边界 99 ms / 100 ms / 199 ms / 200 ms 的分野;
|
||
- 连续缺席第 7 次 / 第 8 次 / 第 9 次的行为差异(停喂触发点);
|
||
- `HAL_GetTick()` 跨越 2^32 回绕后是否仍正确节流。
|
||
|
||
这些用主机侧单测一次性钉死,且**不占 MCU 的 RAM**(本项目 RAM 已 95%,见
|
||
`docs/内存管理/`)。设计上参照项目既有的"共享核心 + 平台后端"做法
|
||
(`App/cfg/cfg_file_store.h`:MCU 端 `cfg_file_store_mcu.c` / 模拟器端
|
||
`cfg_file_store_sim.c`)。
|
||
|
||
### 1.2 被测对象与桩
|
||
|
||
被测源文件以 **`#include` 原样纳入**测试单元,保证测的是**生产代码**而非副本:
|
||
|
||
```
|
||
test/host/watchdog/watchdog_test.c
|
||
├── #include "main.h" → stub/main.h (HAL_GetTick 声明)
|
||
├── #include "iwdg.h" → stub/iwdg.h (IWDG_TypeDef + IWDG 外部声明)
|
||
├── #include "dbg_log.h" → stub/dbg_log.h (DBG_* 映射 + 错误计数)
|
||
└── #include "task_watchdog.c" ← 生产源文件(App/util/),原样纳入
|
||
```
|
||
|
||
三个**平台钩子**(声明在 `App/util/task_watchdog.h` 末段"平台后端(内部)")在主机侧被替换为可观测桩:
|
||
|
||
| 钩子 | MCU 默认实现(`__weak`) | 主机侧桩 |
|
||
|---|---|---|
|
||
| `wd_platform_feed()` | `IWDG->KR = 0x0000AAAAu` | 计数 `s_feed_count++` + 写影子寄存器 |
|
||
| `wd_platform_start()` | `IWDG->KR = 0x0000CCCCu` | 计数 `s_start_count++` + 写影子寄存器 |
|
||
| `wd_platform_tick_ms()` | `HAL_GetTick()` | 返回单测可控的 `s_tick` |
|
||
|
||
> **生产端零增量**:三个钩子在 MCU 构建下就是原来的寄存器写 / HAL 调用,行为逐字一致,
|
||
> 无额外分支、无额外符号(`@build` 0/0 已验)。平台默认实现由
|
||
> `#ifndef TASK_WATCHDOG_UNIT_TEST` 守卫;测试专用复位 `task_wd_test_reset()`
|
||
> 仅在主机侧编译(MCU 构建零代码、零符号)。
|
||
|
||
### 1.3 测试环境
|
||
|
||
| 依赖 | 版本 / 说明 |
|
||
|---|---|
|
||
| 编译器 | 本机 MSVC(VS2019 BuildTools 14.29 / VS2022 任一),由 `run_unit_test.py` 自动探测 |
|
||
| Python | 仓库 `.venv` 或任意 python3(**纯标准库**) |
|
||
| 硬件 | 无(不需要开发板 / 串口 / ST-LINK) |
|
||
|
||
> **为何不调 `vcvars64.bat`**:它内部调用 `reg.exe` 探测环境,本机安全策略禁止
|
||
> `reg.exe`。运行器因此**直接拼 `INCLUDE`/`LIB` 环境变量**并用绝对
|
||
> 路径调 `cl.exe`(只需 MSVC 版本目录 + Windows SDK 的 include/lib),无副作用、可重复。
|
||
>
|
||
> 该工具链逻辑**不在本模块目录内**(2026-10-01 抽取):已沉淀为通用 harness
|
||
> [`../../test/host/common/unit_test_harness.py`](../../test/host/common/unit_test_harness.py),
|
||
> 本模块的 `run_unit_test.py` 只**声明** `TestBuild`(源文件 / 桩目录 / 汇总行标记)。
|
||
> 新增 L0.5 单测时直接复用 harness,见 [`../../test/host/common/README.md`](../../test/host/common/README.md)。
|
||
|
||
### 1.4 运行方式
|
||
|
||
```powershell
|
||
# 仓库根目录(统一用 .venv)
|
||
& '.venv\Scripts\python.exe' test/host/watchdog/run_unit_test.py
|
||
```
|
||
|
||
退出码:`0`=PASS / `1`=测试失败 / `2`=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
|
||
|
||
### 1.5 回归接入
|
||
|
||
| 项 | 说明 |
|
||
|---|---|
|
||
| **触发范围** | `App/util/task_watchdog.c` / `task_watchdog.h`,或 `test/host/watchdog/**`、`test/host/common/unit_test_harness.py` 变动 |
|
||
| **门禁层级** | **L0.5 层**(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑,比板端套件便宜、比静态检查更强 |
|
||
| **pre-commit 钩子** | `watchdog-unit-test`(本地钩子,`.pre-commit-config.yaml` §7) |
|
||
| **汇总行** | `=== Task Watchdog Tests: N/N PASSED (failed=0) ===` |
|
||
| **与板端测试的关系** | 互补:本单测保证**判定逻辑**正确;板端 `@test` 故障注入(V2/V4,见 `docs/子系统设计/看门狗设计说明.md` §7 验证记录)保证**真机复位链路**正确 |
|
||
|
||
---
|
||
|
||
## 2. 测试用例
|
||
|
||
> 命名 `TC-WD-NNN`(WD = Watchdog)。全部在 `watchdog_test.c` 内,主机侧运行。
|
||
|
||
| 用例 | 断言 | 验证的设计点 |
|
||
|---|---|---|
|
||
| **TC-WATCHDOG-001** | 未启动时 `poll` 不喂狗、`is_armed()==0` | 启动前不判定(`s_started` 门卫) |
|
||
| **TC-WATCHDOG-016** | 未启动时即使全员置位也不喂、不更新 `last_mask` | 同上(位图无关) |
|
||
| **TC-WATCHDOG-014** | 重复 `task_wd_start()` 只启动 IWDG 一次 | 启动幂等(IWDG 不可逆,重复启动无意义) |
|
||
| **TC-WATCHDOG-015** | `start` 内部先喂一次(`feed_count==1`) | 首个抽样窗口从启动时刻起算,避免启动即超时 |
|
||
| **TC-WATCHDOG-002** | 50 ms 内 `poll` 不喂;累计 100 ms 喂 1 次 | 100 ms 抽样节流 |
|
||
| **TC-WATCHDOG-003** | 全员置位 → 喂狗 1 次;`last_mask==0x1F7` | 全员到位判定 + 掩码取值(掩码史:0x37→0x77→0x1F7,见设计说明变更记录 2026-10-10/12) |
|
||
| **TC-WATCHDOG-004** | 不重新置位:第二窗(空位图,各位 streak=1)v1.5 **照常喂**;`last_mask==0` 证明位已取走 | **取走清零**语义(旧位不残留);停喂只看按位连击 |
|
||
| **TC-WATCHDOG-005** | 缺 UI(bit4)单窗(streak 1 < 容忍)→ **照常喂**;`last_mask` 精确记录缺失位 | 按位分账(v1.5)+ 缺失定位 |
|
||
| **TC-WATCHDOG-006** | 同一批缺席 1..7 窗不报且**持续喂**(feed_count==8);**第 8 窗**打 `[WD] task stall` 起停喂 | `WD_MISS_TOLERANCE=8`(同一任务 800 ms)停喂触发点 |
|
||
| **TC-WATCHDOG-007** | 停喂后**每 100 ms 复报**一条 stall(现状行为) | 见 §4 已知行为("最后遗言"持续到复位) |
|
||
| **TC-WATCHDOG-008** | 恢复全员到位 → 立即喂狗 + 全部按位账本清零(v1.5:episode 内 1..7 窗已持续喂,恢复窗 feed_count==8) | 停顿可恢复(非锁死状态机) |
|
||
| **TC-WATCHDOG-009** | 99 ms 不判 / 100 ms 判 / 再 99 ms 不判 / 累计 100 ms 判 | 节流窗口**边界**(易错点) |
|
||
| **TC-WATCHDOG-010** | `tick` 跨 2^32 回绕后 100 ms 仍正确判 | 无符号差比较(回绕安全) |
|
||
| **TC-WATCHDOG-011** | `is_armed`/`last_mask`/`miss_count` 语义 | 观测接口正确性 |
|
||
| **TC-WATCHDOG-013** | 非法任务 ID(`>= TASK_WD_ID_MAX`)被忽略,不污染位图;空位图单窗(streak 1)v1.5 照常喂 | 防位图越界(`task_wd_beat` 的守卫) |
|
||
| **TC-WATCHDOG-017** | UI/NET **交替缺席** 20 窗 → 零 stall、20 窗全部持续喂(v1.5 回归钉:旧复合判据会在第 8 窗误申报) | 复合串窗根治(2026-10-25 大窗 16:24/16:28 型误升级) |
|
||
| **TC-WATCHDOG-018** | 同一位(IEC104)缺席 7 窗:不报持续喂;8/9/10 窗连报 stall 且停喂;恢复窗喂并清零账本;第二段 7 窗亚容忍串再从零计、不误报 | 真单任务停摆保留杀伤力 + 账本重启(16:39 型真链仍可抓) |
|
||
|
||
**当前结果**:47/47 PASSED(`failed=0`)。
|
||
|
||
---
|
||
|
||
## 3. 如何扩展用例
|
||
|
||
1. 在 `watchdog_test.c` 加一个 `static void tc_0NN_xxx(void)`;
|
||
2. 用 `fresh()` 开头(复位模块静态态 + 清观测计数 + 时钟归零);
|
||
3. 用 `start_and_clear()` 启动(并清掉 `start` 自身那一次喂狗);
|
||
4. `beat_all()` / `task_wd_beat(...)` 置位,`poll_advance(ms)` 推进时间并抽样;
|
||
5. `CHECK(cond, "描述")` 断言;
|
||
6. 在 `main()` 里 `fresh(); tc_0NN_xxx();` 注册。
|
||
|
||
> ⚠ **别忘 `fresh()`**:`task_wd_poll` 的节流基准 `s_last_tick` 与位图 / 计数都是模块
|
||
> 静态态,跨用例不隔离会导致假失败(本项目开发时即踩到,见 §4)。
|
||
|
||
---
|
||
|
||
## 4. 已知行为与可优化点
|
||
|
||
### 4.1 stall 日志在停喂后复报(TC-WATCHDOG-007,现状)
|
||
|
||
实现(`task_watchdog.c`):
|
||
|
||
```c
|
||
if (s_miss_streak < WD_MISS_TOLERANCE) { s_miss_streak++; }
|
||
if (s_miss_streak >= WD_MISS_TOLERANCE) { DBG_ERROR("[WD] task stall: ..."); }
|
||
```
|
||
|
||
`streak` 封顶 8 后,判定条件 `streak >= 8` 在**后续每轮**仍成立 → 每 100 ms 复报一条,
|
||
直到 IWDG 约 900 ms 后复位(约 9 条/停顿周期)。
|
||
|
||
**评估**:这是**刻意的"最后遗言"**——复位前日志窗口内信息不丢。代价是日志冗余。
|
||
若改为"每停顿周期仅报一次",需加一个 `s_stall_reported` 标志(在 `streak` 归零时清除)。
|
||
**当前保留现状**(单测按现状断言,行为变更须同步改 TC-WATCHDOG-007 与本文)。
|
||
|
||
### 4.2 测试隔离依赖 `task_wd_test_reset()`
|
||
|
||
`task_wd_poll` 的节流基准 `s_last_tick` 曾是**函数内 `static`**,`task_wd_test_reset()`
|
||
够不到它 → 跨用例基线残留 → 假失败。已改为**文件静态**并纳入复位。
|
||
(这同时是更好的生产设计:模块状态应归于模块。)
|
||
|
||
---
|
||
|
||
## 5. 与板端验证的分工
|
||
|
||
| 层 | 覆盖 | 状态 |
|
||
|---|---|---|
|
||
| **本单测(L0.5)** | 判定状态机全部逻辑分支与边界 | ✅ 已落地 |
|
||
| 板端故障注入(L1/L2) | 真机:单任务卡死 → 停喂 → IWDG 真复位 → `RCC_CSR` 记 `IWDGRSTF` | ✅ 已台架验收(V2/V4,2026-10-03) |
|
||
| 长跑(L2) | ≥30 min 无假复位(`miss_count` 稳定) | ✅ 已台架验收(V5,30 min / 120×FTP 零复位,2026-10-03) |
|
||
|
||
> 本单测**不能**替代板端验证:IWDG 实际溢出时间、LSI 频率漂移、`SHCSR`/复位原因
|
||
> 寄存器的真机行为,仍需 `@test` 台架闭环确认。
|
||
|
||
---
|
||
|
||
## 变更记录
|
||
|
||
| 日期 | 变更 |
|
||
|---|---|
|
||
| 2026-10-25 | **v1.5 按位分账同步**:停喂判据从复合连击改为**单个必需位**连击(设计说明 §2.2/§3.2/§5.3),TC-WATCHDOG-004/005/006/008/013 断言按新语义改写(缺席窗在容忍下照常喂)、新增 TC-WATCHDOG-017(交替缺席零误报)/ TC-WATCHDOG-018(同位连击必报 + 账本重启),39→47 断言;顺带修正 TC-WATCHDOG-003 行陈旧的 `0x37`(B4/B5 后应为 `0x1F7`) |
|
||
| 2026-10-01 | §1.3/§1.4/§1.5 更新:工具链逻辑抽取到通用 harness `test/host/common/unit_test_harness.py`(T-3),本模块 `run_unit_test.py` 改为**声明式** `TestBuild`(原 170 行 → 55 行);hooked 触发范围加 `test/host/common/unit_test_harness.py`。行为与退出码契约不变(39/39 PASSED) |
|
||
| 2026-10-01 | 创建:看门狗判定状态机主机侧单元测试(39 用例)+ 平台层抽取(`task_watchdog.h` 末段三个 `__weak` 钩子)+ pre-commit 钩子 `watchdog-unit-test`;记录两处已知行为(stall 复报、测试隔离) |
|