test(NET): net_select 模块(21 条)与 NET 域首份 ch395f 桩,登记两处死路径

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 天生没有黄金参考)三条失败,与本分支无关。
This commit is contained in:
2026-10-23 15:59:14 +08:00
parent 6bdf063f50
commit 658bc40ccc
11 changed files with 603 additions and 16 deletions
@@ -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** 插桩行。
+22
View File
@@ -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": {
+26 -14
View File
@@ -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"
+379
View File
@@ -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 <stdio.h>
#include <string.h>
#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;
}
+55
View File
@@ -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))
+15
View File
@@ -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 <stddef.h>
#include <stdint.h>
#endif /* FREERTOS_H */
+21
View File
@@ -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 <stdint.h>
#endif /* CH395F_H */
+17
View File
@@ -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 <stdint.h>
void osDelay(uint32_t ticks);
#endif /* CMSIS_OS_H */
+17
View File
@@ -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 <stdint.h>
uint32_t HAL_GetTick(void);
#endif /* MAIN_H */
+22
View File
@@ -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 */
+11
View File
@@ -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 */