diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index 2d25a9ab..1677e8c7 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -226,22 +226,25 @@ `test/host/lftpd`(35 条) | `lftpd_inet.c` **127/127 = 100%**、`lftpd_io.c` 85/159、 `lftpd.c` **304/473 = 64.3%**(控制通道闸门 + 17 条命令 + 被动模式 + 传输收尾都进来了; 还剩 `lftpd_start` 主循环与 `send_list`/`send_file` 的分批边界分支) | -| NET 四块之一:`net_select.c` | ✅ 完成 | `test/host/net_select`(21 条) | `net_select.c` 97/114 = **85.1%** | -| `lftpd.c` / `lftpd_inet.c` | ⬜ 未开始 | — | 需假 `net_socket` 层(`time_sync`/stub 那份可扩展) | -| NET 三块之一:`net_listener.c` | ✅ 完成(**125/126 = 99.2%**) | +| NET 拆出的四块 ①:`net_select.c` | ✅ 完成 | `test/host/net_select`(21 条) | `net_select.c` 97/114 = **85.1%** | +| NET 拆出的四块 ②:`net_listener.c` | ✅ 完成(**125/126 = 99.2%**) | `test/host/net_listener`(34 条)+ 共享假芯片 `test/host/common/ch395f_fake.{h,c}` | 四道时间闸门全钉住(30ms 退避 / 400 与 100ms 两级沉降 / 800ms 上限只留痕 / 2000ms 预算交对账器),另有接收区 256B 钳位与 16 轮上限、"清接收→关断→读即清→读状态"的 调用顺序、五种静默放弃条件 | -| NET 三块之二:`net_pending.c` | ✅ 完成(**169/194 = 87.1%**) | `test/host/net_pending`(43 条) | +| NET 拆出的四块 ③:`net_pending.c` | ✅ 完成(**169/194 = 87.1%**) | `test/host/net_pending`(43 条) | 跨上下文契约(**错误码走任务通知值**,靠可观测的 `xTaskNotify` 假件才测得到)、 收尾四条分支(不丢尾包 / 干净报 0 / 异常释放报 CONNRESET / 槽位没了报 CONNRESET)、 发送分块钳在 `NET_SEND_CHUNK_MAX=1024`(真值,非假件自造)、`deadline` 每轮续期 (长文件不被 1000ms 掐死)、停滞超时、取消与 `clear` 不发通知的纪律 | -| NET 三块之三:`net_events.c` | ✅ 完成 | `test/host/net_events`(34 条) | 陈旧中断防误判 | +| NET 拆出的四块 ④:`net_events.c` | ✅ 完成 | `test/host/net_events`(34 条) | 陈旧中断防误判 | (芯片说还在连接就不许拆活会话)、疑似误报延后、超时标异常释放(正是 `net_pending` 收尾报 CONNRESET 的那条输入,**跨文件因果两头都能离线跑才测得到**)、丢中断自愈, 以及对账器的 100ms 周期闸门 / 稳定计数 4·2·1 三档 / 1000ms 冷却 / U11 重同步 | +| 104 服务层:`iec104_serv.c` | ✅ 完成(**117/139 = 84.2%**) | `test/host/iec104_serv`(28 条) | + 服务生命周期:代次一变就重建监听、accept 门控与表项失效自愈、单连接断线只关数据口 + (与监听同号时绝不关)、连续短写判死(成功即复位)、32 分频探活、协议掉回 IDLE 丢弃 + 重生、时钟桥接两个溢出边界(>4G 秒拒绝、未对时回哨兵) | 本批照出的两处**现状**(只登记、**未改语义**;全库唯一调用点 `test/board/net_test_task.c` 传的是 NULL 与非零 tv,今天没有受害者): @@ -267,6 +270,17 @@ 风险在"以后有人拿它去尾部空白"。修它一行即可,但会改变 FTP 参数(含空格的文件名/ 口令)的可见行为,需与 FTP 域一起过 → 现在只用 TC-LFTSTR-005 把现状钉成"改了会响"。 +**P1 收官(2026-10-23 同日,13 个文件全部进分母)**:新增 12 个主机单测模块 +(`net_cfg` / `ftp_cfg` / `prot_set_store` / `task_prof` / `lftpd_io` / +`net_select` / `net_listener` / `net_pending` / `net_events` / `lftpd_inet` / +`lftpd` / `iec104_serv`),共 **308 条断言**;套件 16 → **28** 全绿, +`files_tested` 19 → **32**,未触及 `.c` 47 → **34**(恒等式 `34 = 0(P1) + 5(P2) + 29(P3)`), +分母 2,893 → **4,684** 插桩行,`line=4020/4684(85.8%)`、`func=300/333(90.1%)`。 +百分比从 88.9% 一路"降"到 85.8% —— **这仍然是摊薄不是回退**:`lftpd_io.c`(53.5%) 与 +`lftpd.c`(64.3%) 这类只测了主体的文件也进了分母;按 §5 的口径,这一路该盯的是 +`files_tested`(19→32)与 `files_untested`(125→112),而不是 `line_rate`。 +其中 `lftpd_inet.c` 达到 **100%**(127/127),`net_listener.c` 99.2%、 +`net_events.c` 97.8%。 ### 三处死路径的处置(2026-10-23,用户逐条裁定后实施) 三处都不是改动引入的回归,而是**从没被执行到的分支**:生产路径上要么没有调用者、 diff --git a/scripts/id_registry.json b/scripts/id_registry.json index 55fd0dca..5d704efa 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -1856,6 +1856,28 @@ "subitem_ok": true, "owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);2026-10-23 随覆盖率口径 §9 P1 FTP 批次补登记", "cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌" + }, + { + "name": "TC-HOST-I104SRV", + "class": "TC", + "title": "主机侧单测用例号:IEC 104 上行服务生命周期(test/host/iec104_serv)", + "pattern": "\\bTC-I104SRV-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/iec104_serv/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-I104SRV-041", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);2026-10-23 随覆盖率口径 §9 P1 收官批次补登记", + "cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌" } ], "unmanaged_bare": { diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index eb75b817..30ca48fc 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -120,6 +120,14 @@ "lines_covered": 99, "lines_valid": 99 }, + "App/task/iec104_serv.c": { + "func_rate": 0.875, + "funcs_covered": 7, + "funcs_valid": 8, + "line_rate": 0.8417, + "lines_covered": 117, + "lines_valid": 139 + }, "App/util/sha256.c": { "func_rate": 1.0, "funcs_covered": 7, @@ -310,6 +318,13 @@ "lines_total": 1193, "ok": true }, + "iec104_serv": { + "files": { + "App/task/iec104_serv.c": 139 + }, + "lines_total": 139, + "ok": true + }, "lftpd": { "files": { "Drivers/BSP/NET/lftpd/lftpd.c": 473, @@ -478,6 +493,7 @@ "App/protocol/iec104/iec104_link.c", "App/protocol/iec104/iec104_pts.c", "App/sync/time_sync.c", + "App/task/iec104_serv.c", "App/util/sha256.c", "App/util/sys_clock.c", "App/util/task_prof.c", @@ -497,16 +513,16 @@ ], "totals": { "files_generated_excluded": 5, - "files_tested": 31, - "files_untested": 113, - "func_rate": 0.9015, - "funcs_covered": 293, - "funcs_valid": 325, - "line_rate": 0.8587, - "lines_covered": 3903, - "lines_valid": 4545, - "modules_ok": 27, - "modules_total": 27 + "files_tested": 32, + "files_untested": 112, + "func_rate": 0.9009, + "funcs_covered": 300, + "funcs_valid": 333, + "line_rate": 0.8582, + "lines_covered": 4020, + "lines_valid": 4684, + "modules_ok": 28, + "modules_total": 28 }, "untested_sources": [ { @@ -561,10 +577,6 @@ "code_lines": 204, "path": "Drivers/BSP/UI/ui_standby.c" }, - { - "code_lines": 202, - "path": "App/task/iec104_serv.c" - }, { "code_lines": 171, "path": "App/task/adc_task.c" diff --git a/test/host/iec104_serv/iec104_serv_test.c b/test/host/iec104_serv/iec104_serv_test.c new file mode 100644 index 00000000..3746c0e8 --- /dev/null +++ b/test/host/iec104_serv/iec104_serv_test.c @@ -0,0 +1,527 @@ +/* + * 模块名称:IEC 60870-5-104 上行服务生命周期(iec104_serv)主机侧单元测试 + * 模块功能:在 PC 上离线验证 App/task/iec104_serv.c 的服务侧骨架:监听建立与失败 + * 回收、**网络代次一变就重建监听**(改了 IP 端口必须在新地址上继续服务)、 + * accept 门控与表项失效自愈、单连接 socket 的断线语义(对端 FIN 只关数据 + * 口、绝不关监听)、连续短写判死、活性分频探活、协议状态掉回 IDLE 时丢 + * 连接并立即重建,以及时钟桥接的两个溢出边界(>4G 秒拒绝、未对时返回哨兵 + * 而不是把 1970 当真实时间)。 + * + * 被测对象:真实源文件 ../../../App/task/iec104_serv.c(#include 纳入,故它的 static + * s_bind_listen / s_drop_conn / s_service_link / s_serv_poll / s_send_apdu / + * s_clock_sync / s_epoch_ms 都能直接驱动——**不跑 iec104_serv_task 的无限 + * 主循环**,那是 never-returning 的任务体,不适合单测)。 + * iec104_link.h / iec104_pts.h / prot_set.h / sys_clock.h / sys_events.h / + * task_watchdog.h / net_types.h 全用**真实头**(iec104_state_t 枚举、 + * iec104_link_cfg_t 函数指针签名、TASK_WD_IEC104=7、SYS_EVT_* 位值一改即 + * 编译期报错);链路层(iec104_link_*)与 pts 层给假件——链路协议本身有 + * test/host/iec104 的 97 条专测,本模块只管"服务怎么活、怎么死、怎么重生"。 + * + * 为什么要主机侧单测(而不是只靠台架): + * 1. 104 主站掉线重连是**现场最常见的故障形态**,但台架上没法按需制造"改 IP 时 + * 正好有连接""对端 FIN 与 RST 的区别""连续三次短写"这些时序;而这里每一条 + * 处理错的后果都是端口永久失联或会话卡死,只能靠主站侧超时才发现。 + * 2. 判死用**连续**短写计数(一次成功即复位),写成"累计"就会把偶发拥塞误判成 + * 断线,主站看到的就是莫名掉线——这条只有能精确注入短写/成功序列才可测。 + * 3. 时钟桥接的两个边界:epoch 秒超过 32 位必须拒绝(否则截断成 2106 年前的某个 + * 乱值写进 RTC),未对时(sys_clock_get 返回 UINT32_MAX)必须回哨兵而不是 0, + * 否则对端会以为设备时间是 1970-01-01。 + * 4. 活性分频(每 32 轮才探一次)是 CPU 预算的取舍:写错成每轮都探,就把 100ms + * 轮询变成了每轮两次系统调用;写错成分频相位,TESTFR 会迟迟不发。 + * + * 覆盖点:TC-I104SRV-001..004 建监听;010..013 发送与连续短写判死;020..023 时钟 + * 桥接;030..033 代次重建与 accept 门控;034..036 活性分频与对端 FIN;037..041 + * 错误丢弃与协议掉线重生。 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-23 + */ + +#include +#include + +#include "iec104_serv.h" +#include "net_socket.h" + +#include "iec104_serv.c" + +static int s_total; +static int s_passed; +static int s_failed; + +#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) + +int g_i4_info_count; +int g_i4_error_count; +int g_i4_debug_count; + +/* 真实 sys_events.h 把它声明成 extern,这里给定义(本模块只在主循环里用, + 测试设成 NULL 走"事件组不可用"支路,不阻塞静态辅助函数的驱动)。 */ +osEventFlagsId_t sys_evtHandle; + +/* ===================== 可控 socket 面 ===================== */ + +typedef struct { + int socket_rc; + int bind_rc; + int listen_rc; + int accept_rc; + int send_rc; + int recv_rc; + int can_read; + int armed; + int errno_val; + uint32_t gen; + int sock_exists; + int socket_calls; + int bind_calls; + int listen_calls; + int accept_calls; + int close_calls; + int last_closed_fd; + int last_accept_sock; + int recv_calls; + uint16_t last_bind_port_net; + uint16_t last_bind_family; + uint32_t last_bind_addr; + int delay_calls; +} i4_net_t; + +static i4_net_t g_n; + +/* net_get_sock 要返回一个真 net_sock_t(结构来自真实 net_types.h),先声明再用 */ +static net_sock_t g_sock_probe; + +int net_socket(int domain, int type) { (void)domain; (void)type; g_n.socket_calls++; return g_n.socket_rc; } +int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) +{ + const struct net_sockaddr_in *a = (const struct net_sockaddr_in *)addr; + + (void)sockfd; + (void)addrlen; + g_n.bind_calls++; + g_n.last_bind_port_net = a->sin_port; + g_n.last_bind_family = a->sin_family; + g_n.last_bind_addr = a->sin_addr.s_addr; + return g_n.bind_rc; +} +int net_listen(int sockfd) { (void)sockfd; g_n.listen_calls++; return g_n.listen_rc; } +int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) +{ + (void)addr; + (void)addrlen; + g_n.accept_calls++; + g_n.last_accept_sock = sockfd; + return g_n.accept_rc; +} +int net_close(int sockfd) { g_n.close_calls++; g_n.last_closed_fd = sockfd; return 0; } +int net_send(int sockfd, const void *buf, int len, int flags) +{ + (void)sockfd; + (void)buf; + (void)flags; + return g_n.send_rc; +} +int net_recv_nb(int sockfd, void *buf, int len) +{ + (void)sockfd; + (void)buf; + (void)len; + g_n.recv_calls++; + return g_n.recv_rc; +} +int net_can_read(int sockfd) { (void)sockfd; return g_n.can_read; } +int net_get_errno(void) { return g_n.errno_val; } +int net_listener_armed(int sockfd) { (void)sockfd; return g_n.armed; } +uint32_t net_get_generation(void) { return g_n.gen; } +net_sock_t *net_get_sock(int sockfd) { (void)sockfd; return (g_n.sock_exists != 0) ? &g_sock_probe : NULL; } +uint16_t net_htons(uint16_t v) { return (uint16_t)((v >> 8) | (v << 8)); } + +/* ===================== 假链路层与假 pts ===================== */ + +static int g_lk_init; +static int g_lk_open; +static int g_lk_close; +static int g_lk_poll; +static int g_lk_onrx; +static uint32_t g_lk_last_now; +static iec104_state_t g_lk_state; + +void iec104_link_init(iec104_link_t *lk, const iec104_link_cfg_t *cfg, void *ctx) +{ + (void)lk; (void)cfg; (void)ctx; + g_lk_init++; +} +void iec104_link_open(iec104_link_t *lk, uint32_t now_ms) +{ + (void)lk; + g_lk_open++; + g_lk_last_now = now_ms; +} +void iec104_link_close(iec104_link_t *lk) { (void)lk; g_lk_close++; } +void iec104_link_poll(iec104_link_t *lk, uint32_t now_ms) +{ + (void)lk; + g_lk_poll++; + g_lk_last_now = now_ms; +} +void iec104_link_on_rx(iec104_link_t *lk, const uint8_t *data, uint16_t len, uint32_t now_ms) +{ + (void)lk; (void)data; (void)len; (void)now_ms; + g_lk_onrx++; +} +iec104_state_t iec104_link_state(const iec104_link_t *lk) { (void)lk; return g_lk_state; } + +static int g_pts_init; +void iec104_pts_init(void) { g_pts_init++; } + +/* ===================== 假时钟 / 定值 / 看门狗 / RTOS ===================== */ + +static uint32_t g_clock_val; +static int g_clock_set_rc; +static uint32_t g_clock_last_secs; +static int g_clock_set_calls; +static uint32_t g_dirty; +static int g_evt_set_calls; +static uint32_t g_evt_last; +static int g_wd_beats; +static int g_wd_last_id; +static uint32_t g_now; + +uint32_t sys_clock_get(void) { return g_clock_val; } +int sys_clock_set_unix(uint32_t unix_secs) +{ + g_clock_set_calls++; + g_clock_last_secs = unix_secs; + return g_clock_set_rc; +} +uint8_t prot_set_dirty_peek(void) { return (uint8_t)g_dirty; } +uint32_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags) +{ + (void)ef_id; + g_evt_set_calls++; + g_evt_last = flags; + return flags; +} +uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout) +{ + (void)ef_id; (void)flags; (void)options; (void)timeout; + return 0u; +} +void task_wd_beat(task_wd_id_t id) { g_wd_beats++; g_wd_last_id = (int)id; } +uint32_t osKernelGetTickCount(void) { return g_now; } +void osDelay(uint32_t ticks) +{ + (void)ticks; + g_n.delay_calls++; + if (g_n.delay_calls > 500) { + printf(" ABORT 跑飞(>500 轮 osDelay)\n"); + fflush(stdout); + exit(2); + } +} + +/* ===================== 复位 ===================== */ + +static void reset_all(void) +{ + (void)memset(&g_n, 0, sizeof(g_n)); + g_n.socket_rc = 5; + g_n.bind_rc = 0; + g_n.listen_rc = 0; + g_n.accept_rc = -1; + g_n.send_rc = 0; + g_n.armed = 1; + g_n.sock_exists = 1; + g_n.errno_val = NET_ERR_WOULDBLOCK; + g_n.gen = 100u; + (void)memset(&g_sock_probe, 0, sizeof(g_sock_probe)); + g_lk_init = 0; g_lk_open = 0; g_lk_close = 0; g_lk_poll = 0; g_lk_onrx = 0; + g_lk_last_now = 0u; + g_lk_state = IEC104_ST_RUN; + g_pts_init = 0; + g_clock_val = 1700000000u; + g_clock_set_rc = 0; + g_clock_last_secs = 0u; + g_clock_set_calls = 0; + g_dirty = 0u; + g_evt_set_calls = 0; + g_evt_last = 0u; + g_wd_beats = 0; + g_wd_last_id = -1; + g_now = 5000u; + g_i4_info_count = 0; + g_i4_error_count = 0; + g_i4_debug_count = 0; + sys_evtHandle = NULL; + s_listen = -1; + s_client = -1; + s_gen = 100u; + s_liveness_ctr = 0u; + s_send_fail_run = 0u; + s_conn_dead = 0u; +} + +/* ===================== 建监听 ===================== */ + +static void tc_bind_listen(void) +{ + printf("-- TC-I104SRV-001..004 bind/listen --\n"); + reset_all(); + CHECK((s_bind_listen() == 0) && (s_listen == 5) && (g_n.listen_calls == 1) && + (g_n.last_bind_family == NET_AF_INET) && + (g_n.last_bind_port_net == net_htons(IEC104_SERV_PORT)) && + (g_n.last_bind_addr == NET_INADDR_ANY), + "TC-I104SRV-001 建监听:2404 端口过 htons、绑任意地址、记下监听号"); + + reset_all(); + g_n.socket_rc = -1; + CHECK((s_bind_listen() == -1) && (g_n.bind_calls == 0) && (g_n.close_calls == 0), + "TC-I104SRV-002 建不出 socket 就返回,不去 bind 也不关不存在的口"); + + reset_all(); + g_n.bind_rc = -1; + CHECK((s_bind_listen() == -1) && (g_n.close_calls == 1) && (g_n.listen_calls == 0), + "TC-I104SRV-003 bind 失败要把刚建的口关掉(否则每次重试漏一个 socket 号)"); + + reset_all(); + g_n.listen_rc = -1; + CHECK((s_bind_listen() == -1) && (g_n.close_calls == 1) && (s_listen == -1), + "TC-I104SRV-004 listen 失败同样关口且不留下半个监听"); +} + +/* ===================== 发送与连续短写判死 ===================== */ + +static void tc_send(void) +{ + const uint8_t apdu[4] = { 0x68u, 0x02u, 0x00u, 0x00u }; + + printf("-- TC-I104SRV-010..013 send & fail streak --\n"); + reset_all(); + s_client = -1; + s_send_apdu(NULL, apdu, 4u); + CHECK((g_n.send_rc == 0) && (g_n.close_calls == 0) && (g_i4_error_count == 0), + "TC-I104SRV-010 没连接时直接返回(不发 net_send、不报错)"); + + reset_all(); + s_client = 7; + g_n.send_rc = 4; + s_send_apdu(NULL, apdu, 4u); + CHECK((s_send_fail_run == 0u) && (s_conn_dead == 0u), + "TC-I104SRV-011 完整发出把连败计数清零"); + + reset_all(); + s_client = 7; + g_n.send_rc = 2; /* 短写:只发了 2/4 */ + s_send_apdu(NULL, apdu, 4u); + CHECK((s_send_fail_run == 1u) && (s_conn_dead == 0u) && (g_i4_error_count == 1), + "TC-I104SRV-012 第一次短写:留痕但不判死(拥塞很正常)"); + s_send_apdu(NULL, apdu, 4u); + s_send_apdu(NULL, apdu, 4u); + CHECK((s_send_fail_run == 3u) && (s_conn_dead == 1u), + "TC-I104SRV-012b 连到 3 次才判死(IEC104_SEND_FAIL_MAX)"); + + reset_all(); + s_client = 7; + g_n.send_rc = 1; + s_send_apdu(NULL, apdu, 4u); + s_send_apdu(NULL, apdu, 4u); + g_n.send_rc = 4; + s_send_apdu(NULL, apdu, 4u); /* 中间成功一次 */ + g_n.send_rc = 1; + s_send_apdu(NULL, apdu, 4u); + s_send_apdu(NULL, apdu, 4u); + CHECK((s_conn_dead == 0u) && (s_send_fail_run == 2u), + "TC-I104SRV-013 **成功即复位**:偶发拥塞不会被累计成误杀"); +} + +/* ===================== 时钟桥接边界 ===================== */ + +static void tc_clock(void) +{ + printf("-- TC-I104SRV-020..023 clock bridge --\n"); + reset_all(); + CHECK(s_clock_sync(NULL, 0x100000000ULL * 1000ULL) == -1, + "TC-I104SRV-020 秒数超过 32 位一律拒绝(截断写进 RTC 就是 2106 年后的乱值)"); + CHECK(g_clock_set_calls == 0, "TC-I104SRV-020b 拒绝时根本不该碰时钟"); + + reset_all(); + (void)s_clock_sync(NULL, 1700000123000ULL); + CHECK((g_clock_set_calls == 1) && (g_clock_last_secs == 1700000123u), + "TC-I104SRV-021 毫秒按整除换秒后写时钟"); + + reset_all(); + g_clock_val = UINT32_MAX; /* 未对时的哨兵值 */ + CHECK(s_epoch_ms(NULL) == UINT64_MAX, + "TC-I104SRV-022 未对时回 UINT64_MAX 哨兵(绝不能把 1970 当成设备时间)"); + + reset_all(); + g_clock_val = 1700000123u; + CHECK(s_epoch_ms(NULL) == 1700000123000ULL, "TC-I104SRV-023 正常时对时给出毫秒"); +} + +/* ===================== 代次重建与 accept 门控 ===================== */ + +static void tc_generation(void) +{ + printf("-- TC-I104SRV-030..033 generation & accept gate --\n"); + reset_all(); + s_listen = 5; + s_client = 9; + g_n.gen = 101u; /* 网络重建(现场改了 IP) */ + s_serv_poll(); + CHECK((g_n.close_calls == 2) && (g_n.last_closed_fd == 5) && + (s_client == -1) && (s_liveness_ctr == 0u) && (s_conn_dead == 0u) && + (g_lk_close == 1) && (s_gen == 101u) && (g_n.bind_calls == 1), + "TC-I104SRV-030 代次一变就拆掉旧连接与旧监听并重建(新 IP 上端口必须继续服务)"); + + reset_all(); + s_listen = -1; + s_serv_poll(); + CHECK((g_n.bind_calls == 1) && (s_listen == 5) && (g_n.accept_calls == 0), + "TC-I104SRV-031 监听丢了就补建,这一轮不去 accept"); + + reset_all(); + s_listen = 5; + g_n.armed = 0; /* 监听还没就绪 */ + g_n.sock_exists = 1; + s_serv_poll(); + CHECK((g_n.accept_calls == 0) && (s_listen == 5), + "TC-I104SRV-032 监听门未开时不 accept(否则对着未就绪的号空转)"); + + reset_all(); + s_listen = 5; + g_n.armed = 0; + g_n.sock_exists = 0; /* 表项已不存在 */ + s_serv_poll(); + CHECK((s_listen == -1) && (g_n.accept_calls == 0), + "TC-I104SRV-032b 表项已失效时把监听号丢掉,下一轮重建"); + + reset_all(); + s_listen = 5; + g_n.accept_rc = 9; + s_send_fail_run = 3u; + s_conn_dead = 1u; + s_liveness_ctr = 7u; + s_serv_poll(); + CHECK((s_client == 9) && (g_lk_open == 1) && (g_lk_last_now == 5000u) && + (s_send_fail_run == 0u) && (s_conn_dead == 0u) && (s_liveness_ctr == 0u) && + (g_i4_info_count == 1), + "TC-I104SRV-033 新连接:打开链路、三个失败计数全部复位"); +} + +/* ===================== 活性分频、对端 FIN、错误丢弃 ===================== */ + +static void tc_liveness(void) +{ + printf("-- TC-I104SRV-034..041 liveness / EOF / drop --\n"); + reset_all(); + s_listen = 5; + s_client = 9; + g_n.can_read = 0; + s_liveness_ctr = 0u; + s_serv_poll(); + CHECK((g_n.recv_calls == 0) && (g_lk_poll == 1) && (s_liveness_ctr == 1u), + "TC-I104SRV-034 无可读数据时按 32 分频探活,这一轮不收包(省 CPU 预算)"); + + reset_all(); + s_listen = 5; + s_client = 9; + g_n.can_read = 0; + /* 这一轮收到的是"暂无数据"(非阻塞的空读),而不是 0——0 在 recv_nb 的语义里是 + * 对端 FIN,会直接走 036 那条断开支路,测不到分频本身。 */ + g_n.recv_rc = -1; + g_n.errno_val = NET_ERR_WOULDBLOCK; + s_liveness_ctr = 31u; /* 下一轮正好到分频点 */ + s_serv_poll(); + CHECK((g_n.recv_calls == 1) && (g_lk_poll == 1) && (s_client == 9), + "TC-I104SRV-035 分频到点才去收一次(TESTFR 等活性帧靠它发出去)"); + + reset_all(); + s_listen = 5; + s_client = 9; + g_n.can_read = 1; + g_n.recv_rc = 0; /* 对端 FIN */ + s_serv_poll(); + CHECK((g_n.last_closed_fd == 9) && (s_client == -1) && (g_lk_close == 1) && + (g_n.close_calls == 1) && (s_listen == 5) && (g_n.bind_calls == 0), + "TC-I104SRV-036 对端正常断开:只关数据口、**监听必须留着**(单连接 socket 关错就是端口失联)"); + + reset_all(); + s_listen = 5; + s_client = 5; /* 单连接复用:数据号 == 监听号 */ + g_n.can_read = 1; + g_n.recv_rc = 0; + s_serv_poll(); + CHECK((g_n.close_calls == 0) && (s_client == -1) && (g_lk_close == 1), + "TC-I104SRV-036b 数据号与监听号同号时**不许关**(否则把监听一起毁掉)"); + + reset_all(); + s_listen = 5; + s_client = 9; + g_n.can_read = 1; + g_n.recv_rc = -1; + g_n.errno_val = NET_ERR_WOULDBLOCK; /* 空转:没数据可读 */ + s_serv_poll(); + CHECK((s_client == 9) && (g_lk_close == 0) && (g_i4_error_count == 0), + "TC-I104SRV-037 WOULDBLOCK 是正常空转,不能当成错误丢连接"); + + reset_all(); + s_listen = 5; + s_client = 9; + g_n.can_read = 1; + g_n.recv_rc = -1; + g_n.errno_val = NET_ERR_CONNRESET; /* 真错误 */ + s_serv_poll(); + CHECK((s_client == -1) && (s_listen == 5) && (g_i4_error_count == 1) && + (g_lk_close == 1) && (s_liveness_ctr == 0u), + "TC-I104SRV-038 真错误:丢连接并**当场重建监听**(不留空窗)"); + + reset_all(); + s_listen = 5; + s_client = 9; + s_conn_dead = 1u; + s_service_link(1000u); + CHECK((s_client == -1) && (g_n.close_calls == 1) && (g_i4_error_count == 1), + "TC-I104SRV-039 判死后由 service_link 收口(发送失败线索单独成一条)"); + + reset_all(); + s_listen = 5; + s_client = 9; + g_lk_state = IEC104_ST_IDLE; /* 链路层自己回到了未建立 */ + s_service_link(1000u); + CHECK((s_client == -1) && (g_lk_close >= 1) && (s_listen == 5), + "TC-I104SRV-040 协议状态掉回 IDLE 也要丢并重生(否则服务挂着但永不服务)"); + + reset_all(); + s_listen = 9; + s_client = 9; + g_n.socket_rc = -1; /* 重建失败 */ + g_n.sock_exists = 0; /* 且表项已经没了 */ + g_n.can_read = 1; + g_n.recv_rc = 0; + s_serv_poll(); + CHECK((s_client == -1) && (s_listen == -1) && (g_n.close_calls == 0), + "TC-I104SRV-041 表项没了又重建失败:丢掉假监听号,下一轮继续重试(数据号与监听同号时不许关)"); +} + +int main(void) +{ + printf("=== IEC104 SERV Tests ===\n"); + + tc_bind_listen(); + tc_send(); + tc_clock(); + tc_generation(); + tc_liveness(); + + printf("\n=== IEC104 SERV Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/iec104_serv/run_unit_test.py b/test/host/iec104_serv/run_unit_test.py new file mode 100644 index 00000000..afc448d3 --- /dev/null +++ b/test/host/iec104_serv/run_unit_test.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""IEC 104 上行服务生命周期(iec104_serv)主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/iec104_serv/iec104_serv_test.c(以 #include 纳入生产源 + App/task/iec104_serv.c),离线驱动它的全部 static 辅助函数(**不跑 never-returning + 的任务主循环**),验证监听建立与失败回收、网络代次一变就重建监听、accept 门控与 + 表项失效自愈、单连接 socket 的断线语义、连续短写判死、活性分频探活、协议掉回 IDLE + 时丢弃重生,以及时钟桥接的两个溢出边界。 + +为什么要主机侧单测(而不是只用台架): + 104 主站掉线重连是现场最常见故障形态,但台架无法按需制造"改 IP 时正好有连接" + "对端 FIN 与 RST 的区别""连续三次短写";这几条处理错的后果都是端口永久失联或 + 会话卡死,只能等主站侧超时才发现。判死用**连续**短写计数(成功即复位),写成累计 + 就会把偶发拥塞误判成断线;时钟桥接的 >4G 秒必须拒绝、未对时必须回哨兵而不是 0; + 活性分频写错就成了每轮多一次系统调用或 TESTFR 迟迟不发。 + +桩的边界:iec104_link.h / iec104_pts.h / prot_set.h / sys_clock.h / sys_events.h / + task_watchdog.h / net_types.h 用真实头(iec104_state_t、cfg 的函数指针签名、 + TASK_WD_IEC104=7、SYS_EVT_* 位值一改即编译期报错);链路层与 pts 层给假件——链路 + 协议本身有 test/host/iec104 的 97 条专测,本模块只管服务怎么活、怎么死、怎么重生; + socket 面给可控假件(连 socket 号、发送返回、错误码、代次、门控都能注入)。 + +用法(仓库根目录): + python test/host/iec104_serv/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 + +设计依据:docs/子系统设计/*104* 设计文档、AGENTS.md 模块约定(单连接 socket 断开语义、 +错误码经任务通知值回传)、docs/测试说明/主机单测覆盖率口径.md §9 +创建日期:2026-10-23 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + + +def _header_dirs(base): + """收集 base 下所有**真的放着 .h** 的目录。 + + 104 服务要 include 的 iec104_serv.h / iec104_link.h / iec104_pts.h / + task_watchdog.h / sys_clock.h / sys_events.h / prot_set.h 散在 App 的多个 + 子目录里,逐个手写列表一定会漏(漏一个就是 C1083,而且报错落在被测源上, + 很容易被误读成生产代码坏了)。这里按目录实际内容自动收集,多给几个 /I + 对编译没有代价,却把"漏目录"这类脆错误消掉。 + """ + dirs = [] + for root, _sub, files in os.walk(base): + if any(f.endswith(".h") for f in files): + dirs.append(root) + return sorted(dirs) + + +BUILD = TestBuild( + name="IEC104 SERV Tests", + src=os.path.join(HERE, "iec104_serv_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), # 必须先于真头目录(覆盖 net_socket.h 等) + ] + _header_dirs(os.path.join(ROOT, "App")) + + # sys_clock.h 会拉 sd2506.h(Drivers/BSP/SD2506),BSP 的头也散在多个子目录里, + # 同样按实际内容收集。注意 stub 目录排在最前,桩依然优先于这些真头。 + _header_dirs(os.path.join(ROOT, "Drivers", "BSP")) + [ + os.path.join(ROOT, "Drivers", "BSP", "NET"), + HERE, + ], + summary_marker="=== IEC104 SERV Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) + diff --git a/test/host/iec104_serv/stub/cmsis_os2.h b/test/host/iec104_serv/stub/cmsis_os2.h new file mode 100644 index 00000000..41e32742 --- /dev/null +++ b/test/host/iec104_serv/stub/cmsis_os2.h @@ -0,0 +1,27 @@ +/* + * 模块名称:主机侧单测桩 - cmsis_os2.h(iec104_serv 模块专用) + * 模块功能:给 CMSIS-RTOS2 的六个入口一个宿主等价壳(tick、睡眠、事件组)。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 事件组只做成"记次数":本模块的测试**不驱动 iec104_serv_task 的无限主循环** + * (那会永不返回),只直调它的静态辅助函数,事件相关行为用可见计数器观察。 + * 创建日期:2026-10-23 + */ +#ifndef CMSIS_OS2_H +#define CMSIS_OS2_H + +#include + +typedef void *osEventFlagsId_t; +typedef uint32_t osStatus_t; + +#define osFlagsWaitAny 0x00000000u +#define osFlagsNoClear 0x00000001u +#define osFlagsError 0x80000000u + +uint32_t osKernelGetTickCount(void); +void osDelay(uint32_t ticks); +uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, + uint32_t options, uint32_t timeout); +uint32_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags); + +#endif /* CMSIS_OS2_H */ diff --git a/test/host/iec104_serv/stub/dbg_log.h b/test/host/iec104_serv/stub/dbg_log.h new file mode 100644 index 00000000..5abb3c5c --- /dev/null +++ b/test/host/iec104_serv/stub/dbg_log.h @@ -0,0 +1,19 @@ +/* + * 模块名称:主机侧单测桩 - dbg_log.h(iec104_serv 模块专用) + * 模块功能:DBG_* 计数;本模块的判死/丢连接都靠 ERROR 留痕("i104 drop: send fail" + * 与 "protocol fault" 是两条不同的现场线索)。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ +#ifndef DBG_LOG_H +#define DBG_LOG_H + +extern int g_i4_info_count; +extern int g_i4_error_count; +extern int g_i4_debug_count; + +#define DBG_INFO(fmt, ...) do { (void)(fmt); g_i4_info_count++; } while (0) +#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_i4_debug_count++; } while (0) +#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_i4_error_count++; } while (0) + +#endif /* DBG_LOG_H */ diff --git a/test/host/iec104_serv/stub/main.h b/test/host/iec104_serv/stub/main.h new file mode 100644 index 00000000..a0052993 --- /dev/null +++ b/test/host/iec104_serv/stub/main.h @@ -0,0 +1,15 @@ +/* + * 模块名称:主机侧单测桩 - main.h(NET/BSP 共享形态,本模块用于 iec104_serv) + * 模块功能:只给 HAL_GetTick;某些 BSP 头的 include 链会绕到这里(真实 main.h 拉整棵 + * HAL 头树,PC 上编不动)。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ +#ifndef MAIN_H +#define MAIN_H + +#include + +uint32_t HAL_GetTick(void); + +#endif /* MAIN_H */ diff --git a/test/host/iec104_serv/stub/net_socket.h b/test/host/iec104_serv/stub/net_socket.h new file mode 100644 index 00000000..7d85de0c --- /dev/null +++ b/test/host/iec104_serv/stub/net_socket.h @@ -0,0 +1,31 @@ +/* + * 模块名称:主机侧单测桩 - net_socket.h(NET 域共享形态,见 net_listener 同名文件) + * 模块功能:给 104 服务端一个可控 socket 面:建口/绑定/监听/接受/关闭/发送、 + * 非阻塞收(net_recv_nb)、可读门控(net_can_read)、监听门控 + * (net_listener_armed)、错误码通道(net_get_errno)、网络代次 + * (net_get_generation)与表项查询(net_get_sock)。 + * 地址结构与 NET_AF_INET / NET_SOCK_STREAM / NET_INADDR_ANY / + * NET_ERR_WOULDBLOCK 一律取**真实** net_types.h。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ +#ifndef NET_SOCKET_H +#define NET_SOCKET_H + +#include "net_types.h" + +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_send(int sockfd, const void *buf, int len, int flags); +int net_recv_nb(int sockfd, void *buf, int len); +int net_can_read(int sockfd); +int net_get_errno(void); +int net_listener_armed(int sockfd); +uint32_t net_get_generation(void); +net_sock_t *net_get_sock(int sockfd); +uint16_t net_htons(uint16_t hostshort); + +#endif /* NET_SOCKET_H */ diff --git a/test/host/iec104_serv/stub/sys_clock.h b/test/host/iec104_serv/stub/sys_clock.h new file mode 100644 index 00000000..8baffad7 --- /dev/null +++ b/test/host/iec104_serv/stub/sys_clock.h @@ -0,0 +1,21 @@ +/* + * 模块名称:主机侧单测桩 - sys_clock.h(iec104_serv 模块专用) + * 模块功能:只给本模块真正用到的两个入口:读秒与写秒。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 为什么必须桩这层而不是用真实头:真实 App/util/sys_clock.h 会 include sd2506.h + * (RTC 芯片驱动),那条链一直拉到 stm32f4xx_hal.h 与寄存器定义,PC 上编不动。 + * 两个签名与真实头**逐字一致**(uint32_t sys_clock_get(void) / + * int sys_clock_set_unix(uint32_t)),改签名就会在测试里对不上假件而报错。 + * 本模块最有价值的两条恰好是这两个入口的**边界**:>4G 秒必须拒绝、未对时的 + * UINT32_MAX 哨兵必须传成 UINT64_MAX,所以桩掉实现不影响被测语义。 + * 创建日期:2026-10-23 + */ +#ifndef SYS_CLOCK_H +#define SYS_CLOCK_H + +#include + +uint32_t sys_clock_get(void); +int sys_clock_set_unix(uint32_t unix_secs); + +#endif /* SYS_CLOCK_H */