/* * 模块名称:Time Sync(timeSyncTask 节拍宿主)主机侧单元测试 * 模块功能:在 PC 上对 App/sync/time_sync.c 做离线验证(2 个函数全覆盖: * time_sync_delay_fed / time_sync_task)。 * * 被测对象:真实源文件 ../../../App/sync/time_sync.c(以 #include 原样纳入, * 保证测的是生产代码而非副本)。平台依赖由本文件提供的**可注入桩**实现: * - FreeRTOS/task.h -> vTaskDelay / xTaskGetTickCount(可控时钟) * - net_socket.h -> net_get_generation(脚本化 + 可跳出主循环) * - sys_events.h -> osEventFlagsWait(可注入"未就绪") * - task_watchdog.h -> task_wd_beat / task_wd_bind(记录心跳节奏) * - time_sntp.h -> time_sntp_run(替身:记录轮次与收到的代次) * * 版本史:v1 时代本套件还测 :8888 监听重试与 "TIME" 包重组状态机(80 断言点)。 * 2026-11-04 :8888 对时退役(PR #77 后用户裁定,理由见墙钟维护说明 §15), * 监听/协议/OPER 留痕三组用例随生产代码一并退场——历史覆盖见 git 历史 * (test/host/time_sync/ 2026-10-10 版)。留下并扩强的部分是**这个任务 * 现在的全部生产逻辑**:心跳分片节奏(探针窗口心跳缺口的当事方至今 * 仍是本模块——第 8b 轮消抖、2026-10-02 屏障喂心跳两条台架教训都钉在 * 这里)与"循环按代次驱动 SNTP"。 * * 断言口径(用户裁定 2026-10-27,编号规范 §9):新用例一律**语义行名**, * 不再配 TC 号;旧 TC-TIMESYNC-* 号随用例退役永久作废、不得复用。 * * 通过:全部断言 PASS(main 返回 0)。 * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 * * 创建日期:2026-10-10(2026-11-04 随 :8888 退役收缩重写) */ #include #include #include #include /* ===================== 平台桩(先纳入,覆盖真实头) ===================== */ #include "FreeRTOS.h" #include "task.h" #include "net_socket.h" #include "sys_events.h" #include "task_watchdog.h" /* 真实头(App/util):task_wd_id_t / TASK_WD_TIMESYNC */ #include "time_sntp.h" /* 真实头(App/sync):time_sntp_run 签名原汁,替身必须对表 */ /* ===================== FreeRTOS 桩 ===================== */ static TickType_t s_tick; #define DELAY_LOG_MAX 2048 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; } /* ===================== 看门狗桩 ===================== * ⚠ 计数变量命名带 s_wd_ 前缀:C 的"暂定定义"规则下,同一编译单元里 * 两个同名 `static int x;` 会被静默合并(不报错),历史版本里 task_wd_bind * 与 net_bind 的计数器撞过一次名导致断言全部失真(2026-10-10 踩过)。 */ static int s_beat_calls; static int s_beat_id_bad; 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++; } } /* ===================== net_socket 桩 ===================== * :8888 退役后本任务只剩 net_get_generation 一处网络调用。桩头刻意**不声明** * socket/bind/listen/accept/close/recv——若循环哪天重新伸手碰监听族 API, * 链接期直接爆(编译期绊线,替代退役前的运行时断言)。 */ 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 */ 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; } /* ===================== SNTP 客户机替身 ===================== * 行为断言在 test/host/sntp 独立套件里做;本套件只钉两件事: * ① 每轮循环恰好调一次 time_sntp_run; * ② 收到的代次 = 本轮 net_get_generation() 的返回值(断网重入语义的输入端)。 */ static int s_sntp_run_calls; static uint32_t s_sntp_gen_log[8]; static int s_sntp_gen_log_n; static int s_sntp_run_longjmp_at = -1; /* >0 时第 N 次调用触发 longjmp */ void time_sntp_run(uint32_t net_gen) { int i = s_sntp_run_calls++; if (i < (int)(sizeof(s_sntp_gen_log) / sizeof(s_sntp_gen_log[0]))) { s_sntp_gen_log[i] = net_gen; s_sntp_gen_log_n = i + 1; } if (s_sntp_run_longjmp_at > 0 && (i + 1) >= s_sntp_run_longjmp_at) { longjmp(s_jmp, 3); } } /* ===================== 断言 ===================== */ 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) /* ===================== 被测代码(原样纳入生产源) ===================== */ #include "time_sync.c" /* ===================== 工具 ===================== */ /* * 函数功能:把全部桩复位到"健康"初值 * 入口参数:无 * 返 回 值:无 * 限定条件:无 * 函数说明:默认 = 事件标志健康就绪(屏障一次通过)、gen 恒为 1、sntp 替身直通。 * 各用例按需覆盖。 */ 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_gen_seq_n = 0; s_gen_default = 1u; s_gen_calls = 0; s_gen_longjmp_at = -1; s_sntp_run_calls = 0; s_sntp_gen_log_n = 0; s_sntp_run_longjmp_at = -1; } /* 检查 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; } /* ===================== delay_fed:心跳分片延时 ===================== * 本模块的全部看门狗安全论证都压在这一个函数上:片长恒 ≤50ms、每片前置心跳, * 抽样窗(100ms)内必有心跳——第 8b 轮消抖钉下的节奏,逐片断言。 */ static void tc_delay_fed(void) { fakes_reset(); time_sync_delay_fed(0U); CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "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), "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), "delay_fed(120) splits into 3 slices"); CHECK((s_delays[0] == 50U) && (s_delays[1] == 50U) && (s_delays[2] == 20U), "delay_fed(120) 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), "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), "delay_fed(49) less than one slice handled as a single slice"); fakes_reset(); time_sync_delay_fed(30000U); CHECK(s_delay_calls == 600, "delay_fed(30000) = 600 slices (slice cap is always 50 ms)"); } /* ===================== time_sync_task:屏障 + SNTP 节拍 ===================== */ static void tc_task(void) { /* A) 无就绪屏障(sys_evtHandle == NULL):一轮完整节拍后于第 2 轮顶部脱出 */ fakes_reset(); s_gen_longjmp_at = 2; /* iter1 顶部(1) + iter2 顶部(2) -> 跳出 */ if (setjmp(s_jmp) == 0) { time_sync_task(NULL); } CHECK((s_wd_bind_calls == 1) && (s_wd_bind_id_bad == 0), "task entry registers the watchdog beat bit (task_wd_bind once)"); CHECK(s_evt_calls == 0, "skips the readiness barrier when sys_evtHandle==NULL"); CHECK((s_gen_calls == 2) && (s_sntp_run_calls == 1), "one full tick: generation read once per round, SNTP driven once per round"); CHECK((s_sntp_gen_log_n == 1) && (s_sntp_gen_log[0] == 1u), "time_sntp_run receives this round's generation value"); CHECK(s_delay_calls == 4, "tick wait 200 ms = 4 slices (slice cap 50 ms unchanged)"); CHECK(delays_all(50U, 4), "every tick slice sleeps exactly 50 ms"); CHECK(s_beat_calls == 6, "beat count = loop heads 2 (2nd beat precedes the bail-out gen) + slices 4"); CHECK(s_beat_id_bad == 0, "every beat sets the TIMESYNC bit"); /* B) 代次变化原样下传:1 -> 2(断网重建后 SNTP 内部自弃旧 socket,循环本身 * 不再有任何监听簿记——桩头不声明 socket 族 API,伸手即链接不过) */ 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 = 3; /* 第 3 轮顶部跳出:已跑满 2 个完整节拍 */ if (setjmp(s_jmp) == 0) { time_sync_task(NULL); } CHECK(s_sntp_run_calls == 2, "two full ticks before bail-out"); CHECK((s_sntp_gen_log_n == 2) && (s_sntp_gen_log[0] == 1u) && (s_sntp_gen_log[1] == 2u), "generation change is handed to the SNTP client verbatim, round by round"); CHECK(s_delay_calls == 8, "each round waits its own 200 ms (4+4 slices)"); CHECK(s_beat_calls == 11, "beat count = (loop head + 4 slices) x 2 rounds + loop head of round 3"); /* C) 就绪屏障: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; /* 屏障通过后第 1 轮顶部跳出 */ if (setjmp(s_jmp) == 0) { time_sync_task(NULL); } CHECK(s_evt_calls == 3, "readiness barrier polled 3 times (2 not ready + 1 ready)"); CHECK(s_evt_last_timeout == 100U, "barrier waits are sliced at 100 ms"); CHECK(s_evt_last_flags == SYS_EVT_NET_READY, "waits for the NET_READY bit"); CHECK(s_evt_last_options == (osFlagsWaitAny | osFlagsNoClear), "the ready bit is not consumed (ftpTask and friends may still see it)"); CHECK(s_beat_calls == 3, "two not-ready rounds each beat once + 1 at the loop head"); CHECK(s_sntp_run_calls == 0, "no SNTP round before the barrier passes"); CHECK(s_wd_bind_calls == 1, "still registers the beat routing first"); /* D) 屏障期间持续喂心跳(就绪位长期不置位,2026-10-02 台架根因的回归防线) */ 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, "barrier waited 4 times, none ready (loop did not hang)"); CHECK(s_beat_calls == 3, "beats once after each not-ready round (before the 4th breaks out)"); CHECK((s_gen_calls == 0) && (s_sntp_run_calls == 0), "network untouched until NET_READY: no generation read, no SNTP round"); } /* ===================== SNTP 驻留后的循环延续 ===================== * time_sntp_run 的契约是"单次驻留 ≤50ms,调用方在片间喂心跳"——替身模拟一次 * 驻留后返回,循环必须按原节拍继续(心跳不欠账)。 */ static void tc_sntp_dwell_then_continue(void) { fakes_reset(); s_sntp_run_longjmp_at = 2; /* 第 2 轮驻留中脱出(模拟慢收片场景收尾) */ if (setjmp(s_jmp) == 0) { time_sync_task(NULL); } CHECK(s_sntp_run_calls == 2, "loop re-enters the SNTP client every tick"); CHECK(s_beat_calls == 6, "beats = (head + 4 slices) for round 1 + head of round 2 (dwell bail-out)"); CHECK(s_delay_calls == 4, "round 1 waited its full 200 ms; round 2 bailed mid-tick"); CHECK(s_beat_id_bad == 0, "the beat bit stays TIMESYNC across rounds"); } /* ===================== main ===================== */ int main(void) { printf("=== Time Sync Tests ===\n"); printf("-- delay_fed (heartbeat-fed slicing) --\n"); tc_delay_fed(); printf("-- time_sync_task (barrier / SNTP cadence) --\n"); tc_task(); printf("-- sntp dwell then continue --\n"); tc_sntp_dwell_then_continue(); printf("\n=== Time Sync Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; }