Files
STM32F4-Base/test/host/watchdog/watchdog_test.c
T
wandering1 f754d59218 docs(测试说明): 按用途分六个子目录并同步全库引用
分层:流程与规范/ 单元测试/ 板级测试/ 专项验证/ 测试工具/ 测试数据/,
顶层只留新增的目录导航 README.md(归层判据 + 硬约束 + 本次处置记录)。

- 搬迁 28 个路径:测试数据三个目录 58 个文件为纯改名(R100,内容逐字节不变)
- 全库引用同步 124 个文件:AGENTS / docs 索引 / skills / 门禁注释 / scripts
- 门禁面:scripts/check_test_traceability.py 的 glob 改 rglob
  (分层后不递归扫描=追溯门禁直接瞎掉)
- scripts/rename_ref.py:收尾的 ℹ 提示在 GBK 代码页抛 UnicodeEncodeError,
  让进程以退出码 1 收场而改写其实已落盘(RC 与真实结果相反);改为把
  stdout/stderr reconfigure 成 UTF-8,与 check_doc_links.py 等同法
- docs/文档规范.md §1.1 补「子目录文件过多时再分一层」;§3.6 补 rename_ref
  搬整目录的两个实测坑:① 会改写目录内部文件的内容 ② 内部文件的 ../ 出链
  不补层级(本次 23 个被误改的证据文件已按 HEAD 原始字节逐字节还原)
- docs/README.md §6 改为与六个子目录一一对应,并补登原先漏登记的
  「启动日志原子性复验流程」一行
- .cubemx_survival_baseline.json:App/util/task_watchdog.c 的注释路径也随分层更新,
  该文件是存活审计的 watchdog-module 漂移锚点,按脚本自己的提示跑 --write 重建基线
  (实测重建后仅 watchdog-module 一条哈希变化)
- 铁律 4 例外(本次在任务树里改了单写者文件):AGENTS.md 与
  .pre-commit-config.yaml 只被 rename_ref 改写路径字面量(后者全在注释行),
  任务表块与钩子声明一字未动

验证:check_doc_links.py --sections-strict 覆盖全部 88 个改动 .md 全绿;
check_test_traceability.py 输出与主树逐行一致;196 个文本文件无 BOM、无新增
CR、无 U+FFFD、无行尾空白;陈旧引用面由主树 25 处降到 6 处(无新增断链)。
2026-10-30 07:16:07 +08:00

