Files
wandering1 2e608118ce feat(对时): 8888 对时补一条操作记录留痕——与 104 对时对称,被拒不落痕
task-49 盘点盘出的唯一供给缺口,task-50 实现(道H 交付)。为什么这算缺陷而不是「维护工装不值得留痕」:
对时改变的是**其后每一条记录**的时标,而 104 侧的对时**已经留痕** —— 同一类行为两种结果,这个不对称
本身就是缺陷。FTP 上传不留痕则维持原状(那是传输动作,与「装置时钟被改了」不同类),本笔
刻意不扩面到它。

实现:`time_sync_handle_conn` 里 `sys_clock_set_unix` 的返回值原先是**裸丢弃**,现改为接住判
0,==0 才
投 `SYSLOG_OPER`(走 quiet 口,netTask 侧纪律,栈成本已在注释写明);**被拒零投**(不把失败记成改过时
钟);无合并锁存(连续两次对时留两条,不折叠);**epoch 语义零触碰、零时区补偿** —— 时基口径仍在用
户手里,本笔不替产品决定。

新用例四组十二断言:成功恰一条 / 被拒不落痕 / 连续两次两条 / 魔术字不符皆零。假件把 rc 做成可脚本,
并专门钉「文案出现次数」以证不合并;`sys_log.h` 用真实头(纯 stdint,走 task_watchdog 先例),零桩漂移面。

**停发口径的第一批**:新断言**不配 TC 号**,改用语义化用例名 + 函数名追溯,注释里标了口径出处 —— 裁定
能不能落地看的就是第一批,这条留在这里给后来者当样例。

证据:time_sync 92/92(存量 82 零回归);@build **0 错 0 警**,Code=235268 RO=26308 RW
=1572 ZI=136924
(与上一笔的差值是 728d6067 合账所致,不是本笔);栈**以表为证**:timeSyncTask 链 1272→1272 B 零增
量,own frame 152 不变,handle_conn 200 / post_quiet 40 / post 264 逐函数帧全等 ——
按今天刚定稿的栈判
据,「零增量」这句话必须是表,不能是论证。id-check --strict 0(零新号零裸形)、乱码/嵌套/BOM 净。

台账:`ram_baseline.py --update` 已记当日行(IRAM1 61.9% / CCM 87.9%)。覆盖率基线**留给 L
ead 重建** ——
上一版基线是在 time_sync 套件编译不过的破树下打的,少算了该套件的覆盖。

台账同步:本笔代码改了体积(Code=235268),RAM 预算台账受管区与文档快照区按 12:46 那次权威构建回填(ram_baselin
e --write 加 check_mem_numbers --write)。代码与数字不同笔,就是下一次 mem 钩子假红的来源——上一笔 d
ocs 提交被 mem-numbers 与 mem-budget 双双拦下,正是这个原因。
2026-10-27 13:49:38 +08:00

