Files
super_agent_bot 3a2f9b21fb
CI / 静态门禁 (pre-commit) (push) Failing after 43s
CI / 主机侧单测 (MSVC) (push) Failing after 45s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 45s
CI / 主机侧单测 (MSVC) (pull_request) Failing after 45s
refactor(time_sync)!: 退役 TCP :8888 对时任务,timeSyncTask 转纯 SNTP 校时节拍宿主(#78)
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。
2026-11-04 18:07:03 +08:00

355 lines
14 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(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;
}