380 lines
16 KiB
C
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 主机侧单元测试
* 模块功能:在 PC 上对 App/util/task_watchdog.c 的**判定状态机**做穷举验证。
*
* 被测对象:真实源文件 ../App/util/task_watchdog.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本)。硬件访问由本文件提供的三个强符号
* 平台钩子(wd_platform_feed/start/tick_ms)替换为可观测桩。
*
* 隔离:每个用例前调用 task_wd_test_reset()(生产文件中由
* TASK_WATCHDOG_UNIT_TEST 守卫的测试专用复位,MCU 构建零开销)
* 把模块静态态恢复到"刚上电"。
*
* 覆盖点(对应 docs/测试说明/单元测试/看门狗单元测试说明.md 的 TC-WD-xxx):
* TC-WATCHDOG-001 未启动时 poll 不做任何判定(也不喂狗)
* TC-WATCHDOG-002 节流:< 100ms 的 poll 直接返回
* TC-WATCHDOG-003 全员到位置位 → 喂狗一次,且位图取走清零
* TC-WATCHDOG-004 取走清零语义:旧位不残留(下一轮必须重新置位)
* TC-WATCHDOG-005 掩码判定:缺任一位不喂,last_mask 精确记录
* TC-WATCHDOG-006 连续 1..7 次缺席不喂不报;第 8 次打 [WD] task stall
* TC-WATCHDOG-007 停喂后 stall 日志持续复报(现状行为,见用例内说明)
* TC-WATCHDOG-008 恢复:重新全员到位 → 喂狗 + streak 归零
* TC-WATCHDOG-009 节流窗口边界 99/100/199/200
* TC-WATCHDOG-010 tick 回绕安全(无符号差)
* TC-WATCHDOG-011 观测接口:last_mask / is_armed
* TC-WATCHDOG-013 非法任务 ID 被忽略(防位图越界)
* TC-WATCHDOG-014 start 幂等
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-01
*/
/* 打开生产文件里的测试专用复位钩子(MCU 构建不定义此宏) */
#define TASK_WATCHDOG_UNIT_TEST 1
#include <stdint.h>
#include <stdio.h>
/*
* 先纳入桩头文件,再纳入生产源码——保证 IWDG_TypeDef / HAL_GetTick 等
* 类型与声明在使用前可见(生产源码内部是同名 include,走 include guard 不再重复)。
* - stub/main.h → HAL_GetTick 声明
* - stub/iwdg.h → IWDG_TypeDef + IWDG 外部声明
* - stub/dbg_log.h→ DBG_* 映射与 g_wdtest_err_count
*
* 注意:生产源码里的三个 __weak 平台默认实现由 TASK_WATCHDOG_UNIT_TEST 守卫
* 而不编入(避免与本文件桩重名),故主机侧无需处理 __weak 关键字。
*/
#include "main.h"
#include "iwdg.h"
#include "dbg_log.h"
/* ===================== 平台桩:时间与 IWDG ===================== */
/* 可控毫秒时钟(单测手动推进;真实 MCU 由 TIM7 累加) */
static uint32_t s_tick = 0u;
/* IWDG 影子寄存器:真实代码写 IWDG->KR = x,这里落到本对象 */
static IWDG_TypeDef s_iwdg_shadow;
IWDG_TypeDef *IWDG = &s_iwdg_shadow;
/* 喂狗 / 启动 计数(平台钩子观测) */
static int s_feed_count = 0;
static int s_start_count = 0;
/* DBG_ERROR 计数(停喂时应打 [WD] task stall) */
int g_wdtest_err_count = 0;
/* ---- 强符号覆盖:三个平台钩子(覆盖 task_watchdog.c 的 __weak 默认实现) ---- */
void wd_platform_feed(void) { s_feed_count++; s_iwdg_shadow.KR = 0x0000AAAAu; }
void wd_platform_start(void) { s_start_count++; s_iwdg_shadow.KR = 0x0000CCCCu; }
uint32_t wd_platform_tick_ms(void) { return s_tick; }
/* main.h 声明的 HAL 时基 */
uint32_t HAL_GetTick(void) { return s_tick; }
/* ===================== 测试框架(极简) ===================== */
static int s_total = 0, s_passed = 0, s_failed = 0;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== 被测代码 ===================== */
/* 原样纳入生产源文件(stub/ 里的 main.h / iwdg.h / dbg_log.h 生效) */
#include "task_watchdog.c"
/* ===================== 辅助 ===================== */
/* 每个用例前的干净起点:复位模块静态态 + 清空观测计数 + 时钟归零 */
static void fresh(void) {
task_wd_test_reset();
s_tick = 0u;
s_feed_count = 0;
s_start_count = 0;
g_wdtest_err_count = 0;
s_iwdg_shadow.KR = 0u;
}
/* 推进时间并触发一次 poll */
static void poll_advance(uint32_t ms) {
s_tick += ms;
task_wd_poll();
}
/*
* 启动并清空喂狗计数:task_wd_start() 内部会先喂一次(让首个抽样窗口从启动
* 时刻起算,见生产代码注释),该次喂狗由 TC-WATCHDOG-015 单独断言;其余用例只关心
* "启动之后 poll 的喂狗行为",故此处把计数归零。
*/
static void start_and_clear(void) {
task_wd_start();
s_feed_count = 0;
}
/* 全员置位(默认必需掩码 0x1F7 = bit0|1|2|4|5|6|7|8,含 adc/iec104/protect) */
static void beat_all(void) {
task_wd_beat(TASK_WD_DEFAULT);
task_wd_beat(TASK_WD_NET);
task_wd_beat(TASK_WD_FTP);
task_wd_beat(TASK_WD_UI);
task_wd_beat(TASK_WD_TIMESYNC);
task_wd_beat(TASK_WD_ADC);
task_wd_beat(TASK_WD_IEC104);
task_wd_beat(TASK_WD_PROTECT);
}
/* ===================== 用例 ===================== */
static void tc_001_not_armed(void) {
printf("TC-WATCHDOG-001 poll makes no decision before start\n");
beat_all();
poll_advance(1000);
CHECK(s_feed_count == 0, "poll does not feed before start");
CHECK(task_wd_is_armed() == 0, "is_armed == 0 (not started)");
}
static void tc_002_throttle(void) {
printf("TC-WATCHDOG-002 throttle: < 100ms returns directly\n");
start_and_clear();
beat_all();
poll_advance(50); /* 50ms < 100ms → 不抽样 */
CHECK(s_feed_count == 0, "poll within 50ms does not feed");
poll_advance(50); /* 累计 100ms → 抽样 */
CHECK(s_feed_count == 1, "after 100ms accumulated, fed exactly once");
}
static void tc_003_all_beat_feeds(void) {
printf("TC-WATCHDOG-003 all tasks present -> feed once\n");
start_and_clear();
beat_all();
poll_advance(100);
CHECK(s_feed_count == 1, "all present feeds exactly once");
CHECK(task_wd_last_mask() == 0x1F7u, "last_mask == 0x1F7 (bit0|1|2|4|5|6|7|8)");
}
static void tc_004_take_and_clear(void) {
printf("TC-WATCHDOG-004 take-and-clear: no stale bits left\n");
start_and_clear();
beat_all();
poll_advance(100); /* 消费位图并喂狗 */
CHECK(s_feed_count == 1, "first round fed");
poll_advance(100); /* 不重新置位:空位图窗(各位 streak=1 < 容忍) */
CHECK(s_feed_count == 2, "v1.5: an empty-bitmap single window still feeds (streak 1 < tol)");
CHECK(task_wd_last_mask() == 0u, "bitmap was taken and cleared (no stale bits survive)");
}
static void tc_005_mask_required(void) {
printf("TC-WATCHDOG-005 mask check: one-window miss (streak 1) feeds; ledger records it\n");
start_and_clear();
/* 故意缺 UI(bit4) */
task_wd_beat(TASK_WD_DEFAULT);
task_wd_beat(TASK_WD_NET);
task_wd_beat(TASK_WD_FTP);
task_wd_beat(TASK_WD_TIMESYNC);
task_wd_beat(TASK_WD_ADC);
task_wd_beat(TASK_WD_IEC104);
task_wd_beat(TASK_WD_PROTECT);
poll_advance(100);
CHECK(s_feed_count == 1, "UI absent for ONE window (streak 1 < tol) -> still feeds (v1.5)");
CHECK(task_wd_last_mask() == (0x1F7u & ~(1u << TASK_WD_UI)),
"last_mask records the missing bit exactly");
}
static void tc_006_stall_8(void) {
printf("TC-WATCHDOG-006 8 consecutive all-miss windows -> stall at the 8th, feeding stops\n");
start_and_clear();
beat_all(); poll_advance(100); /* streak 清零 */
int err0 = g_wdtest_err_count;
for (int i = 1; i <= 7; i++) {
poll_advance(100);
CHECK(g_wdtest_err_count == err0, "misses 1..7 do not report stall");
}
poll_advance(100); /* 第 8 次 */
CHECK(g_wdtest_err_count == err0 + 1, "8th miss prints [WD] task stall");
CHECK(s_feed_count == 8, "v1.5: windows 1..7 (streak < tol) kept feeding; the 8th stops");
}
static void tc_007_stall_repeats_until_reset(void) {
printf("TC-WATCHDOG-007 after feeding stops the stall log keeps repeating (current design: one per 100ms)\n");
start_and_clear();
for (int i = 0; i < 8; i++) poll_advance(100); /* 第 8 次触发 */
int err8 = g_wdtest_err_count;
CHECK(err8 == 1, "after the 8th miss exactly 1 stall line was reported");
poll_advance(100); /* 第 9 次 */
poll_advance(100); /* 第 10 次 */
/*
* 当前实现:streak 封顶 8,但判定条件 `streak >= 8` 在后续每轮仍成立
* → 每 100ms 复报一条("最后遗言"持续到 IWDG ~900ms 后复位)。
* 这是**刻意记录**的现状行为:它让复位前的日志窗口内信息不丢;
* 代价是日志冗余(约 9 条/停顿周期)。若改为"每停顿周期仅报一次",
* 需加一个 s_stall_reported 标志,见文档"已知可优化点"。
*/
CHECK(g_wdtest_err_count == err8 + 2, "after feeding stops every sample re-reports stall (current behaviour)");
}
static void tc_008_recover(void) {
printf("TC-WATCHDOG-008 recovery: all present again -> feed + streak reset\n");
start_and_clear();
for (int i = 0; i < 8; i++) poll_advance(100); /* 触发 stall */
beat_all();
poll_advance(100);
CHECK(s_feed_count == 8, "feeds immediately after recovery (7 sub-tol windows fed + recovery)");
int err0 = g_wdtest_err_count;
poll_advance(100); /* 再缺一次 */
CHECK(g_wdtest_err_count == err0, "streak already reset: a single miss does not report stall");
}
/* 全员置位但跳过一位(按位连击用例的激励器;0x1F7 = 默认必需掩码,同 tc_005 用法) */
static void beat_all_except(task_wd_id_t skip) {
uint32_t bits = 0x1F7u;
uint32_t i;
for (i = 0u; i < (uint32_t)TASK_WD_ID_MAX; i++) {
if (((bits & (1u << i)) != 0u) && (i != (uint32_t)skip)) {
task_wd_beat((task_wd_id_t)i);
}
}
}
/* v1.5 按位分账·修复本体:交替缺席(本窗缺 UI、下窗缺 NET……)任何单位连击都
* 到不了容忍值——旧复合判据会在第 8 窗误报 stall 并停喂(2026-10-25 大窗
* 16:24/16:28 型),新判据必须全程零申报、持续喂狗 */
static void tc_017_mixed_alternating_no_false_stall(void) {
printf("TC-WATCHDOG-017 alternating per-bit misses never chain into a stall\n");
start_and_clear();
for (int i = 1; i <= 20; i++) {
if ((i % 2) != 0) {
beat_all_except(TASK_WD_UI); /* 奇数窗:UI 缺席 */
} else {
beat_all_except(TASK_WD_NET); /* 偶数窗:NET 缺席 */
}
poll_advance(100);
}
CHECK(g_wdtest_err_count == 0, "20 mixed-miss windows report ZERO stall (composite would have fired at 8th)");
CHECK(s_feed_count == 20, "feeding continues in every mixed window");
}
/* v1.5 按位分账·保留杀伤力:同一任务连续缺席到容忍值必须照报停摆、停喂;
* 恢复后账本清零,第二段亚容忍缺席重新从零计(16:39 型真单任务串仍会被抓) */
static void tc_018_single_bit_streak_stalls_and_restarts(void) {
printf("TC-WATCHDOG-018 same-bit streak reaches tol -> stall + feeding stops; ledger restarts after recovery\n");
start_and_clear();
for (int i = 1; i <= 7; i++) {
beat_all_except(TASK_WD_IEC104);
poll_advance(100);
}
CHECK(g_wdtest_err_count == 0 && s_feed_count == 7,
"7 windows missing the SAME bit stay below tol: no stall, feeding continues");
for (int i = 8; i <= 10; i++) {
beat_all_except(TASK_WD_IEC104);
poll_advance(100);
}
CHECK(g_wdtest_err_count == 3, "8th/9th/10th consecutive same-bit miss each report stall");
CHECK(s_feed_count == 7, "feeding stops from the 8th window onward");
beat_all();
poll_advance(100);
CHECK(s_feed_count == 8, "recovery window feeds and clears the per-bit ledger");
for (int i = 1; i <= 7; i++) {
beat_all_except(TASK_WD_IEC104);
poll_advance(100);
}
CHECK(g_wdtest_err_count == 3 && s_feed_count == 15,
"second sub-tol streak starts from zero again: no new stall");
}
static void tc_009_throttle_boundary(void) {
printf("TC-WATCHDOG-009 throttle window edges 99/100/199/200\n");
start_and_clear();
beat_all();
poll_advance(99); CHECK(s_feed_count == 0, "99ms: no decision");
poll_advance(1); CHECK(s_feed_count == 1, "100ms: decides (feed)");
beat_all();
poll_advance(99); CHECK(s_feed_count == 1, "another 99ms: no decision");
poll_advance(1); CHECK(s_feed_count == 2, "accumulated 100ms: decides");
}
static void tc_010_tick_wrap(void) {
printf("TC-WATCHDOG-010 tick wraparound is safe\n");
start_and_clear();
s_tick = 0xFFFFFF00u; /* 推到接近 2^32 回绕 */
task_wd_poll(); /* 建立基线(此处可能不判,取决节流) */
beat_all();
s_feed_count = 0;
poll_advance(0x100u); /* 跨越回绕:0xFFFFFF00 + 0x100 = 0 */
CHECK(s_feed_count == 1, "across the wraparound 100ms still decides correctly (unsigned diff)");
}
static void tc_011_observers(void) {
printf("TC-WATCHDOG-011 observation API semantics\n");
start_and_clear();
CHECK(task_wd_is_armed() == 1, "is_armed == 1 (started)");
beat_all();
poll_advance(100);
CHECK(task_wd_last_mask() == 0x1F7u, "last_mask reflects the most recent sample");
CHECK(task_wd_miss_count() == 0u, "all rounds met -> miss_count == 0");
}
static void tc_013_bad_id(void) {
printf("TC-WATCHDOG-013 invalid task id is ignored\n");
start_and_clear();
task_wd_beat((task_wd_id_t)(TASK_WD_ID_MAX + 3)); /* 非法:应被忽略 */
poll_advance(100);
CHECK(s_feed_count == 1, "invalid id ignored; one all-miss window still feeds (streak 1 < tol, v1.5)");
CHECK(task_wd_last_mask() == 0u, "bitmap stays 0 (invalid bit never set)");
}
static void tc_014_start_idempotent(void) {
printf("TC-WATCHDOG-014 start is idempotent\n");
task_wd_start();
task_wd_start();
CHECK(s_start_count == 1, "repeated start launches IWDG only once");
CHECK(task_wd_is_armed() == 1, "is_armed == 1");
}
static void tc_015_start_feeds_once(void) {
printf("TC-WATCHDOG-015 start feeds once first (first window counted from the start instant)\n");
task_wd_start();
CHECK(s_start_count == 1, "start launches IWDG once");
CHECK(s_feed_count == 1, "start feeds once internally (avoids a timeout right at boot)");
}
static void tc_016_stall_not_armed_no_feed(void) {
printf("TC-WATCHDOG-016 even with all tasks present, nothing feeds before start\n");
/* 未 start:poll 直接返回 */
beat_all();
poll_advance(500);
CHECK(s_feed_count == 0, "not started -> no feed (whatever the bitmap)");
CHECK(task_wd_last_mask() == 0u, "not started -> last_mask not updated");
}
int main(void) {
printf("=== Task Watchdog host-side unit tests ===\n");
printf("Target: App/util/task_watchdog.c (decision state machine)\n\n");
fresh(); tc_001_not_armed();
fresh(); tc_016_stall_not_armed_no_feed();
fresh(); tc_014_start_idempotent();
fresh(); tc_015_start_feeds_once();
fresh(); tc_002_throttle();
fresh(); tc_003_all_beat_feeds();
fresh(); tc_004_take_and_clear();
fresh(); tc_005_mask_required();
fresh(); tc_006_stall_8();
fresh(); tc_007_stall_repeats_until_reset();
fresh(); tc_008_recover();
fresh(); tc_017_mixed_alternating_no_false_stall();
fresh(); tc_018_single_bit_streak_stalls_and_restarts();
fresh(); tc_009_throttle_boundary();
fresh(); tc_010_tick_wrap();
fresh(); tc_011_observers();
fresh(); tc_013_bad_id();
printf("\n=== Task Watchdog Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}