Files
STM32F4-Base/test/host/time_sync/time_sync_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

737 lines
29 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.
/*
* 模块名称:Time Sync(PC 本地时间推送接收)主机侧单元测试
* 模块功能:在 PC 上对 App/sync/time_sync.c 做离线验证(4 个函数全覆盖:
* time_sync_delay_fed / time_sync_open_listener / time_sync_handle_conn /
* time_sync_task)。
*
* 被测对象:真实源文件 ../../../App/sync/time_sync.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本)。平台依赖由本文件提供的**可注入桩**实现:
* - FreeRTOS/task.h -> vTaskDelay / xTaskGetTickCount(可控时钟)
* - net_socket.h -> socket/bind/listen/accept/close/recv_to/
* get_generation/htons(全部脚本化)
* - sys_events.h -> osEventFlagsWait(可注入"未就绪")
* - sys_clock.h -> sys_clock_set_unix(记录 epoch)
* - task_watchdog.h -> task_wd_beat / task_wd_bind(记录心跳节奏)
* - dbg_log.h -> DBG_ERROR/DBG_INFO 计数
*
* 为什么要主机侧单测(而不是只用板端套件):
* 1. **本模块是 N-17 的当事方**:登记簿 N-17 记录"time_sync 监听重建期间心跳缺口
* 800ms~1s,距 IWDG 最坏超时 1701ms 余量仅 ~0.7s"。缺口的成因全部是本模块的
* **纯逻辑**——分片延时(delay_fed)的心跳节奏、监听重试退避(500/2000ms)、
* accept 轮询退避(200ms)、收尾延时(50ms)、以及"屏障等待态也要喂心跳"。
* 这些在真机上只能靠"跑起来看复位不复位"间接观察,主机侧可**逐片断言**。
* 2. **时间包重组状态机**(handle_conn)是纯逻辑:累积到 8 字节才解析、30s 总预算
* 随 tick 推进耗尽、TCP 分段送达、EOF/真错误/超时三种返回码分支——板端套件
* 无法精确构造"分段 + 预算耗尽 + tick 回绕"的组合。
* 3. 不占 MCU 的 RAM,毫秒级完成。
*
* 覆盖点(TC-TS-xxx):
* —— time_sync_delay_fed(可喂心跳的分片延时)——
* TC-TIMESYNC-001 0ms 不睡眠不喂狗
* TC-TIMESYNC-002 恰好一片(50ms)
* TC-TIMESYNC-003 非整片(120ms)切成 50/50/20,每片喂心跳
* TC-TIMESYNC-004 长延时(2000ms)= 40 片 × 50ms,每片喂心跳
* TC-TIMESYNC-005 不足一片(49ms)单片处理
* TC-TIMESYNC-006 总预算 30s = 600 片(片长上限恒 50ms)
* —— time_sync_open_listener(监听建立与重试)——
* TC-TIMESYNC-010 首次成功:socket/bind/listen 各一次,端口经 net_htons,无退避
* TC-TIMESYNC-011 bind 失败 -> 关 socket + 退避 2000ms + 留痕,随后重试成功
* TC-TIMESYNC-012 listen 失败 -> 关 socket + 退避 2000ms,随后重试成功
* TC-TIMESYNC-013 socket 连续失败 -> 5 次后返回 -1(5 条 ERROR,5×500ms 退避)
* TC-TIMESYNC-014 第 3 次重试成功并返回新描述符
* TC-TIMESYNC-015 每次重试前 + 每片延时各喂一次心跳,且心跳位恒为 TIMESYNC
* —— time_sync_handle_conn(时间包重组状态机)——
* TC-TIMESYNC-020 8 字节一次收齐 -> 大端解析 epoch + 更新墙钟 + 关连接
* TC-TIMESYNC-021 分段送达(4+4)-> 累积后解析;第二次只请求剩余 12 字节
* TC-TIMESYNC-022 一次到达 16 字节 -> 只取前 8 字节
* TC-TIMESYNC-023 前缀非 "TIME" -> 不更新墙钟,但仍关连接
* TC-TIMESYNC-024 对端关闭(n==0)-> 立即退出、不解析
* TC-TIMESYNC-025 真错误(n==-1)-> 立即退出、不解析
* TC-TIMESYNC-026 只收到 4 字节后 EOF -> 不足 8 字节不解析
* TC-TIMESYNC-027 30s 预算耗尽 -> 留痕 "no TIME packet in 30s, drop conn"
* TC-TIMESYNC-028 剩余预算不足一片 -> 末片按剩余(20ms)切分
* TC-TIMESYNC-029 32 位 tick 回绕 -> 预算计算仍正确
* —— time_sync_task(任务主循环)——
* TC-TIMESYNC-040 sys_evtHandle==NULL:跳过就绪屏障;accept 轮询->成功->处理->收尾
* TC-TIMESYNC-041 就绪屏障:100ms 分片等待 NET_READY(osFlagsNoClear 不消费就绪位)
* TC-TIMESYNC-042 屏障未就绪期间持续喂心跳(2026-10-02 台架根因的回归防线)
* TC-TIMESYNC-043 网络代次变化 -> 关闭旧监听并重监听
* TC-TIMESYNC-044 监听建立失败 -> 5 次重试后放弃并退避 2000ms
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-10
*/
#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
/* ===================== 平台桩(先纳入,覆盖真实头) ===================== */
#include "FreeRTOS.h"
#include "task.h"
#include "net_socket.h"
#include "sys_clock.h"
#include "sys_events.h"
#include "task_watchdog.h" /* 真实头(App/util):task_wd_id_t / TASK_WD_TIMESYNC */
/* ===================== FreeRTOS 桩 ===================== */
static TickType_t s_tick;
#define DELAY_LOG_MAX 800
static uint32_t s_delays[DELAY_LOG_MAX];
static int s_delay_calls;
void vTaskDelay(TickType_t xTicksToDelay) {
if (s_delay_calls < DELAY_LOG_MAX) {
s_delays[s_delay_calls] = (uint32_t)xTicksToDelay;
}
s_delay_calls++;
}
TickType_t xTaskGetTickCount(void) {
return s_tick;
}
/* ===================== CMSIS-RTOS 事件标志桩 ===================== */
osEventFlagsId_t sys_evtHandle = NULL;
static uint32_t s_evt_seq[8];
static int s_evt_seq_n;
static uint32_t s_evt_default = 0xFFFFFFFFu; /* 未脚本化时的返回值(默认:就绪) */
static int s_evt_calls;
static int s_evt_longjmp_at = -1; /* >0 时第 N 次调用触发 longjmp */
static uint32_t s_evt_last_flags;
static uint32_t s_evt_last_options;
static uint32_t s_evt_last_timeout;
static jmp_buf s_jmp;
uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout) {
int i = s_evt_calls;
(void)ef_id;
s_evt_last_flags = flags;
s_evt_last_options = options;
s_evt_last_timeout = timeout;
s_evt_calls++;
if (s_evt_longjmp_at > 0 && (i + 1) >= s_evt_longjmp_at) {
longjmp(s_jmp, 2); /* 跳出 time_sync_task 的屏障循环(用例收尾) */
}
if (i < s_evt_seq_n) {
return s_evt_seq[i];
}
return s_evt_default;
}
/* ===================== 看门狗桩 ===================== */
static int s_beat_calls;
static int s_beat_id_bad;
/* ⚠ 命名必须与 net_socket 桩的 net_bind 计数区分开:C 的"暂定定义"规则下,
* 同一编译单元里两个 `static int x;` 会被**静默合并为同一个对象**(不报错),
* 于是 task_wd_bind 与 net_bind 会共用计数器,断言全部失真(2026-10-10 踩过)。 */
static int s_wd_bind_calls;
static int s_wd_bind_id_bad;
void task_wd_beat(task_wd_id_t id) {
s_beat_calls++;
if (id != TASK_WD_TIMESYNC) {
s_beat_id_bad++;
}
}
void task_wd_bind(task_wd_id_t id) {
s_wd_bind_calls++;
if (id != TASK_WD_TIMESYNC) {
s_wd_bind_id_bad++;
}
}
/* ===================== sys_clock 桩 ===================== */
static int s_set_unix_calls;
static uint32_t s_set_unix_last;
int sys_clock_set_unix(uint32_t unix_secs) {
s_set_unix_calls++;
s_set_unix_last = unix_secs;
return 0;
}
/* ===================== net_socket 桩 ===================== */
static uint16_t s_htons_last;
static int s_socket_calls;
static int s_socket_seq[8];
static int s_socket_seq_n;
static int s_socket_default = 5;
static int s_bind_calls;
static int s_bind_seq[8];
static int s_bind_seq_n;
static uint16_t s_bind_port;
static uint16_t s_bind_family;
static int s_listen_calls;
static int s_listen_seq[8];
static int s_listen_seq_n;
static int s_accept_calls;
static int s_accept_seq[16];
static int s_accept_seq_n;
static int s_close_calls;
static int s_close_socks[32];
static uint32_t s_gen_seq[8];
static int s_gen_seq_n;
static uint32_t s_gen_default = 1u;
static int s_gen_calls;
static int s_gen_longjmp_at = -1; /* >0 时第 N 次调用触发 longjmp */
uint16_t net_htons(uint16_t hostshort) {
s_htons_last = hostshort;
return (uint16_t)(((hostshort & 0xFFu) << 8) | (hostshort >> 8));
}
int net_socket(int domain, int type) {
int i = s_socket_calls++;
(void)domain;
(void)type;
return (i < s_socket_seq_n) ? s_socket_seq[i] : s_socket_default;
}
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) {
const struct net_sockaddr_in *a = (const struct net_sockaddr_in *)addr;
int i = s_bind_calls++;
(void)sockfd;
(void)addrlen;
s_bind_family = a->sin_family;
s_bind_port = a->sin_port;
return (i < s_bind_seq_n) ? s_bind_seq[i] : 0;
}
int net_listen(int sockfd) {
int i = s_listen_calls++;
(void)sockfd;
return (i < s_listen_seq_n) ? s_listen_seq[i] : 0;
}
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) {
int i = s_accept_calls++;
(void)sockfd;
(void)addr;
(void)addrlen;
return (i < s_accept_seq_n) ? s_accept_seq[i] : -1;
}
int net_close(int sockfd) {
if (s_close_calls < 32) {
s_close_socks[s_close_calls] = sockfd;
}
s_close_calls++;
return 0;
}
/* ---- net_drop_conn(E-02 收口):计数 + 记录 fd ---- */
static int s_drop_calls;
static int s_drop_socks[32];
int net_drop_conn(int sockfd) {
if (s_drop_calls < 32) {
s_drop_socks[s_drop_calls] = sockfd;
}
s_drop_calls++;
return 0;
}
uint32_t net_get_generation(void) {
int i = s_gen_calls++;
if (s_gen_longjmp_at > 0 && (i + 1) >= s_gen_longjmp_at) {
longjmp(s_jmp, 1); /* 跳出 time_sync_task 的主循环(用例收尾) */
}
return (i < s_gen_seq_n) ? s_gen_seq[i] : s_gen_default;
}
/* ---- net_recv_to:脚本化返回码 + 可推进 tick ---- */
typedef struct {
int n; /* 返回码:>0 字节数 / 0 EOF / -1 错误 / -2 超时无数据 */
uint8_t data[16]; /* 要"送达"的字节 */
int len; /* data 有效长度 */
uint32_t tick_adv; /* 返回前把假时钟推进多少 ms */
} recv_step_t;
static recv_step_t s_recv_seq[16];
static int s_recv_seq_n;
static int s_recv_idx;
static int s_recv_calls;
static int s_recv_default = 0; /* 脚本用尽后的返回码(0=EOF,保证循环终止) */
static uint32_t s_recv_to_log[64];
static int s_recv_len_log[64];
int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) {
int i = s_recv_calls;
(void)sockfd;
if (i < 64) {
s_recv_to_log[i] = timeout_ms;
s_recv_len_log[i] = len;
}
s_recv_calls++;
if (s_recv_idx >= s_recv_seq_n) {
return s_recv_default;
}
{
const recv_step_t *st = &s_recv_seq[s_recv_idx];
int copy;
s_recv_idx++;
s_tick += st->tick_adv;
if (st->n <= 0) {
return st->n;
}
copy = (st->n <= len) ? st->n : len;
if (copy > st->len) {
copy = st->len;
}
if ((copy > 0) && (buf != NULL)) {
memcpy(buf, st->data, (size_t)copy);
}
return st->n;
}
}
/* ===================== 断言 ===================== */
static int s_total = 0, s_passed = 0, s_failed = 0;
int g_tstest_err_count = 0;
int g_tstest_info_count = 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)
/* ===================== 被测代码(原样纳入生产源) ===================== */
#include "time_sync.c"
/* ===================== 工具 ===================== */
/*
* 函数功能:把全部桩复位到"健康"初值
* 入口参数:无
* 返 回 值:无
* 限定条件:无
* 函数说明:默认 = 网络全成功、gen 恒为 1、recv 脚本为空(脚本用尽返回 EOF)、
* 事件标志恒就绪。各用例按需覆盖。
*/
static void fakes_reset(void) {
s_tick = 0;
s_delay_calls = 0;
sys_evtHandle = NULL;
s_evt_seq_n = 0;
s_evt_default = SYS_EVT_NET_READY; /* 健康默认:就绪 */
s_evt_calls = 0;
s_evt_longjmp_at = -1;
s_evt_last_flags = 0;
s_evt_last_options = 0;
s_evt_last_timeout = 0;
s_beat_calls = 0;
s_beat_id_bad = 0;
s_wd_bind_calls = 0;
s_wd_bind_id_bad = 0;
s_set_unix_calls = 0;
s_set_unix_last = 0;
s_htons_last = 0;
s_socket_calls = 0;
s_socket_seq_n = 0;
s_socket_default = 5;
s_bind_calls = 0;
s_bind_seq_n = 0;
s_bind_port = 0;
s_bind_family = 0;
s_listen_calls = 0;
s_listen_seq_n = 0;
s_accept_calls = 0;
s_accept_seq_n = 0;
s_close_calls = 0;
s_drop_calls = 0;
s_gen_seq_n = 0;
s_gen_default = 1u;
s_gen_calls = 0;
s_gen_longjmp_at = -1;
s_recv_seq_n = 0;
s_recv_idx = 0;
s_recv_calls = 0;
s_recv_default = 0;
g_tstest_err_count = 0;
g_tstest_info_count = 0;
}
/* 装配一个 recv 脚本步 */
static void recv_set(int idx, int n, const uint8_t *data, int len, uint32_t tick_adv) {
memset(&s_recv_seq[idx], 0, sizeof(s_recv_seq[idx]));
s_recv_seq[idx].n = n;
s_recv_seq[idx].len = len;
s_recv_seq[idx].tick_adv = tick_adv;
if ((data != NULL) && (len > 0)) {
int copy = (len > 16) ? 16 : len;
memcpy(s_recv_seq[idx].data, data, (size_t)copy);
}
}
/* 装配一个 "TIME" + uint32 大端 时间包 */
static void make_time_packet(uint8_t *out, uint32_t epoch) {
out[0] = (uint8_t)'T';
out[1] = (uint8_t)'I';
out[2] = (uint8_t)'M';
out[3] = (uint8_t)'E';
out[4] = (uint8_t)(epoch >> 24);
out[5] = (uint8_t)(epoch >> 16);
out[6] = (uint8_t)(epoch >> 8);
out[7] = (uint8_t)(epoch);
}
/* 检查 s_delays[0..n) 是否全部等于 v */
static int delays_all(uint32_t v, int n) {
int k;
for (k = 0; k < n; k++) {
if (s_delays[k] != v) {
return 0;
}
}
return 1;
}
/* ===================== TC-TIMESYNC-001..006:delay_fed ===================== */
static void tc_delay_fed(void) {
fakes_reset();
time_sync_delay_fed(0U);
CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "TC-TIMESYNC-001 delay_fed(0) neither sleeps nor beats");
fakes_reset();
time_sync_delay_fed(50U);
CHECK((s_beat_calls == 1) && (s_delay_calls == 1) && (s_delays[0] == 50U),
"TC-TIMESYNC-002 delay_fed(50) = one 50 ms slice + 1 beat");
fakes_reset();
time_sync_delay_fed(120U);
CHECK((s_beat_calls == 3) && (s_delay_calls == 3), "TC-TIMESYNC-003 delay_fed(120) splits into 3 slices");
CHECK((s_delays[0] == 50U) && (s_delays[1] == 50U) && (s_delays[2] == 20U),
"TC-TIMESYNC-003b slice lengths 50/50/20 (last slice is the remainder)");
fakes_reset();
time_sync_delay_fed(2000U);
CHECK((s_delay_calls == 40) && (s_beat_calls == 40) && delays_all(50U, 40),
"TC-TIMESYNC-004 delay_fed(2000) = 40 slices x 50 ms, one beat per slice");
fakes_reset();
time_sync_delay_fed(49U);
CHECK((s_beat_calls == 1) && (s_delays[0] == 49U), "TC-TIMESYNC-005 less than one slice (49 ms) handled as a single slice");
fakes_reset();
time_sync_delay_fed(30000U);
CHECK(s_delay_calls == 600, "TC-TIMESYNC-006 total budget 30 s = 600 slices (slice cap is always 50 ms)");
}
/* ===================== TC-TIMESYNC-010..015:open_listener ===================== */
static void tc_open_listener(void) {
fakes_reset();
{
int s = time_sync_open_listener();
CHECK(s == 5, "TC-TIMESYNC-010 open_listener success returns the socket descriptor");
CHECK((s_socket_calls == 1) && (s_bind_calls == 1) && (s_listen_calls == 1),
"TC-TIMESYNC-010b socket / bind / listen called once each");
CHECK(s_htons_last == (uint16_t)TIME_SYNC_PORT, "TC-TIMESYNC-010c port goes through net_htons(8888)");
CHECK(s_bind_port == net_htons((uint16_t)TIME_SYNC_PORT), "TC-TIMESYNC-010d sin_port is in network byte order");
CHECK(s_bind_family == (uint16_t)NET_AF_INET, "TC-TIMESYNC-010e sin_family = NET_AF_INET");
CHECK(s_delay_calls == 0, "TC-TIMESYNC-010f no backoff delay on the success path");
}
fakes_reset();
s_bind_seq[0] = -1;
s_bind_seq_n = 1;
{
int s = time_sync_open_listener();
CHECK(s == 5, "TC-TIMESYNC-011 bind succeeds on retry after one failure");
CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-011b bind failure closes that socket");
CHECK(s_delay_calls == 40, "TC-TIMESYNC-011c bind failure backs off 2000 ms (40 slices)");
CHECK(g_tstest_err_count == 1, "TC-TIMESYNC-011d bind failure leaves a DBG_ERROR trace");
}
fakes_reset();
s_listen_seq[0] = -1;
s_listen_seq_n = 1;
{
int s = time_sync_open_listener();
CHECK(s == 5, "TC-TIMESYNC-012 listen succeeds on retry after one failure");
CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-012b listen failure closes that socket");
CHECK(s_delay_calls == 40, "TC-TIMESYNC-012c listen failure backs off 2000 ms");
}
fakes_reset();
s_socket_default = -1;
{
int s = time_sync_open_listener();
CHECK(s == -1, "TC-TIMESYNC-013 socket keeps failing -> returns -1");
CHECK(s_socket_calls == 5, "TC-TIMESYNC-013b retry cap is 5 attempts");
CHECK(g_tstest_err_count == 5, "TC-TIMESYNC-013c every failure leaves its own trace (5 DBG_ERROR lines)");
CHECK(s_delay_calls == 50, "TC-TIMESYNC-013d every failure backs off 500 ms (5 x 10 slices)");
}
fakes_reset();
s_socket_seq[0] = -1;
s_socket_seq[1] = -1;
s_socket_seq[2] = 9;
s_socket_seq_n = 3;
{
int s = time_sync_open_listener();
CHECK(s == 9, "TC-TIMESYNC-014 3rd retry succeeds and returns the new descriptor");
CHECK(s_socket_calls == 3, "TC-TIMESYNC-014b exactly 3 attempts");
CHECK(s_delay_calls == 20, "TC-TIMESYNC-014c two failures back off 500 ms x 2");
}
fakes_reset();
s_socket_default = -1;
(void)time_sync_open_listener();
CHECK(s_beat_calls == 55, "TC-TIMESYNC-015 every retry round (5) + every delay slice (50) beats once");
CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-015b the beat bit is always TASK_WD_TIMESYNC");
}
/* ===================== TC-TIMESYNC-020..029:handle_conn =====================
* ⚠ 契约变更:handle_conn **不再** net_close
* (单连接模式下 conn==listen,close 会连监听一起毁);会话收尾只返回外层,
* 由 time_sync_task 在 `conn != ls`(模拟器多连接后端)时才关数据 fd。
* 故本组断言一律为 s_close_calls == 0;任务级关闭见 TC-TIMESYNC-040e。 */
static void tc_handle_conn(void) {
uint8_t pkt[16];
/* 1) 8 字节一次收齐 */
fakes_reset();
make_time_packet(pkt, 0x12345678u);
recv_set(0, 8, pkt, 8, 0U);
s_recv_seq_n = 1;
s_tick = 100U;
time_sync_handle_conn(7);
CHECK(s_set_unix_calls == 1, "TC-TIMESYNC-020 TIME packet in one shot -> wall clock updated once");
CHECK(s_set_unix_last == 0x12345678u, "TC-TIMESYNC-020b epoch parsed big-endian");
CHECK((s_recv_calls == 1) && (s_recv_len_log[0] == 16), "TC-TIMESYNC-020c the first request asks for 16 bytes");
CHECK(s_recv_to_log[0] == 50U, "TC-TIMESYNC-020d single wait slice length = 50 ms");
CHECK(s_close_calls == 0, "TC-TIMESYNC-020e after handling, the connection is **not** closed (close is the outer conn!=ls branch job)");
CHECK(s_drop_calls == 0, "TC-TIMESYNC-020g a successful session is not torn down (drop is only for give-up paths)");
CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-020f leaves the wall clock updated trace");
/* 2) 分段送达:4 + 4 */
fakes_reset();
make_time_packet(pkt, 10u);
recv_set(0, 4, pkt, 4, 10U);
recv_set(1, 4, pkt + 4, 4, 10U);
s_recv_seq_n = 2;
time_sync_handle_conn(3);
CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 10u), "TC-TIMESYNC-021 pieces accumulate and then parse correctly");
CHECK(s_recv_calls == 2, "TC-TIMESYNC-021b read twice");
CHECK((s_recv_len_log[0] == 16) && (s_recv_len_log[1] == 12),
"TC-TIMESYNC-021c the second request asks only for the remaining 12 bytes");
CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), "TC-TIMESYNC-021d 50 ms per slice (budget is ample)");
/* 3) 16 字节一次到达(只取前 8) */
fakes_reset();
make_time_packet(pkt, 5u);
recv_set(0, 16, pkt, 16, 0U);
s_recv_seq_n = 1;
time_sync_handle_conn(4);
CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 5u), "TC-TIMESYNC-022 an over-long packet only takes the first 8 bytes");
CHECK(s_recv_calls == 1, "TC-TIMESYNC-022b stops as soon as one read is enough");
/* 4) 前缀不是 TIME */
fakes_reset();
pkt[0] = (uint8_t)'X';
pkt[1] = (uint8_t)'X';
pkt[2] = (uint8_t)'X';
pkt[3] = (uint8_t)'X';
pkt[4] = 0U;
pkt[5] = 0U;
pkt[6] = 0U;
pkt[7] = 1U;
recv_set(0, 8, pkt, 8, 0U);
s_recv_seq_n = 1;
time_sync_handle_conn(4);
CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-023 a non-TIME prefix does not update the wall clock");
CHECK(s_close_calls == 0, "TC-TIMESYNC-023b a non-TIME prefix also does not close the connection");
/* 5) 对端关闭(EOF) */
fakes_reset();
recv_set(0, 0, NULL, 0, 0U);
s_recv_seq_n = 1;
time_sync_handle_conn(6);
CHECK((s_set_unix_calls == 0) && (s_recv_calls == 1), "TC-TIMESYNC-024 peer close (EOF) exits immediately without parsing");
CHECK(s_close_calls == 0, "TC-TIMESYNC-024b EOF also does not close the connection (net-layer auto_relisten re-arms the listener)");
/* 6) 真错误 */
fakes_reset();
recv_set(0, -1, NULL, 0, 0U);
s_recv_seq_n = 1;
time_sync_handle_conn(6);
CHECK((s_recv_calls == 1) && (s_set_unix_calls == 0), "TC-TIMESYNC-025 a real error exits immediately without parsing");
/* 7) 4 字节后 EOF */
fakes_reset();
make_time_packet(pkt, 7u);
recv_set(0, 4, pkt, 4, 0U);
recv_set(1, 0, NULL, 0, 0U);
s_recv_seq_n = 2;
time_sync_handle_conn(6);
CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-026 fewer than 8 bytes are not parsed");
CHECK(s_recv_calls == 2, "TC-TIMESYNC-026b keeps waiting for the remaining bytes");
/* 8) 30s 预算耗尽 */
fakes_reset();
recv_set(0, -2, NULL, 0, 30000U);
s_recv_seq_n = 1;
time_sync_handle_conn(6);
CHECK(s_recv_calls == 1, "TC-TIMESYNC-027 no more reads once the budget is exhausted (only 1 read)");
CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-027b leaves the no TIME packet in 30s, drop conn trace");
CHECK(s_close_calls == 0, "TC-TIMESYNC-027c budget exhausted still does not close the connection");
CHECK(s_drop_calls == 1, "TC-TIMESYNC-027d budget exhausted drops the session on purpose (E-02 wrap-up: net_drop_conn kills the connection, keeps the listener)");
/* 9) 剩余预算不足一片 */
fakes_reset();
recv_set(0, -2, NULL, 0, 29980U);
s_recv_seq_n = 1;
time_sync_handle_conn(6);
CHECK(s_recv_calls == 2, "TC-TIMESYNC-028 ends after two waits");
CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 20U),
"TC-TIMESYNC-028b second slice length = the remaining 20 ms");
/* 10) 32 位 tick 回绕 */
fakes_reset();
s_tick = 0xFFFFFF00U;
recv_set(0, -2, NULL, 0, 512U); /* 推进后回绕到 0x00000100 */
s_recv_seq_n = 1;
time_sync_handle_conn(6);
CHECK((s_recv_calls == 2) && (s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U),
"TC-TIMESYNC-029 after a 32-bit tick wraparound the budget math is still correct");
}
/* ===================== TC-TIMESYNC-040..044:time_sync_task ===================== */
static void tc_task(void) {
uint8_t pkt[16];
/* A) 无就绪屏障(sys_evtHandle == NULL):accept 轮询 2 次失败后成功 */
fakes_reset();
make_time_packet(pkt, 9u);
recv_set(0, 8, pkt, 8, 0U);
s_recv_seq_n = 1;
s_accept_seq[0] = -1;
s_accept_seq[1] = -1;
s_accept_seq[2] = 7;
s_accept_seq_n = 3;
/* gen 调用序:iter1 顶部(1) + accept 两次失败各一次(2,3) + iter2 顶部(4) → 第 4 次跳出 */
s_gen_longjmp_at = 4;
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK((s_wd_bind_calls == 1) && (s_wd_bind_id_bad == 0),
"TC-TIMESYNC-040 task entry registers the watchdog beat bit (task_wd_bind once)");
CHECK(s_evt_calls == 0, "TC-TIMESYNC-040b skips the readiness barrier when sys_evtHandle==NULL");
CHECK(s_accept_calls == 3, "TC-TIMESYNC-040c accept polled 3 times (2 with no connection + 1 success)");
CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 9u), "TC-TIMESYNC-040d the time packet arrived and the wall clock was updated");
CHECK(s_close_calls == 1, "TC-TIMESYNC-040e the connection is closed once");
CHECK(s_delay_calls == 9, "TC-TIMESYNC-040f delay slice count = 4+4+1 (200+200+50 ms)");
CHECK(delays_all(50U, 9), "TC-TIMESYNC-040g every delay is sliced at 50 ms");
CHECK(s_beat_calls == 13, "TC-TIMESYNC-040h beat count = main loop 1 + backoff 8 + accepted connection 1 + wrap-up 1 + next round 1");
CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-040i every beat sets the TIMESYNC bit");
/* B) 就绪屏障:2 次未就绪 + 1 次就绪 */
fakes_reset();
sys_evtHandle = (osEventFlagsId_t)0x55;
s_evt_seq[0] = osFlagsErrorTimeout;
s_evt_seq[1] = osFlagsErrorTimeout;
s_evt_seq[2] = SYS_EVT_NET_READY;
s_evt_seq_n = 3;
s_gen_longjmp_at = 1;
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK(s_evt_calls == 3, "TC-TIMESYNC-041 readiness barrier polled 3 times (2 not ready + 1 ready)");
CHECK(s_evt_last_timeout == 100U, "TC-TIMESYNC-041b barrier waits are sliced at 100 ms");
CHECK(s_evt_last_flags == SYS_EVT_NET_READY, "TC-TIMESYNC-041c waits for the NET_READY bit");
CHECK(s_evt_last_options == (osFlagsWaitAny | osFlagsNoClear), "TC-TIMESYNC-041d the ready bit is not consumed");
CHECK(s_beat_calls == 3, "TC-TIMESYNC-041e the two not-ready rounds each beat once (+ 1 in the main loop)");
CHECK(s_wd_bind_calls == 1, "TC-TIMESYNC-041f still registers the beat routing first");
/* C) 屏障期间持续喂心跳(就绪位长期不置位) */
fakes_reset();
sys_evtHandle = (osEventFlagsId_t)0x55;
s_evt_default = osFlagsErrorTimeout;
s_evt_longjmp_at = 4;
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK(s_evt_calls == 4, "TC-TIMESYNC-042 barrier waited 4 times, none ready (loop did not hang)");
CHECK(s_beat_calls == 3, "TC-TIMESYNC-042b beats once after each not-ready round (before the 4th breaks out)");
/* D) 网络代次变化 -> 关闭旧监听并重监听 */
fakes_reset();
s_gen_seq[0] = 1u;
s_gen_seq[1] = 2u;
s_gen_seq_n = 2;
s_gen_default = 2u;
s_gen_longjmp_at = 4;
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-043 closes the old listener when the network generation changes");
/* INFO 三条:两次 open_listener 成功各一条 "TCP listen on" + 一条 "network re-init, relisten" */
CHECK(g_tstest_info_count == 3, "TC-TIMESYNC-043b leaves the network re-init, relisten trace (plus 2 listen-success lines)");
CHECK(s_accept_calls == 2, "TC-TIMESYNC-043c polls accept only once per loop then gives up");
CHECK((s_socket_calls == 2) && (s_bind_calls == 2) && (s_listen_calls == 2),
"TC-TIMESYNC-043d the second loop rebuilt the listener");
/* E) 监听建立失败 -> 5 次重试后放弃并退避 2000ms */
fakes_reset();
s_socket_default = -1;
s_gen_longjmp_at = 2;
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK(s_socket_calls == 5, "TC-TIMESYNC-044 open_listener gives up after 5 internal retries");
CHECK(g_tstest_err_count == 6, "TC-TIMESYNC-044b 5 net_socket FAIL lines + 1 retry later line");
CHECK(s_delay_calls == 90, "TC-TIMESYNC-044c backoff 5x500 ms + 2000 ms = 90 slices");
CHECK(s_beat_calls == 97, "TC-TIMESYNC-044d beat count = 2 + 5 + 50 + 40");
CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-044e every beat sets the TIMESYNC bit");
}
/* ===================== main ===================== */
int main(void) {
printf("=== Time Sync Tests ===\n");
printf("-- delay_fed (heartbeat-fed slicing) --\n");
tc_delay_fed();
printf("-- open_listener (retry / backoff) --\n");
tc_open_listener();
printf("-- handle_conn (packet reassembly / budget) --\n");
tc_handle_conn();
printf("-- time_sync_task (barrier / accept / relisten) --\n");
tc_task();
printf("\n=== Time Sync Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}