Files
STM32F4-Base/docs/测试说明/时间同步单元测试说明.md
T
wandering1 506eccc68f docs: 错误码传播审计收口并删除该文档(E-xx 转历史编号族)
本次会话先后销项与关闭:
- 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。
2026-10-15 07:39:38 +08:00

15 KiB
Raw Blame History

时间同步单元测试说明(主机侧)

模块:Time Sync(App/sync/time_sync.c + App/sync/time_sync.h) 测试类型:主机侧纯逻辑单元测试(L0.5 层)——在 PC 上离线验证 TCP 时间同步协议 状态机(分片喂狗延时 / 监听建立与退避 / 时间包重组 / 任务主循环),零硬件、零烧录、 毫秒级、不占 MCU 的 RAM。 状态:✅ 已落地(80 断言点,/W4 0 告警,已挂 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 为什么单独做主机侧单测

本模块是两件已知风险的当事方,而两件风险的成因全部是本模块的纯逻辑:

  1. N-17(看门狗余量):登记簿 N-17 记录"time_sync 监听重建期间心跳缺口 800ms~1s, 距 IWDG 最坏超时 1701ms 余量仅约 0.7s"。该缺口的每一段都由本模块产生—— time_sync_delay_fed() 的分片喂狗节奏、监听重试退避(500 / 2000ms)、 accept 轮询退避(200ms)、收尾延时(50ms),以及"就绪屏障等待态也要喂心跳"。 真机上只能靠"跑起来看复位不复位"间接观察;主机侧可逐片断言心跳次数与片长。
  2. 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. 如何扩展用例

  1. 纯延时类:直接调 time_sync_delay_fed(ms),断言 s_delay_calls(片数)与 s_delays[](每片时长)以及 s_beat_calls(心跳数)。
  2. 脚本化网络:给 s_gen_seq[] / s_evt_seq[] / recv_step_t 数组填序列, 再驱动被测函数。
  3. 跳出任务循环:生产 time_sync_task 是 for(;;),无法自然返回。桩在脚本耗尽时 调 longjmp(s_jmp, N) 强行跳出(见 §4.1),跳出后正常断言。
  4. 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