diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01a569b2..e1396897 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -260,6 +260,36 @@ repos: files: ^(App/algo/adc_input\.(c|h)|test/host/adc_input/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false + # 7f) 卷锁(vol_lock)主机侧单元测试(2026-10-10 加) + # 背景:vol_lock 的**核心不变量是 fail-open**——互斥量创建失败时 + # `vol_lock_wait()` 必须**返回 1 且绝不阻塞**,因为它的调用者 defaultTask 是 + # 看门狗喂狗点,一旦在此阻塞即整机复位。板端该路径**只在内存耗尽时才可能走到**, + # 无法人为复现;本钩子用 MSVC 离线编译真实源文件(#include 纳入)+ CMSIS 互斥桩, + # 把"创建失败 -> 直接放行"、"创建成功 -> 正常加解锁"、"未持锁释放"、 + # 超时参数透传等分支一次性钉死。 + # files 含被测源与其头,以及 harness 本身。 + - id: vol-lock-unit-test + name: 卷锁单测(主机侧) + entry: python test/host/vol_lock/run_unit_test.py + language: system + files: ^(App/fs/vol_lock\.(c|h)|test/host/vol_lock/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + + # 7g) 时间同步(time_sync)主机侧单元测试(2026-10-10 加) + # 背景:time_sync.c 是 TCP 时间同步协议状态机,也是 N-17 号看门狗风险的**全部 + # 逻辑贡献者**——"listen 重建 -> 800ms~1s 心跳间隔"由本模块的分片延时/退避/ + # 结算等待组成,板端只能看到"是否复位"这一结果,看不到各段延时是否如设计。本钩子 + # 用 MSVC 离线编译真实源文件(#include 纳入)+ 可脚本化的网络/事件桩(含 longjmp + # 跳出 for(;;) 任务循环),把分片喂狗节奏、监听退避、报文重组(半包/粘连/EOF/真错/ + # 预算耗尽/tick 回绕)、重听门控逐条钉死。 + # files 含被测源与其头、被测的真实看门狗头,以及 harness 本身。 + - id: time-sync-unit-test + name: 时间同步单测(主机侧) + entry: python test/host/time_sync/run_unit_test.py + language: system + files: ^(App/sync/time_sync\.(c|h)|App/util/task_watchdog\.h|test/host/time_sync/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + # 8) 测试套件开关门禁(2026-10-01 加) # 背景:test_config.h 的 TEST_SUITE_* 决定固件里编入哪个测试套件, # 全部注释 = 产品构建。但此前**无任何门禁检查**——实测已提交的 HEAD 里 diff --git a/test/host/time_sync/run_unit_test.py b/test/host/time_sync/run_unit_test.py new file mode 100644 index 00000000..d23c58ec --- /dev/null +++ b/test/host/time_sync/run_unit_test.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""时间同步(time_sync)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/time_sync/time_sync_test.c(它以 #include 纳入 + **生产源文件** App/sync/time_sync.c),在 PC 上离线跑: + 分片延时(心跳节奏)/ 监听建立与退避 / 时间包重组状态机(分段 + 30s 预算 + + tick 回绕)/ 任务主循环(就绪屏障、accept 轮询、代次变化重监听)。 + 失败即非 0 退出。零硬件、零烧录、毫秒级。 + + 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么要主机侧单测(而不是只用板端套件): + 本模块是登记簿 N-17 的当事方——"监听重建期间心跳缺口 800ms~1s,距 IWDG 最坏 + 超时 1701ms 余量仅 ~0.7s"。缺口成因全是本模块的**纯逻辑**(分片延时的喂狗 + 节奏、500/2000ms 监听退避、200ms accept 退避、50ms 收尾、屏障等待态喂心跳)。 + 真机上只能"跑起来看复位不复位",主机侧可**逐片断言**。时间包重组状态机 + (分段送达 / 预算耗尽 / EOF / 真错误 / tick 回绕)同理。 + +用法(仓库根目录): + python test/host/time_sync/run_unit_test.py + 或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/time_sync/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:App/sync/time_sync.c 文件头注释 + docs/待处理内容.md N-17; +创建日期:2026-10-10 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +# harness 位于 test/host/common/ +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 目录必须**先于**生产目录,才能覆盖 FreeRTOS.h / task.h / +# net_socket.h / sys_events.h / sys_clock.h / dbg_log.h(真实头会拖入整套 +# FreeRTOS / CMSIS-RTOS / HAL)。task_watchdog.h 用**真实头**(App/util,只依赖 +# stdint.h),time_sync.h 用真实头(接口契约原汁)。 +BUILD = TestBuild( + name="Time Sync Tests", + src=os.path.join(HERE, "time_sync_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "App", "util"), + os.path.join(ROOT, "App", "sync"), + HERE, + ], + summary_marker="=== Time Sync Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/time_sync/stub/FreeRTOS.h b/test/host/time_sync/stub/FreeRTOS.h new file mode 100644 index 00000000..0bfaf684 --- /dev/null +++ b/test/host/time_sync/stub/FreeRTOS.h @@ -0,0 +1,34 @@ +/* + * 主机侧单测桩:FreeRTOS.h + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的最小 FreeRTOS 时基类型与宏。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * 真实头在 Middlewares/Third_Party/FreeRTOS/Source/include/,会拖入 portmacro / + * projdefs / portable 层,主机侧不引入。 + * + * 保真点:TickType_t = uint32_t;configTICK_RATE_HZ = 1000(Inc/FreeRTOSConfig.h) + * → pdMS_TO_TICKS(ms) == ms。被断言"片长 = 50ms"的用例依赖此等式。 + * + * 创建日期:2026-10-10 + */ + +#ifndef FREERTOS_H +#define FREERTOS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef uint32_t TickType_t; + +/* 与 Inc/FreeRTOSConfig.h 一致:1000 Hz → 1 tick = 1 ms */ +#define configTICK_RATE_HZ 1000U +#define pdMS_TO_TICKS(ms) ((TickType_t)((uint32_t)(ms) * (1000U / configTICK_RATE_HZ))) + +#ifdef __cplusplus +} +#endif + +#endif /* FREERTOS_H */ diff --git a/test/host/time_sync/stub/cmsis_os2.h b/test/host/time_sync/stub/cmsis_os2.h new file mode 100644 index 00000000..c49d0f59 --- /dev/null +++ b/test/host/time_sync/stub/cmsis_os2.h @@ -0,0 +1,59 @@ +/* + * 主机侧单测桩:cmsis_os2.h + * 模块功能:为 App/sync/time_sync.c(经 sys_events.h)提供主机可编译的 CMSIS-RTOS v2 + * 事件标志接口与类型。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * 真实头在 Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/,会拖入整套 + * FreeRTOS + CMSIS 内核对象,主机侧不引入。 + * + * 保真点(对齐 cmsis_os2.h): + * - osEventFlagsId_t = void*(:195)、osMutexId_t = void*(:198); + * - osStatus_t 枚举含 osOK = 0; + * - 标志位 osFlagsWaitAny=0 / osFlagsNoClear=0x2 / osFlagsError=0x80000000 + * / osFlagsErrorTimeout=0xFFFFFFFE(:153~164); + * - uint32_t osEventFlagsWait(id, flags, options, timeout)(:539)。 + * osEventFlagsWait 由测试单元给强符号桩(可注入"未就绪/超时"并驱动屏障循环)。 + * + * 创建日期:2026-10-10 + */ + +#ifndef CMSIS_OS2_H +#define CMSIS_OS2_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 不透明句柄 */ +typedef void *osEventFlagsId_t; +typedef void *osMutexId_t; + +/* 状态码(只保留被测代码用到的子集) */ +typedef enum { + osOK = 0, + osError = -1, + osErrorTimeout = -2, + osErrorResource = -3, + osErrorParameter = -4 +} osStatus_t; + +/* 事件标志选项(cmsis_os2.h:153~164) */ +#define osFlagsWaitAny 0x00000000U +#define osFlagsWaitAll 0x00000001U +#define osFlagsNoClear 0x00000002U +#define osFlagsError 0x80000000U +#define osFlagsErrorUnknown 0xFFFFFFFFU +#define osFlagsErrorTimeout 0xFFFFFFFEU +#define osFlagsErrorResource 0xFFFFFFFDU +#define osFlagsErrorParameter 0xFFFFFFFCU + +uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout); + +#ifdef __cplusplus +} +#endif + +#endif /* CMSIS_OS2_H */ diff --git a/test/host/time_sync/stub/dbg_log.h b/test/host/time_sync/stub/dbg_log.h new file mode 100644 index 00000000..e24d84bd --- /dev/null +++ b/test/host/time_sync/stub/dbg_log.h @@ -0,0 +1,31 @@ +/* + * 主机侧单测桩:dbg_log.h + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的日志宏(映射到计数)。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * + * 注意:time_sync.c 在 `#include "dbg_log.h"` 之前自行 `#define` 了 + * DBG_LINE_BUF_MAX 与 DBG_TAG(真实头会用到它们),故本桩**不得**再定义这两个宏, + * 否则触发 C4005 宏重定义告警(/W4)。 + * + * DBG_ERROR / DBG_INFO 计入计数器,供单测断言: + * - "net_socket/bind/listen FAIL"、"open listener FAIL, retry later" → ERROR + * - "no TIME packet in 30s, drop conn"、"wall clock updated"、"network re-init" → INFO + * + * 创建日期:2026-10-10 + */ + +#ifndef DBG_LOG_H +#define DBG_LOG_H + +#include + +/* 计数(在 time_sync_test.c 实现) */ +extern int g_tstest_err_count; +extern int g_tstest_info_count; + +#define DBG_INFO(fmt, ...) do { g_tstest_info_count++; } while (0) +#define DBG_DEBUG(fmt, ...) do { } while (0) +#define DBG_ERROR(fmt, ...) do { g_tstest_err_count++; } while (0) + +#endif /* DBG_LOG_H */ diff --git a/test/host/time_sync/stub/net_socket.h b/test/host/time_sync/stub/net_socket.h new file mode 100644 index 00000000..92b1df16 --- /dev/null +++ b/test/host/time_sync/stub/net_socket.h @@ -0,0 +1,71 @@ +/* + * 主机侧单测桩:net_socket.h(含内联 net_types.h 子集) + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的网络 Socket API 声明与类型。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * 真实头(Drivers/BSP/NET/net_socket.h)包含 net_types.h → net_config.h,并经 + * FreeRTOS.h / task.h 拖入整套 FreeRTOS;本桩改为包含同名桩头。 + * + * 保真点: + * - 地址族/类型/特殊地址常量与 Drivers/BSP/NET/net_types.h 逐字一致; + * - struct net_sockaddr / net_in_addr / net_sockaddr_in 字段与真实头一致 + * (被测代码写 sin_family / sin_port / sin_addr.s_addr,桩按同布局读取); + * - 7 个被测代码调用的接口签名与真实头一致(net_socket / net_bind / net_listen / + * net_accept / net_close / net_recv_to / net_get_generation / net_htons)。 + * 全部由测试单元给强符号桩(脚本化返回值 + 调用记录)。 + * + * 创建日期:2026-10-10 + */ + +#ifndef NET_SOCKET_H +#define NET_SOCKET_H + +#include + +#include "FreeRTOS.h" +#include "task.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ===== 以下取自 Drivers/BSP/NET/net_types.h(逐字) ===== */ + +#define NET_AF_INET 2 /* IPv4 */ +#define NET_SOCK_STREAM 1 /* TCP */ +#define NET_SOCK_DGRAM 2 /* UDP */ +#define NET_INADDR_ANY 0 /* 任意地址 */ +#define NET_INADDR_NONE 0xFFFFFFFF /* 无效地址 */ + +struct net_sockaddr { + uint16_t sa_family; /* 地址族 */ + char sa_data[14]; /* 地址数据 */ +}; + +struct net_in_addr { + uint32_t s_addr; /* IP 地址(网络字节序) */ +}; + +struct net_sockaddr_in { + uint16_t sin_family; /* 地址族,必须为 AF_INET */ + uint16_t sin_port; /* 端口号(网络字节序) */ + struct net_in_addr sin_addr; /* IP 地址(网络字节序) */ + char sin_zero[8]; /* 填充字节 */ +}; + +/* ===== 被测代码用到的 Socket API(签名与真实头一致) ===== */ + +int net_socket(int domain, int type); +int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen); +int net_listen(int sockfd); +int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); +int net_close(int sockfd); +int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); +uint32_t net_get_generation(void); +uint16_t net_htons(uint16_t hostshort); + +#ifdef __cplusplus +} +#endif + +#endif /* NET_SOCKET_H */ diff --git a/test/host/time_sync/stub/sys_clock.h b/test/host/time_sync/stub/sys_clock.h new file mode 100644 index 00000000..81c19706 --- /dev/null +++ b/test/host/time_sync/stub/sys_clock.h @@ -0,0 +1,29 @@ +/* + * 主机侧单测桩:sys_clock.h + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的墙钟设置接口声明。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * 真实头(App/util/sys_clock.h)含日历/闰年等无关声明;本桩只保留被测代码 + * 真正调用的那一个(签名与真实头逐字一致:int sys_clock_set_unix(uint32_t))。 + * + * 由测试单元给强符号桩,记录"是否被调用"与"最后一次的 epoch"。 + * + * 创建日期:2026-10-10 + */ + +#ifndef SYS_CLOCK_H +#define SYS_CLOCK_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int sys_clock_set_unix(uint32_t unix_secs); + +#ifdef __cplusplus +} +#endif + +#endif /* SYS_CLOCK_H */ diff --git a/test/host/time_sync/stub/sys_events.h b/test/host/time_sync/stub/sys_events.h new file mode 100644 index 00000000..3840d899 --- /dev/null +++ b/test/host/time_sync/stub/sys_events.h @@ -0,0 +1,39 @@ +/* + * 主机侧单测桩:sys_events.h + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的系统就绪事件组句柄与事件位。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * 真实头(App/app/sys_events.h)包含 cmsis_os2.h;本桩改为包含同名桩, + * 避免拖入整套 CMSIS-RTOS/FreeRTOS。 + * + * 事件位取值与真实头逐位一致(本模块只用到 SYS_EVT_NET_READY)。 + * + * 创建日期:2026-10-10 + */ + +#ifndef SYS_EVENTS_H +#define SYS_EVENTS_H + +#include "cmsis_os2.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* 系统就绪事件组句柄(真实工程由 MX_FREERTOS_Init 创建;主机侧由测试单元定义) */ +extern osEventFlagsId_t sys_evtHandle; + +/* 各子系统"就绪"事件位(与 App/app/sys_events.h 一致) */ +#define SYS_EVT_NET_READY (1u << 0) +#define SYS_EVT_STORAGE_READY (1u << 1) +#define SYS_EVT_FTP_READY (1u << 2) +#define SYS_EVT_TIMESYNC_READY (1u << 3) +#define SYS_EVT_NET_RECONFIG (1u << 4) +#define SYS_EVT_CFG_SAVE (1u << 5) +#define SYS_EVT_CONSOLE_TX (1u << 6) + +#ifdef __cplusplus +} +#endif + +#endif /* SYS_EVENTS_H */ diff --git a/test/host/time_sync/stub/task.h b/test/host/time_sync/stub/task.h new file mode 100644 index 00000000..e6b6cbc7 --- /dev/null +++ b/test/host/time_sync/stub/task.h @@ -0,0 +1,29 @@ +/* + * 主机侧单测桩:task.h + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的 FreeRTOS 任务时基接口声明。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * + * 两个接口由测试单元给强符号桩:vTaskDelay 记录每片睡眠时长(断言"≤50ms 分片"), + * xTaskGetTickCount 由测试单元提供可控时钟(驱动 30s 预算耗尽与 32 位回绕用例)。 + * + * 创建日期:2026-10-10 + */ + +#ifndef TASK_H +#define TASK_H + +#include "FreeRTOS.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void vTaskDelay(TickType_t xTicksToDelay); +TickType_t xTaskGetTickCount(void); + +#ifdef __cplusplus +} +#endif + +#endif /* TASK_H */ diff --git a/test/host/time_sync/time_sync_test.c b/test/host/time_sync/time_sync_test.c new file mode 100644 index 00000000..734c4c72 --- /dev/null +++ b/test/host/time_sync/time_sync_test.c @@ -0,0 +1,717 @@ +/* + * 模块名称:Time Sync(PC 本地时间推送接收)主机侧单元测试 + * 模块功能:在 PC 上对 App/sync/time_sync.c 做离线验证(4 个函数全覆盖: + * time_sync_delay_fed / time_sync_open_listener / time_sync_handle_conn / + * time_sync_task)。 + * + * 被测对象:真实源文件 ../../../App/sync/time_sync.c(以 #include 原样纳入, + * 保证测的是生产代码而非副本)。平台依赖由本文件提供的**可注入桩**实现: + * - FreeRTOS/task.h -> vTaskDelay / xTaskGetTickCount(可控时钟) + * - net_socket.h -> socket/bind/listen/accept/close/recv_to/ + * get_generation/htons(全部脚本化) + * - sys_events.h -> osEventFlagsWait(可注入"未就绪") + * - sys_clock.h -> sys_clock_set_unix(记录 epoch) + * - task_watchdog.h -> task_wd_beat / task_wd_bind(记录心跳节奏) + * - dbg_log.h -> DBG_ERROR/DBG_INFO 计数 + * + * 为什么要主机侧单测(而不是只用板端套件): + * 1. **本模块是 N-17 的当事方**:登记簿 N-17 记录"time_sync 监听重建期间心跳缺口 + * 800ms~1s,距 IWDG 最坏超时 1701ms 余量仅 ~0.7s"。缺口的成因全部是本模块的 + * **纯逻辑**——分片延时(delay_fed)的心跳节奏、监听重试退避(500/2000ms)、 + * accept 轮询退避(200ms)、收尾延时(50ms)、以及"屏障等待态也要喂心跳"。 + * 这些在真机上只能靠"跑起来看复位不复位"间接观察,主机侧可**逐片断言**。 + * 2. **时间包重组状态机**(handle_conn)是纯逻辑:累积到 8 字节才解析、30s 总预算 + * 随 tick 推进耗尽、TCP 分段送达、EOF/真错误/超时三种返回码分支——板端套件 + * 无法精确构造"分段 + 预算耗尽 + tick 回绕"的组合。 + * 3. 不占 MCU 的 RAM,毫秒级完成。 + * + * 覆盖点(TC-TS-xxx): + * —— time_sync_delay_fed(可喂心跳的分片延时)—— + * TC-TS-001 0ms 不睡眠不喂狗 + * TC-TS-002 恰好一片(50ms) + * TC-TS-003 非整片(120ms)切成 50/50/20,每片喂心跳 + * TC-TS-004 长延时(2000ms)= 40 片 × 50ms,每片喂心跳 + * TC-TS-005 不足一片(49ms)单片处理 + * TC-TS-006 总预算 30s = 600 片(片长上限恒 50ms) + * —— time_sync_open_listener(监听建立与重试)—— + * TC-TS-010 首次成功:socket/bind/listen 各一次,端口经 net_htons,无退避 + * TC-TS-011 bind 失败 -> 关 socket + 退避 2000ms + 留痕,随后重试成功 + * TC-TS-012 listen 失败 -> 关 socket + 退避 2000ms,随后重试成功 + * TC-TS-013 socket 连续失败 -> 5 次后返回 -1(5 条 ERROR,5×500ms 退避) + * TC-TS-014 第 3 次重试成功并返回新描述符 + * TC-TS-015 每次重试前 + 每片延时各喂一次心跳,且心跳位恒为 TIMESYNC + * —— time_sync_handle_conn(时间包重组状态机)—— + * TC-TS-020 8 字节一次收齐 -> 大端解析 epoch + 更新墙钟 + 关连接 + * TC-TS-021 分段送达(4+4)-> 累积后解析;第二次只请求剩余 12 字节 + * TC-TS-022 一次到达 16 字节 -> 只取前 8 字节 + * TC-TS-023 前缀非 "TIME" -> 不更新墙钟,但仍关连接 + * TC-TS-024 对端关闭(n==0)-> 立即退出、不解析 + * TC-TS-025 真错误(n==-1)-> 立即退出、不解析 + * TC-TS-026 只收到 4 字节后 EOF -> 不足 8 字节不解析 + * TC-TS-027 30s 预算耗尽 -> 留痕 "no TIME packet in 30s, drop conn" + * TC-TS-028 剩余预算不足一片 -> 末片按剩余(20ms)切分 + * TC-TS-029 32 位 tick 回绕 -> 预算计算仍正确 + * —— time_sync_task(任务主循环)—— + * TC-TS-040 sys_evtHandle==NULL:跳过就绪屏障;accept 轮询->成功->处理->收尾 + * TC-TS-041 就绪屏障:100ms 分片等待 NET_READY(osFlagsNoClear 不消费就绪位) + * TC-TS-042 屏障未就绪期间持续喂心跳(2026-10-02 台架根因的回归防线) + * TC-TS-043 网络代次变化 -> 关闭旧监听并重监听 + * TC-TS-044 监听建立失败 -> 5 次重试后放弃并退避 2000ms + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-10 + */ + +#include +#include +#include +#include + +/* ===================== 平台桩(先纳入,覆盖真实头) ===================== */ +#include "FreeRTOS.h" +#include "task.h" +#include "net_socket.h" +#include "sys_clock.h" +#include "sys_events.h" +#include "task_watchdog.h" /* 真实头(App/util):task_wd_id_t / TASK_WD_TIMESYNC */ + +/* ===================== FreeRTOS 桩 ===================== */ +static TickType_t s_tick; +#define DELAY_LOG_MAX 800 +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; +} + +/* ===================== 看门狗桩 ===================== */ +static int s_beat_calls; +static int s_beat_id_bad; +/* ⚠ 命名必须与 net_socket 桩的 net_bind 计数区分开:C 的"暂定定义"规则下, + * 同一编译单元里两个 `static int x;` 会被**静默合并为同一个对象**(不报错), + * 于是 task_wd_bind 与 net_bind 会共用计数器,断言全部失真(2026-10-10 踩过)。 */ +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++; + } +} + +/* ===================== sys_clock 桩 ===================== */ +static int s_set_unix_calls; +static uint32_t s_set_unix_last; + +int sys_clock_set_unix(uint32_t unix_secs) { + s_set_unix_calls++; + s_set_unix_last = unix_secs; + return 0; +} + +/* ===================== net_socket 桩 ===================== */ +static uint16_t s_htons_last; + +static int s_socket_calls; +static int s_socket_seq[8]; +static int s_socket_seq_n; +static int s_socket_default = 5; + +static int s_bind_calls; +static int s_bind_seq[8]; +static int s_bind_seq_n; +static uint16_t s_bind_port; +static uint16_t s_bind_family; + +static int s_listen_calls; +static int s_listen_seq[8]; +static int s_listen_seq_n; + +static int s_accept_calls; +static int s_accept_seq[16]; +static int s_accept_seq_n; + +static int s_close_calls; +static int s_close_socks[32]; + +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 */ + +uint16_t net_htons(uint16_t hostshort) { + s_htons_last = hostshort; + return (uint16_t)(((hostshort & 0xFFu) << 8) | (hostshort >> 8)); +} + +int net_socket(int domain, int type) { + int i = s_socket_calls++; + (void)domain; + (void)type; + return (i < s_socket_seq_n) ? s_socket_seq[i] : s_socket_default; +} + +int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { + const struct net_sockaddr_in *a = (const struct net_sockaddr_in *)addr; + int i = s_bind_calls++; + + (void)sockfd; + (void)addrlen; + s_bind_family = a->sin_family; + s_bind_port = a->sin_port; + return (i < s_bind_seq_n) ? s_bind_seq[i] : 0; +} + +int net_listen(int sockfd) { + int i = s_listen_calls++; + (void)sockfd; + return (i < s_listen_seq_n) ? s_listen_seq[i] : 0; +} + +int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) { + int i = s_accept_calls++; + (void)sockfd; + (void)addr; + (void)addrlen; + return (i < s_accept_seq_n) ? s_accept_seq[i] : -1; +} + +int net_close(int sockfd) { + if (s_close_calls < 32) { + s_close_socks[s_close_calls] = sockfd; + } + s_close_calls++; + return 0; +} + +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; +} + +/* ---- net_recv_to:脚本化返回码 + 可推进 tick ---- */ +typedef struct { + int n; /* 返回码:>0 字节数 / 0 EOF / -1 错误 / -2 超时无数据 */ + uint8_t data[16]; /* 要"送达"的字节 */ + int len; /* data 有效长度 */ + uint32_t tick_adv; /* 返回前把假时钟推进多少 ms */ +} recv_step_t; + +static recv_step_t s_recv_seq[16]; +static int s_recv_seq_n; +static int s_recv_idx; +static int s_recv_calls; +static int s_recv_default = 0; /* 脚本用尽后的返回码(0=EOF,保证循环终止) */ +static uint32_t s_recv_to_log[64]; +static int s_recv_len_log[64]; + +int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { + int i = s_recv_calls; + + (void)sockfd; + if (i < 64) { + s_recv_to_log[i] = timeout_ms; + s_recv_len_log[i] = len; + } + s_recv_calls++; + + if (s_recv_idx >= s_recv_seq_n) { + return s_recv_default; + } + + { + const recv_step_t *st = &s_recv_seq[s_recv_idx]; + int copy; + s_recv_idx++; + s_tick += st->tick_adv; + if (st->n <= 0) { + return st->n; + } + copy = (st->n <= len) ? st->n : len; + if (copy > st->len) { + copy = st->len; + } + if ((copy > 0) && (buf != NULL)) { + memcpy(buf, st->data, (size_t)copy); + } + return st->n; + } +} + +/* ===================== 断言 ===================== */ +static int s_total = 0, s_passed = 0, s_failed = 0; +int g_tstest_err_count = 0; +int g_tstest_info_count = 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、recv 脚本为空(脚本用尽返回 EOF)、 + * 事件标志恒就绪。各用例按需覆盖。 + */ +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_set_unix_calls = 0; + s_set_unix_last = 0; + + s_htons_last = 0; + s_socket_calls = 0; + s_socket_seq_n = 0; + s_socket_default = 5; + s_bind_calls = 0; + s_bind_seq_n = 0; + s_bind_port = 0; + s_bind_family = 0; + s_listen_calls = 0; + s_listen_seq_n = 0; + s_accept_calls = 0; + s_accept_seq_n = 0; + s_close_calls = 0; + s_gen_seq_n = 0; + s_gen_default = 1u; + s_gen_calls = 0; + s_gen_longjmp_at = -1; + + s_recv_seq_n = 0; + s_recv_idx = 0; + s_recv_calls = 0; + s_recv_default = 0; + + g_tstest_err_count = 0; + g_tstest_info_count = 0; +} + +/* 装配一个 recv 脚本步 */ +static void recv_set(int idx, int n, const uint8_t *data, int len, uint32_t tick_adv) { + memset(&s_recv_seq[idx], 0, sizeof(s_recv_seq[idx])); + s_recv_seq[idx].n = n; + s_recv_seq[idx].len = len; + s_recv_seq[idx].tick_adv = tick_adv; + if ((data != NULL) && (len > 0)) { + int copy = (len > 16) ? 16 : len; + memcpy(s_recv_seq[idx].data, data, (size_t)copy); + } +} + +/* 装配一个 "TIME" + uint32 大端 时间包 */ +static void make_time_packet(uint8_t *out, uint32_t epoch) { + out[0] = (uint8_t)'T'; + out[1] = (uint8_t)'I'; + out[2] = (uint8_t)'M'; + out[3] = (uint8_t)'E'; + out[4] = (uint8_t)(epoch >> 24); + out[5] = (uint8_t)(epoch >> 16); + out[6] = (uint8_t)(epoch >> 8); + out[7] = (uint8_t)(epoch); +} + +/* 检查 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; +} + +/* ===================== TC-TS-001..006:delay_fed ===================== */ + +static void tc_delay_fed(void) { + fakes_reset(); + time_sync_delay_fed(0U); + CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "TC-TS-001 delay_fed(0) 不睡眠不喂狗"); + + fakes_reset(); + time_sync_delay_fed(50U); + CHECK((s_beat_calls == 1) && (s_delay_calls == 1) && (s_delays[0] == 50U), + "TC-TS-002 delay_fed(50) = 1 片 50ms + 1 次心跳"); + + fakes_reset(); + time_sync_delay_fed(120U); + CHECK((s_beat_calls == 3) && (s_delay_calls == 3), "TC-TS-003 delay_fed(120) 切成 3 片"); + CHECK((s_delays[0] == 50U) && (s_delays[1] == 50U) && (s_delays[2] == 20U), + "TC-TS-003b 片长 50/50/20(末片为余数)"); + + fakes_reset(); + time_sync_delay_fed(2000U); + CHECK((s_delay_calls == 40) && (s_beat_calls == 40) && delays_all(50U, 40), + "TC-TS-004 delay_fed(2000) = 40 片 × 50ms,每片喂一次心跳"); + + fakes_reset(); + time_sync_delay_fed(49U); + CHECK((s_beat_calls == 1) && (s_delays[0] == 49U), "TC-TS-005 不足一片(49ms)单片处理"); + + fakes_reset(); + time_sync_delay_fed(30000U); + CHECK(s_delay_calls == 600, "TC-TS-006 总预算 30s = 600 片(片长上限恒 50ms)"); +} + +/* ===================== TC-TS-010..015:open_listener ===================== */ + +static void tc_open_listener(void) { + fakes_reset(); + { + int s = time_sync_open_listener(); + CHECK(s == 5, "TC-TS-010 open_listener 成功返回 socket 描述符"); + CHECK((s_socket_calls == 1) && (s_bind_calls == 1) && (s_listen_calls == 1), + "TC-TS-010b socket / bind / listen 各一次"); + CHECK(s_htons_last == (uint16_t)TIME_SYNC_PORT, "TC-TS-010c 端口经 net_htons(8888)"); + CHECK(s_bind_port == net_htons((uint16_t)TIME_SYNC_PORT), "TC-TS-010d sin_port 为网络序"); + CHECK(s_bind_family == (uint16_t)NET_AF_INET, "TC-TS-010e sin_family = NET_AF_INET"); + CHECK(s_delay_calls == 0, "TC-TS-010f 成功路径无退避延时"); + } + + fakes_reset(); + s_bind_seq[0] = -1; + s_bind_seq_n = 1; + { + int s = time_sync_open_listener(); + CHECK(s == 5, "TC-TS-011 bind 失败一次后重试成功"); + CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TS-011b bind 失败关闭该 socket"); + CHECK(s_delay_calls == 40, "TC-TS-011c bind 失败退避 2000ms(40 片)"); + CHECK(g_tstest_err_count == 1, "TC-TS-011d bind 失败留痕 DBG_ERROR"); + } + + fakes_reset(); + s_listen_seq[0] = -1; + s_listen_seq_n = 1; + { + int s = time_sync_open_listener(); + CHECK(s == 5, "TC-TS-012 listen 失败一次后重试成功"); + CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TS-012b listen 失败关闭该 socket"); + CHECK(s_delay_calls == 40, "TC-TS-012c listen 失败退避 2000ms"); + } + + fakes_reset(); + s_socket_default = -1; + { + int s = time_sync_open_listener(); + CHECK(s == -1, "TC-TS-013 socket 连续失败 -> 返回 -1"); + CHECK(s_socket_calls == 5, "TC-TS-013b 重试上限 5 次"); + CHECK(g_tstest_err_count == 5, "TC-TS-013c 每次失败各留痕一条(5 条 DBG_ERROR)"); + CHECK(s_delay_calls == 50, "TC-TS-013d 每次失败退避 500ms(5 × 10 片)"); + } + + fakes_reset(); + s_socket_seq[0] = -1; + s_socket_seq[1] = -1; + s_socket_seq[2] = 9; + s_socket_seq_n = 3; + { + int s = time_sync_open_listener(); + CHECK(s == 9, "TC-TS-014 第 3 次重试成功并返回新描述符"); + CHECK(s_socket_calls == 3, "TC-TS-014b 恰好尝试 3 次"); + CHECK(s_delay_calls == 20, "TC-TS-014c 两次失败退避 500ms × 2"); + } + + fakes_reset(); + s_socket_default = -1; + (void)time_sync_open_listener(); + CHECK(s_beat_calls == 55, "TC-TS-015 每轮重试(5) + 每片延时(50) 各喂一次心跳"); + CHECK(s_beat_id_bad == 0, "TC-TS-015b 心跳位恒为 TASK_WD_TIMESYNC"); +} + +/* ===================== TC-TS-020..029:handle_conn ===================== */ + +static void tc_handle_conn(void) { + uint8_t pkt[16]; + + /* 1) 8 字节一次收齐 */ + fakes_reset(); + make_time_packet(pkt, 0x12345678u); + recv_set(0, 8, pkt, 8, 0U); + s_recv_seq_n = 1; + s_tick = 100U; + time_sync_handle_conn(7); + CHECK(s_set_unix_calls == 1, "TC-TS-020 TIME 包一次收齐 -> 更新墙钟一次"); + CHECK(s_set_unix_last == 0x12345678u, "TC-TS-020b epoch 按大端解析"); + CHECK((s_recv_calls == 1) && (s_recv_len_log[0] == 16), "TC-TS-020c 首次请求 16 字节"); + CHECK(s_recv_to_log[0] == 50U, "TC-TS-020d 单次等待片长 = 50ms"); + CHECK((s_close_calls == 1) && (s_close_socks[0] == 7), "TC-TS-020e 处理完关闭连接"); + CHECK(g_tstest_info_count == 1, "TC-TS-020f 留痕 wall clock updated"); + + /* 2) 分段送达:4 + 4 */ + fakes_reset(); + make_time_packet(pkt, 10u); + recv_set(0, 4, pkt, 4, 10U); + recv_set(1, 4, pkt + 4, 4, 10U); + s_recv_seq_n = 2; + time_sync_handle_conn(3); + CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 10u), "TC-TS-021 分段送达累积后解析正确"); + CHECK(s_recv_calls == 2, "TC-TS-021b 读了两次"); + CHECK((s_recv_len_log[0] == 16) && (s_recv_len_log[1] == 12), + "TC-TS-021c 第二次只请求剩余 12 字节"); + CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), "TC-TS-021d 每片 50ms(预算充裕)"); + + /* 3) 16 字节一次到达(只取前 8) */ + fakes_reset(); + make_time_packet(pkt, 5u); + recv_set(0, 16, pkt, 16, 0U); + s_recv_seq_n = 1; + time_sync_handle_conn(4); + CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 5u), "TC-TS-022 超长包只取前 8 字节"); + CHECK(s_recv_calls == 1, "TC-TS-022b 一次读够即停"); + + /* 4) 前缀不是 TIME */ + fakes_reset(); + pkt[0] = (uint8_t)'X'; + pkt[1] = (uint8_t)'X'; + pkt[2] = (uint8_t)'X'; + pkt[3] = (uint8_t)'X'; + pkt[4] = 0U; + pkt[5] = 0U; + pkt[6] = 0U; + pkt[7] = 1U; + recv_set(0, 8, pkt, 8, 0U); + s_recv_seq_n = 1; + time_sync_handle_conn(4); + CHECK(s_set_unix_calls == 0, "TC-TS-023 非 TIME 前缀不更新墙钟"); + CHECK(s_close_calls == 1, "TC-TS-023b 仍关闭连接"); + + /* 5) 对端关闭(EOF) */ + fakes_reset(); + recv_set(0, 0, NULL, 0, 0U); + s_recv_seq_n = 1; + time_sync_handle_conn(6); + CHECK((s_set_unix_calls == 0) && (s_recv_calls == 1), "TC-TS-024 对端关闭(EOF)立即退出不解析"); + CHECK(s_close_calls == 1, "TC-TS-024b 关闭连接"); + + /* 6) 真错误 */ + fakes_reset(); + recv_set(0, -1, NULL, 0, 0U); + s_recv_seq_n = 1; + time_sync_handle_conn(6); + CHECK((s_recv_calls == 1) && (s_set_unix_calls == 0), "TC-TS-025 真错误立即退出不解析"); + + /* 7) 4 字节后 EOF */ + fakes_reset(); + make_time_packet(pkt, 7u); + recv_set(0, 4, pkt, 4, 0U); + recv_set(1, 0, NULL, 0, 0U); + s_recv_seq_n = 2; + time_sync_handle_conn(6); + CHECK(s_set_unix_calls == 0, "TC-TS-026 不足 8 字节不解析"); + CHECK(s_recv_calls == 2, "TC-TS-026b 继续等待后续字节"); + + /* 8) 30s 预算耗尽 */ + fakes_reset(); + recv_set(0, -2, NULL, 0, 30000U); + s_recv_seq_n = 1; + time_sync_handle_conn(6); + CHECK(s_recv_calls == 1, "TC-TS-027 预算耗尽后不再读(只读 1 次)"); + CHECK(g_tstest_info_count == 1, "TC-TS-027b 留痕 no TIME packet in 30s, drop conn"); + CHECK(s_close_calls == 1, "TC-TS-027c 仍关闭连接"); + + /* 9) 剩余预算不足一片 */ + fakes_reset(); + recv_set(0, -2, NULL, 0, 29980U); + s_recv_seq_n = 1; + time_sync_handle_conn(6); + CHECK(s_recv_calls == 2, "TC-TS-028 两次等待后结束"); + CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 20U), + "TC-TS-028b 第二次片长 = 剩余 20ms"); + + /* 10) 32 位 tick 回绕 */ + fakes_reset(); + s_tick = 0xFFFFFF00U; + recv_set(0, -2, NULL, 0, 512U); /* 推进后回绕到 0x00000100 */ + s_recv_seq_n = 1; + time_sync_handle_conn(6); + CHECK((s_recv_calls == 2) && (s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), + "TC-TS-029 32 位 tick 回绕后预算计算仍正确"); +} + +/* ===================== TC-TS-040..044:time_sync_task ===================== */ + +static void tc_task(void) { + uint8_t pkt[16]; + + /* A) 无就绪屏障(sys_evtHandle == NULL):accept 轮询 2 次失败后成功 */ + fakes_reset(); + make_time_packet(pkt, 9u); + recv_set(0, 8, pkt, 8, 0U); + s_recv_seq_n = 1; + s_accept_seq[0] = -1; + s_accept_seq[1] = -1; + s_accept_seq[2] = 7; + s_accept_seq_n = 3; + /* gen 调用序:iter1 顶部(1) + accept 两次失败各一次(2,3) + iter2 顶部(4) → 第 4 次跳出 */ + s_gen_longjmp_at = 4; + if (setjmp(s_jmp) == 0) { + time_sync_task(NULL); + } + CHECK((s_wd_bind_calls == 1) && (s_wd_bind_id_bad == 0), + "TC-TS-040 任务入口登记心跳位(task_wd_bind 一次)"); + CHECK(s_evt_calls == 0, "TC-TS-040b sys_evtHandle==NULL 时跳过就绪屏障"); + CHECK(s_accept_calls == 3, "TC-TS-040c accept 轮询 3 次(2 次无连接 + 1 次成功)"); + CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 9u), "TC-TS-040d 收到时间包并更新墙钟"); + CHECK(s_close_calls == 1, "TC-TS-040e 连接被关闭一次"); + CHECK(s_delay_calls == 9, "TC-TS-040f 延时片数 = 4+4+1(200+200+50ms)"); + CHECK(delays_all(50U, 9), "TC-TS-040g 所有延时均按 50ms 分片"); + CHECK(s_beat_calls == 13, "TC-TS-040h 心跳次数 = 主循环1 + 退避8 + 收连接1 + 收尾1 + 次轮1"); + CHECK(s_beat_id_bad == 0, "TC-TS-040i 所有心跳均打 TIMESYNC 位"); + + /* B) 就绪屏障: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; + if (setjmp(s_jmp) == 0) { + time_sync_task(NULL); + } + CHECK(s_evt_calls == 3, "TC-TS-041 就绪屏障轮询 3 次(2 次未就绪 + 1 次就绪)"); + CHECK(s_evt_last_timeout == 100U, "TC-TS-041b 屏障等待按 100ms 分片"); + CHECK(s_evt_last_flags == SYS_EVT_NET_READY, "TC-TS-041c 等待 NET_READY 位"); + CHECK(s_evt_last_options == (osFlagsWaitAny | osFlagsNoClear), "TC-TS-041d 就绪位不被消费"); + CHECK(s_beat_calls == 3, "TC-TS-041e 未就绪的两次各喂一次心跳(+主循环 1 次)"); + CHECK(s_wd_bind_calls == 1, "TC-TS-041f 仍先登记心跳路由"); + + /* C) 屏障期间持续喂心跳(就绪位长期不置位) */ + 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, "TC-TS-042 屏障等待 4 次均未就绪(循环未挂死)"); + CHECK(s_beat_calls == 3, "TC-TS-042b 每次未就绪后各喂一次心跳(第 4 次跳出前)"); + + /* D) 网络代次变化 -> 关闭旧监听并重监听 */ + 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 = 4; + if (setjmp(s_jmp) == 0) { + time_sync_task(NULL); + } + CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TS-043 代次变化时关闭旧监听"); + /* INFO 三条:两次 open_listener 成功各一条 "TCP listen on" + 一条 "network re-init, relisten" */ + CHECK(g_tstest_info_count == 3, "TC-TS-043b 留痕 network re-init, relisten(另含 2 条 listen 成功)"); + CHECK(s_accept_calls == 2, "TC-TS-043c 每次循环只轮询一次 accept 即放弃"); + CHECK((s_socket_calls == 2) && (s_bind_calls == 2) && (s_listen_calls == 2), + "TC-TS-043d 第二次循环重建了监听"); + + /* E) 监听建立失败 -> 5 次重试后放弃并退避 2000ms */ + fakes_reset(); + s_socket_default = -1; + s_gen_longjmp_at = 2; + if (setjmp(s_jmp) == 0) { + time_sync_task(NULL); + } + CHECK(s_socket_calls == 5, "TC-TS-044 open_listener 内部重试 5 次后放弃"); + CHECK(g_tstest_err_count == 6, "TC-TS-044b 5 条 net_socket FAIL + 1 条 retry later"); + CHECK(s_delay_calls == 90, "TC-TS-044c 退避 5×500ms + 2000ms = 90 片"); + CHECK(s_beat_calls == 97, "TC-TS-044d 心跳次数 = 2 + 5 + 50 + 40"); + CHECK(s_beat_id_bad == 0, "TC-TS-044e 所有心跳均打 TIMESYNC 位"); +} + +/* ===================== main ===================== */ + +int main(void) { + printf("=== Time Sync Tests ===\n"); + + printf("-- delay_fed (heartbeat-fed slicing) --\n"); + tc_delay_fed(); + printf("-- open_listener (retry / backoff) --\n"); + tc_open_listener(); + printf("-- handle_conn (packet reassembly / budget) --\n"); + tc_handle_conn(); + printf("-- time_sync_task (barrier / accept / relisten) --\n"); + tc_task(); + + printf("\n=== Time Sync Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/vol_lock/run_unit_test.py b/test/host/vol_lock/run_unit_test.py new file mode 100644 index 00000000..17db483c --- /dev/null +++ b/test/host/vol_lock/run_unit_test.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""卷锁(vol_lock)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/vol_lock/vol_lock_test.c(它以 #include 纳入 + **生产源文件** App/fs/vol_lock.c),在 PC 上离线跑创建/幂等/等待/超时/ + **fail-open**/释放,失败即非 0 退出。零硬件、零烧录、毫秒级。 + + 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么要主机侧单测(而不是只用板端套件): + 本模块最要紧的语义是 **fail-open**——锁创建失败时 vol_lock_wait 必须返回 1 + 且**绝不阻塞**(defaultTask 是看门狗喂狗点,等锁 = 停喂 = IWDG 误复位)。 + 真机上 osMutexNew 失败几乎不可复现(需堆耗尽),板端套件无法构造;主机侧 + 一次钉死。幂等性 / 超时透传 / 未创建时 release 无害 同属纯状态逻辑。 + +用法(仓库根目录): + python test/host/vol_lock/run_unit_test.py + 或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/vol_lock/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:App/fs/vol_lock.h 文件头「使用约定(铁律)」; +创建日期:2026-10-10 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +# harness 位于 test/host/common/ +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 目录必须**先于**生产目录,才能覆盖 cmsis_os.h +# (真实头在 Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/,会拖入整套 +# FreeRTOS)。vol_lock.h 用真实头(接口契约原汁);三个 osMutex* 由测试单元给强符号桩。 +BUILD = TestBuild( + name="Volume Lock Tests", + src=os.path.join(HERE, "vol_lock_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "App", "fs"), + HERE, + ], + summary_marker="=== Volume Lock Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/vol_lock/stub/cmsis_os.h b/test/host/vol_lock/stub/cmsis_os.h new file mode 100644 index 00000000..51f1bc3a --- /dev/null +++ b/test/host/vol_lock/stub/cmsis_os.h @@ -0,0 +1,56 @@ +/* + * 主机侧单测桩:cmsis_os.h(CMSIS-RTOS v1 兼容层) + * 模块功能:为 App/fs/vol_lock.c 提供主机可编译的 CMSIS-RTOS 互斥量接口。 + * + * ⚠ 仅供 test/host/vol_lock/ 使用,不参与 MCU 构建。 + * 真实头来自 Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h, + * 会拖入整套 FreeRTOS(FreeRTOS.h / task.h / queue.h / semphr.h …),主机侧不引入。 + * + * 保真点(对齐 cmsis_os.h + cmsis_os2.h): + * - osMutexId_t = void*(cmsis_os2.h:198); + * - osMutexNew(const osMutexAttr_t *attr)(cmsis_os2.h:552); + * - osMutexWait(osMutexId_t, uint32_t) —— 真实头是 `#define osMutexWait osMutexAcquire`, + * 两者签名一致(cmsis_os.h:617); + * - osMutexRelease(osMutexId_t)(cmsis_os.h:626); + * - osStatus_t 枚举含 osOK = 0(cmsis_os.h:204)。 + * 三个接口由测试单元给强符号桩(可注入"创建失败 / 等待超时")。 + * + * 创建日期:2026-10-10 + */ + +#ifndef CMSIS_OS_H +#define CMSIS_OS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 不透明句柄(真实定义见 cmsis_os2.h:195/198) */ +typedef void *osMutexId_t; +typedef void *osEventFlagsId_t; + +/* 状态码(cmsis_os.h:204 起的枚举;此处只保留被测代码用到的子集) */ +typedef enum { + osOK = 0, /* 成功 */ + osError = -1, /* 通用错误 */ + osErrorTimeout = -2, /* 超时 */ + osErrorResource = -3, /* 资源不可用 */ + osErrorParameter = -4 /* 参数错误 */ +} osStatus_t; + +/* 互斥量属性:被测代码只传 NULL,故仅占位 */ +typedef struct { + uint32_t attr_bits; +} osMutexAttr_t; + +osMutexId_t osMutexNew(const osMutexAttr_t *attr); +osStatus_t osMutexWait(osMutexId_t mutex_id, uint32_t millisec); +osStatus_t osMutexRelease(osMutexId_t mutex_id); + +#ifdef __cplusplus +} +#endif + +#endif /* CMSIS_OS_H */ diff --git a/test/host/vol_lock/vol_lock_test.c b/test/host/vol_lock/vol_lock_test.c new file mode 100644 index 00000000..5c48416d --- /dev/null +++ b/test/host/vol_lock/vol_lock_test.c @@ -0,0 +1,224 @@ +/* + * 模块名称:Volume Lock(卷锁)主机侧单元测试 + * 模块功能:在 PC 上对 App/fs/vol_lock.c 做离线验证(3 个公开函数全覆盖)。 + * + * 被测对象:真实源文件 ../../../App/fs/vol_lock.c(以 #include 原样纳入, + * 保证测的是生产代码而非副本)。平台依赖由本文件提供的**可注入桩**实现: + * - cmsis_os.h -> osMutexNew / osMutexWait / osMutexRelease + * (可注入 创建失败 / 等待超时 / 获取成功) + * + * 为什么要主机侧单测(而不是只用板端套件): + * 1. 本模块最要紧的语义是 **fail-open**(锁创建失败时 `vol_lock_wait` 必须返回 1 + * 且**绝不阻塞**)——这是"defaultTask 喂狗点绝不能因等锁而停摆"的最后一道保险。 + * 真机上 `osMutexNew` 失败几乎不可能复现(需堆耗尽),板端套件无法构造。 + * 2. 幂等性(重复 `vol_lock_init` 不得重复创建)、等待超时透传、未创建时 release 无害, + * 都是纯状态逻辑,主机侧一次钉死。 + * 3. 不占 MCU 的 RAM,毫秒级完成。 + * + * 覆盖点: + * TC-VL-001 init 首次调用创建互斥量并返回 0 + * TC-VL-002 init 幂等(重复调用不再创建) + * TC-VL-003 init 在 osMutexNew 失败时返回 -1 + * TC-VL-004 init 失败后可重试成功(s_lock 未被污染) + * TC-VL-005 wait 成功后返回 1,且把 timeout 原样透传给 osMutexWait + * TC-VL-006 wait 超时(osMutexWait != osOK)返回 0 + * TC-VL-007 wait 在锁未创建时自动 init,再正常获取 + * TC-VL-008 ⭐ fail-open:锁未创建且创建失败 -> 返回 1 且**不调用** osMutexWait + * TC-VL-009 fail-open 之后 release 无害(s_lock 仍为 NULL,不调用 osMutexRelease) + * TC-VL-010 已创建时 wait 不再调用 osMutexNew + * TC-VL-011 release 在已持锁时调用 osMutexRelease 一次 + * TC-VL-012 release 在锁未创建时静默返回(不崩溃、不调用) + * TC-VL-013 完整周期 init -> wait -> release -> wait -> release + * TC-VL-014 wait(0) 的 timeout 透传为 0(defaultTask 的 try 语义) + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-10 + */ + +#include +#include +#include + +/* ===================== 平台桩(先纳入,覆盖真实头) ===================== */ +#include "cmsis_os.h" + +/* ===================== CMSIS-RTOS 互斥量桩 ===================== + * s_new_fail osMutexNew 返回 NULL(注入创建失败) + * s_wait_ok 0 -> osMutexWait 返回 osErrorTimeout(注入超时) + * s_fake_mutex 成功创建时返回的伪句柄(非 NULL 即可) + * s_last_timeout 记录 osMutexWait 收到的 millisec(断言透传) + */ +static int s_new_calls; +static int s_new_fail; +static int s_wait_calls; +static int s_wait_ok; +static int s_release_calls; +static uint32_t s_last_timeout; +static void *s_fake_mutex = (void *)0x1234; + +osMutexId_t osMutexNew(const osMutexAttr_t *attr) { + (void)attr; + s_new_calls++; + return s_new_fail ? NULL : (osMutexId_t)s_fake_mutex; +} + +osStatus_t osMutexWait(osMutexId_t mutex_id, uint32_t millisec) { + (void)mutex_id; + s_wait_calls++; + s_last_timeout = millisec; + return s_wait_ok ? osOK : osErrorTimeout; +} + +osStatus_t osMutexRelease(osMutexId_t mutex_id) { + (void)mutex_id; + s_release_calls++; + return osOK; +} + +/* ===================== 断言 ===================== */ +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 "vol_lock.c" + +/* ===================== 工具 ===================== */ + +/* + * 函数功能:把全部桩与模块静态态复位到"刚上电"初值 + * 入口参数:无 + * 返 回 值:无 + * 限定条件:无 + * 函数说明:`s_lock` 是 vol_lock.c 的文件静态量(源文件已被纳入同一编译单元), + * 直接置 NULL 等价于"未初始化过"——这是白盒单测的可接受代价。 + */ +static void fakes_reset(void) { + s_new_calls = 0; + s_new_fail = 0; + s_wait_calls = 0; + s_wait_ok = 1; + s_release_calls = 0; + s_last_timeout = 0xFFFFFFFFu; + s_fake_mutex = (void *)0x1234; + s_lock = NULL; +} + +/* ===================== TC-VL-001..004:vol_lock_init ===================== */ + +static void tc_init(void) { + fakes_reset(); + CHECK(vol_lock_init() == 0, "TC-VL-001 init 首次调用返回 0"); + CHECK(s_new_calls == 1, "TC-VL-001b init 创建互斥量一次(osMutexNew)"); + CHECK(s_lock != NULL, "TC-VL-001c 模块静态态已持有句柄"); + + CHECK(vol_lock_init() == 0, "TC-VL-002 init 重复调用仍返回 0(幂等)"); + CHECK(s_new_calls == 1, "TC-VL-002b 幂等:不再创建第二个互斥量"); + + fakes_reset(); + s_new_fail = 1; + CHECK(vol_lock_init() == -1, "TC-VL-003 osMutexNew 失败 -> init 返回 -1"); + CHECK(s_lock == NULL, "TC-VL-003b 失败时不留下半初始化句柄"); + + s_new_fail = 0; + CHECK(vol_lock_init() == 0, "TC-VL-004 失败后重试可成功(返回 0)"); + CHECK(s_new_calls == 2, "TC-VL-004b 重试确实再次调用 osMutexNew"); +} + +/* ===================== TC-VL-005..010:vol_lock_wait ===================== */ + +static void tc_wait(void) { + /* --- 已创建:超时值原样透传 --- */ + fakes_reset(); + (void)vol_lock_init(); + CHECK(vol_lock_wait(100U) == 1, "TC-VL-005 wait 获取成功返回 1"); + CHECK(s_wait_calls == 1, "TC-VL-005b osMutexWait 被调用一次"); + CHECK(s_last_timeout == 100U, "TC-VL-005c timeout 原样透传给 osMutexWait"); + + fakes_reset(); + (void)vol_lock_init(); + CHECK(vol_lock_wait(0U) == 1, "TC-VL-014 wait(0) 获取成功返回 1(try 语义)"); + CHECK(s_last_timeout == 0U, "TC-VL-014b wait(0) 的 timeout 透传为 0"); + + /* --- 超时:osMutexWait != osOK -> 返回 0 --- */ + fakes_reset(); + (void)vol_lock_init(); + s_wait_ok = 0; + CHECK(vol_lock_wait(50U) == 0, "TC-VL-006 osMutexWait 非 osOK -> wait 返回 0"); + CHECK(s_wait_calls == 1, "TC-VL-006b 超时路径确实尝试过等待"); + + /* --- 锁未创建:自动 init 后正常获取 --- */ + fakes_reset(); + CHECK(vol_lock_wait(10U) == 1, "TC-VL-007 锁未创建时 wait 自动 init 并获取"); + CHECK(s_new_calls == 1, "TC-VL-007b 自动 init 创建了互斥量"); + CHECK(s_wait_calls == 1, "TC-VL-007c 随后执行了一次等待"); + + /* --- ⭐ fail-open:创建失败必须返回 1 且绝不阻塞 --- */ + fakes_reset(); + s_new_fail = 1; + CHECK(vol_lock_wait(0U) == 1, "TC-VL-008 fail-open:锁创建失败仍返回 1(不阻塞喂狗点)"); + CHECK(s_wait_calls == 0, "TC-VL-008b fail-open 时**不调用** osMutexWait(零阻塞)"); + CHECK(s_lock == NULL, "TC-VL-008c fail-open 后仍无句柄"); + + /* --- fail-open 之后 release 无害 --- */ + vol_lock_release(); + CHECK(s_release_calls == 0, "TC-VL-009 fail-open 后 release 不调用 osMutexRelease(判空静默)"); + + /* --- 已创建时 wait 不再创建 --- */ + fakes_reset(); + (void)vol_lock_init(); + (void)vol_lock_wait(0U); + (void)vol_lock_wait(0U); + CHECK(s_new_calls == 1, "TC-VL-010 多次 wait 不重复创建互斥量"); + CHECK(s_wait_calls == 2, "TC-VL-010b 每次 wait 各等待一次"); +} + +/* ===================== TC-VL-011..013:vol_lock_release ===================== */ + +static void tc_release(void) { + /* --- 已持锁:释放一次 --- */ + fakes_reset(); + (void)vol_lock_init(); + (void)vol_lock_wait(0U); + vol_lock_release(); + CHECK(s_release_calls == 1, "TC-VL-011 已持锁时 release 调用 osMutexRelease 一次"); + + /* --- 锁未创建:静默返回 --- */ + fakes_reset(); + vol_lock_release(); + CHECK(s_release_calls == 0, "TC-VL-012 锁未创建时 release 静默返回(不调用、不崩溃)"); + + /* --- 完整周期 --- */ + fakes_reset(); + CHECK(vol_lock_init() == 0, "TC-VL-013 周期:init -> 0"); + CHECK(vol_lock_wait(0U) == 1, "TC-VL-013b 周期:wait -> 1"); + vol_lock_release(); + CHECK(vol_lock_wait(100U) == 1, "TC-VL-013c 周期:再 wait -> 1(可重复获取)"); + vol_lock_release(); + CHECK(s_new_calls == 1, "TC-VL-013d 周期:全程只创建一次互斥量"); + CHECK(s_release_calls == 2, "TC-VL-013e 周期:释放两次"); + CHECK(s_last_timeout == 100U, "TC-VL-013f 周期:第二次 wait 的 timeout 透传正确"); +} + +/* ===================== main ===================== */ + +int main(void) { + printf("=== Volume Lock Tests ===\n"); + + printf("-- vol_lock_init --\n"); + tc_init(); + printf("-- vol_lock_wait (incl. fail-open) --\n"); + tc_wait(); + printf("-- vol_lock_release --\n"); + tc_release(); + + printf("\n=== Volume Lock Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +}