Files
STM32F4-Base/test/host/sntp/sntp_test.c
T
super_agent_bot 912c16b757
CI / 静态门禁 (pre-commit) (push) Failing after 7m28s
CI / 主机侧单测 (MSVC) (push) Successful in 29s
CI / 静态门禁 (pre-commit) (pull_request) Has been cancelled
CI / 主机侧单测 (MSVC) (pull_request) Has been cancelled
feat(sntp): 板端 SNTP 校时接入——coreSNTP 序列化器 + [sntp] 段 + 设置页(#73)
- vendored coreSNTP v2.0.0 序列化器(MIT,仅 serializer+LICENSE,零堆),
  48 字节报文编解码在库,节奏与准入判据在本方(RFC 4330 §10 调研口径)
- App/sync/time_sntp.c 挂 timeSyncTask 的 8888 事件循环,等待切片 ≤50 ms,
  不饿任务狗与 IWDG;懒建 UDP socket,代际变化/禁用/死错误码即关闭归还
- 防伪 = OriginTimestamp 逐位等于发出时 T1(含低 16 位随机小数位);
  mode/stratum/闰秒指示/KoD 按 RFC 拒收;DENY·RSTR 换服务器、RATE 只退避
- 主备轮换 + 失败指数退避(5 s 起、60 s 封顶)+ 首轮 1~5 分钟随机错峰;
  成功恢复 poll_min 周期(1~1440 分,出厂默认 60,默认不启用不发包)
- 钟面 = 本地时间:UTC+8 固定偏移落 sys_clock_set_unix(§1.1 时基裁定,
  第三个写入口,装置仍不存时区);|offset|<2 s 只记日志不步进,冷启动或
  更大偏差步进并回写 SD2506,留痕 [SNTP] step 与 syslog OPER
- 首个校验通过的同步置 SYS_EVT_TIMESYNC_READY(就绪位,消费方按需 NoClear)
- [sntp] 配置段(enable/server1/server2/poll_min)属主 sntp_cfg.c,
  注册在 net_cfg_load();UI 页 31「SNTP 设置」(密码叶):启用开关 +
  主/备服务器 octet 编辑 + 周期(分) + 保存,复用 net_cfg_save_request 链
- 测试与门禁:test/host/sntp 新套件(报文/防伪/KoD/退避周期/阶跃域/配置
  注册表)、net_cfg 与 time_sync 套件补 fake、黄金参考 66→74 张零漂移、
  字符集 228 字、Keil 0 错 0 警(RO +5,848 B 含字体)、覆盖率基线回填
- 文档:墙钟维护说明 §16、网络配置说明 [sntp] 节、配置文件设计说明变更记录、
  产品需求 FR-TIME-3 与 8.3/9.1.2 台账、UI 补齐规划/UI补齐 31 叶口径、
  登记簿销项并登记「SNTP 校时真机判据」等台架(PC 时间源 192.168.31.161/.13)
2026-11-04 16:02:28 +08:00

968 lines
34 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 SNTP 主机单测(test/host/sntp)
* 模块功能:把 App/sync/time_sntp.c + Drivers/BSP/NETCFG/sntp_cfg.c +
* vendor Lib/coreSNTP/core_sntp_serializer.c 一起编进本 TU,
* 用脚本化 net/sys_clock/dbg fake 驱动状态机,验证:
* RFC 4330 §10 节奏(初始散布/失败翻倍封顶/成功重排)、§5.3 防欺骗
* (源 IP:port / originate 精确匹配 / mode / LI=3 / stratum / KoD)、
* §5.5 步进策略(±2s 死区、冷启动直采 T3、时区面 +28800)、
* poll 钳位与 set 入口、代次/停用收口。
* ([sntp] 段描述符注册断言在 test/host/net_cfg——分层门禁后
* 描述符由 net_cfg.c 组装,本套件只并 sntp_cfg.c 数据本体。)
* 约束:生产 .c 以 #include 方式并入——static 白盒可达;fake 计数前缀唯一,
* 避免同名 tentative static 静默合并(2026-10-10 教训)。
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
/* stub 头先行:fake 定义要用到这些类型/宏 */
#include "FreeRTOS.h"
#include "task.h"
#include "cmsis_os2.h"
#include "net_types.h"
#include "net_socket.h"
#include "sys_clock.h"
#include "sys_events.h"
#include "sys_log.h"
#include "dbg_log.h"
/* ---- fake 存储(stub/dbg_log.h 已声明 g_dbg_*_count extern,此处落定义) ---- */
int g_dbg_err_count;
int g_dbg_warn_count;
int g_dbg_info_count;
/* 系统 tick(ms) */
static uint32_t g_tick;
/* 时钟面:UINT32_MAX = 未初始化;其余 = 本地秒 */
static uint32_t g_face;
static uint32_t g_face_set_last;
static int g_face_set_calls;
/* 事件标志 / 操作日志 */
osEventFlagsId_t sys_evtHandle; /* 测试里指到假对象(非 NULL) */
static int g_evt_calls;
static uint32_t g_evt_last;
static int g_syslog_calls;
static syslog_id_t g_syslog_last_id;
/* ---- net fake ---- */
#define SNTP_T_NET_SEND_MAX 8
#define SNTP_T_NET_RECV_MAX 8
#define SNTP_T_NET_IP(o1,o2,o3,o4) { (o1), (o2), (o3), (o4) }
static uint32_t g_gen;
static int g_sock_next_fd = 100;
static int g_sock_opens;
static int g_sock_close_calls;
static int g_last_close_fd = -1;
static int g_bind_calls;
static int g_bind_ret; /* 脚本:0 成功 / -1 失败 */
static int g_errno_val;
static int g_send_calls;
static uint8_t g_sent_ip[4];
static uint16_t g_sent_port_be;
static uint8_t g_sent_pkt[128];
static int g_sent_len;
static int g_send_ret; /* 脚本:48 / -1 */
static int g_send_history_n; /* 每轮 send_calls 快照(历史计数) */
struct sntp_t_recv {
int kind; /* NET_RECV_* */
int len; /* kind==DATA 有效 */
uint8_t data[128];
uint8_t ip[4];
uint16_t port_be;
};
static struct sntp_t_recv g_recv_q[SNTP_T_NET_RECV_MAX];
static int g_recv_head, g_recv_tail;
static uint32_t g_recv_last_timeout_ms; /* 最近一次 recv 的片超时(50ms 断言) */
/* ---- cfg fake:[sntp] 段描述符组装已移 net_cfg.c(分层门禁),注册内容
* 断言归 test/host/net_cfg;本套件只需 sntp_cfg.c 数据本体可用,无引擎调用 ---- */
/* ---- dbg 宏(stub/dbg_log.h 已声明 g_dbg_*,此处无需再定义宏) ---- */
/* ---- sys_clock / sys_events / sys_log / cmsis fake 实现 ---- */
uint32_t sys_clock_get(void)
{
return g_face;
}
int sys_clock_set_unix(uint32_t unix_secs)
{
g_face_set_calls++;
g_face_set_last = unix_secs;
g_face = unix_secs;
return 0;
}
uint32_t xTaskGetTickCount(void)
{
return g_tick;
}
uint32_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t events)
{
(void)ef_id;
g_evt_calls++;
g_evt_last = events;
return events;
}
void syslog_post_quiet(syslog_id_t id, const char *desc)
{
(void)desc;
g_syslog_calls++;
g_syslog_last_id = id;
}
/* ---- net fake 实现 ---- */
static void sntp_t_net_reset(void)
{
g_sock_next_fd = 100;
g_sock_opens = 0;
g_sock_close_calls = 0;
g_last_close_fd = -1;
g_bind_calls = 0;
g_bind_ret = 0;
g_errno_val = 0;
g_send_calls = 0;
g_send_ret = 48;
g_send_history_n = 0;
g_recv_head = g_recv_tail = 0;
g_recv_last_timeout_ms = 0;
memset(g_sent_ip, 0, sizeof(g_sent_ip));
memset(g_sent_pkt, 0, sizeof(g_sent_pkt));
g_sent_len = 0;
g_sent_port_be = 0;
}
static void sntp_t_push_recv(int kind, const uint8_t *data, int len,
const uint8_t *ip, uint16_t port_be)
{
struct sntp_t_recv *r;
if (g_recv_tail - g_recv_head >= SNTP_T_NET_RECV_MAX) {
printf("recv queue overflow\n");
return;
}
r = &g_recv_q[g_recv_tail++ % SNTP_T_NET_RECV_MAX];
r->kind = kind;
r->len = len;
r->port_be = port_be;
if (data != NULL && len > 0) {
memcpy(r->data, data, (size_t)len);
}
if (ip != NULL) {
memcpy(r->ip, ip, 4);
} else {
memset(r->ip, 0, 4);
}
}
int net_socket(int domain, int type)
{
(void)domain;
(void)type;
g_sock_opens++;
return g_sock_next_fd++;
}
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen)
{
(void)sockfd;
(void)addr;
(void)addrlen;
g_bind_calls++;
return g_bind_ret;
}
int net_sendto(int sockfd, const void *buf, int len, int flags,
const struct net_sockaddr *dest, int addrlen)
{
const struct net_sockaddr_in *d = (const struct net_sockaddr_in *)dest;
(void)sockfd;
(void)flags;
(void)addrlen;
g_send_calls++;
g_send_history_n = g_send_calls;
if (len > 0 && len <= (int)sizeof(g_sent_pkt)) {
memcpy(g_sent_pkt, buf, (size_t)len);
}
g_sent_len = len;
if (dest != NULL) {
memcpy(g_sent_ip, &d->sin_addr.s_addr, 4);
g_sent_port_be = d->sin_port;
}
return g_send_ret;
}
int net_recvfrom_to(int sockfd, void *buf, int len, uint32_t timeout_ms,
struct net_sockaddr *src, int *addrlen)
{
struct sntp_t_recv *r;
struct net_sockaddr_in *sin = (struct net_sockaddr_in *)src;
(void)sockfd;
g_recv_last_timeout_ms = timeout_ms;
if (g_recv_head == g_recv_tail) {
return -2; /* 队列空 = 无应答(合同 -2) */
}
r = &g_recv_q[g_recv_head++ % SNTP_T_NET_RECV_MAX];
if (r->kind != NET_RECV_DATA) {
return r->kind; /* -2 超时 / -1 错误 / 0 防御 */
}
if (r->len > len) {
r->len = len;
}
memcpy(buf, r->data, (size_t)r->len);
if (src != NULL && addrlen != NULL && *addrlen >= (int)sizeof(*sin)) {
memset(sin, 0, sizeof(*sin));
sin->sin_family = NET_AF_INET;
sin->sin_port = r->port_be;
memcpy(&sin->sin_addr.s_addr, r->ip, 4);
}
return r->len;
}
int net_close(int sockfd)
{
g_sock_close_calls++;
g_last_close_fd = sockfd;
return 0;
}
int net_get_errno(void)
{
return g_errno_val;
}
uint32_t net_get_generation(void)
{
return g_gen;
}
/* ---- 生产代码并入(顺序:vendor → 配置 → 客户端) ---- */
#include "core_sntp_serializer.c"
#include "sntp_cfg.c"
#include "time_sntp.c"
/* ---- 测试框架 ---- */
static int g_total, g_pass, g_fail;
#define CHECK(cond, name) \
do { \
g_total++; \
if (cond) { g_pass++; } \
else { g_fail++; printf(" FAIL %s (line %d)\n", (name), __LINE__); } \
} while (0)
static void s_case_reset(void)
{
sntp_cfg_t *c;
/* 配置先归零,再借代次跳变走"生产复位通道"(关 socket + 状态回 IDLE) */
c = (sntp_cfg_t *)(void *)sntp_cfg_get();
c->enable = 0u;
memset(c->server_pri, 0, 4);
memset(c->server_bak, 0, 4);
c->poll_min = 60u;
g_gen++;
(void)time_sntp_run(g_gen);
g_tick = 10000u; /* 避开 0 */
g_face = UINT32_MAX;
g_face_set_calls = 0;
g_face_set_last = 0u;
g_evt_calls = 0;
g_evt_last = 0;
g_syslog_calls = 0;
g_dbg_err_count = g_dbg_warn_count = g_dbg_info_count = 0;
sntp_t_net_reset();
sys_evtHandle = (osEventFlagsId_t)&g_evt_calls; /* 假句柄:非 NULL */
s_ctx.srv = 0u;
}
static void s_cfg_enable(const uint8_t *pri, const uint8_t *bak, uint16_t poll)
{
sntp_cfg_t *c = (sntp_cfg_t *)(void *)sntp_cfg_get();
c->enable = 1u;
if (pri != NULL) { memcpy(c->server_pri, pri, 4); }
if (bak != NULL) { memcpy(c->server_bak, bak, 4); }
c->poll_min = poll;
}
/* 推进到"下一次"发出请求(基线=当前计数,有界轮询),返回 1=新发一包 */
static int s_run_until_send(uint32_t max_ms)
{
uint32_t t;
int base = g_send_calls;
for (t = 0; t < max_ms; t += 200u) {
g_tick += 200u;
time_sntp_run(g_gen);
if (g_send_calls > base) {
return 1;
}
}
return 0;
}
/* 从 g_sent_pkt 的 transmit 时间戳(40..47)拼一条合法应答(不含 KoD) */
static void s_build_resp(uint8_t *out, uint8_t li, uint8_t stratum,
uint32_t t2_sec, uint32_t t2_frac,
uint32_t t3_sec, uint32_t t3_frac)
{
memset(out, 0, 48);
out[0] = (uint8_t)(((li & 3u) << 6) | (4u << 3) | 4u); /* VN4 mode=4 */
out[1] = stratum;
memcpy(&out[24], &g_sent_pkt[40], 8); /* originate = T1 原样 */
/* recvTs 非零(serializer 要求三时间戳非零) */
out[32] = (uint8_t)(t2_sec >> 24); out[33] = (uint8_t)(t2_sec >> 16);
out[34] = (uint8_t)(t2_sec >> 8); out[35] = (uint8_t)t2_sec;
out[36] = (uint8_t)(t2_frac >> 24); out[37] = (uint8_t)(t2_frac >> 16);
out[38] = (uint8_t)(t2_frac >> 8); out[39] = (uint8_t)t2_frac;
out[40] = (uint8_t)(t3_sec >> 24); out[41] = (uint8_t)(t3_sec >> 16);
out[42] = (uint8_t)(t3_sec >> 8); out[43] = (uint8_t)t3_sec;
out[44] = (uint8_t)(t3_frac >> 24); out[45] = (uint8_t)(t3_frac >> 16);
out[46] = (uint8_t)(t3_frac >> 8); out[47] = (uint8_t)t3_frac;
}
static uint32_t s_be32(const uint8_t *p)
{
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) | (uint32_t)p[3];
}
/* 等应答态喂一包(服务器地址 = 本轮实际发送目的),返回后状态自取 */
static void s_feed(const uint8_t *resp, int len, const uint8_t *src_ip,
uint16_t src_port)
{
sntp_t_push_recv(NET_RECV_DATA, resp, len, src_ip, src_port);
g_tick += 200u;
time_sntp_run(g_gen);
}
/* ============================ 测试用例 ============================ */
static void tc_poll_clamp(void)
{
sntp_cfg_t *c;
/* [sntp] 段描述符/键表/默认值的注册断言在 test/host/net_cfg(描述符
已改由 net_cfg.c 组装);这里只钉客户端实际消费的 poll 钳位口 */
s_case_reset();
c = (sntp_cfg_t *)(void *)sntp_cfg_get();
c->poll_min = 0u; CHECK(sntp_cfg_poll_min() == 60u, "cfg: poll 0 -> 60");
c->poll_min = 1441u; CHECK(sntp_cfg_poll_min() == 60u, "cfg: poll 1441 -> 60");
c->poll_min = 2u; CHECK(sntp_cfg_poll_min() == 2u, "cfg: poll 2 kept");
CHECK(c->poll_min == 0u || c->poll_min == 2u || c->poll_min == 1441u,
"cfg: clamp no writeback");
c->poll_min = 60u;
/* set(NULL) 不改写 + set 整包拷贝(UI 编辑入口的行为钉) */
sntp_cfg_set(NULL);
CHECK(sntp_cfg_poll_min() == 60u, "cfg: set(NULL) no-op");
}
static void tc_disabled_silent(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
s_case_reset();
s_cfg_enable(pri, NULL, 60u);
((sntp_cfg_t *)sntp_cfg_get())->enable = 0u;
(void)time_sntp_run(g_gen);
CHECK(g_send_calls == 0 && g_sock_opens == 0, "disable: no send/no socket");
}
static void tc_first_send_window(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
uint32_t t0;
s_case_reset();
t0 = g_tick;
s_cfg_enable(pri, NULL, 60u);
CHECK(s_run_until_send(320000u) == 1, "first: send happened");
CHECK(g_tick - t0 >= SNTP_FIRST_DELAY_MIN_MS && g_tick - t0 <=
SNTP_FIRST_DELAY_MIN_MS + SNTP_FIRST_DELAY_SPAN_MS, "first: 1..5min window");
CHECK(g_send_calls == 1, "first: single send");
CHECK(g_sent_len == 48, "first: 48B request");
CHECK(g_sent_pkt[0] == 0x23u, "first: LI0 VN4 mode3");
CHECK(g_sent_port_be == net_htons(123u), "first: dst port 123 BE");
CHECK(memcmp(g_sent_ip, pri, 4) == 0, "first: dst = server1");
CHECK(s_be32(&g_sent_pkt[40]) != 0u, "first: T1 nonzero");
CHECK(g_sock_opens == 1 && g_bind_calls == 1, "first: socket opened+bound");
}
static void tc_cold_start_step_t3(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
uint8_t resp[48];
s_case_reset();
s_cfg_enable(pri, NULL, 60u);
(void)s_run_until_send(320000u);
g_tick = (g_tick / 1000u) * 1000u; /* 分数归零,期望精确 */
/* 冷启动:偏移乱也无妨——直接采用服务器 T3(调研 §5.5)。
* T3 用真实当前时刻(服务器不受本地冷钟 epoch 起点影响) */
{
const uint32_t real_unix = 1750000000u; /* 2025-06-15,域内 */
s_build_resp(resp, 0u, 5u,
SNTP_TIME_AT_UNIX_EPOCH_SECS + real_unix + 1u, 0u,
SNTP_TIME_AT_UNIX_EPOCH_SECS + real_unix, 0u);
s_feed(resp, 48, pri, net_htons(123u));
CHECK(time_sntp_sync_count() >= 1u, "cold: sync counted");
CHECK(g_face_set_calls == 1, "cold: step once");
CHECK(g_face_set_last == real_unix + 28800u,
"cold: face = T3 unix + 8h");
}
CHECK(g_syslog_calls == 1 && g_syslog_last_id == SYSLOG_OPER, "cold: oper log");
CHECK(g_evt_calls == 1 && g_evt_last == SYS_EVT_TIMESYNC_READY, "cold: ready evt");
/* 成功节奏:下一轮 = poll_min(60 分钟),退避复位 */
CHECK(s_ctx.state == SNTP_ST_WAIT_DUE && s_ctx.timeout_ms == 5000u,
"cold: backoff reset");
}
static void tc_step_valid_clock(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
uint8_t resp[48];
uint32_t t1s;
uint32_t face_before = 1750000123u + 28800u; /* 有效本地钟(2025+) */
s_case_reset();
g_face = face_before;
s_cfg_enable(pri, NULL, 60u);
(void)s_run_until_send(320000u);
t1s = s_be32(&g_sent_pkt[40]);
g_tick = (g_tick / 1000u) * 1000u;
/* 服务器快 30s:T2=T3=T1+30s,T4=T1 → offset=+30000ms(>2s 死区 → 步进) */
s_build_resp(resp, 0u, 5u, t1s + 30u, 0u, t1s + 30u, 0u);
s_feed(resp, 48, pri, net_htons(123u));
CHECK(g_face_set_calls == 1, "step: stepped once");
CHECK(g_face_set_last == face_before + 30u, "step: face += 30s");
CHECK(time_sntp_last_offset_ms() >= 29499 && time_sntp_last_offset_ms() <= 30001,
"step: offset ~30000ms");
}
static void tc_deadband_no_step(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
uint8_t resp[48];
uint32_t t1s;
uint32_t cnt0 = time_sntp_sync_count();
s_case_reset();
g_face = 1750000000u + 28800u;
s_cfg_enable(pri, NULL, 60u);
(void)s_run_until_send(320000u);
t1s = s_be32(&g_sent_pkt[40]);
g_tick = (g_tick / 1000u) * 1000u;
/* ±1s < 2s 死区:只记录不拨针(防 SD2506 磨损,调研 §5.5) */
s_build_resp(resp, 0u, 5u, t1s, 0u, t1s + 1u, 0u);
s_feed(resp, 48, pri, net_htons(123u));
CHECK(g_face_set_calls == 0, "deadband: no step");
CHECK(g_syslog_calls == 0, "deadband: no oper log");
CHECK(time_sntp_sync_count() == cnt0 + 1u, "deadband: counted sync");
CHECK(s_ctx.state == SNTP_ST_WAIT_DUE, "deadband: next poll scheduled");
}
static void tc_anti_spoof_src(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
uint8_t other[] = SNTP_T_NET_IP(192, 168, 31, 66);
uint8_t resp[48];
uint32_t t1s;
s_case_reset();
s_cfg_enable(pri, NULL, 60u);
(void)s_run_until_send(320000u);
t1s = s_be32(&g_sent_pkt[40]);
s_build_resp(resp, 0u, 5u, t1s, 0u, t1s, 0u);
/* 源 IP 不对:丢弃继续等(§5.3 第一道) */
s_feed(resp, 48, other, net_htons(123u));
CHECK(g_face_set_calls == 0, "spoof: wrong ip not stepped");
CHECK(s_ctx.state == SNTP_ST_WAIT_RESP, "spoof: still waiting");
/* 源端口不对(123→999):同样丢弃 */
s_feed(resp, 48, pri, net_htons(999u));
CHECK(g_face_set_calls == 0 && s_ctx.state == SNTP_ST_WAIT_RESP, "spoof: wrong port");
/* originate 篡改:丢弃 */
resp[24] ^= 0xA5u;
s_feed(resp, 48, pri, net_htons(123u));
CHECK(g_face_set_calls == 0 && s_ctx.state == SNTP_ST_WAIT_RESP, "spoof: bad originate");
/* mode=3(服务器回显错 mode):serializer 判无效 → 丢弃 */
s_build_resp(resp, 0u, 5u, t1s, 0u, t1s, 0u);
resp[0] = (uint8_t)((resp[0] & 0xF8u) | 3u);
s_feed(resp, 48, pri, net_htons(123u));
CHECK(g_face_set_calls == 0 && s_ctx.state == SNTP_ST_WAIT_RESP, "spoof: bad mode");
/* 短包(<48):丢弃 */
s_feed(resp, 40, pri, net_htons(123u));
CHECK(g_face_set_calls == 0 && s_ctx.state == SNTP_ST_WAIT_RESP, "spoof: short pkt");
/* 最后合法包仍可达:链路未被误伤 */
s_build_resp(resp, 0u, 5u, t1s, 0u, t1s, 0u);
g_face = 1750000000u + 28800u;
s_feed(resp, 48, pri, net_htons(123u));
CHECK(g_face_set_calls == 0, "spoof: valid after noise (deadband)");
CHECK(s_ctx.state == SNTP_ST_WAIT_DUE, "spoof: recovered to schedule");
}
static void tc_li3_stratum(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
uint8_t bak[] = SNTP_T_NET_IP(192, 168, 31, 13);
uint8_t resp[48];
uint32_t t1s;
int sends_at_li;
s_case_reset();
s_cfg_enable(pri, bak, 60u);
(void)s_run_until_send(320000u);
/* LI=3:服务器没同步 → 立即失败 + 切备 + 翻倍退避(serializer 不拦,我们拦) */
t1s = s_be32(&g_sent_pkt[40]);
s_build_resp(resp, 3u, 5u, t1s, 0u, t1s, 0u);
sends_at_li = g_send_calls;
s_feed(resp, 48, pri, net_htons(123u));
CHECK(g_face_set_calls == 0, "li3: no step");
CHECK(s_ctx.state == SNTP_ST_WAIT_DUE && s_ctx.timeout_ms == 10000u,
"li3: doubled backoff");
CHECK(s_ctx.srv == 1u, "li3: switched to backup");
/* 备机应答 stratum=16(非法):失败退避但不再切换(只拦包不怪服务器方向?
* 调研:stratum 非法 = 无效应答 → 退避;主备保持) */
(void)s_run_until_send(20000u);
CHECK(g_send_calls == sends_at_li + 1, "li3: retry to backup");
CHECK(memcmp(g_sent_ip, bak, 4) == 0, "li3: dst=backup now");
t1s = s_be32(&g_sent_pkt[40]);
s_build_resp(resp, 0u, 16u, t1s, 0u, t1s, 0u);
s_feed(resp, 48, bak, net_htons(123u));
CHECK(s_ctx.state == SNTP_ST_WAIT_DUE && s_ctx.timeout_ms == 20000u,
"stratum16: backoff");
/* 本仓无 DBG_WARN 宏(dbg_log.h 只有 ERROR/INFO/DEBUG,net_udp.c:756 钉),
* 生产侧 WARN 走 DBG_INFO("WARN: …")——计数看 info */
CHECK(g_dbg_info_count >= 2, "stratum/li3: warned");
}
static void tc_kod(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
uint8_t bak[] = SNTP_T_NET_IP(192, 168, 31, 13);
uint8_t resp[48];
uint32_t t1s;
s_case_reset();
s_cfg_enable(pri, bak, 60u);
/* KoD stratum=0 + refId 码:DENY → 切备(§5.3/RFC) */
(void)s_run_until_send(320000u);
t1s = s_be32(&g_sent_pkt[40]);
s_build_resp(resp, 0u, 0u, t1s, 0u, t1s, 0u);
resp[12] = 'D'; resp[13] = 'E'; resp[14] = 'N'; resp[15] = 'Y';
s_feed(resp, 48, pri, net_htons(123u));
CHECK(g_face_set_calls == 0 && s_ctx.srv == 1u, "kod DENY: switch");
/* RATE:退避但保留当前服务器 */
(void)s_run_until_send(20000u);
t1s = s_be32(&g_sent_pkt[40]);
s_build_resp(resp, 0u, 0u, t1s, 0u, t1s, 0u);
resp[12] = 'R'; resp[13] = 'A'; resp[14] = 'T'; resp[15] = 'E';
s_feed(resp, 48, bak, net_htons(123u));
CHECK(s_ctx.srv == 1u && s_ctx.timeout_ms == 20000u, "kod RATE: keep+backoff");
/* 未知码:按 OtherCode 切走 */
(void)s_run_until_send(40000u);
t1s = s_be32(&g_sent_pkt[40]);
s_build_resp(resp, 0u, 0u, t1s, 0u, t1s, 0u);
resp[12] = '?'; resp[13] = '?'; resp[14] = '?'; resp[15] = '?';
s_feed(resp, 48, bak, net_htons(123u));
CHECK(s_ctx.srv == 0u, "kod other: switch");
}
static void tc_timeout_backoff_cap(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
uint32_t t;
int i;
s_case_reset();
s_cfg_enable(pri, NULL, 60u);
(void)s_run_until_send(320000u);
/* 无应答:5s 片到点 → 退避翻倍;socket 保留 */
for (t = 0; t < 6000u; t += 200u) {
g_tick += 200u;
time_sntp_run(g_gen);
}
CHECK(s_ctx.state == SNTP_ST_WAIT_DUE && s_ctx.timeout_ms == 10000u,
"timeout: doubled 5s->10s");
CHECK(g_sock_close_calls == 0, "timeout: socket kept");
/* 连打封顶 60s(RFC 间隔递增),单参数化推进:每轮 = 应窗 + 应答窗 */
for (i = 0; i < 10 && s_ctx.timeout_ms < SNTP_RESP_TIMEOUT_MAX_MS; i++) {
uint32_t win = s_ctx.timeout_ms * 2u + 2000u;
(void)s_run_until_send(win);
for (t = 0; t < win && s_ctx.state == SNTP_ST_WAIT_RESP; t += 200u) {
g_tick += 200u;
time_sntp_run(g_gen);
}
}
CHECK(s_ctx.timeout_ms == SNTP_RESP_TIMEOUT_MAX_MS, "timeout: cap 60s");
}
static void tc_poll_pacing_after_success(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
uint8_t resp[48];
uint32_t t1s, t;
int send_at_success;
s_case_reset();
s_cfg_enable(pri, NULL, 2u); /* 2 分钟一轮(钳位带内) */
(void)s_run_until_send(320000u);
t1s = s_be32(&g_sent_pkt[40]);
s_build_resp(resp, 0u, 5u, t1s, 0u, t1s, 0u);
s_feed(resp, 48, pri, net_htons(123u));
send_at_success = g_send_calls;
/* 下一轮 = poll_min*60s:1 分 50s 内不应有第 2 包 */
for (t = 0; t < 110000u; t += 200u) {
g_tick += 200u;
time_sntp_run(g_gen);
}
CHECK(g_send_calls == send_at_success, "poll: not early (<2min)");
for (t = 0; t < 20000u; t += 200u) {
g_tick += 200u;
time_sntp_run(g_gen);
if (g_send_calls > send_at_success) { break; }
}
CHECK(g_send_calls == send_at_success + 1, "poll: second round on time");
}
static void tc_no_server_warn_once(void)
{
uint32_t t;
s_case_reset();
s_cfg_enable(NULL, NULL, 60u); /* enable 但两侧全零 */
for (t = 0; t < 700000u; t += 1000u) {
g_tick += 1000u;
time_sntp_run(g_gen);
}
CHECK(g_send_calls == 0, "nosrv: never sends");
CHECK(g_dbg_info_count >= 1, "nosrv: warned");
{
int w0 = g_dbg_info_count;
g_tick += 1000u;
time_sntp_run(g_gen);
CHECK(g_dbg_info_count == w0, "nosrv: warn once only");
}
/* 配置补齐即恢复(不发请求不算失败,无退避拖尾) */
{
sntp_cfg_t *c = (sntp_cfg_t *)(void *)sntp_cfg_get();
uint8_t pri[] = SNTP_T_NET_IP(10, 0, 0, 9);
memcpy(c->server_pri, pri, 4);
CHECK(s_run_until_send(320000u) == 1, "nosrv: recovers on config");
CHECK(memcmp(g_sent_ip, pri, 4) == 0, "nosrv: new server used");
}
}
static void tc_disable_closes_socket(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
sntp_cfg_t *c;
s_case_reset();
s_cfg_enable(pri, NULL, 60u);
(void)s_run_until_send(320000u);
CHECK(g_sock_opens == 1, "disable: opened once");
c = (sntp_cfg_t *)(void *)sntp_cfg_get();
c->enable = 0u;
g_tick += 200u;
time_sntp_run(g_gen);
CHECK(g_sock_close_calls == 1 && s_ctx.state == SNTP_ST_IDLE, "disable: closed+idle");
g_tick += 200u;
time_sntp_run(g_gen);
CHECK(g_sock_close_calls == 1, "disable: no double close");
}
static void tc_gen_change_resets(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
int before;
s_case_reset();
s_cfg_enable(pri, NULL, 60u);
(void)s_run_until_send(320000u);
before = g_send_calls;
/* 网络代次变化(UI 保存重建 / 链路复位):弃 socket 重新散布;
* 同一次 run 内 enable==1 时 IDLE 立即重排为 WAIT_DUE(重新 1..5min 散布) */
g_gen++;
g_tick += 200u;
time_sntp_run(g_gen);
CHECK(g_sock_close_calls == 1 && s_ctx.state == SNTP_ST_WAIT_DUE &&
s_ctx.gen == g_gen, "gen: close+re-spread");
{
uint32_t t;
for (t = 0; t < 59000u; t += 200u) { /* 首 59s 不该再发 */
g_tick += 200u;
time_sntp_run(g_gen);
}
CHECK(g_send_calls == before, "gen: fresh 1..5min re-spread");
}
}
static void tc_send_fail_switch(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
uint8_t bak[] = SNTP_T_NET_IP(192, 168, 31, 13);
s_case_reset();
s_cfg_enable(pri, bak, 60u);
g_send_ret = -1;
g_errno_val = NET_ERR_NOSPACE;
(void)s_run_until_send(320000u);
CHECK(s_ctx.state == SNTP_ST_WAIT_DUE && s_ctx.timeout_ms == 10000u,
"sendfail: backoff");
CHECK(s_ctx.srv == 1u, "sendfail: switched");
g_send_ret = 48;
(void)s_run_until_send(20000u);
CHECK(memcmp(g_sent_ip, bak, 4) == 0, "sendfail: retry to backup");
}
static void tc_recv_err_deadfd(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
uint32_t t;
s_case_reset();
s_cfg_enable(pri, NULL, 60u);
(void)s_run_until_send(320000u);
sntp_t_push_recv(NET_RECV_ERR, NULL, 0, NULL, 0);
g_errno_val = NET_ERR_CONNRESET;
g_tick += 200u;
time_sntp_run(g_gen);
CHECK(g_last_close_fd >= 100 && s_ctx.fd == -1, "recverr: dead fd closed");
CHECK(s_ctx.state == SNTP_ST_WAIT_DUE, "recverr: backoff");
/* 下一轮到点自动重开 socket */
for (t = 0; t < 12000u; t += 200u) {
g_tick += 200u;
time_sntp_run(g_gen);
if (g_send_calls >= 2) { break; }
}
CHECK(g_send_calls == 2 && g_sock_opens == 2, "recverr: reopened");
}
static void tc_oversized_resp_ok(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
uint8_t resp[64];
uint32_t t1s;
s_case_reset();
g_face = 1750000000u + 28800u;
s_cfg_enable(pri, NULL, 60u);
(void)s_run_until_send(320000u);
t1s = s_be32(&g_sent_pkt[40]);
memset(resp, 0xAA, sizeof(resp));
s_build_resp(resp, 0u, 5u, t1s, 0u, t1s, 0u); /* 前 48 合法,尾部垃圾 */
s_feed(resp, 56, pri, net_htons(123u)); /* 截断交付 = BSD 语义 */
CHECK(time_sntp_sync_count() >= 1u && s_ctx.state == SNTP_ST_WAIT_DUE,
"oversize: parsed first 48");
}
static void tc_recv_slice_50ms(void)
{
uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161);
s_case_reset();
s_cfg_enable(pri, NULL, 60u);
(void)s_run_until_send(320000u);
g_recv_last_timeout_ms = 0u;
g_tick += 200u;
time_sntp_run(g_gen);
CHECK(g_recv_last_timeout_ms == SNTP_RECV_SLICE_MS, "slice: 50ms watchdog-safe");
}
static void tc_helpers_direct(void)
{
uint32_t x, i, prev;
s_case_reset();
/* 回绕安全到期比较 */
CHECK(s_tick_due(1000u, 900u) == 1u, "due: past");
CHECK(s_tick_due(800u, 900u) == 0u, "due: future");
CHECK(s_tick_due(5u, 0xFFFFFFF0u) == 1u, "due: wrap past");
CHECK(s_tick_due(0xFFFFFFF0u - 5u, 0xFFFFFFF0u) == 0u, "due: wrap future");
/* int64 截断 */
CHECK(s_off_clamp(3000LL) == 3000, "clamp: passthrough");
CHECK(s_off_clamp(9999999999999LL) == 2147483647, "clamp: hi");
CHECK(s_off_clamp(-9999999999999LL) == -2147483647, "clamp: lo");
/* xorshift 永不为 0(0 会卡死) */
s_rng_seed_once();
prev = s_rng_next();
CHECK(prev != 0u, "rng: nonzero");
for (i = 0; i < 100000u; i++) {
x = s_rng_next();
if (x == 0u) { break; }
prev = x;
}
CHECK(prev != 0u && i == 100000u, "rng: stream nonzero 100k");
/* 冷启动兜底 T1 域:2036 前进位用 uint32 环绕语义(seconds 直接 ++ 可回绕) */
{
SntpTimestamp_t ts;
g_face = 0xFFFFFFFFu - 1000u; /* 钟面 > 0x7FFFFFFF → 视为无效域外? */
/* s_utc_now: face>=MIN+TZ 且 !=UINT32_MAX → 有效;epoch 加法环绕 */
s_ts_now(&ts);
{
/* uint32 模运算逐项环绕,与 s_ts_now 内部语义一致 */
uint32_t expu = (uint32_t)((0xFFFFFFFFu - 1000u - 28800u) +
SNTP_TIME_AT_UNIX_EPOCH_SECS);
CHECK(ts.seconds == expu, "ts: era wrap");
}
g_face = UINT32_MAX;
}
}
/* 把 host 序 32bit 以大端铺进 p[0..3](origin/receive/transmit 字段都要大端) */
static void s_fill_be(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16);
p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v;
}
static void tc_serializer_vendored(void)
{
SntpTimestamp_t t1, t4;
SntpResponseData_t r;
SntpTimestamp_t zero = { 0u, 0u };
uint8_t req[48];
uint8_t resp[48];
uint32_t us = 0u, us2 = 0u;
s_case_reset();
t1.seconds = SNTP_TIME_AT_UNIX_EPOCH_SECS + 1000000000UL; /* 2001+,era0 */
t1.fractions = 0x12345678UL;
CHECK(Sntp_SerializeRequest(&t1, 0xAABBCCDDu, req, 48u) == SntpSuccess, "ser: ok");
CHECK(req[0] == 0x23u, "ser: LI0 VN4 mode3");
/* 防欺骗核心:randomNumber>>16(16bit)OR 进 T1 分数低位 + 回写 pRequestTime
* ——应答必须原样 echo 这个合成值,否则判无效 */
CHECK(t1.fractions == (0x12345678u | 0xAABBu), "ser: random merged into fractions");
CHECK(s_be32(&req[40]) == t1.seconds && s_be32(&req[44]) == t1.fractions,
"ser: BE transmit stamped");
CHECK(Sntp_SerializeRequest(&zero, 0u, req, 48u) == SntpErrorBadParameter,
"ser: zero T1 rejected");
CHECK(Sntp_SerializeRequest(&t1, 0u, req, 47u) == SntpErrorBufferTooSmall,
"ser: small buf");
/* 合法应答 round-trip:origin = 序列化后的 t1 精确回显(大端) */
memset(resp, 0, 48);
resp[0] = (uint8_t)((0u << 6) | (4u << 3) | 4u);
resp[1] = 5u;
s_fill_be(&resp[24], t1.seconds); s_fill_be(&resp[28], t1.fractions);
s_fill_be(&resp[32], t1.seconds + 1u); s_fill_be(&resp[36], 0u);
s_fill_be(&resp[40], t1.seconds + 2u); s_fill_be(&resp[44], 0x00000102u);
t4 = t1;
CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r) == SntpSuccess,
"ser: deserialize ok");
CHECK(r.leapSecondType == NoLeapSecond, "ser: leap none");
CHECK(r.serverTime.seconds == t1.seconds + 2u, "ser: T3 parsed");
/* originate 不匹配 → InvalidResponse(客户机据此丢弃继续等待) */
resp[24] ^= 0xFFu;
CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r) == SntpInvalidResponse,
"ser: origin mismatch rejected");
resp[24] ^= 0xFFu;
/* mode≠4:回显包/伪造包直接判无效 */
resp[0] = (uint8_t)((resp[0] & 0xF8u) | 3u);
CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r) == SntpInvalidResponse,
"ser: mode!=4 rejected");
/* KoD:stratum=0 + refId 码分类(DENY/RSTR 换服、RATE 退避、其余 OtherCode) */
resp[0] = (uint8_t)((4u << 3) | 4u);
resp[1] = 0u;
resp[12] = 'D'; resp[13] = 'E'; resp[14] = 'N'; resp[15] = 'Y';
CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r)
== SntpRejectedResponseChangeServer, "ser: KoD DENY");
resp[12] = 'R'; resp[13] = 'A'; resp[14] = 'T'; resp[15] = 'E';
CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r)
== SntpRejectedResponseRetryWithBackoff, "ser: KoD RATE");
resp[12] = 'R'; resp[13] = 'S'; resp[14] = 'T'; resp[15] = 'R';
CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r)
== SntpRejectedResponseChangeServer, "ser: KoD RSTR");
resp[12] = '?';
CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r)
== SntpRejectedResponseOtherCode, "ser: KoD other");
resp[1] = 5u;
/* 2036 纪元语义:era0 尾回减、era1 头回加 */
{
SntpTimestamp_t e;
e.seconds = SNTP_TIME_AT_UNIX_EPOCH_SECS; /* 1970-01-01 恰在界上 */
e.fractions = 0u;
CHECK(Sntp_ConvertToUnixTime(&e, &us, &us2) == SntpSuccess && us == 0u,
"ser: epoch boundary -> unix 0");
e.seconds = SNTP_TIME_AT_UNIX_EPOCH_SECS - 1u; /* era-1 起点附近 */
CHECK(Sntp_ConvertToUnixTime(&e, &us, &us2) == SntpSuccess &&
us == UNIX_TIME_SECS_AT_SNTP_ERA_1_SMALLEST_TIME +
(SNTP_TIME_AT_UNIX_EPOCH_SECS - 1u), "ser: era1 wrap");
}
}
static void tc_offset_math(void)
{
/* 四元组中点:T1 含随机低分数(fractionsToMs 向下取整抹掉)、T4 无 → 精确 5000ms */
SntpTimestamp_t t1, t4;
SntpResponseData_t r;
uint8_t resp[48];
int64_t exp;
s_case_reset();
t1.seconds = SNTP_TIME_AT_UNIX_EPOCH_SECS + 1500000000UL;
t1.fractions = (4295000u * 123u) | 0x1234u; /* 123ms + 亚 ms 随机噪声 */
memset(resp, 0, 48);
resp[0] = (uint8_t)((4u << 3) | 4u);
resp[1] = 3u;
s_fill_be(&resp[24], t1.seconds); s_fill_be(&resp[28], t1.fractions);
{
uint32_t f = 4295000u * 123u; /* 服务器对齐到整 ms */
uint32_t s = t1.seconds + 5u; /* T2 = T3 = T1 + 5s */
s_fill_be(&resp[32], s); s_fill_be(&resp[36], f);
s_fill_be(&resp[40], s); s_fill_be(&resp[44], f);
}
t4.seconds = t1.seconds;
t4.fractions = 4295000u * 123u; /* 本地 ms 分量(无随机) */
CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r) == SntpSuccess, "off: ok");
exp = r.clockOffsetMs;
CHECK(exp == 5000LL, "off: (T2-T1+T3-T4)/2 == 5000ms exact");
}
int main(void)
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("=== SNTP Client Tests ===\n");
#define TC(fn) do { printf("[" #fn "]\n"); fn(); } while (0)
TC(tc_poll_clamp);
TC(tc_disabled_silent);
TC(tc_first_send_window);
TC(tc_cold_start_step_t3);
TC(tc_step_valid_clock);
TC(tc_deadband_no_step);
TC(tc_anti_spoof_src);
TC(tc_li3_stratum);
TC(tc_kod);
TC(tc_timeout_backoff_cap);
TC(tc_poll_pacing_after_success);
TC(tc_no_server_warn_once);
TC(tc_disable_closes_socket);
TC(tc_gen_change_resets);
TC(tc_send_fail_switch);
TC(tc_recv_err_deadfd);
TC(tc_oversized_resp_ok);
TC(tc_recv_slice_50ms);
TC(tc_helpers_direct);
TC(tc_serializer_vendored);
TC(tc_offset_math);
#undef TC
printf("\n=== SNTP Client Tests: %d/%d PASSED (failed=%d) ===\n",
g_pass, g_total, g_fail);
return (g_fail == 0) ? 0 : 1;
}