Files
STM32F4-Base/docs/测试说明/单元测试/看门狗单元测试说明.md
T
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

178 lines
11 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.
# 看门狗单元测试说明(主机侧)
> **模块**:Task Watchdog(IWDG + 任务心跳位图,`App/util/task_watchdog.c`)
> **测试类型**:**主机侧纯逻辑单元测试**(见 §1.5)——在 PC 上离线验证
> 判定状态机,零硬件、零烧录、秒级、不占 MCU 的 RAM。
> **状态**:✅ 已落地(47 断言,`/keil-build` 0/0,已挂 pre-commit 钩子)
> **日期**:2026-10-01(2026-10-25 v1.5 按位分账改造;改造史与设计依据见 `docs/子系统设计/看门狗设计说明.md` 变更记录——本文档不再设变更记录节)
> **关联文档**:
> [`../../子系统设计/看门狗设计说明.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 调用,行为逐字一致,
> 无额外分支、无额外符号(`/keil-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`(源文件 / 桩目录 / 汇总行标记)。
> 新增主机侧纯逻辑单测时直接复用 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` 变动 |
| **门禁层级** | **主机侧纯逻辑单测**(介于静态门禁与板端测试套件之间):主机侧、秒级、纯逻辑,比板端套件便宜、比静态检查更强 |
| **pre-commit 钩子** | `watchdog-unit-test`(本地钩子,`.pre-commit-config.yaml` §7) |
| **汇总行** | `=== Task Watchdog Tests: N/N PASSED (failed=0) ===` |
| **与板端测试的关系** | 互补:本单测保证**判定逻辑**正确;板端 `/serial-boot-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. 与板端验证的分工
| 层 | 覆盖 | 状态 |
|---|---|---|
| **本单测(主机侧纯逻辑单测)** | 判定状态机全部逻辑分支与边界 | ✅ 已落地 |
| 板端故障注入(板端测试套件 / 主机侧回归) | 真机:单任务卡死 → 停喂 → IWDG 真复位 → `RCC_CSR` 记 `IWDGRSTF` | ✅ 已台架验收(V2/V4,2026-10-03) |
| 长跑(主机侧回归) | ≥30 min 无假复位(`miss_count` 稳定) | ✅ 已台架验收(V5,30 min / 120×FTP 零复位,2026-10-03) |
> 本单测**不能**替代板端验证:IWDG 实际溢出时间、LSI 频率漂移、`SHCSR`/复位原因
> 寄存器的真机行为,仍需 `/serial-boot-test` 台架闭环确认。