Files
STM32F4-Base/test/host/time_sync/time_sync_test.c
T
wandering1 ad87f979b1 docs(规约): 删除编号规范.md,生效规则并入文档规范 §7.1
用户裁定:docs/编号规范.md 在项目里已无意义,删除。

依据:该文 253 行里真正仍生效的只有两条规则(新条目一律不发编号、
人读文档存量编号继续清退)和一张存量族表;其余内容早有归属——两道
编号门禁的退役记录在 docs/门禁说明/id-check.md 与 id-check-new.md,
手工普查与恢复机器面在 scripts/archive/id_gate/README.md。留着就是
第二真相源,同一事实写在两处迟早漂移。

处理:
- docs/文档规范.md 新增 §7.1「条目与编号:一律不发」,承接规则、
  场景表、三条硬规矩、存量族表、已停发旧号族清单、不是编号的清单、
  清理动作与「没有机器门禁兜底」四段。
- 活跃引用 62 处逐条改指 §7.1 或归档 README:AGENTS.md 2 处、
  门禁说明三篇 18 处、其余文档 11 篇、代码与脚本 10 处、rename_maps
  七个 record 字段、.dsh/skills 四个文件 8 处。
- 归档区 107 处去掉 docs/ 前缀当纯文本提及;编号治理历史.md 45 处
  改为「原编号规范 §N」,避免被 doc-links 当自引用解析。
- 顺带修正 scripts/archive/id_gate/README.md 里仍称旧文为现行规范
  的表述。

门禁:check_doc_links --all --sections-strict 220 篇全通过、无法判定 0;
pre-commit 全 Passed。
2026-11-06 18:32:04 +08:00

355 lines
14 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(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,判据见 docs/文档规范.md 7.1 节):新用例一律**语义行名**,
* 不再配 TC 号;旧 TC-TIMESYNC-* 号随用例退役永久作废、不得复用。
*
* 通过:全部断言 PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-10(2026-11-04 随 :8888 退役收缩重写)
*/
#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
/* ===================== 平台桩(先纳入,覆盖真实头) ===================== */
#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;
}