From 0a3b9323950ebd8cf67029e226fe4cd1e2afc934 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 23 Oct 2026 21:39:05 +0800 Subject: [PATCH] =?UTF-8?q?test(NET):=20=E5=81=87=20CH395F=20=E8=8A=AF?= =?UTF-8?q?=E7=89=87=E8=90=BD=E5=9C=B0=20+=20net=5Flistener=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=8834=20=E6=9D=A1=EF=BC=8C=E8=A6=86=E7=9B=96=209?= =?UTF-8?q?9.2%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 §9 规格做假芯片的第一批消费者。新增 test/host/common/ch395f_fake.{h,c}:15 个 函数的**真实原型逐字照抄**(含 ch395f_sock_state_t{open,tcp_state} 与 ERR_SUCCESS/ PROTO_TYPE_TCP/SOCKET_CLOSED/TCP_CLOSED/TCP_LISTEN 的真实取值),脚本化模型沿用本仓 既有假件风格:读函数按调用序号取返回队列(耗尽回落最后一个值)、写函数记次数与 最近入参、外加一条**调用顺序日志**让时序契约可断言。两条芯片怪癖必须复刻: ① get_sock_int_status **整体读即清**、无按位清除手段(AGENTS 模块约定 B3 实证坑, 不复刻就照不出"顺手全量清一遍把别的在用 Socket 门焊死");② *_cached 是**缓存值** 而非现场值,"监听还活着"的判据是 (open != CLOSED && tcp_state == LISTEN)。 各模块 stub/ch395f.h **只做转发** include 共享件(规格里拍板的方案):三份假芯片各自 漂移会让"假件与被测代码同时错"再也报不出来,签名与常量只能有一份定义。 新增 test/host/net_listener/,34 条断言,net_listener.c 覆盖 125/126 = **99.2%**。 钉住的契约(真机上一闪而过、台架凑不齐的组合): - 四道互相牵制的时间闸门:30ms 退避(20ms 不重试、30ms 重试)、隔离级 400ms 与 优雅级 100ms(399ms/99ms 不放行、400ms/100ms 放行)、**800ms 上限只留痕且不放弃 请求**(留痕只打一次,到 2000ms 预算耗尽才报 ERROR 交对账器)。排期登记里那句 "沉降窗 800ms 上限只留痕"至此第一次有了可执行的验证。 - 该不该重建:芯片仍报 LISTEN 就不重建且清空请求;有挂起 recv/send 时让路但**保留 请求**(挂起结束自动补做);五种条件静默放弃(非 LISTENING/CLOSED 态、 auto_relisten=0、非 STREAM、local_port=0、in_use=0)。 - 失败不留半成品:open 失败关回去并回 BUSY;listen 失败也必须 close_socket。 - 重建后簿记:转 LISTENING、清 send_ready/recv_bytes/remote_ip(_arr)/remote_port、旧 remote_port 记进 last_peer_port、清 lost_conn_seen/released_abnormal;源端口与协议 取自表项本身(监听端口不能漂)。 - 芯片时序:清接收 → 关断 → 读即清 → 读状态(顺序不可换);清理接收区单次读钳到 256B、最多 16 轮、读到空即停。越界 fd 全程静默且不打扰芯片;请求去重保留首个 reason。 测试自查修了两处(都不是生产问题但值得记): ① reset_all 只复位假世界、**没复位被测模块自己的 static**(s_relisten_req/_next/_dead/ _quarantine/_settle_*/_capped),一上来红了 9 条;改用模块自带的 net_listener_clear() 清,顺带验证了 clear 的完整性。 ② 沉降窗边界必须在**独立场景**测:放行判定与 30ms 退避共用 s_relisten_next,同一条 链里"恰好 400ms"那轮 tick 会被上一轮设的退避槽挡掉。这本身是个该知道的事实—— **hold 到点后最坏还要再等一个退避周期(实测 ≤30ms)才真正放行**,已写进用例注释。 另留痕一处死分支:net_relisten_locked 的 NET_ERR_BADF 从 tick **不可达**(tick 在调用 前就因表项为空放弃),TC-NETLIS-026 记着,新增调用点时要重新审视。 桩的口径:net_listener.h/net_types.h/net_config.h/net_pending.h 用**真实头**(net_sock_t 字段与各枚举一改即编译期报错);net_socket.h 是桩,但 net_set_errno 与 NET_ERR_RETURN_1 照抄真实头同名宏(net_socket.h:528/532);真实 net_pending.h 原型带 TaskHandle_t,故 宿主 FreeRTOS.h 补一个 void* typedef(只出现在签名里,被测代码从不传它)。 覆盖率与台账:套件 22 → 23 全绿;files_tested 26 → 27;未触及 .c 40 → 39;分母 3,446 → 3,572 行;line=3128/3572(87.6%,本批净涨因为新进来的文件几乎全绿)、 func=243/266(91.3%)。恒等式 39 = 5(P1 剩) + 5(P2) + 29(P3) 仍咬合。编号补建 TC-HOST-NETLIS(游标 026),族数 16 → 17;注册表仍只做增量插入。 自查:test-cppcheck Passed(连假芯片一起过);check_ids --strict 通过;doc-links 35 处 (= 存量);新增文件 BOM/CR/U+FFFD/行尾逐项净。**本增量零生产代码改动**,未跑 @build。 --all-files 仍只有 mem-numbers / mem-budget(.map 台账漂移,等 Lead 合并后统一回填) 与 ui-golden(ref/*.bin 不入库,新 worktree 天生没有黄金参考)三条失败,与本分支无关。 --- docs/测试说明/主机单测覆盖率口径.md | 11 +- scripts/id_registry.json | 22 + scripts/test_coverage_baseline.json | 40 +- test/host/common/ch395f_fake.c | 198 +++++++ test/host/common/ch395f_fake.h | 127 ++++ test/host/net_listener/net_listener_test.c | 545 ++++++++++++++++++ test/host/net_listener/run_unit_test.py | 62 ++ test/host/net_listener/stub/FreeRTOS.h | 19 + test/host/net_listener/stub/ch395f.h | 11 + test/host/net_listener/stub/cmsis_os.h | 10 + test/host/net_listener/stub/dbg_log.h | 16 + test/host/net_listener/stub/main.h | 12 + .../host/net_listener/stub/net_latency_test.h | 9 + test/host/net_listener/stub/net_socket.h | 20 + test/host/net_listener/stub/task.h | 9 + test/host/net_listener/stub/test_config.h | 10 + 16 files changed, 1104 insertions(+), 17 deletions(-) create mode 100644 test/host/common/ch395f_fake.c create mode 100644 test/host/common/ch395f_fake.h create mode 100644 test/host/net_listener/net_listener_test.c create mode 100644 test/host/net_listener/run_unit_test.py create mode 100644 test/host/net_listener/stub/FreeRTOS.h create mode 100644 test/host/net_listener/stub/ch395f.h create mode 100644 test/host/net_listener/stub/cmsis_os.h create mode 100644 test/host/net_listener/stub/dbg_log.h create mode 100644 test/host/net_listener/stub/main.h create mode 100644 test/host/net_listener/stub/net_latency_test.h create mode 100644 test/host/net_listener/stub/net_socket.h create mode 100644 test/host/net_listener/stub/task.h create mode 100644 test/host/net_listener/stub/test_config.h diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index df99e751..b992609e 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -227,9 +227,14 @@ 未测(要先把假 `FIL/FILINFO` 的字段语义做实,否则断言落在假件自己写的规则上) | | 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_pending`/`net_events`/`net_listener`) | ⬜ 未开始 | — | - 本批新建的 `stub/ch395f.h` 是**空壳**,只服务"不调芯片函数"的文件;这三块直接调 - `ch395f_*` 十余处,要的是能脚本化返回的**假芯片**,不是这个空壳 | +| 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`/`net_events`) | ⬜ 未开始 | 假芯片已就绪,直接接即可 | + `net_pending` 要 `net_listener_request` 的记录型假件;`net_events` 依赖最多 + (`net_sock_raw`/`net_listener_settle`/`net_pending_cancel`/`xTaskGetTickCount`)放最后 | | 104 服务层 | ⬜ 未开始 | — | 桩最省(复用 `cmsis_os2.h` + `net_socket.h`) | 本批照出的两处**现状**(只登记、**未改语义**;全库唯一调用点 `test/board/net_test_task.c` diff --git a/scripts/id_registry.json b/scripts/id_registry.json index 57fa726f..44d2ca1f 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -1746,6 +1746,28 @@ "subitem_ok": true, "owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);2026-10-23 随覆盖率口径 §9 P1 新建模块补登记", "cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌" + }, + { + "name": "TC-HOST-NETLIS", + "class": "TC", + "title": "主机侧单测用例号:NET 层监听自动重建(test/host/net_listener)", + "pattern": "\\bTC-NETLIS-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/net_listener/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-NETLIS-026", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);2026-10-23 随覆盖率口径 §9 假 CH395F 立项第一批补登记", + "cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌" } ], "unmanaged_bare": { diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index a6f7d3b9..5ddf8060 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -184,6 +184,14 @@ "lines_covered": 7, "lines_valid": 7 }, + "Drivers/BSP/NET/net_listener.c": { + "func_rate": 1.0, + "funcs_covered": 7, + "funcs_valid": 7, + "line_rate": 0.9921, + "lines_covered": 125, + "lines_valid": 126 + }, "Drivers/BSP/NET/net_select.c": { "func_rate": 1.0, "funcs_covered": 5, @@ -292,6 +300,13 @@ "lines_total": 112, "ok": true }, + "net_listener": { + "files": { + "Drivers/BSP/NET/net_listener.c": 126 + }, + "lines_total": 126, + "ok": true + }, "net_select": { "files": { "Drivers/BSP/NET/net_select.c": 117 @@ -410,22 +425,23 @@ "Drivers/BSP/NET/lftpd/lftpd_auth.c", "Drivers/BSP/NET/lftpd/lftpd_io.c", "Drivers/BSP/NET/lftpd/lftpd_string.c", + "Drivers/BSP/NET/net_listener.c", "Drivers/BSP/NET/net_select.c", "Drivers/BSP/NETCFG/ftp_cfg.c", "Drivers/BSP/NETCFG/net_cfg.c" ], "totals": { "files_generated_excluded": 5, - "files_tested": 26, - "files_untested": 118, - "func_rate": 0.9112, - "funcs_covered": 236, - "funcs_valid": 259, - "line_rate": 0.8714, - "lines_covered": 3003, - "lines_valid": 3446, - "modules_ok": 22, - "modules_total": 22 + "files_tested": 27, + "files_untested": 117, + "func_rate": 0.9135, + "funcs_covered": 243, + "funcs_valid": 266, + "line_rate": 0.8757, + "lines_covered": 3128, + "lines_valid": 3572, + "modules_ok": 23, + "modules_total": 23 }, "untested_sources": [ { @@ -496,10 +512,6 @@ "code_lines": 202, "path": "App/task/iec104_serv.c" }, - { - "code_lines": 191, - "path": "Drivers/BSP/NET/net_listener.c" - }, { "code_lines": 171, "path": "App/task/adc_task.c" diff --git a/test/host/common/ch395f_fake.c b/test/host/common/ch395f_fake.c new file mode 100644 index 00000000..11a03613 --- /dev/null +++ b/test/host/common/ch395f_fake.c @@ -0,0 +1,198 @@ +/* + * 模块名称:假 CH395F 芯片实现(NET 域主机侧单测共享件) + * 模块功能:实现 ch395f_fake.h 声明的假芯片。由测试单元 #include 本 .c 使用 + * (本仓主机单测的既有做法:生产源与假件都进同一个 TU)。 + * 创建日期:2026-10-23 + */ + +#include + +#include "ch395f_fake.h" + +ch395f_fake_world_t g_fake; +ch395f_fake_obs_t g_chip; + +static void fake_log(char c) +{ + if (g_chip.log_n < (CH395F_FAKE_LOG_LEN - 1)) { + g_chip.log[g_chip.log_n++] = c; + g_chip.log[g_chip.log_n] = '\0'; + } +} + +void ch395f_fake_reset(void) +{ + int i; + + (void)memset(&g_fake, 0, sizeof(g_fake)); + (void)memset(&g_chip, 0, sizeof(g_chip)); + for (i = 0; i < CH395F_FAKE_SOCK_MAX; i++) { + g_fake.cached_open[i] = CH395F_SOCKET_CLOSED; + g_fake.cached_tcp[i] = CH395F_TCP_CLOSED; + g_fake.status_byte0[i] = CH395F_SOCKET_CLOSED; + g_fake.status_byte1[i] = CH395F_TCP_CLOSED; + g_fake.open_rc[i] = CH395F_ERR_SUCCESS; + g_fake.listen_rc[i] = CH395F_ERR_SUCCESS; + } +} + +void ch395f_fake_push_recv_len(uint8_t sock, uint16_t v) +{ + if ((sock < CH395F_FAKE_SOCK_MAX) && + (g_fake.recv_len_n[sock] < CH395F_FAKE_QUEUE_LEN)) { + g_fake.recv_len_q[sock][g_fake.recv_len_n[sock]++] = v; + } +} + +void ch395f_fake_set_listening(uint8_t sock) +{ + if (sock < CH395F_FAKE_SOCK_MAX) { + g_fake.cached_open[sock] = CH395F_SOCKET_OPEN; + g_fake.cached_tcp[sock] = CH395F_TCP_LISTEN; + } +} + +ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn) +{ + ch395f_sock_state_t s; + + g_chip.cached_calls++; + g_chip.last_sock = sn; + s.open = (sn < CH395F_FAKE_SOCK_MAX) ? g_fake.cached_open[sn] : CH395F_SOCKET_CLOSED; + s.tcp_state = (sn < CH395F_FAKE_SOCK_MAX) ? g_fake.cached_tcp[sn] : CH395F_TCP_CLOSED; + return s; +} + +ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn) +{ + return ch395f_sock_state_cached(sn); +} + +void ch395f_get_socket_status(uint8_t sock, uint8_t *status) +{ + g_chip.get_status_calls++; + g_chip.last_sock = sock; + fake_log('s'); + if (status != NULL) { + status[0] = (sock < CH395F_FAKE_SOCK_MAX) ? g_fake.status_byte0[sock] : CH395F_SOCKET_CLOSED; + status[1] = (sock < CH395F_FAKE_SOCK_MAX) ? g_fake.status_byte1[sock] : CH395F_TCP_CLOSED; + } +} + +/* 整体读即清:返回后该 socket 的挂起位图清零(真实芯片没有按位清除手段) */ +uint8_t ch395f_get_sock_int_status(uint8_t sock) +{ + uint8_t v = 0U; + + g_chip.int_status_calls++; + g_chip.last_sock = sock; + fake_log('i'); + if (sock < CH395F_FAKE_SOCK_MAX) { + v = g_fake.int_status[sock]; + g_fake.int_status[sock] = 0U; + } + return v; +} + +uint16_t ch395f_get_recv_len(uint8_t sock) +{ + uint16_t v; + + g_chip.last_sock = sock; + fake_log('R'); + if ((sock >= CH395F_FAKE_SOCK_MAX) || (g_fake.recv_len_n[sock] == 0)) { + return 0U; + } + /* 按调用序号取;耗尽后回落最后一个值(便于表达"一直有数据"的持续态) */ + if (g_fake.recv_len_i[sock] < g_fake.recv_len_n[sock]) { + v = g_fake.recv_len_q[sock][g_fake.recv_len_i[sock]++]; + } else { + v = g_fake.recv_len_q[sock][g_fake.recv_len_n[sock] - 1]; + } + return v; +} + +void ch395f_read_recv_buf(uint8_t sock, uint8_t *p_data, uint16_t len) +{ + g_chip.read_calls++; + g_chip.last_sock = sock; + g_chip.last_read_len = len; + fake_log('r'); + if ((p_data != NULL) && (len > 0U)) { + (void)memset(p_data, 0, len); /* 交出去的是零,测试不依赖内容 */ + } +} + +void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len) +{ + (void)p_data; + g_chip.write_calls++; + g_chip.last_sock = sock; + g_chip.last_read_len = len; + fake_log('w'); +} + +uint8_t ch395f_open_socket(uint8_t sock) +{ + g_chip.open_calls++; + g_chip.last_sock = sock; + fake_log('o'); + return (sock < CH395F_FAKE_SOCK_MAX) ? g_fake.open_rc[sock] : 0x01U; +} + +uint8_t ch395f_close_socket(uint8_t sock) +{ + g_chip.close_calls++; + g_chip.last_sock = sock; + fake_log('c'); + return CH395F_ERR_SUCCESS; +} + +uint8_t ch395f_tcp_listen(uint8_t sock) +{ + g_chip.listen_calls++; + g_chip.last_sock = sock; + fake_log('l'); + return (sock < CH395F_FAKE_SOCK_MAX) ? g_fake.listen_rc[sock] : 0x01U; +} + +void ch395f_set_proto_type(uint8_t sock, uint8_t proto) +{ + g_chip.set_proto_calls++; + g_chip.last_sock = sock; + g_chip.last_proto = proto; + fake_log('P'); +} + +void ch395f_set_sour_port(uint8_t sock, uint16_t port) +{ + g_chip.set_port_calls++; + g_chip.last_sock = sock; + g_chip.last_port = port; + fake_log('p'); +} + +void ch395f_get_remot_ipp(uint8_t sock, uint8_t *p_buf) +{ + (void)sock; + if (p_buf != NULL) { + (void)memset(p_buf, 0, 4U); + } +} + +void ch395f_set_keepalive_enable(uint8_t sock, uint8_t enable) +{ + g_chip.keepalive_calls++; + g_chip.last_sock = sock; + g_chip.last_keepalive = enable; +} + +uint8_t ch395f_link_up_cached(void) +{ + return 1U; +} + +uint16_t ch395f_link_change_count(void) +{ + return 0U; +} diff --git a/test/host/common/ch395f_fake.h b/test/host/common/ch395f_fake.h new file mode 100644 index 00000000..8f612326 --- /dev/null +++ b/test/host/common/ch395f_fake.h @@ -0,0 +1,127 @@ +/* + * 模块名称:假 CH395F 芯片(NET 域主机侧单测共享件) + * 模块功能:给 NET 域的主机侧单测一个**可脚本化的假芯片**。真实 ch395f.h 拉 HAL 与 + * SPI 寄存器定义,PC 上编不动;而 net_listener / net_pending / net_events + * 的行为恰恰完全由"芯片此刻报什么状态"决定,必须有假件才能测。 + * + * ⚠ 仅供 test/host/** 使用,不参与 MCU 构建。 + * + * 为什么值得共享一份(而不是每模块自带桩):三份假芯片各自漂移时,"假件与被测代码 + * 同时错"这类问题再也不会报出来。各模块的 stub/ch395f.h 只做一件事——include 本文件, + * 于是签名与常量只有一份定义。 + * + * 常量与类型必须与 Drivers/BSP/CH395F/ch395f.h 逐字一致(2026-10-23 从真实头抄录): + * CH395F_ERR_SUCCESS 0x00 / CH395F_PROTO_TYPE_TCP 0x03 / CH395F_SOCKET_CLOSED 0x00 + * CH395F_TCP_CLOSED 0x00 / CH395F_TCP_LISTEN 0x01;ch395f_sock_state_t 为 + * { uint8_t open; uint8_t tcp_state; } 两字节。 + * + * 必须复刻的两条芯片怪癖(不复刻就等于把被测代码的坑假件掉): + * 1. ch395f_get_sock_int_status() 是**整体读即清**、芯片没有按位清除手段。本假件 + * 返回该 socket 的挂起位图并同步清零。AGENTS 模块约定里 B3 的实证坑("顺手 + * 全量清一遍"把别的在用 Socket 的门焊死)靠这条才照得出来。 + * 2. *_cached 系列返回的是**缓存值**而不是现场值:open 状态与 TCP 状态分开、且 + * "监听还活着"的判据是 (open != CLOSED && tcp_state == LISTEN)。测试据此能构造 + * "缓存陈旧"的形态。 + * + * 脚本化模型(沿用本仓既有假件风格:nand_port 的真头+假 diskio、lftpd_io 的可注入假堆): + * - 每个读函数一条按调用序号取的返回队列,队列耗尽后回落到**最后一个值**(便于 + * 写"一直可读""一直关不掉"这类持续状态); + * - 每个写/控制函数一条调用记录(次数 + 最近一次入参),另给逐函数调用计数, + * 让"这一轮根本不该碰芯片"成为可断言的事; + * - g_chip.log[] 记录**调用顺序**(短名),用于断言"先清接收、再关、再读即清"这类 + * 时序契约; + * - ch395f_fake_reset() 一次全清。 + * + * 创建日期:2026-10-23 + */ + +#ifndef CH395F_FAKE_H +#define CH395F_FAKE_H + +#include + +/* —— 与真实 ch395f.h 逐字一致的常量(值抄自真实头,勿"顺手改")—— */ +#define CH395F_ERR_SUCCESS 0x00U +#define CH395F_PROTO_TYPE_TCP 0x03U +#define CH395F_SOCKET_CLOSED 0x00U +#define CH395F_SOCKET_OPEN 0x01U +#define CH395F_TCP_CLOSED 0x00U +#define CH395F_TCP_LISTEN 0x01U +#define CH395F_TCP_ESTABLISHED 0x05U + +#define CH395F_FAKE_SOCK_MAX 8 /* 与 NET_MAX_SOCKETS 同量级,测试自用 */ +#define CH395F_FAKE_QUEUE_LEN 24 +#define CH395F_FAKE_LOG_LEN 96 + +typedef struct { + uint8_t open; + uint8_t tcp_state; +} ch395f_sock_state_t; + +/* —— 可预置的芯片侧世界 —— */ +typedef struct { + uint8_t cached_open[CH395F_FAKE_SOCK_MAX]; /* sock_state_cached().open */ + uint8_t cached_tcp[CH395F_FAKE_SOCK_MAX]; /* sock_state_cached().tcp_state */ + uint8_t status_byte0[CH395F_FAKE_SOCK_MAX]; /* get_socket_status 的 sock 状态 */ + uint8_t status_byte1[CH395F_FAKE_SOCK_MAX]; /* get_socket_status 的 TCP 状态 */ + uint8_t int_status[CH395F_FAKE_SOCK_MAX]; /* 读即清的挂起位图 */ + uint16_t recv_len_q[CH395F_FAKE_SOCK_MAX][CH395F_FAKE_QUEUE_LEN]; + int recv_len_n[CH395F_FAKE_SOCK_MAX]; + int recv_len_i[CH395F_FAKE_SOCK_MAX]; + uint8_t open_rc[CH395F_FAKE_SOCK_MAX]; /* open_socket 返回码 */ + uint8_t listen_rc[CH395F_FAKE_SOCK_MAX]; /* tcp_listen 返回码 */ +} ch395f_fake_world_t; + +typedef struct { + /* 调用计数("不该碰芯片"的断言面) */ + int open_calls; + int close_calls; + int listen_calls; + int int_status_calls; + int get_status_calls; + int read_calls; + int write_calls; + int set_proto_calls; + int set_port_calls; + int cached_calls; + /* 最近一次入参 */ + uint8_t last_sock; + uint16_t last_read_len; + uint16_t last_port; + uint8_t last_proto; + uint8_t last_keepalive; + int keepalive_calls; + /* 调用顺序日志(短名首字母): + * 'c'=close 'i'=int_status(读即清) 's'=get_socket_status 'r'=read_recv_buf + * 'R'=get_recv_len 'o'=open_socket 'l'=tcp_listen 'p'=set_sour_port + * 'P'=set_proto_type */ + char log[CH395F_FAKE_LOG_LEN]; + int log_n; +} ch395f_fake_obs_t; + +extern ch395f_fake_world_t g_fake; +extern ch395f_fake_obs_t g_chip; + +void ch395f_fake_reset(void); /* 全清并把所有 socket 设为"已关闭" */ +void ch395f_fake_push_recv_len(uint8_t sock, uint16_t v); +void ch395f_fake_set_listening(uint8_t sock); /* 缓存:open!=CLOSED 且 tcp=LISTEN */ + +/* —— 假芯片实现(签名与真实 ch395f.h 一字不改)—— */ +ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn); +ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn); +void ch395f_get_socket_status(uint8_t sock, uint8_t *status); +uint8_t ch395f_get_sock_int_status(uint8_t sock); +uint16_t ch395f_get_recv_len(uint8_t sock); +void ch395f_read_recv_buf(uint8_t sock, uint8_t *p_data, uint16_t len); +void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len); +uint8_t ch395f_open_socket(uint8_t sock); +uint8_t ch395f_close_socket(uint8_t sock); +uint8_t ch395f_tcp_listen(uint8_t sock); +void ch395f_set_proto_type(uint8_t sock, uint8_t proto); +void ch395f_set_sour_port(uint8_t sock, uint16_t port); +void ch395f_get_remot_ipp(uint8_t sock, uint8_t *p_buf); +void ch395f_set_keepalive_enable(uint8_t sock, uint8_t enable); +uint8_t ch395f_link_up_cached(void); +uint16_t ch395f_link_change_count(void); + +#endif /* CH395F_FAKE_H */ diff --git a/test/host/net_listener/net_listener_test.c b/test/host/net_listener/net_listener_test.c new file mode 100644 index 00000000..30af09af --- /dev/null +++ b/test/host/net_listener/net_listener_test.c @@ -0,0 +1,545 @@ +/* + * 模块名称:NET 层监听重建(net_listener)主机侧单元测试 + * 模块功能:在 PC 上离线验证 Drivers/BSP/NET/net_listener.c 的**自动重开监听**状态机: + * 请求去重、五种放弃条件、挂起 recv 让路、30ms 退避与 2000ms 预算、 + * 隔离/优雅两级沉降窗(400ms / 100ms)、800ms 上限"只留痕不放弃"、 + * 接收区清理的 256 字节钳位与 16 轮上限、重建后的 peer 簿记,以及 + * "先清接收 → 关断 → 读即清 → 读状态"的芯片调用顺序。 + * + * 被测对象:真实源文件 ../../../Drivers/BSP/NET/net_listener.c(#include 原样纳入)。 + * net_listener.h / net_types.h / net_config.h / net_pending.h 全用**真实头** + * (net_sock_t 字段名、NET_SOCK_STATE_* / NET_SOCK_STREAM / NET_ERR_* / + * net_listener_req_t 枚举一改就编译期报错)。假件: + * - 假 CH395F 芯片:test/host/common/ch395f_fake.h/.c(15 个真实原型 + + * 按调用序号取值的返回队列 + 调用计数与**调用顺序日志**); + * - 可控 socket 表(net_get_sock 可写回项)、可控时基(HAL_GetTick 由测试 + * 显式设定,400/100/800/2000 四条时间线都能卡点)、挂起 recv 的有无、 + * dbg_log 计数、net_set_errno 记录。 + * + * 为什么要主机侧单测(而不是只靠台架): + * 1. CH395F 是**单连接 socket**:监听与数据通道复用同一号,断线后必须重开成 LISTEN, + * 否则端口"连接被拒"。重开时机由"芯片此刻报什么状态"决定,而这些状态在真机上 + * 一闪而过——对端配合、时序、总线全都得凑齐才见得到一次,台架无法稳定复现。 + * 2. 这里的时间窗是**四道互相牵制的闸门**(30ms 退避 / 400 与 100 沉降 / 800 留痕 / + * 2000 预算),排期登记里那句"沉降窗 800ms 上限只留痕"就是从这里来的:芯片永远 + * 关不掉时,代码**不能放弃请求**,也不能每 30ms 刷一条日志。真机上验证一次要等 + * 到真断线,主机侧把 tick 做成显式量后,1399ms 与 1400ms 的差别就是一行断言。 + * 3. GET_SOCK_INT_STATUS 是**整体读即清**,"顺手全量清一遍"会把别的在用 socket 的 + * 门焊死(AGENTS 模块约定 B3 实证坑)。假芯片必须复刻读即清,本文件用调用顺序 + * 日志钉住"只读自己这一路"的时序。 + * + * 覆盖点: + * TC-NETLIS-001 越界 fd:settle/request/clear 静默返回,tick 不产生任何芯片调用 + * TC-NETLIS-010 芯片显示仍在监听 → 不重建,且请求被 clear(下次还能受理) + * TC-NETLIS-011 有挂起 recv / 挂起 send 时让路,但**保留请求**(不丢重建) + * TC-NETLIS-012 五种放弃条件(非 LISTENING/CLOSED 态、auto_relisten=0、非 STREAM、 + * local_port=0、表项 in_use=0)→ 静默 clear,不报 ERROR + * TC-NETLIS-013 重建成功后请求清空,重复 tick 不重复重建 + * TC-NETLIS-014 open 失败:关回去 + 报 BUSY + 30ms 退避(20ms 后不重试、30ms 重试), + * 首轮建立 2000ms 预算 + * TC-NETLIS-015 tcp_listen 失败也要关断 socket + * TC-NETLIS-016 重建成功后簿记:转 LISTENING、清 send_ready/recv_bytes/remote_*、 + * 把旧 remote_port 记进 last_peer_port、清 lost_conn_seen/released_abnormal + * TC-NETLIS-017 重建用的源端口与协议取自表项本身(端口不能漂) + * TC-NETLIS-018 预算耗尽:清请求 + 报一次 ERROR 交对账器接手,之后不再自己试 + * TC-NETLIS-019 请求去重:已有请求时后来的 reason 不覆盖首个 + * TC-NETLIS-020 隔离级沉降:关不掉就等,且**只在第一次**打 INFO + * TC-NETLIS-021 隔离级 hold = 400ms(399ms 不放行、1400ms 放行) + * TC-NETLIS-021b 优雅级 hold = 100ms(99ms 不放行、1100ms 放行) + * TC-NETLIS-022 800ms 上限**只留痕一次且不放弃**,到 2000ms 才交给对账器 + * TC-NETLIS-023 清理接收区:单次读钳到 256 字节、最多 16 轮(读到空即停) + * TC-NETLIS-024 芯片调用顺序:清接收 → 关断 → 读即清 → 读状态 + * TC-NETLIS-025 读即清真的清零(第二次读到 0) + * TC-NETLIS-026 表项不存在由 tick 直接放弃(net_relisten_locked 里的 BADF 分支 + * 从 tick 不可达 —— 死分支留痕,见注释) + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-23 + */ + +#include +#include + +#include "net_listener.h" +#include "net_pending.h" +#include "net_socket.h" +#include "ch395f.h" + +/* 假芯片实现与生产源进同一个 TU(本仓主机单测既有做法) */ +#include "ch395f_fake.c" +#include "net_listener.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) + +/* ===================== dbg_log 桩计数 ===================== */ + +int g_nlis_info_count; +int g_nlis_error_count; +int g_nlis_debug_count; + +/* ===================== 可控 socket 表 ===================== */ + +static net_sock_t g_sock[NET_MAX_SOCKETS]; +static uint8_t g_sock_valid[NET_MAX_SOCKETS]; + +net_sock_t *net_get_sock(int sockfd) +{ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return NULL; + } + if (g_sock_valid[sockfd] == 0u) { + return NULL; + } + return &g_sock[sockfd]; +} + +/* ===================== 挂起 recv / send 的有无 ===================== */ + +static int g_pending[NET_MAX_SOCKETS]; +static int g_send_pending[NET_MAX_SOCKETS]; + +int net_pending_active(int sockfd) +{ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return 0; + } + return g_pending[sockfd]; +} + +int net_send_pending_active(int sockfd) +{ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return 0; + } + return g_send_pending[sockfd]; +} + +/* ===================== 可控时基与 errno ===================== */ + +static uint32_t g_now; +static int g_errno_last; +static int g_errno_calls; + +uint32_t HAL_GetTick(void) +{ + return g_now; /* 由测试显式设定:400/100/800/2000 四条线要能卡点 */ +} + +int net_set_errno(int err) +{ + g_errno_last = err; + g_errno_calls++; + return err; +} + +/* ===================== 复位与建表 ===================== */ + +static void reset_all(void) +{ + (void)memset(g_sock, 0, sizeof(g_sock)); + (void)memset(g_sock_valid, 0, sizeof(g_sock_valid)); + (void)memset(g_pending, 0, sizeof(g_pending)); + (void)memset(g_send_pending, 0, sizeof(g_send_pending)); + g_now = 1000u; + g_errno_last = 0; + g_errno_calls = 0; + g_nlis_info_count = 0; + g_nlis_error_count = 0; + g_nlis_debug_count = 0; + ch395f_fake_reset(); + /* 关键:被测模块的 static 状态(s_relisten_req / _next / _dead / _quarantine / + * _settle_start / _settle_dead / _capped)不会随假世界一起复位。上一场景残留的 + * next/dead 会把后面每一条时序断言都带偏(实测一次红了 9 条)。用模块自己的 + * net_listener_clear() 清——它清的正是这些项,也顺带验证了 clear 的完整性。 */ + for (int i = 0; i < NET_MAX_SOCKETS; i++) { + net_listener_clear(i); + } +} + +/* 一个"刚断线、等待重开监听"的槽位 */ +static void mk_closed_listen(int fd, uint16_t port) +{ + g_sock_valid[fd] = 1u; + g_sock[fd].in_use = 1u; + g_sock[fd].type = NET_SOCK_STREAM; + g_sock[fd].state = NET_SOCK_STATE_CLOSED; + g_sock[fd].auto_relisten = 1u; + g_sock[fd].local_port = port; +} + +/* ===================== 边界与去重 ===================== */ + +static void tc_bounds(void) +{ + printf("-- TC-NETLIS-001/019 bounds --\n"); + reset_all(); + net_listener_settle(-1, NET_LISTENER_SETTLE_GRACEFUL); + net_listener_settle(NET_MAX_SOCKETS, NET_LISTENER_SETTLE_QUARANTINE); + net_listener_request(-1, NET_LISTENER_REQ_RECON); + net_listener_request(NET_MAX_SOCKETS, NET_LISTENER_REQ_RECON); + net_listener_clear(-1); + net_listener_clear(NET_MAX_SOCKETS); + net_listener_tick(); + CHECK((g_chip.open_calls == 0) && (g_chip.close_calls == 0) && + (g_nlis_error_count == 0), + "TC-NETLIS-001 越界 fd 静默返回,不打扰芯片"); + + reset_all(); + mk_closed_listen(0, 2404); + net_listener_request(0, NET_LISTENER_REQ_ACCEPT); + net_listener_request(0, NET_LISTENER_REQ_DROP); /* 应被忽略 */ + CHECK(s_relisten_req[0] == (uint8_t)(NET_LISTENER_REQ_ACCEPT + 1U), + "TC-NETLIS-019 请求去重:后来的 reason 不覆盖首个"); +} + +/* ===================== 该不该重建 ===================== */ + +static void tc_should_or_not(void) +{ + printf("-- TC-NETLIS-010..013 --\n"); + reset_all(); + mk_closed_listen(0, 2404); + ch395f_fake_set_listening(0); /* 芯片说:还在 LISTEN */ + net_listener_request(0, NET_LISTENER_REQ_ACCEPT); + net_listener_tick(); + CHECK((g_chip.open_calls == 0) && (s_relisten_req[0] == 0u), + "TC-NETLIS-010 监听仍活着就不重建,且请求已 clear"); + g_fake.cached_open[0] = CH395F_SOCKET_CLOSED; /* 之后才真死了 */ + net_listener_request(0, NET_LISTENER_REQ_RECON); + net_listener_tick(); + CHECK(g_chip.open_calls == 1, + "TC-NETLIS-010b 上一步确实清掉了请求(否则这里会被去重吞掉)"); + + reset_all(); + mk_closed_listen(0, 2404); + g_pending[0] = 1; + net_listener_request(0, NET_LISTENER_REQ_EOF); + net_listener_tick(); + CHECK((g_chip.open_calls == 0) && (s_relisten_req[0] != 0u), + "TC-NETLIS-011 挂起 recv 未结束时让路但保留请求"); + g_pending[0] = 0; + g_send_pending[0] = 1; + net_listener_tick(); + CHECK((g_chip.open_calls == 0) && (s_relisten_req[0] != 0u), + "TC-NETLIS-011b 挂起 send 同样挡路"); + g_send_pending[0] = 0; + net_listener_tick(); + CHECK((g_chip.open_calls == 1) && (s_relisten_req[0] == 0u), + "TC-NETLIS-011c 挂起结束后自动补上重建"); + + reset_all(); + mk_closed_listen(0, 2404); + net_listener_request(0, NET_LISTENER_REQ_RECON); + net_listener_tick(); + net_listener_tick(); + CHECK(g_chip.open_calls == 1, "TC-NETLIS-013 重建成功后不重复重建"); +} + +static void tc_giveup_conditions(void) +{ + int i; + int opened[5]; + int cleared[5]; + int errs[5]; + + printf("-- TC-NETLIS-012 五种放弃条件 --\n"); + reset_all(); + for (i = 0; i < 5; i++) { + uint8_t before_open = (uint8_t)g_chip.open_calls; + (void)before_open; + reset_all(); + mk_closed_listen(0, 2404); + switch (i) { + case 0: g_sock[0].auto_relisten = 0u; break; /* 明确不自动重开 */ + case 1: g_sock[0].type = (NET_SOCK_STREAM + 1); break; /* 不是 TCP */ + case 2: g_sock[0].local_port = 0u; break; /* 没绑端口 */ + case 3: g_sock[0].in_use = 0u; break; /* 槽位已释放 */ + default: g_sock[0].state = NET_SOCK_STATE_ESTABLISHED; break; + } + net_listener_request(0, NET_LISTENER_REQ_RECON); + net_listener_tick(); + opened[i] = g_chip.open_calls; + cleared[i] = (s_relisten_req[0] == 0u) ? 1 : 0; + errs[i] = g_nlis_error_count; + } + CHECK((opened[0] == 0) && (opened[1] == 0) && (opened[2] == 0) && + (opened[3] == 0) && (opened[4] == 0), + "TC-NETLIS-012 五种条件下都不碰芯片"); + CHECK((cleared[0] == 1) && (cleared[1] == 1) && (cleared[2] == 1) && + (cleared[3] == 1) && (cleared[4] == 1), + "TC-NETLIS-012b 放弃即清请求(不留在表里反复白试)"); + CHECK((errs[0] == 0) && (errs[2] == 0) && (errs[4] == 0), + "TC-NETLIS-012c 放弃是静默的(不该报 ERROR,那是对账器的活)"); + + reset_all(); + g_sock_valid[0] = 0u; /* 连表项都没有 */ + net_listener_request(0, NET_LISTENER_REQ_RECON); + net_listener_tick(); + CHECK((g_chip.open_calls == 0) && (s_relisten_req[0] == 0u), + "TC-NETLIS-026 表项不存在由 tick 直接放弃"); + /* 注:net_relisten_locked() 里的 NET_ERR_BADF 分支因此从 tick **不可达** + * (net_listener.c:112 那段只在 p_sock 非空时才可能被调到)——留痕:它是 + * 内部兜底,不是活路径;将来若新增调用点,要重新审视这条。 */ +} + +/* ===================== 失败退避与预算 ===================== */ + +static void tc_backoff_and_budget(void) +{ + printf("-- TC-NETLIS-014/015/018 --\n"); + reset_all(); + mk_closed_listen(0, 2404); + g_fake.open_rc[0] = 0x01U; /* open 永远失败 */ + g_now = 1000u; + net_listener_request(0, NET_LISTENER_REQ_RECON); + net_listener_tick(); + CHECK((g_chip.open_calls == 1) && (g_chip.close_calls == 1) && + (g_errno_last == NET_ERR_BUSY) && (s_relisten_req[0] != 0u) && + (s_relisten_next[0] == 1030u) && (s_relisten_dead[0] == 3000u), + "TC-NETLIS-014 open 失败:关回去 + BUSY + 30ms 退避 + 首轮建 2000ms 预算"); + g_now = 1020u; + net_listener_tick(); + CHECK(g_chip.open_calls == 1, "TC-NETLIS-014b 退避未到不重试(不空转打芯片)"); + g_now = 1030u; + net_listener_tick(); + CHECK(g_chip.open_calls == 2, "TC-NETLIS-014c 退避到点再试一次"); + + reset_all(); + mk_closed_listen(0, 2404); + g_fake.listen_rc[0] = 0x01U; /* open 成功、listen 失败 */ + net_listener_request(0, NET_LISTENER_REQ_RECON); + net_listener_tick(); + CHECK((g_chip.listen_calls == 1) && (g_chip.close_calls == 1) && + (s_relisten_req[0] != 0u), + "TC-NETLIS-015 listen 失败也要关断(不留下开了一半的 socket)"); + + reset_all(); + mk_closed_listen(0, 2404); + g_fake.open_rc[0] = 0x01U; + g_now = 1000u; + net_listener_request(0, NET_LISTENER_REQ_RECON); + net_listener_tick(); /* 建预算 dead=3000 */ + g_fake.open_rc[0] = CH395F_ERR_SUCCESS; /* 芯片后来能开了 */ + g_now = 3000u; /* 但预算已耗尽 */ + net_listener_tick(); + CHECK((g_nlis_error_count == 1) && (s_relisten_req[0] == 0u) && + (g_chip.open_calls == 1), + "TC-NETLIS-018 预算耗尽:报一次 ERROR 交对账器,之后不再自己试"); +} + +/* ===================== 成功重建的簿记 ===================== */ + +static void tc_bookkeeping(void) +{ + net_sock_t *p; + + printf("-- TC-NETLIS-016/017 bookkeeping --\n"); + reset_all(); + mk_closed_listen(0, 2404); + p = &g_sock[0]; + p->state = NET_SOCK_STATE_CLOSED; + p->remote_port = 1234u; + p->remote_ip = 0x0A000001u; + p->remote_ip_arr[0] = 9u; + p->remote_ip_arr[3] = 7u; + p->send_ready = 1u; + p->recv_bytes = 77u; + p->released_abnormal = 1u; + p->lost_conn_seen = 1u; + net_listener_request(0, NET_LISTENER_REQ_DROP); + net_listener_tick(); + CHECK((p->state == NET_SOCK_STATE_LISTENING) && (p->send_ready == 0u) && + (p->recv_bytes == 0u) && (p->remote_port == 0u) && + (p->remote_ip == 0u), + "TC-NETLIS-016 重建后回到干净监听态"); + CHECK((p->last_peer_port == 1234u) && (p->lost_conn_seen == 0u) && + (p->released_abnormal == 0u) && (p->remote_ip_arr[0] == 0u) && + (p->remote_ip_arr[3] == 0u), + "TC-NETLIS-016b 旧对端端口进 last_peer_port,其余对端痕迹清零"); + CHECK((g_chip.set_port_calls == 1) && (g_chip.last_port == 2404u) && + (g_chip.set_proto_calls == 1) && + (g_chip.last_proto == CH395F_PROTO_TYPE_TCP), + "TC-NETLIS-017 源端口与协议取自表项本身(监听端口不能漂)"); +} + +/* ===================== 沉降窗:隔离 400 / 优雅 100 ===================== */ + +static void tc_settle_windows(void) +{ + printf("-- TC-NETLIS-020/021 沉降窗 --\n"); + reset_all(); + mk_closed_listen(0, 2404); + g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; /* 芯片关不掉 */ + g_fake.status_byte1[0] = CH395F_TCP_ESTABLISHED; + net_listener_settle(0, NET_LISTENER_SETTLE_QUARANTINE); + net_listener_request(0, NET_LISTENER_REQ_DROP); + g_now = 1000u; + net_listener_tick(); + CHECK((g_chip.open_calls == 0) && (g_nlis_info_count == 1) && + (s_relisten_settle_start[0] == 1000u) && + (s_relisten_settle_dead[0] == 1800u) && (g_errno_last == NET_ERR_BUSY), + "TC-NETLIS-020 关不掉就等:首次记 start 并给 800ms 留痕线,返回 BUSY"); + g_now = 1030u; + net_listener_tick(); + CHECK(g_nlis_info_count == 1, + "TC-NETLIS-020b 等待期间**只在第一次**打日志(否则每 30ms 刷一条)"); + + /* 边界必须在**独立场景**里测:放行判定与 30ms 退避共用 s_relisten_next, + * 上一轮等待会把 next 设成"当次 + 30",于是同一条链里"恰好 400ms"那一次 tick + * 根本不会发起尝试。这本身是个该知道的事实——**hold 到点后最坏还要再等一个 + * 退避周期才真正放行**(实测最多晚 30ms),写在这里免得后人以为是漏判。 */ + reset_all(); + mk_closed_listen(0, 2404); + g_fake.status_byte0[0] = CH395F_SOCKET_CLOSED; + g_fake.status_byte1[0] = CH395F_TCP_CLOSED; + net_listener_settle(0, NET_LISTENER_SETTLE_QUARANTINE); + net_listener_request(0, NET_LISTENER_REQ_DROP); + g_now = 1000u; + net_listener_tick(); /* 记 start=1000,next=1030 */ + g_now = 1399u; + net_listener_tick(); + CHECK((g_chip.open_calls == 0) && (s_relisten_quarantine[0] != 0u), + "TC-NETLIS-021 隔离级 hold=400ms:第 399ms 仍不放行"); + + reset_all(); + mk_closed_listen(0, 2404); + g_fake.status_byte0[0] = CH395F_SOCKET_CLOSED; + g_fake.status_byte1[0] = CH395F_TCP_CLOSED; + net_listener_settle(0, NET_LISTENER_SETTLE_QUARANTINE); + net_listener_request(0, NET_LISTENER_REQ_DROP); + g_now = 1000u; + net_listener_tick(); + g_now = 1400u; + net_listener_tick(); + CHECK((g_chip.open_calls == 1) && (s_relisten_quarantine[0] == 0u), + "TC-NETLIS-021b 第 400ms 放行并清隔离(独立场景,不受退避槽遮挡)"); + + reset_all(); + mk_closed_listen(0, 2404); + net_listener_settle(0, NET_LISTENER_SETTLE_GRACEFUL); + net_listener_request(0, NET_LISTENER_REQ_EOF); + g_now = 1000u; + net_listener_tick(); + CHECK((g_chip.open_calls == 0) && (s_relisten_settle_start[0] == 1000u), + "TC-NETLIS-021c 优雅级同样先等(0 < 100ms)"); + g_now = 1099u; + net_listener_tick(); + CHECK(g_chip.open_calls == 0, "TC-NETLIS-021c 优雅级 hold=100ms:第 99ms 不放行"); + + reset_all(); + mk_closed_listen(0, 2404); + net_listener_settle(0, NET_LISTENER_SETTLE_GRACEFUL); + net_listener_request(0, NET_LISTENER_REQ_EOF); + g_now = 1000u; + net_listener_tick(); + g_now = 1100u; + net_listener_tick(); + CHECK(g_chip.open_calls == 1, "TC-NETLIS-021d 优雅级第 100ms 放行(比隔离级快 4 倍)"); +} + +static void tc_cap_is_trace_only(void) +{ + printf("-- TC-NETLIS-022 800ms 上限只留痕 --\n"); + reset_all(); + mk_closed_listen(0, 2404); + g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; /* 永远关不掉 */ + g_fake.status_byte1[0] = CH395F_TCP_ESTABLISHED; + net_listener_settle(0, NET_LISTENER_SETTLE_QUARANTINE); + net_listener_request(0, NET_LISTENER_REQ_PENDING); + g_now = 1000u; + net_listener_tick(); /* start=1000 settle_dead=1800 */ + g_now = 1800u; + net_listener_tick(); + CHECK((s_relisten_capped[0] == 1u) && (g_nlis_info_count == 2) && + (g_chip.open_calls == 0) && (s_relisten_req[0] != 0u), + "TC-NETLIS-022 到 800ms 只**留痕一次**,既不重建也**不放弃请求**"); + g_now = 1830u; + net_listener_tick(); + CHECK(g_nlis_info_count == 2, + "TC-NETLIS-022b 留痕只打一次(之后继续默默等,不刷屏)"); + g_now = 3000u; + net_listener_tick(); + CHECK((g_nlis_error_count == 1) && (s_relisten_req[0] == 0u), + "TC-NETLIS-022c 最终由 2000ms 预算接手:报 ERROR 并交出请求"); +} + +/* ===================== 芯片交互:清理、顺序、读即清 ===================== */ + +static void tc_chip_interaction(void) +{ + printf("-- TC-NETLIS-023/024/025 --\n"); + reset_all(); + mk_closed_listen(0, 2404); + net_listener_settle(0, NET_LISTENER_SETTLE_GRACEFUL); + ch395f_fake_push_recv_len(0, 1000u); /* 一直有 1000 字节 */ + net_listener_request(0, NET_LISTENER_REQ_DROP); + g_now = 1000u; + net_listener_tick(); + CHECK((g_chip.read_calls == 16) && (g_chip.last_read_len == 256u), + "TC-NETLIS-023 清接收区:单次钳到 256 字节、最多 16 轮(有守卫不死循环)"); + + reset_all(); + mk_closed_listen(0, 2404); + net_listener_settle(0, NET_LISTENER_SETTLE_GRACEFUL); + ch395f_fake_push_recv_len(0, 5u); + ch395f_fake_push_recv_len(0, 0u); /* 第二轮就空了 */ + net_listener_request(0, NET_LISTENER_REQ_DROP); + g_now = 1000u; + net_listener_tick(); + CHECK((g_chip.read_calls == 1) && (g_chip.last_read_len == 5u), + "TC-NETLIS-023b 读到空立刻停手(不硬凑 16 轮)"); + + reset_all(); + mk_closed_listen(0, 2404); + g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; /* 停在等待分支,便于看顺序 */ + net_listener_settle(0, NET_LISTENER_SETTLE_QUARANTINE); + net_listener_request(0, NET_LISTENER_REQ_DROP); + g_now = 1000u; + net_listener_tick(); + CHECK((strcmp(g_chip.log, "Rcis") == 0) && + (g_chip.int_status_calls == 1) && (g_chip.get_status_calls == 1), + "TC-NETLIS-024 顺序:清接收 → 关断 → 读即清 → 读状态(不可换)"); + + reset_all(); + mk_closed_listen(0, 2404); + g_fake.int_status[0] = 0x3FU; + g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; + net_listener_settle(0, NET_LISTENER_SETTLE_QUARANTINE); + net_listener_request(0, NET_LISTENER_REQ_DROP); + g_now = 1000u; + net_listener_tick(); + CHECK((g_chip.int_status_calls >= 1) && (g_fake.int_status[0] == 0u), + "TC-NETLIS-025 读即清真的清零(复刻芯片无按位清除的怪癖)"); +} + +/* ===================== 入口 ===================== */ + +int main(void) +{ + printf("=== NET Listener Tests ===\n"); + + tc_bounds(); + tc_should_or_not(); + tc_giveup_conditions(); + tc_backoff_and_budget(); + tc_bookkeeping(); + tc_settle_windows(); + tc_cap_is_trace_only(); + tc_chip_interaction(); + + printf("\n=== NET Listener Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/net_listener/run_unit_test.py b/test/host/net_listener/run_unit_test.py new file mode 100644 index 00000000..2a1c936e --- /dev/null +++ b/test/host/net_listener/run_unit_test.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""NET 层监听自动重建(net_listener)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/net_listener/net_listener_test.c(以 #include 纳入生产 + 源 Drivers/BSP/NET/net_listener.c 与共享假芯片 test/host/common/ch395f_fake.c), + 在 PC 上离线验证自动重开监听的整套闸门:请求去重、五种放弃条件、挂起 recv/send + 让路、30ms 退避、400/100ms 两级沉降窗、800ms 上限只留痕、2000ms 预算交对账器、 + 接收区清理的 256B 钳位与 16 轮上限、重建后的 peer 簿记与芯片调用顺序。 + 工具链与判定委托 test/host/common/unit_test_harness.py。 + +为什么要主机侧单测(而不是只用台架): + CH395F 是单连接 socket——监听与数据通道复用同一号,断线后必须重开成 LISTEN,否则 + 端口"连接被拒"。而重开时机取决于"芯片此刻报什么状态",这些状态真机上一闪而过, + 要凑齐对端行为、时序与总线才见得到一次。四道时间闸门(30/400·100/800/2000ms)互相 + 牵制,台架无法稳定复现;把 HAL_GetTick 做成显式量后,399ms 与 400ms 的差别就是一行 + 断言。GET_SOCK_INT_STATUS 是整体读即清("顺手全量清一遍"会把别的在用 socket 的门 + 焊死),必须由假芯片复刻才照得出来。 + +桩的边界:net_listener.h / net_types.h / net_config.h / net_pending.h 用真实头(net_sock_t + 字段名与各枚举一改即编译期报错);net_socket.h 是桩(真实那份带 netTask 消息队列, + PC 上编不动),但 net_set_errno 与 NET_ERR_RETURN_1 的写法照抄真实头同名宏; + ch395f.h 只做转发,真身在 test/host/common/ch395f_fake.h/.c —— NET 域三个模块共享 + 一份假芯片,签名与常量只有一处定义。main.h / cmsis_os.h / FreeRTOS.h / task.h / + test_config.h / net_latency_test.h / dbg_log.h 是本目录桩;刻意不定义 + TEST_LATENCY_PROBE(延迟探针不是被测对象,且它要写真实时间)。 + +用法(仓库根目录): + python test/host/net_listener/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 + +设计依据:docs/网络/网络自愈架构设计.md §4.10、docs/测试说明/主机单测覆盖率口径.md §9(假 CH395F 实施规格) +创建日期: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 + +# 顺序敏感:stub 必须最先(覆盖 ch395f.h / net_socket.h / main.h / dbg_log.h); +# common 提供共享假芯片;NET 目录提供真实的 net_types.h / net_pending.h 等。 +BUILD = TestBuild( + name="NET Listener Tests", + src=os.path.join(HERE, "net_listener_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "test", "host", "common"), + os.path.join(ROOT, "Drivers", "BSP", "NET"), + HERE, + ], + summary_marker="=== NET Listener Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) + diff --git a/test/host/net_listener/stub/FreeRTOS.h b/test/host/net_listener/stub/FreeRTOS.h new file mode 100644 index 00000000..b7590a95 --- /dev/null +++ b/test/host/net_listener/stub/FreeRTOS.h @@ -0,0 +1,19 @@ +/* + * 模块名称:主机侧单测桩 - FreeRTOS.h(net_listener 模块专用) + * 模块功能:本模块不调任何内核 API,但真实 net_pending.h 的注册类原型里带 + * TaskHandle_t 参数(include 链绕不开),故这里给出它的宿主等价物。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * TaskHandle_t 在 FreeRTOS 里就是 void * 不透明句柄,这里保持一致——它只出现在 + * 原型签名中、net_listener.c 从不传它,所以宿主侧用什么表示都不影响被测语义; + * 但**原型必须能编**,否则真实 net_pending.h 就进不来(进不来就丢掉接口真实性)。 + * 创建日期:2026-10-23 + */ +#ifndef FREERTOS_H +#define FREERTOS_H + +#include +#include + +typedef void *TaskHandle_t; /* 与 FreeRTOS 一致:不透明指针句柄 */ + +#endif /* FREERTOS_H */ diff --git a/test/host/net_listener/stub/ch395f.h b/test/host/net_listener/stub/ch395f.h new file mode 100644 index 00000000..dab4fe65 --- /dev/null +++ b/test/host/net_listener/stub/ch395f.h @@ -0,0 +1,11 @@ +/* + * 模块名称:主机侧单测桩 - ch395f.h(net_listener 模块入口) + * 模块功能:本文件**只做转发**——真正的假芯片在 test/host/common/ch395f_fake.h/.c。 + * ⚠ 转发而不是各写一份:NET 域三个模块共用同一份假芯片,签名与常量只有一处定义, + * 避免"三份桩各自漂移、假件与被测代码同时错却互相圆场"。 + * 创建日期:2026-10-23 + */ +#ifndef CH395F_H +#define CH395F_H +#include "ch395f_fake.h" +#endif /* CH395F_H */ diff --git a/test/host/net_listener/stub/cmsis_os.h b/test/host/net_listener/stub/cmsis_os.h new file mode 100644 index 00000000..e2742671 --- /dev/null +++ b/test/host/net_listener/stub/cmsis_os.h @@ -0,0 +1,10 @@ +/* + * 模块名称:主机侧单测桩 - cmsis_os.h(net_listener 模块专用) + * 模块功能:空壳。net_listener.c include 它但不用内核 API(重建在 tick 里同步做完)。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ +#ifndef CMSIS_OS_H +#define CMSIS_OS_H +#include +#endif /* CMSIS_OS_H */ diff --git a/test/host/net_listener/stub/dbg_log.h b/test/host/net_listener/stub/dbg_log.h new file mode 100644 index 00000000..a1e3232d --- /dev/null +++ b/test/host/net_listener/stub/dbg_log.h @@ -0,0 +1,16 @@ +/* + * 模块名称:主机侧单测桩 - dbg_log.h(net_listener 模块专用) + * 模块功能:DBG_* 换成计数,让"只在第一次打日志""上限只留痕一次""预算耗尽才报 ERROR" + * 这三条日志契约可断言——它们正是排期登记里"沉降窗 800ms 上限只留痕"的原话。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ +#ifndef DBG_LOG_H +#define DBG_LOG_H +extern int g_nlis_info_count; +extern int g_nlis_error_count; +extern int g_nlis_debug_count; +#define DBG_INFO(fmt, ...) do { (void)(fmt); g_nlis_info_count++; } while (0) +#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_nlis_debug_count++; } while (0) +#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_nlis_error_count++; } while (0) +#endif /* DBG_LOG_H */ diff --git a/test/host/net_listener/stub/main.h b/test/host/net_listener/stub/main.h new file mode 100644 index 00000000..877990f6 --- /dev/null +++ b/test/host/net_listener/stub/main.h @@ -0,0 +1,12 @@ +/* + * 模块名称:主机侧单测桩 - main.h(net_listener 模块专用) + * 模块功能:只给 HAL_GetTick(沉降窗/退避/预算三条时间轴的唯一致度源)。 + * ⚠ 仅供 test/host/net_listener/ 使用,不参与 MCU 构建。测试把 tick 做成"写多少 + * 就是多少"的显式量,于是 400ms / 100ms / 800ms / 2000ms 四条线都能精确卡点。 + * 创建日期:2026-10-23 + */ +#ifndef MAIN_H +#define MAIN_H +#include +uint32_t HAL_GetTick(void); +#endif /* MAIN_H */ diff --git a/test/host/net_listener/stub/net_latency_test.h b/test/host/net_listener/stub/net_latency_test.h new file mode 100644 index 00000000..1b13ea26 --- /dev/null +++ b/test/host/net_listener/stub/net_latency_test.h @@ -0,0 +1,9 @@ +/* + * 模块名称:主机侧单测桩 - net_latency_test.h(net_listener 模块专用) + * 模块功能:空壳,配合 TEST_LATENCY_PROBE 不定义即被全部条件编译掉。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ +#ifndef NET_LATENCY_TEST_H +#define NET_LATENCY_TEST_H +#endif /* NET_LATENCY_TEST_H */ diff --git a/test/host/net_listener/stub/net_socket.h b/test/host/net_listener/stub/net_socket.h new file mode 100644 index 00000000..8ec7ecf8 --- /dev/null +++ b/test/host/net_listener/stub/net_socket.h @@ -0,0 +1,20 @@ +/* + * 模块名称:主机侧单测桩 - net_socket.h(NET 域共享形态) + * 模块功能:net_listener.c 需要 socket 表项类型与三个入口:net_get_sock()(本表项 + * 可变指针,重建时要写回簿记)、net_set_errno() 与 NET_ERR_RETURN_1()。 + * 状态类型与错误码常量一律取**真实** net_types.h(字段名/取值一改即编译期 + * 报错);net_set_errno 与 NET_ERR_RETURN_1 的写法照抄真实 net_socket.h:528/532。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。刻意不 include 真实 net_socket.h:它带着 + * 整条 netTask 消息队列与芯片依赖,主机侧编不动。 + * 创建日期:2026-10-23 + */ +#ifndef NET_SOCKET_H +#define NET_SOCKET_H + +#include "net_types.h" + +net_sock_t *net_get_sock(int sockfd); +int net_set_errno(int err); +#define NET_ERR_RETURN_1(err) (net_set_errno(err), -1) + +#endif /* NET_SOCKET_H */ diff --git a/test/host/net_listener/stub/task.h b/test/host/net_listener/stub/task.h new file mode 100644 index 00000000..190841d3 --- /dev/null +++ b/test/host/net_listener/stub/task.h @@ -0,0 +1,9 @@ +/* + * 模块名称:主机侧单测桩 - task.h(net_listener 模块专用) + * 模块功能:空壳。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ +#ifndef TASK_H +#define TASK_H +#endif /* TASK_H */ diff --git a/test/host/net_listener/stub/test_config.h b/test/host/net_listener/stub/test_config.h new file mode 100644 index 00000000..f00c70c1 --- /dev/null +++ b/test/host/net_listener/stub/test_config.h @@ -0,0 +1,10 @@ +/* + * 模块名称:主机侧单测桩 - test_config.h(net_listener 模块专用) + * 模块功能:空壳。net_listener.c 只在 TEST_LATENCY_PROBE 打开时才用延迟探针宏, + * 主机单测刻意**不开**该宏(探针不是被测对象,且它要写真实时间)。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ +#ifndef TEST_CONFIG_H +#define TEST_CONFIG_H +#endif /* TEST_CONFIG_H */