Files
STM32F4-Base/test/host/watchdog/watchdog_test.c
T
wandering1 924ac3d428 test(host): 断言字面量统一纯 ASCII,30 文件 1535 行
用户裁定「统一」。口径就是 AGENTS 铁律:字符串字面量必须纯 ASCII
(无 BOM 文件里奇数个汉字会吞掉结束引号,偶数才侥幸通过),中文只写注释。
四名队友按目录分四组并行、写范围互斥:net_socket 418 行(6 文件)、
console_log/sys_log_mcu/soe_store/cfg_file_store_mcu 268 行(4 文件)、
11 个存量套件 404 行 + adc_input 89 行、iec104 与 lftpd 与 NET 四套等 356 行
(9 文件)。runner 的 .py 文档串不在口径内(Python 源不经 ARMCC 代码页)。

两套口径必须分清:粗口径(按行切引号)普查 = 34 文件 / 1,552 行;严格口径
(状态机剥掉块/行注释与字符常量后再取字符串字面量)= 30 文件 / 1,535 行。
差值那 4 个文件的命中全在注释里(ns_peers.c、net_socket/stub/ns_rtos.h、
common/ch395f_fake.h、common/fatfs_fake.c)——按口径一处都不该改,改了等于
把中文从注释里搬走、正好违背口径。粗口径绝不能当交付判据,否则要么白改
注释、要么留下「这行为什么没改」的悬案。

Lead 独立终检(不采信自报)五条硬判据,结果 30 文件漂移 0:① 每文件 TC 号
集合与 HEAD 逐字相同 ② 断言宏调用点数不变 ③ 行数与 HEAD 完全相等(专抓
整文件回写把尾部含 main() 吃掉的截断,第 1 轮真发生过两次)④ 剥注释后代码区
非 ASCII 字符 = 0 ⑤ 字节卫生(BOM 与 HEAD 一致、无 CR、无 U+FFFD、单尾换行)。
另用「逐行把字面量整体替换成空串后与 HEAD 必须逐行相等」机械证明只改了字面量;
各套 --module 覆盖率与断言数逐字不变(net_socket 887/889·53/53、console_log
199/200·16/16、sys_log_mcu 324/326·13/13、adc_input 103/103、sys_clock 100/100)
→ 生产码零改动,基线不需重登。

闸拦下一处真实回归(我的验收器没抓到):ns_s6_edges.c 翻译时把一个上一轮
已整改成平实描述的编号又写回了 CHECK 标签。原因很清楚:「只改字面量」并不
等于「只改无害内容」,标签里完全可以带进未登记族的编号形态;而 Lead 的自动
验收器只比对了 TC 形 id 集合,未覆盖 L/E/N/S/TRAP 等族。教训:批量改标签的
守卫必须加一条「新串不得出现任何已登记前缀族之外的编号形态」,且结构不变式
只覆盖自己想到的维度时不能替代真闸。
Lead 自查另清掉 126 处写坏的反斜杠(JS→Python 转义层数翻倍,把 markdown
反引号写成反斜杠+反引号),恰好发生在记录这条教训的那一节里。

文档:docs/测试说明/主机单测覆盖率口径.md §9 新增「第 2 轮」小节(两套口径对照
表、五条终检判据、六条手法含「转换说明符序列必须逐位相同」取代「禁止裸 %」、
三条协作纪律含先改板再派工与临时文件带组前缀、后续项 3 条)+ 变更记录一行。

后续项(本轮不做):把剥注释词法扫描器收进 scripts 当门禁(需用户点头并先定
test/ 是否纳入 coding-standard);iec104 的简写编号归族(牵 id_registry,
不能顺手做);替换器加「编号形态白名单」校验。

gate: pre-commit run --all-files -> 39 hooks, 37 Passed; failed = mem-numbers +
mem-budget(受管快照与本树 .map 的漂移,属第 1 轮生产码改动的遗留,非本批
增量)。本批只碰 test/host/**,不进 Keil 编译面也不进模拟器 CMake,**RAM/Flash
增量 = 0**;实测水位 RW_IRAM1 57.4%、RW_CCM 87.9%,均低于 92% 红线。
id-check 0 台账外新账(可判定 84.63% > 基线 82.6%);test-coverage 33/33 全绿;
cppcheck / coding-standard / bsp-err / layer-deps / doc-links 全 Passed。
按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
2026-10-24 21:00:17 +08:00

320 lines
13 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); /* 不重新置位 */
CHECK(s_feed_count == 1, "no re-set -> second round does not feed (bitmap already taken and cleared)");
}
static void tc_005_mask_required(void) {
printf("TC-WATCHDOG-005 mask check: missing any bit -> no feed\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 == 0, "UI bit missing -> no feed");
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 misses -> print stall and stop feeding\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 == 1, "fed exactly once at the start only (nothing fed during the missing period)");
}
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 == 1, "feeds immediately after 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");
}
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 == 0, "invalid id does not pollute the bitmap (all required bits missing -> no feed)");
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_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;
}