本次会话先后销项与关闭: - E-61 SD2506 清 INTAF:原实现只读取 CTR1,而按手册 ARST=0 时读操作 不清标志,属空操作;改为读-改-写 CTR1,并按手册订正 CTR1 位宏 (INTAF/INTDF/BLF 位置,ARST 归 CTR3) - E-16 cfg_file:新增 CFG_FILE_MAX_NAME_LEN(段名/键名 ≤15),注册时 拒绝超长键名;s_gen_document 两处 snprintf 改检查式;补 TC-CFG-024 用例(主机单测 149→151 PASSED) - E-23 TC704:PHASE7_MULTI_COUNT 8→5(并发上限 = 8 − 3 个常驻监听), accept 失败分支加网络代次检查(网络重建即重建监听) - E-26(UDP)按“保留 API + 文档定案”关闭;E-30 评估为不必要后关闭; N-4(ACCEPT 决定不实施)关闭 审计文档剩余 5 项 P2 + 2 项监督,经用户裁定“都不重要”后整体删除: - 删除 docs/问题记录/错误码传播审计.md(634 行) - 九类缺陷模式表迁入 docs/编号规范.md §4.2.1(PAT-01~09) - E-xx 转为纯历史编号族:编号规范 §4.1 属主改“无(原属主已删除)”, §4.3 保持冻结;全仓 26 处引用按“保留编号、去悬空文档名”处理 - 顺带修正 N-4 收口遗留:排期详述 C1 才是 E-25/E-27/E-28 的承载处 (审计 §4 曾误写为登记簿);README 两处 C1 旧名改回“net 层跨上下文收口” 一并提交此前遗留的体系层评审报告 2026-10-14 复核 WIP(六维评分复核、 未闭环 Top 5→3、三区水位实测刷新)。 校验:Keil 构建 0 错误 0 警告(Code=212344);check_doc_links --sections-strict 与 --all(142 文档)通过;内存双台账刷新后均 PASS。
15 KiB
时间同步单元测试说明(主机侧)
模块:Time Sync(
App/sync/time_sync.c+App/sync/time_sync.h) 测试类型:主机侧纯逻辑单元测试(L0.5 层)——在 PC 上离线验证 TCP 时间同步协议 状态机(分片喂狗延时 / 监听建立与退避 / 时间包重组 / 任务主循环),零硬件、零烧录、 毫秒级、不占 MCU 的 RAM。 状态:✅ 已落地(80 断言点,/W40 告警,已挂 pre-commit 钩子) 日期:2026-10-10 关联文档:../../AGENTS.md(平台约束与看门狗铁律)、测试开发标准流程.md(测试分层与门禁总览)、看门狗单元测试说明.md(喂狗位图判定状态机)、卷锁单元测试说明.md(同批落地的姊妹篇)、../子系统设计/看门狗设计说明.md(required_mask/ 抽样窗 / 长耗时白名单)、../任务规划/README.md(N-17 登记条目)、 历史审计 E-02(netTask 头阻塞;属主文档已于 2026-10-15 删除,现状见../网络/Net层阻塞模型架构设计.md)
1. 概述
1.1 为什么单独做主机侧单测
本模块是两件已知风险的当事方,而两件风险的成因全部是本模块的纯逻辑:
- N-17(看门狗余量):登记簿 N-17 记录"time_sync 监听重建期间心跳缺口 800ms~1s,
距 IWDG 最坏超时 1701ms 余量仅约 0.7s"。该缺口的每一段都由本模块产生——
time_sync_delay_fed()的分片喂狗节奏、监听重试退避(500 / 2000ms)、accept轮询退避(200ms)、收尾延时(50ms),以及"就绪屏障等待态也要喂心跳"。 真机上只能靠"跑起来看复位不复位"间接观察;主机侧可逐片断言心跳次数与片长。 - E-02(头阻塞):旧实现用单次 30s 阻塞
net_recv等时间包,独占netTask, 使其余任务的网络操作整体停摆。现改为"分片有界读 + 总预算",本单测钉死其分片语义 (单次等待片长恒 ≤ 50ms、总预算 30s 不因分片而改变)。
次要理由:时间包重组状态机(累积到 8 字节才解析、TCP 分段送达、EOF / 真错误 / 本片超时三种返回码分支、tick 回绕)是纯逻辑,板端套件无法精确构造 "分段 + 预算耗尽 + tick 回绕"这类组合。
1.2 被测对象与桩
测试单元原样 #include 生产源文件,平台依赖由 7 个桩头替身 + 1 个真实头组成:
test/host/time_sync/time_sync_test.c
├── #include "FreeRTOS.h" / "task.h" / "cmsis_os2.h" ← 桩
├── #include "net_socket.h" / "sys_clock.h" /
│ "sys_events.h" / "dbg_log.h" ← 桩
├── #include "task_watchdog.h" ← 真实头(App/util/)
└── #include "time_sync.c" ← 生产源文件(App/sync/),原样纳入
| 头文件 | 来源 | 桩提供的能力 |
|---|---|---|
FreeRTOS.h |
桩 | TickType_t = uint32_t;configTICK_RATE_HZ = 1000(故 pdMS_TO_TICKS(ms) == ms) |
task.h |
桩 | vTaskDelay(记录每片时长)、xTaskGetTickCount(返回可控 tick) |
cmsis_os2.h |
桩 | osEventFlagsWait(按脚本返回就绪 / 超时) |
sys_events.h |
桩 | sys_evtHandle + SYS_EVT_NET_READY 等位定义 |
sys_clock.h |
桩 | sys_clock_set_unix(记录 epoch 与调用次数) |
dbg_log.h |
桩 | DBG_ERROR / DBG_INFO 计数(g_tstest_err_count / g_tstest_info_count) |
net_socket.h |
桩 | socket / bind / listen / accept / close / recv_to / get_generation / htons 全部脚本化 |
task_watchdog.h |
真实(App/util/) |
提供 task_wd_id_t / TASK_WD_TIMESYNC;task_wd_beat / task_wd_bind 由测试文件实现(记录心跳节奏与位号) |
time_sync.h |
真实(App/sync/) |
只 #include <stdint.h>,无平台依赖 |
include_dirs 顺序为 stub → App/util → App/sync → 本目录:桩目录必须排在
前面,否则会被真实头抢先命中。
⚠
dbg_log.h桩不得定义DBG_LINE_BUF_MAX/DBG_TAG——生产源(time_sync.c:28-29) 已先定义它们,桩再定义会触发 C4005 宏重定义告警。
1.3 关键约定:分片喂心跳与报文重组契约
(一)心跳必须落在同一个 100ms 抽样窗内。
看门狗抽样窗为 WD_POLL_PERIOD_MS = 100ms(App/util/task_watchdog.c)。若一次长等待
(如 2000ms 退避)期间不喂心跳,或心跳节奏恰好滑过两个连续抽样窗,该任务就会被判"卡死"。
故 time_sync_delay_fed() 把任意长等待切成 ≤ TIME_SYNC_WAIT_SLICE_MS = 50ms 的短片,
每片前喂一次心跳——语义等价于一次长延时,仅把心跳节奏钉在窗内(round 8b 消抖)。
(二)net_recv_to() 的四态返回。
time_sync_handle_conn() 的重组循环依赖该契约:
| 返回 | 含义 | 本模块动作 |
|---|---|---|
> 0 |
收到 n 字节 |
累加;>= 8 即收齐并解析,否则继续等余下字节 |
0 |
对端关闭(EOF) | 退出循环,不解析 |
-1 |
真错误 | 退出循环,不解析 |
-2 |
本片预算等满、无数据 | 重算剩余预算,下一片继续 |
(三)总预算不因分片而改变。
单次 net_recv_to 的等待片长 ≤ 50ms,但跨片累积仍以 t0 = xTaskGetTickCount()
为基准、总预算 TIME_SYNC_TOTAL_BUDGET_MS = 30000ms。tick 回绕由无符号减法天然处理。
1.4 测试环境
| 依赖 | 版本 / 说明 |
|---|---|
| 编译器 | 本机 MSVC(VS2019/2022/2026 任一),由 run_unit_test.py 自动探测 |
| Python | 仓库 .venv 或任意 python3(纯标准库) |
| 硬件 | 无(不需要开发板 / 串口 / ST-LINK / 对端 PC) |
工具链探测 / 编译运行 / 退出码判定全部委托通用 harness
../../test/host/common/unit_test_harness.py,
本模块 run_unit_test.py 只声明 TestBuild。
1.5 运行方式
# 仓库根目录(统一用 .venv)
& '.venv\Scripts\python.exe' test/host/time_sync/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
1.6 回归接入
| 项 | 说明 |
|---|---|
| 触发范围 | App/sync/time_sync.c / time_sync.h、App/util/task_watchdog.h,或 test/host/time_sync/**、test/host/common/unit_test_harness.py 变动 |
| 门禁层级 | L0.5 层(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑 |
| pre-commit 钩子 | time-sync-unit-test(本地钩子,.pre-commit-config.yaml §7g) |
| 汇总行 | === Time Sync Tests: 80/80 PASSED (failed=0) === |
| 与板端测试的关系 | 互补:本单测保证延时节奏与重组逻辑符合设计;板端保证真实 TCP 时序 + IWDG 不误复位(N-17 的实际余量只能由板端长跑判定) |
2. 测试用例
命名
TC-TS-NNN(TS = Time Sync)。断言数(80)多于用例数(27),因为每个用例通常 包含 2~9 条子断言(后缀b/c…)。桩状态在每用例前由fakes_reset()复位。
2.1 time_sync_delay_fed — 分片喂心跳延时(TC-TS-001..006)
| 用例 | 断言 |
|---|---|
| TC-TS-001 | delay_fed(0) 既不睡眠也不喂狗(零开销快路径) |
| TC-TS-002 | 恰好一片(50ms):1 次 vTaskDelay(50) + 1 次心跳 |
| TC-TS-003 | 非整片(120ms)切成 50 / 50 / 20 三片,每片喂一次心跳 |
| TC-TS-004 | 长延时(2000ms)= 40 片 × 50ms,每片喂心跳(监听退避路径) |
| TC-TS-005 | 不足一片(49ms)单片处理,不被拆成 0 |
| TC-TS-006 | 总预算 30s = 600 片,片长上限恒 50ms |
2.2 time_sync_open_listener — 监听建立与退避(TC-TS-010..015)
| 用例 | 断言 |
|---|---|
| TC-TS-010 | 首次成功:socket / bind / listen 各 1 次;端口经 net_htons(8888);sin_family = NET_AF_INET;成功路径无退避延时 |
| TC-TS-011 | bind 失败 → 关闭该 socket + 退避 2000ms(40 片)+ 留痕 DBG_ERROR,随后重试成功 |
| TC-TS-012 | listen 失败 → 关闭该 socket + 退避 2000ms,随后重试成功 |
| TC-TS-013 | socket 连续失败 → 5 次后返回 -1;5 条 DBG_ERROR;5 × 500ms 退避(50 片) |
| TC-TS-014 | 第 3 次重试成功并返回新描述符;恰好尝试 3 次;两次失败退避 500ms × 2 |
| TC-TS-015 | 心跳总次数 = 每轮重试(5)+ 每片延时(50);心跳位恒为 TASK_WD_TIMESYNC |
2.3 time_sync_handle_conn — 时间包重组状态机(TC-TS-020..029)
| 用例 | 断言 |
|---|---|
| TC-TS-020 | 8 字节一次收齐 → 大端解析 epoch + 更新墙钟 1 次 + 关连接;首次请求 16 字节;单次等待片长 = 50ms;留痕 wall clock updated |
| TC-TS-021 | 分段送达(4+4)→ 累积后解析正确;读了两次;第二次只请求剩余 12 字节;每片 50ms |
| TC-TS-022 | 一次到达 16 字节 → 只取前 8 字节;一次读够即停 |
| TC-TS-023 | 前缀非 "TIME" → 不更新墙钟,但仍关闭连接 |
| TC-TS-024 | 对端关闭(n == 0)→ 立即退出、不解析;关闭连接 |
| TC-TS-025 | 真错误(n == -1)→ 立即退出、不解析 |
| TC-TS-026 | 只收到 4 字节后 EOF → 不足 8 字节不解析;期间继续等待后续字节 |
| TC-TS-027 | 30s 预算耗尽 → 不再读(只读 1 次)+ 留痕 no TIME packet in 30s, drop conn + 关连接 |
| TC-TS-028 | 剩余预算不足一片 → 末片按剩余(20ms)切分 |
| TC-TS-029 | 32 位 tick 回绕 → 预算计算仍正确(无符号减法) |
2.4 time_sync_task — 任务主循环(TC-TS-040..044)
| 用例 | 断言 |
|---|---|
| TC-TS-040 | sys_evtHandle == NULL → 跳过就绪屏障;accept 轮询 3 次(2 次无连接 + 1 次成功);收到时间包并更新墙钟;延时片数 = 4+4+1(200+200+50ms)且全部按 50ms 分片;心跳次数 = 主循环 1 + 退避 8 + 收连接 1 + 收尾 1 + 次轮 1 = 13;所有心跳均打 TASK_WD_TIMESYNC 位 |
| TC-TS-041 | 就绪屏障轮询 3 次(2 次未就绪 + 1 次就绪);屏障等待按 100ms 分片;等待 SYS_EVT_NET_READY 位;osFlagsWaitAny | osFlagsNoClear(就绪位不被消费);未就绪的两次各喂一次心跳;仍先 task_wd_bind 登记心跳路由 |
| TC-TS-042 | 屏障等待 4 次均未就绪 → 循环未挂死(跳出并继续);每次未就绪后各喂一次心跳(2026-10-02 台架根因的回归防线) |
| TC-TS-043 | 网络代次变化 → 关闭旧监听并重监听;留痕 network re-init, relisten(另含 2 条 listen 成功);每次循环只轮询一次 accept 即放弃 |
| TC-TS-044 | 监听建立失败 → 内部重试 5 次后放弃;6 条 DBG_ERROR(5 条 net_socket FAIL + 1 条 retry later);退避 5×500ms + 2000ms = 90 片;心跳次数 = 2 + 5 + 50 + 40 = 97;所有心跳均打 TIMESYNC 位 |
当前结果:80/80 PASSED(failed=0)。
3. 如何扩展用例
- 纯延时类:直接调
time_sync_delay_fed(ms),断言s_delay_calls(片数)与s_delays[](每片时长)以及s_beat_calls(心跳数)。 - 脚本化网络:给
s_gen_seq[]/s_evt_seq[]/recv_step_t数组填序列, 再驱动被测函数。 - 跳出任务循环:生产
time_sync_task是for(;;),无法自然返回。桩在脚本耗尽时 调longjmp(s_jmp, N)强行跳出(见 §4.1),跳出后正常断言。 CHECK(cond, "描述")断言;在main()里注册(并同步更新本文档 §2 与变更记录)。
4. 已知边界与设计取舍
4.1 用 setjmp / longjmp 跳出 for(;;)
time_sync_task 是永不返回的任务循环。本单测在 main() 里 setjmp 设跳转点,
桩在"脚本化调用计数达到预设值"时 longjmp 回来,然后断言循环期间产生的副作用
(心跳次数 / 延时片数 / accept 次数 / 墙钟更新)。
取舍:这比"把循环体抽成可测函数"更贴近真实——测的就是任务主体本身,
不需要为了可测性改动生产代码结构。代价是必须精确设定跳出点:跳出点设错
(多一次或少一次桩调用)会导致断言数不符,TC-TS-040 的跳出点曾因此修正。
4.2 ⚠ C 的"暂定定义"会静默合并同名静态量
本文件曾出现一个无任何编译告警的缺陷:文件作用域下两处 static int s_bind_calls;
(一处给 task_wd_bind 桩、一处给 net_bind 桩)被 C 的暂定定义(tentative
definition)规则静默合并为同一个对象——于是 task_wd_bind 与 net_bind 共用一个计数器,
8 条断言失败(72/80)。
教训:同一编译单元内所有 static 变量名必须唯一,哪怕是不同桩的"各自的"计数器。
修复手法是把看门狗侧改名为 s_wd_bind_calls / s_wd_bind_id_bad,并加注释说明。
扩展用例时务必先 grep 目标名字是否已被占用。
4.3 桩不模拟真实 TCP 时序
net_recv_to / net_accept 桩按脚本返回,不模拟真实的分段到达时刻、重传、窗口、
CH395F 缓冲区边界。因此本单测能保证的是"给定返回码序列,状态机反应正确",
不能保证真实链路上的时序行为——后者属板端与 PC 对端(test/host/time_sync_server.py)职责。
4.4 N-17 的实际余量仍由板端判定
本单测把"每片喂心跳、片长 ≤ 50ms、退避值正确"这些逻辑前提钉死,但 N-17 关注的
是端到端心跳缺口 vs IWDG 超时——这还取决于真实调度延迟、netTask 排队时长、
Flash 写耗时等主机侧无法建模的因素。故:
- 本单测 ✅ 保证"逻辑上不会出现超过 50ms 的无喂狗窗口";
- 板端 ✅ 保证"实测缺口 + 调度抖动仍小于 1701ms"。
两者都过,N-17 才能闭环。
5. 变更记录
| 日期 | 变更 |
|---|---|
| 2026-10-10 | 创建;80 断言点全绿(TC-TS-001..006 / 010..015 / 020..029 / 040..044);覆盖分片喂狗延时、监听建立与退避、时间包重组(半包 / 粘连 / EOF / 真错 / 预算耗尽 / tick 回绕)、任务主循环(就绪屏障 / accept 轮询 / 代次变化重听 / 监听失败退避);修复静态量暂定定义合并缺陷;挂 pre-commit 钩子 time-sync-unit-test |