Files
STM32F4-Base/test/host/net_select/net_select_test.c
T
wandering1 658bc40ccc 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 天生没有黄金参考)三条失败,与本分支无关。
2026-10-23 23:44:48 +08:00

380 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称: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;
}