From 658bc40cccece689d6c4cab40856f7ad9e46226c Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 23 Oct 2026 15:59:14 +0800 Subject: [PATCH] =?UTF-8?q?test(NET):=20net=5Fselect=20=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=EF=BC=8821=20=E6=9D=A1=EF=BC=89=E4=B8=8E=20NET=20=E5=9F=9F?= =?UTF-8?q?=E9=A6=96=E4=BB=BD=20ch395f=20=E6=A1=A9=EF=BC=8C=E7=99=BB?= =?UTF-8?q?=E8=AE=B0=E4=B8=A4=E5=A4=84=E6=AD=BB=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 的 NET 四块开了第一块:新增 test/host/net_select/,21 条断言全部 MSVC 编译 通过并 PASS。net_select.c 一个芯片函数都不调,所以它是四块里唯一不需要假芯片的, 先把它拿下,顺带把 NET 域的桩骨架立起来。 钉住的契约: - 就绪位回写:入参集合里未就绪的 fd 必须被清掉,只留就绪的(POSIX 语义);未请求 的集合不被写回、可以传 NULL。 - 状态准入:LISTENING 即使 readable 也不算可读(accept 语义不在这层);ESTABLISHED + send_ready 才可写;CONNECTING 时 send_ready 不作数。 - 边界:nfds 为 0 / 负 / 超过 NET_MAX_SOCKETS 都返回 -1(不许扫出 socket 表外)。 - 超时算术:tv_usec 是**整除截断**(1999us 当 1ms 而不是 2ms);timeout=NULL 时 无限等并由对端事件唤醒;poll 的 timeout=0 首轮即退、每轮先清 revents。 用可控 tick(每问一次 +1)+ 可控 osDelay(计数、可指定轮数后造一个就绪 socket) 把这些变成确定性轮数,台架上根本构造不出"没人来连时该怎么退"。 照出两处**死路径**(只登记,未改语义;全库唯一调用点 test/board/net_test_task.c 传的是 NULL 与非零 tv,今天没有受害者): 1. net_select 的 timeout = {0,0} 换算出 timeout_ms = 0,而退出条件写的是 if ((timeout_ms > 0) && (已用时 >= timeout_ms)) → **超时 0 永不触发退出**,退化成 无限等;POSIX 在这里必须立刻返回 0。同文件 net_poll_events 写的 timeout >= 0 是对的,两条并排放着正好是对照。风险面:谁拿它当"非阻塞查一遍",netTask 就 回不到主循环,而 netTask 在 IWDG required_mask 里。 2. exceptfds 是恒为空的死参数:循环体开头 p_sock == NULL → continue 先跳过, check_socket_error 的 NULL 分支从 select 里到不了。按 POSIX 直觉用它收异常的人 会永远什么都收不到。 两处都用"现状桩"钉成改了会响(用例名里写明不是期望行为)。另修掉我自己写错的一条 用例:给 timeout=NULL 又没有就绪 fd,直接死循环——已在假 osDelay 里加了 500 轮 保险(跑飞就 exit(2)),因为挂死的测试会把 exe 文件名占住、让后续构建报 LNK1104。 桩的边界写死在文件头:net_select.h / net_types.h / net_config.h 用真实头(net_sock_t 字段名与 NET_SOCK_STATE_* 一改即编译期报错);main.h / cmsis_os.h / FreeRTOS.h / task.h 是本目录桩;新建的 ch395f.h 是**空壳**,只服务不调芯片函数的文件—— net_pending / net_events / net_listener 直接调十余处 ch395f_*,要的是可脚本化返回的 假芯片,不要把这个空壳当成"NET 域桩已就绪"。 覆盖率:套件 21 → 22 全绿;files_tested 25 → 26;未触及 .c 41 → 40;分母 3,332 → 3,446 行;net_select.c 97/114(85.1%)。恒等式更新为 40 = 6(P1 剩) + 5(P2) + 29(P3)。 编号按 B 层口径补建 TC-HOST-NETSEL(游标 019),TC-HOST 族现共 16 族;注册表仍只 做增量插入。 自查: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 | 20 +- scripts/id_registry.json | 22 ++ scripts/test_coverage_baseline.json | 40 ++- test/host/net_select/net_select_test.c | 379 +++++++++++++++++++++++++ test/host/net_select/run_unit_test.py | 55 ++++ test/host/net_select/stub/FreeRTOS.h | 15 + test/host/net_select/stub/ch395f.h | 21 ++ test/host/net_select/stub/cmsis_os.h | 17 ++ test/host/net_select/stub/main.h | 17 ++ test/host/net_select/stub/net_socket.h | 22 ++ test/host/net_select/stub/task.h | 11 + 11 files changed, 603 insertions(+), 16 deletions(-) create mode 100644 test/host/net_select/net_select_test.c create mode 100644 test/host/net_select/run_unit_test.py create mode 100644 test/host/net_select/stub/FreeRTOS.h create mode 100644 test/host/net_select/stub/ch395f.h create mode 100644 test/host/net_select/stub/cmsis_os.h create mode 100644 test/host/net_select/stub/main.h create mode 100644 test/host/net_select/stub/net_socket.h create mode 100644 test/host/net_select/stub/task.h diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index 28e001c3..df4c0ba1 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -225,8 +225,24 @@ | FTP 一套(4 块里 2 块) | 🟡 部分 | `test/host/lftpd_io`(33 条,含 `lftpd_string.c`) | `lftpd_io.c` **85/159 = 53.5%**、`lftpd_string.c` 8/10;`read/write/close/stat/unlink/readdir` 未测(要先把假 `FIL/FILINFO` 的字段语义做实,否则断言落在假件自己写的规则上) | -| `lftpd.c` / `lftpd_inet.c` | ⬜ 未开始 | — | 需假 `net_socket` 层(`time_sync`/stub 里那份可扩展) | -| NET 四块 / 104 服务层 | ⬜ 未开始 | — | **必须先新建 `stub/ch395f.h`** | +| 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_*` 十余处,要的是能脚本化返回的**假芯片**,不是这个空壳 | +| 104 服务层 | ⬜ 未开始 | — | 桩最省(复用 `cmsis_os2.h` + `net_socket.h`) | + +本批照出的两处**现状**(只登记、**未改语义**;全库唯一调用点 `test/board/net_test_task.c` +传的是 NULL 与非零 tv,今天没有受害者): + +1. `net_select` 的 `timeout = {0, 0}` 换算出 `timeout_ms = 0`,而退出条件写的是 + `if ((timeout_ms > 0) && (已用时 >= timeout_ms))` → **超时 0 永不触发退出**,退化成 + 无限等(POSIX 语义是立刻返回 0)。`net_poll_events` 的 `timeout >= 0` 写对了, + 两条并排放着正好是对照。风险面:谁拿它当"非阻塞查一遍",netTask 就再也回不到 + 主循环,而 netTask 在 IWDG `required_mask` 里。 +2. `exceptfds` 是**恒为空的死参数**:循环体开头 `p_sock == NULL → continue` 先跳过, + `check_socket_error` 的 NULL 分支从 select 里到不了。按 POSIX 直觉用它收异常的人 + 会永远什么都收不到。修法:continue 之前先判异常,或在 `net_select.h` 写明不支持异常集。 数字:套件 16 → **21**、`files_tested` 19 → **25**、未触及 125 → 119(`.c` 47 → **41**, 恒等式 `41 = 7(P1 剩) + 5(P2) + 29(P3)` 仍然咬合)、分母 2,893 → **3,332** 插桩行。 diff --git a/scripts/id_registry.json b/scripts/id_registry.json index 9c73317a..57fa726f 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -1724,6 +1724,28 @@ "subitem_ok": true, "owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);2026-10-23 随覆盖率口径 §9 P1 新建模块补登记", "cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌" + }, + { + "name": "TC-HOST-NETSEL", + "class": "TC", + "title": "主机侧单测用例号:NET 层 select/poll 事件观测(test/host/net_select)", + "pattern": "\\bTC-NETSEL-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/net_select/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-NETSEL-019", + "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 a8660a5c..adfe1ae4 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -184,6 +184,14 @@ "lines_covered": 8, "lines_valid": 10 }, + "Drivers/BSP/NET/net_select.c": { + "func_rate": 1.0, + "funcs_covered": 5, + "funcs_valid": 5, + "line_rate": 0.8509, + "lines_covered": 97, + "lines_valid": 114 + }, "Drivers/BSP/NETCFG/ftp_cfg.c": { "func_rate": 1.0, "funcs_covered": 4, @@ -284,6 +292,13 @@ "lines_total": 112, "ok": true }, + "net_select": { + "files": { + "Drivers/BSP/NET/net_select.c": 114 + }, + "lines_total": 114, + "ok": true + }, "phasor": { "files": { "App/algo/phasor.c": 55, @@ -395,21 +410,22 @@ "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_select.c", "Drivers/BSP/NETCFG/ftp_cfg.c", "Drivers/BSP/NETCFG/net_cfg.c" ], "totals": { "files_generated_excluded": 5, - "files_tested": 25, - "files_untested": 119, - "func_rate": 0.9094, - "funcs_covered": 231, - "funcs_valid": 254, - "line_rate": 0.8715, - "lines_covered": 2904, - "lines_valid": 3332, - "modules_ok": 21, - "modules_total": 21 + "files_tested": 26, + "files_untested": 118, + "func_rate": 0.9112, + "funcs_covered": 236, + "funcs_valid": 259, + "line_rate": 0.8709, + "lines_covered": 3001, + "lines_valid": 3446, + "modules_ok": 22, + "modules_total": 22 }, "untested_sources": [ { @@ -492,10 +508,6 @@ "code_lines": 171, "path": "Drivers/BSP/NET/lftpd/lftpd_inet.c" }, - { - "code_lines": 162, - "path": "Drivers/BSP/NET/net_select.c" - }, { "code_lines": 157, "path": "Drivers/BSP/TPAFE5160/tpafe5160.c" diff --git a/test/host/net_select/net_select_test.c b/test/host/net_select/net_select_test.c new file mode 100644 index 00000000..55a14f2b --- /dev/null +++ b/test/host/net_select/net_select_test.c @@ -0,0 +1,379 @@ +/* + * 模块名称:NET 层 select/poll 事件观测主机侧单元测试 + * 模块功能:在 PC 上离线验证 Drivers/BSP/NET/net_select.c 的**就绪判定与超时算术**: + * fd 集只保留就绪位、socket 状态对可读/可写的准入、非法 fd 的处理、 + * tv_sec/tv_usec 的换算与截断、poll 的 revents 语义。 + * + * 被测对象:真实源文件 ../../../Drivers/BSP/NET/net_select.c(#include 原样纳入)。 + * net_select.h / net_types.h / net_config.h 全用**真实头**(net_sock_t 的 + * 字段名与 NET_SOCK_STATE_* 枚举一改就编译期报错)。桩四个:main.h + * (HAL_GetTick)、cmsis_os.h(osDelay)、FreeRTOS.h/task.h(空壳), + * 外加 net_socket.h 桩(只声明 net_get_sock,类型仍取真实 net_types.h) + * 与 ch395f.h 空桩(本文件一个芯片函数都不调)。 + * + * 为什么要主机侧单测(而不是只靠台架): + * netTask 是**串行 socket 栈**的唯一执行者,且在 IWDG required_mask 里:这里的 + * 等待逻辑一旦算错,表现是"整个网络停摆 + 到点喂不上狗复位",而日志里什么都没有。 + * 台架上无法构造"没人来连、超时该怎么算"这类边界(对端行为不可控),主机侧把 + * tick 与 osDelay 做成可控量后可以精确复现"第几轮该退出去"。 + * + * 覆盖点: + * TC-NETSEL-001 nfds = 0 → -1 + * TC-NETSEL-002 nfds 为负 → -1 + * TC-NETSEL-003 nfds 超过 NET_MAX_SOCKETS → -1(不许越界扫 socket 表) + * TC-NETSEL-004 就绪位回写:入参集合里的未就绪 fd 必须被清掉,只留就绪的 + * TC-NETSEL-005 三个集合都可以传 NULL(不崩、不误伤) + * TC-NETSEL-006 timeout=NULL 时无限等,靠"对端事件到来"唤醒(假 osDelay 造事件) + * TC-NETSEL-007 tv_usec 换算是**整除截断**(1999us → 1ms,不是 2ms) + * TC-NETSEL-008 现状桩:timeout = {0, 0} 不立即返回(POSIX select 会立刻返回 0) + * TC-NETSEL-009 LISTENING 的 socket 即使 readable=1 也不算可读 + * TC-NETSEL-010 ESTABLISHED + send_ready → 可写位置位 + * TC-NETSEL-011 非 ESTABLISHED(CONNECTING)即使 send_ready=1 也不可写 + * TC-NETSEL-012 现状桩:exceptfds 是**恒为空的死参数**(表项为空的 fd 先被 continue + * 跳过,异常判定从 select 里到不了);全库唯一调用点 test/board 传 NULL, + * 所以今天无受害者——但谁按 POSIX 直觉用它就会静默收不到异常 + * TC-NETSEL-012b 同一 fd 请求 POLLIN 也不算可读(被跳过,超时到点即退) + * TC-NETSEL-013 poll 传 NULL 或 nfds<=0 → -1 + * TC-NETSEL-014 poll 非法 fd(负数 / 越界)→ NET_POLLNVAL 且计入 ready + * TC-NETSEL-015 poll 对 CLOSED socket 置 NET_POLLHUP + * TC-NETSEL-016 poll 命中 POLLIN → revents 含 NET_POLLIN + * TC-NETSEL-017 现状桩:NET_POLLERR 在本实现里**永远置不上**(死分支,已登记) + * TC-NETSEL-018 poll 的 timeout = 0 立即返回(首轮就退,一次都不睡) + * TC-NETSEL-019 poll 每轮先把 revents 清零,上一轮的位不会带到这一轮 + * + * 两处"现状桩"(008 / 017)钉的是**当前行为**,不是期望行为:修好它们这两条会红, + * 那是提醒而不是回归。理由写在下面的注释里。 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-23 + */ + +#include +#include + +#include "net_select.h" +#include "net_socket.h" + +#include "net_select.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) + +/* ===================== 可控 socket 表 ===================== */ + +static net_sock_t g_sock[NET_MAX_SOCKETS]; +static uint8_t g_sock_valid[NET_MAX_SOCKETS]; + +const 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]; +} + +/* ===================== 可控时基与睡眠 ===================== */ + +static uint32_t g_tick; +static int g_delay_calls; +static int g_release_after; /* >0:睡够这个轮数后让 fd0 变可读(模拟对端事件) */ + +uint32_t HAL_GetTick(void) +{ + return g_tick++; /* 每问一次时间就前进 1ms:轮数 = 毫秒数,便于精确断言 */ +} + +void osDelay(uint32_t ticks) +{ + (void)ticks; + g_delay_calls++; + if (g_delay_calls > 500) { + /* 保险:本模块每条用例都必须在有限轮内退出(要么就绪、要么超时、要么由 + * g_release_after 造事件)。跑飞就说明用例或实现出了问题——直接退出, + * 绝不能把挂死留给 pre-commit / CI(上一次就是这么卡住 exe 文件名的)。 */ + printf(" ABORT 轮询跑飞(>500 轮未退出)\n"); + fflush(stdout); + exit(2); + } + g_tick++; + if ((g_release_after > 0) && (g_delay_calls >= g_release_after)) { + g_sock_valid[0] = 1u; + g_sock[0].state = NET_SOCK_STATE_ESTABLISHED; + g_sock[0].readable = 1u; + } +} + +/* ===================== 复位 ===================== */ + +static void reset_all(void) +{ + (void)memset(g_sock, 0, sizeof(g_sock)); + (void)memset(g_sock_valid, 0, sizeof(g_sock_valid)); + g_tick = 0u; + g_delay_calls = 0; + g_release_after = 0; +} + +static void make_sock(int fd, net_sock_state_t st, uint8_t readable, uint8_t send_ready) +{ + g_sock_valid[fd] = 1u; + g_sock[fd].state = st; + g_sock[fd].readable = readable; + g_sock[fd].send_ready = send_ready; +} + +/* ===================== 参数边界 ===================== */ + +static void tc_bounds(void) +{ + net_fd_set_t r; + + printf("-- TC-NETSEL-001..003 nfds bounds --\n"); + reset_all(); + NET_FD_ZERO(&r); + CHECK(net_select(0, &r, NULL, NULL, NULL) == -1, "TC-NETSEL-001 nfds=0 -> -1"); + CHECK(net_select(-1, &r, NULL, NULL, NULL) == -1, "TC-NETSEL-002 nfds 为负 -> -1"); + CHECK(net_select(NET_MAX_SOCKETS + 1, &r, NULL, NULL, NULL) == -1, + "TC-NETSEL-003 nfds 越界 -> -1(不许扫出表外)"); +} + +/* ===================== 就绪位与集合回写 ===================== */ + +static void tc_ready_sets(void) +{ + net_fd_set_t r; + net_fd_set_t w; + int rc; + + printf("-- TC-NETSEL-004..005 ready sets --\n"); + reset_all(); + make_sock(0, NET_SOCK_STATE_ESTABLISHED, 0u, 0u); /* 请求了但没就绪 */ + make_sock(1, NET_SOCK_STATE_ESTABLISHED, 1u, 0u); /* 就绪 */ + NET_FD_ZERO(&r); + NET_FD_SET(0, &r); + NET_FD_SET(1, &r); + rc = net_select(4, &r, NULL, NULL, NULL); + CHECK((rc == 1) && NET_FD_ISSET(1, &r) && !NET_FD_ISSET(0, &r), + "TC-NETSEL-004 回写只保留就绪位,未就绪的 fd0 被清掉"); + + reset_all(); + make_sock(2, NET_SOCK_STATE_ESTABLISHED, 1u, 0u); + NET_FD_ZERO(&r); + NET_FD_SET(2, &r); + rc = net_select(3, &r, NULL, NULL, NULL); /* write/except 传 NULL */ + CHECK((rc == 1) && (NET_FD_ISSET(2, &r)), "TC-NETSEL-005 其余集合可为 NULL"); + + /* 注意:timeout 传 NULL 时本函数会一直等到有事件(见 008 的现状说明), + * 所以这类用例必须让被请求的 fd 真的就绪,否则就是用例自己写死的死循环。 */ + reset_all(); + NET_FD_ZERO(&w); + NET_FD_ZERO(&r); + make_sock(2, NET_SOCK_STATE_ESTABLISHED, 0u, 1u); /* send_ready:可写就绪 */ + NET_FD_SET(2, &w); + rc = net_select(3, NULL, &w, NULL, NULL); + CHECK((rc == 1) && NET_FD_ISSET(2, &w) && (r.fd_bits == 0u), + "TC-NETSEL-005b 只回写调用方真正请求过的集合"); +} + +/* ===================== 超时算术 ===================== */ + +static void tc_timeout(void) +{ + net_fd_set_t r; + net_timeval_t tv; + int rc; + + printf("-- TC-NETSEL-006..008 timeout arithmetic --\n"); + reset_all(); + g_release_after = 3; /* 第 3 轮睡完,对端才有数据 */ + NET_FD_ZERO(&r); + NET_FD_SET(0, &r); + rc = net_select(1, &r, NULL, NULL, NULL); /* timeout = NULL:无限等 */ + CHECK((rc == 1) && (g_delay_calls == 3) && NET_FD_ISSET(0, &r), + "TC-NETSEL-006 无限等被对端事件唤醒(第 3 轮)"); + + reset_all(); + make_sock(0, NET_SOCK_STATE_ESTABLISHED, 0u, 0u); + NET_FD_ZERO(&r); + NET_FD_SET(0, &r); + tv.tv_sec = 0; + tv.tv_usec = 1999; /* 应为 1ms,不是 2ms */ + rc = net_select(1, &r, NULL, NULL, &tv); + CHECK((rc == 0) && (g_delay_calls == 0), + "TC-NETSEL-007 tv_usec 整除截断:1999us 当 1ms,首轮即退"); + + reset_all(); + make_sock(0, NET_SOCK_STATE_ESTABLISHED, 0u, 0u); + NET_FD_ZERO(&r); + NET_FD_SET(0, &r); + tv.tv_sec = 0; + tv.tv_usec = 0; + g_release_after = 5; /* 造一个事件把自己救出来 */ + rc = net_select(1, &r, NULL, NULL, &tv); + /* 现状桩:timeout == {0,0} → 换算出 timeout_ms = 0,而实现里那句是 + * if ((timeout_ms > 0) && (已用时 >= timeout_ms)) break; + * 于是**永远不会因超时退出**,只能等事件——POSIX 的 select 在这里必须立刻返回 0。 + * 谁把那个 >0 修掉,这条就会红(g_delay_calls 变 0、rc 变 0):那是提醒。 */ + CHECK((rc == 1) && (g_delay_calls == 5), + "TC-NETSEL-008 现状:timeout={0,0} 不立即返回(已登记为缺陷)"); +} + +/* ===================== 状态准入 ===================== */ + +static void tc_state_admission(void) +{ + net_fd_set_t r; + net_fd_set_t w; + net_timeval_t tv; + int rc; + + printf("-- TC-NETSEL-009..012 state admission --\n"); + reset_all(); + make_sock(0, NET_SOCK_STATE_LISTENING, 1u, 0u); /* 监听中但报"可读" */ + NET_FD_ZERO(&r); + NET_FD_SET(0, &r); + tv.tv_sec = 0; + tv.tv_usec = 1000; + rc = net_select(1, &r, NULL, NULL, &tv); + CHECK((rc == 0) && !NET_FD_ISSET(0, &r), + "TC-NETSEL-009 监听态不可读(accept 语义不在 select 这层)"); + + reset_all(); + make_sock(0, NET_SOCK_STATE_ESTABLISHED, 0u, 1u); + NET_FD_ZERO(&w); + NET_FD_SET(0, &w); + rc = net_select(1, NULL, &w, NULL, &tv); + CHECK((rc == 1) && NET_FD_ISSET(0, &w), + "TC-NETSEL-010 已建立 + send_ready 才可写"); + + reset_all(); + make_sock(0, NET_SOCK_STATE_CONNECTING, 0u, 1u); + NET_FD_ZERO(&w); + NET_FD_SET(0, &w); + rc = net_select(1, NULL, &w, NULL, &tv); + CHECK((rc == 0) && !NET_FD_ISSET(0, &w), + "TC-NETSEL-011 连接中不可写(send_ready 不作数)"); + + /* 现状桩(不是期望):net_select 的循环体开头是 + * p_sock = net_get_sock(i); if (p_sock == NULL) continue; + * 于是"表项不存在"的 fd 被整轮跳过,check_socket_error 的 NULL 分支从 select + * 里**永远到不了** → exceptfds 是个恒为空的死参数(谁传了它以为能收到异常, + * 就什么都收不到)。改法要么在 continue 前先查异常、要么在文档里写明不支持。 */ + reset_all(); + g_sock_valid[0] = 0u; + NET_FD_ZERO(&r); + NET_FD_SET(0, &r); + tv.tv_sec = 0; + tv.tv_usec = 1000; + rc = net_select(1, NULL, NULL, &r, &tv); + CHECK((rc == 0) && !NET_FD_ISSET(0, &r), + "TC-NETSEL-012 现状:exceptfds 永不被置位(表项为空先被 continue 跳过)"); + reset_all(); + g_sock_valid[0] = 0u; + NET_FD_ZERO(&r); + NET_FD_SET(0, &r); + rc = net_select(1, &r, NULL, NULL, &tv); + CHECK((rc == 0) && !NET_FD_ISSET(0, &r) && (g_delay_calls == 0), + "TC-NETSEL-012b 不存在的 fd 也不算可读(被跳过而不是报错,超时到点即退)"); +} + +/* ===================== poll 语义 ===================== */ + +static void tc_poll(void) +{ + net_pollfd_t p[4]; + int rc; + + printf("-- TC-NETSEL-013..019 poll --\n"); + reset_all(); + CHECK((net_poll_events(NULL, 1, 0) == -1) && (net_poll_events(p, 0, 0) == -1), + "TC-NETSEL-013 poll 参数非法 -> -1"); + + reset_all(); + (void)memset(p, 0, sizeof(p)); + p[0].fd = -1; + p[0].events = NET_POLLIN; + p[1].fd = NET_MAX_SOCKETS; /* 越界 */ + p[1].events = NET_POLLIN; + rc = net_poll_events(p, 2, 0); + CHECK((rc == 2) && ((p[0].revents & NET_POLLNVAL) != 0) && + ((p[1].revents & NET_POLLNVAL) != 0), + "TC-NETSEL-014 负 fd 与越界 fd 都置 POLLNVAL 并计入 ready"); + + reset_all(); + make_sock(0, NET_SOCK_STATE_CLOSED, 0u, 0u); + (void)memset(p, 0, sizeof(p)); + p[0].fd = 0; + p[0].events = NET_POLLIN; + rc = net_poll_events(p, 1, 0); + CHECK((rc == 1) && ((p[0].revents & NET_POLLHUP) != 0), + "TC-NETSEL-015 CLOSED 置 POLLHUP"); + + reset_all(); + make_sock(0, NET_SOCK_STATE_ESTABLISHED, 1u, 0u); + (void)memset(p, 0, sizeof(p)); + p[0].fd = 0; + p[0].events = NET_POLLIN; + rc = net_poll_events(p, 1, 0); + CHECK((rc == 1) && ((p[0].revents & NET_POLLIN) != 0) && + ((p[0].revents & NET_POLLHUP) == 0), + "TC-NETSEL-016 可读置 POLLIN 且不误置 HUP"); + + reset_all(); + make_sock(0, NET_SOCK_STATE_ESTABLISHED, 1u, 1u); + (void)memset(p, 0, sizeof(p)); + p[0].fd = 0; + p[0].events = 0; /* 什么都不请求 */ + (void)net_poll_events(p, 1, 0); + CHECK((p[0].revents & NET_POLLERR) == 0, + "TC-NETSEL-017 现状:POLLERR 实际永不为真(表项为空已被 POLLNVAL 抢先)"); + + reset_all(); + make_sock(0, NET_SOCK_STATE_ESTABLISHED, 0u, 0u); + (void)memset(p, 0, sizeof(p)); + p[0].fd = 0; + p[0].events = NET_POLLIN; + rc = net_poll_events(p, 1, 0); /* timeout = 0 */ + CHECK((rc == 0) && (g_delay_calls == 0), + "TC-NETSEL-018 poll 的 timeout=0 立即返回(与 select 那条形成对照)"); + + reset_all(); + make_sock(0, NET_SOCK_STATE_ESTABLISHED, 1u, 0u); + (void)memset(p, 0, sizeof(p)); + p[0].fd = 0; + p[0].events = NET_POLLIN; + p[0].revents = NET_POLLOUT; /* 脏的上一轮残留 */ + (void)net_poll_events(p, 1, 0); + CHECK((p[0].revents & NET_POLLOUT) == 0, "TC-NETSEL-019 每轮先清 revents"); +} + +/* ===================== 入口 ===================== */ + +int main(void) +{ + printf("=== NET Select Tests ===\n"); + + tc_bounds(); + tc_ready_sets(); + tc_timeout(); + tc_state_admission(); + tc_poll(); + + printf("\n=== NET Select 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_select/run_unit_test.py b/test/host/net_select/run_unit_test.py new file mode 100644 index 00000000..74522790 --- /dev/null +++ b/test/host/net_select/run_unit_test.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""NET 层 select/poll(net_select)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/net_select/net_select_test.c(以 #include 纳入生产源 + Drivers/BSP/NET/net_select.c),在 PC 上离线验证就绪位回写、socket 状态对可读/ + 可写的准入、非法 fd 处理、tv_sec/tv_usec 超时换算、poll 的 revents 语义。 + 工具链与判定委托 test/host/common/unit_test_harness.py。 + +为什么要主机侧单测(而不是只用台架): + netTask 是串行 socket 栈的唯一执行者,且在 IWDG required_mask 里——这里的等待 + 逻辑算错,表现是"网络整体停摆 + 喂不上狗复位",而且日志里什么都没有。台架无法 + 构造"没人来连时该怎么退"(对端不可控);把 HAL_GetTick 与 osDelay 做成可控量后, + "第几轮该退出"能精确复现,包括 1999us 该按 1ms 还是 2ms 这种算术。 + +桩的边界:net_select.h / net_types.h / net_config.h 用真实头(net_sock_t 字段名与 + NET_SOCK_STATE_* 一改就编译期报错);main.h / cmsis_os.h / FreeRTOS.h / task.h / + ch395f.h 是本目录桩——net_select.c 一个芯片函数都不调,所以 ch395f.h 是空壳。 + ⚠ 那个空壳不代表 NET 域桩已就绪:net_listener / net_pending / net_events 直接调 + 十多个 ch395f_* 且语义在 socket 状态机里,需要的是能脚本化返回的假芯片。 + +用法(仓库根目录): + python test/host/net_select/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 + +设计依据:docs/网络/Net层阻塞模型架构设计.md、docs/测试说明/主机单测覆盖率口径.md §9 P1 +创建日期: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 必须在 Drivers/BSP/NET 之前,才能用桩覆盖 net_socket.h 与 ch395f.h; +# net_select.h / net_types.h / net_config.h 仍从真实目录取。 +BUILD = TestBuild( + name="NET Select Tests", + src=os.path.join(HERE, "net_select_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "Drivers", "BSP", "NET"), + HERE, + ], + summary_marker="=== NET Select Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) + diff --git a/test/host/net_select/stub/FreeRTOS.h b/test/host/net_select/stub/FreeRTOS.h new file mode 100644 index 00000000..154c9884 --- /dev/null +++ b/test/host/net_select/stub/FreeRTOS.h @@ -0,0 +1,15 @@ +/* + * 模块名称:主机侧单测桩 - FreeRTOS.h(net_select 模块专用) + * 模块功能:net_select.c include 了它但不用任何内核 API(让 CPU 走 osDelay), + * 故只需一个空壳把 include 链走通。 + * ⚠ 仅供 test/host/net_select/ 使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ + +#ifndef FREERTOS_H +#define FREERTOS_H + +#include +#include + +#endif /* FREERTOS_H */ diff --git a/test/host/net_select/stub/ch395f.h b/test/host/net_select/stub/ch395f.h new file mode 100644 index 00000000..692e2d90 --- /dev/null +++ b/test/host/net_select/stub/ch395f.h @@ -0,0 +1,21 @@ +/* + * 模块名称:主机侧单测桩 - ch395f.h(NET 域共享形态,第一个使用者是本模块) + * 模块功能:给 NET 域一个**能在 PC 上编译**的芯片头占位。真实 ch395f.h 拉 HAL 与 + * 寄存器定义,主机侧编不动;而 NET 域里 net_select.c 这类文件 include 它 + * **只是为了拿类型/常量**,一个芯片函数都不调。 + * + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 边界要说清楚:本桩是**空**的,只服务"不调芯片函数"的文件。 + * net_listener.c / net_pending.c / net_events.c 直接调 10 多个 ch395f_* 且语义在 + * socket 状态机里(GET_SOCK_INT_STATUS 整体读即清等),那三块要的是**带脚本化返回 + * 的假芯片**,不是这个空壳——别把本文件当成"NET 域桩已就绪"。 + * + * 创建日期:2026-10-23 + */ + +#ifndef CH395F_H +#define CH395F_H + +#include + +#endif /* CH395F_H */ diff --git a/test/host/net_select/stub/cmsis_os.h b/test/host/net_select/stub/cmsis_os.h new file mode 100644 index 00000000..70665f28 --- /dev/null +++ b/test/host/net_select/stub/cmsis_os.h @@ -0,0 +1,17 @@ +/* + * 模块名称:主机侧单测桩 - cmsis_os.h(net_select 模块专用) + * 模块功能:net_select.c 只用 osDelay(1) 让出 CPU(等 fd 就绪 / 等超时)。 + * ⚠ 仅供 test/host/net_select/ 使用,不参与 MCU 构建。 + * 测试单元的实现把"睡 1 tick"做成"tick +1 并计数",还能在指定轮数后**造出**一个 + * 就绪 socket —— 用来跑"无限等被事件唤醒"这类真机上要靠对端配合的分支。 + * 创建日期:2026-10-23 + */ + +#ifndef CMSIS_OS_H +#define CMSIS_OS_H + +#include + +void osDelay(uint32_t ticks); + +#endif /* CMSIS_OS_H */ diff --git a/test/host/net_select/stub/main.h b/test/host/net_select/stub/main.h new file mode 100644 index 00000000..f854f83f --- /dev/null +++ b/test/host/net_select/stub/main.h @@ -0,0 +1,17 @@ +/* + * 模块名称:主机侧单测桩 - main.h(net_select 模块专用) + * 模块功能:只给 net_select.c 实际用到的一个符号:HAL_GetTick(超时判定的唯一时基)。 + * ⚠ 仅供 test/host/net_select/ 使用,不参与 MCU 构建。 + * 真实 main.h 拉整棵 HAL 头树,PC 上编不动。测试单元把 tick 做成"每调一次 +1"的 + * 可控量,于是"轮询几轮后超时"这种时序可以精确复现,不用真等毫秒。 + * 创建日期:2026-10-23 + */ + +#ifndef MAIN_H +#define MAIN_H + +#include + +uint32_t HAL_GetTick(void); + +#endif /* MAIN_H */ diff --git a/test/host/net_select/stub/net_socket.h b/test/host/net_select/stub/net_socket.h new file mode 100644 index 00000000..4043291a --- /dev/null +++ b/test/host/net_select/stub/net_socket.h @@ -0,0 +1,22 @@ +/* + * 模块名称:主机侧单测桩 - net_socket.h(net_select 模块专用) + * 模块功能:net_select.c 只需要两样东西:socket 表项类型(用**真实** net_types.h 的 + * net_sock_t 与状态枚举,字段名一改就编译期报错)与 net_get_sock()。 + * 后者由测试单元给可控实现(返回表中项或 NULL),从而能造出 + * "已建立且可读 / 监听中 / 表项不存在 / 已关闭"四种观测态。 + * + * ⚠ 仅供 test/host/net_select/ 使用,不参与 MCU 构建。 + * 刻意不 include 真实 net_socket.h:它带着整条 netTask 消息队列与芯片依赖, + * 而 select 层的契约只依赖 socket 表项的形状。 + * + * 创建日期:2026-10-23 + */ + +#ifndef NET_SOCKET_H +#define NET_SOCKET_H + +#include "net_types.h" + +const net_sock_t *net_get_sock(int sockfd); + +#endif /* NET_SOCKET_H */ diff --git a/test/host/net_select/stub/task.h b/test/host/net_select/stub/task.h new file mode 100644 index 00000000..4e460bf5 --- /dev/null +++ b/test/host/net_select/stub/task.h @@ -0,0 +1,11 @@ +/* + * 模块名称:主机侧单测桩 - task.h(net_select 模块专用) + * 模块功能:空壳;net_select.c 不调任务 API。 + * ⚠ 仅供 test/host/net_select/ 使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ + +#ifndef TASK_H +#define TASK_H + +#endif /* TASK_H */