824 lines
33 KiB
C
Raw Permalink 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. **本模块是探针窗口心跳缺口的当事方**:当时的记录写着"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
* 对时操作留痕(道G task-50,语义行名不配号——编号规范已停发测试用例号):
* "sync trace on success" / "… rejected" / "… two in a row" / "… bad magic" 四组
*
* 通过:全部 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 */
#include "sys_log.h" /* 真实头(App/log,纯 stdint 无 OS 依赖):syslog_id_t / SYSLOG_OPER 契约,假件签名与之逐字对齐 */
/* ===================== 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;
static int s_set_unix_rc; /* 可脚本化返回值:非 0 = 对时被拒(task-50) */
int sys_clock_set_unix(uint32_t unix_secs) {
s_set_unix_calls++;
s_set_unix_last = unix_secs;
return s_set_unix_rc;
}
/* ===================== sys_log 投递桩(task-50) =====================
* 假件按投递顺序记录:条数 / 日志 ID / 末条描述 / 对时文案出现次数。
* "出现次数"专为钉"连续两次不合并"——若生产侧哪天改成边沿锁存,
* 第三条用例会红,而那正是要拦的语义漂移。 */
static int s_post_calls;
static int s_post_id;
static char s_post_last_desc[32];
static int s_post_desc_count;
void syslog_post_quiet(syslog_id_t id, const char *desc) {
s_post_calls++;
s_post_id = (int)id;
if (desc != NULL) {
(void)strncpy(s_post_last_desc, desc, sizeof(s_post_last_desc) - 1u);
s_post_last_desc[sizeof(s_post_last_desc) - 1u] = '\0';
if (strcmp(desc, "clock set by 8888") == 0) {
s_post_desc_count++;
}
}
}
/* ===================== 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(头阻塞收口):计数 + 记录 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_set_unix_rc = 0;
s_post_calls = 0;
s_post_id = -1;
s_post_last_desc[0] = '\0';
s_post_desc_count = 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 (head-of-line 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 cycle 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");
}
/* ===================== 对时操作留痕四组(道G task-50) =========================
* 成功应用投一条 quiet OPER;被拒不投(失败不得记成"改过时钟");
* 连续两次各一条不合并;魔术字不符不投。
* ⚠ 断言标签用语义行名:编号规范 §9 批次三已对测试用例号停发(用户裁定 2026-10-27),
* 本组是新口径下写的——追溯靠函数名 tc_oper_trace + 下面的语义标签。 */
static void tc_oper_trace(void) {
uint8_t pkt[16];
/* 1) 对时成功 → 恰一条 OPER */
fakes_reset();
make_time_packet(pkt, 0x20251027u);
recv_set(0, 8, pkt, 8, 0U);
s_recv_seq_n = 1;
s_set_unix_rc = 0;
time_sync_handle_conn(7);
CHECK(s_set_unix_calls == 1, "sync trace: clock layer applied once");
CHECK(s_post_calls == 1, "sync trace on success: exactly one operation record");
CHECK(s_post_id == (int)SYSLOG_OPER, "sync trace: record goes to the operation log");
CHECK(strcmp(s_post_last_desc, "clock set by 8888") == 0,
"sync trace: wording of the same family as the 104 time-sync record");
/* 2) 对时被拒(时钟层返非 0)→ 零留痕 */
fakes_reset();
make_time_packet(pkt, 7u);
recv_set(0, 8, pkt, 8, 0U);
s_recv_seq_n = 1;
s_set_unix_rc = -1;
time_sync_handle_conn(7);
CHECK(s_set_unix_calls == 1, "sync trace rejected: call still reaches the clock layer once");
CHECK(s_post_calls == 0, "sync trace rejected: NO operation record written");
/* 3) 连续两次成功会话 → 两条独立记录,不合并 */
fakes_reset();
s_set_unix_rc = 0;
make_time_packet(pkt, 100u);
recv_set(0, 8, pkt, 8, 0U);
recv_set(1, 8, pkt, 8, 0U);
s_recv_seq_n = 2;
time_sync_handle_conn(7);
time_sync_handle_conn(7);
CHECK(s_set_unix_calls == 2, "sync trace twice: two applied syncs");
CHECK(s_post_calls == 2, "sync trace twice: two independent records, not merged");
CHECK(s_post_desc_count == 2, "sync trace twice: both records are the time-sync wording (no coalescing latch)");
/* 4) 前缀不是 TIME → 时钟层都没到,更不该留痕 */
fakes_reset();
(void)memset(pkt, 'X', 8);
recv_set(0, 8, pkt, 8, 0U);
s_recv_seq_n = 1;
s_set_unix_rc = 0;
time_sync_handle_conn(7);
CHECK((s_set_unix_calls == 0) && (s_post_calls == 0),
"sync trace bad magic: neither moves the clock nor writes a record");
}
/* ===================== 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("-- operation trace on sync (task-50) --\n");
tc_oper_trace();
printf("\n=== Time Sync Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}