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:
+18
-2
@@ -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** 插桩行。
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
Reference in New Issue
Block a user