3a2f9b21fb
SNTP 客户端已随 #73/PR #77 上线合入(merge 2b848c4a),用户裁定
8888 对时任务退出历史舞台(gitea:77 线程原话,跟踪 Issue #78)。
生产侧(App/sync/time_sync.c v2.0,任务入口名不变,freertos.c 零改动):
- 删除 :8888 监听、TIME 包重组、sys_clock 写入、OPER 落痕、
net_drop_conn 队头自救整条链(TIME_SYNC_PORT 宏随退役删除)
- 任务体收敛为:就绪屏障(100ms 分片、osFlagsNoClear)→
200ms 校时节拍 task_wd_beat → time_sntp_run(net_get_generation())
→ 分片延时;喂狗节奏(TASK_WD_TIMESYNC=5,required_mask)不变
- 时钟写入口自此 = SNTP / UI 手工 / 104 C_CS,双写竞态(后写者赢)
随之消失(墙钟说明 §15 退役公告、§16 现势口径)
工具与测试:
- 删除 test/host/time_sync_server.py(对时源)与 e02_probe.py
(队头阻塞探针,载体随监听退役;判据脚本 v5_log_gap_report.py
保留供旧日志解读)
- time_sync 主机单测重写 v2.0:33 断言覆盖分片延时 / 屏障 /
SNTP 节拍与网络代次当轮交接 / 未出网守门;stub 的
net_socket.h 裁剪至 net_get_generation 作编译期绊线
- 覆盖率基线 --update-baseline 收口(time_sync.c 101→20 行为刻意
缩减,全仓 48/48 套绿)
文档与图:墙钟说明 §15 改退役公告(教训保留:CH395F FIN 丢未读
缓冲)、§16 单一网络来源;产品需求 FR-TIME-2 退役注记、§6.3 重写;
架构总览 / 软件架构规格 / 网络配置 / 网络自愈 §9.3 / BSD 指南 /
看门狗说明心跳与白名单行 / 实机测试指南 / 部署环境 / 继保规划 /
UI补齐时基口径 / 页面补齐规划 SNTP 页行 / 任务规划 README 叶数与
暂缓项行(SNTP 叶已绑、UI_PAGE_MAP_COUNT=31 口径)等约 30 处现势
口径同步;arch / net_stack 两图生成器与 SVG 同笔重跑。
台账:ram_baseline --update + check_mem_numbers --write + 体积凭证
(Code 254,648→253,428,−1,220 B)。静态链复测发现 timeSyncTask
1,904 B / iec104Task 1,600 B 低于 ISR 硬线:master 2b848c4a 同判据
timeSyncTask 即 2,056 B(本 PR 只减不增,−152 B 旧任务自身帧),
双 FAIL 为 #74/#77 后既有债;抬栈属 Ask-first 未擅自执行,登记簿
新增「静态链硬线双 FAIL」待台架复采,复测快照入 docs/内存管理/data。
验证:keil-build 0 错 0 警;time_sync/sntp 主机单测绿;
ci_local 全绿;doc-links --all 0。
355 lines
14 KiB
C
355 lines
14 KiB
C
/*
|
||
* 模块名称: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 <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;
|
||
}
|