Files
STM32F4-Base/test/host/net_select/net_select_test.c
T
wandering1 5224f21622 fix(NET,lftpd): 处置覆盖率照出的三处死路径(改名 + 修 timeout=0 + 激活异常集)
三处都由本轮 P1 新增的主机单测照出,用户逐条裁定后实施。共同性质:都不是改动引入
的回归,而是**从没被执行到的分支**——生产路径上要么没有调用者、要么没走边界,所以
编译器(0 错 0 警)、cppcheck、真机日志对它都无话可说。

1) 缺陷一(选 a:改名,行为零变化)
   lftpd_string_trim 的尾部循环起点落在 strlen(p) 那一格 NUL,isspace(0) 为假 →
   循环体一次都不执行,"尾部空白从未被裁",而名字承诺两头都去。改名
   lftpd_string_ltrim + 删掉那条死循环 + 在 .h/.c 写明"只去前导"。不去尾才是对的:
   RFC 959 以命令终止符 CRLF 界定命令,参数(PASS 口令、含空格的文件名)的尾随
   空格属于参数本体,去掉等于静默改数据;CRLF 已由 lftpd_inet_read_line 截断。
   调用点 lftpd.c 同步改名。全库无遗留真引用(余 6 处旧名均为解释改名历史的散文)。

2) 缺陷二(按推荐修)
   net_select 的 timeout = {0,0} 换算出 timeout_ms = 0,而退出条件写着
   if ((timeout_ms > 0) && (已用时 >= timeout_ms)) → 超时永不生效,退化成死等;
   POSIX 语义是"只查一轮立即返回"。去掉 timeout_ms > 0 这个错误前置即可。就绪
   检查在本判断之前,故有就绪时仍立刻带计数返回;同文件 net_poll_events 的
   timeout >= 0 本来就对,现在两条对齐。风险面:拿它当"非阻塞查一遍"会让 netTask
   回不到主循环,而 netTask 在 IWDG required_mask 里(串行 socket 栈唯一执行者)。

3) 缺陷三(选 ①:按 POSIX 置位)
   exceptfds 是恒为空的死参数:循环开头 p_sock == NULL → continue 先跳过,而
   check_socket_error() 只有"表项为空 / fd 越界"才返回 1,越界的 i 又不在本循环
   范围内 → 调用方按 POSIX 直觉用它收异常,永远什么都收不到,且返回码不报错。
   把异常判定提到 continue 之前:表项不存在的 fd 计入 exceptfds 与返回计数,并更新
   函数注释。选 ① 会改变返回计数语义,故补一条"未请求异常集时不计数"的用例,
   确保只查可读的老调用方不被误伤(TC-NETSEL-012b)。

测试同步翻转:原 TC-NETSEL-008 / TC-NETSEL-012 是"现状桩、改了会响",现在改成正向
契约,并新增 TC-NETSEL-012c(异常位只落到被请求的那个集合);TC-LFTSTR-005 从
"尾部不裁"的现状登记改为正向契约(尾随空格必须保留)。net_select 22/22、
lftpd_io 33/33 全绿。

@build:0 错误 0 警告。实测体积 ER_IROM1 232,512 B(快照 232,536 → **−24 B**)、
Flash 233,704 B(−20 B)、RW_IRAM1 75,272 B(+8 B 是上一提交 net_cfg 那个字节,
本批零新增)、**RW_CCM 57,376 B 不变**;四档百分比 22.2/57.4/87.9/22.3 全不变。
Code 减少 36 B 即删掉的死循环。

覆盖率台账:门禁如实报了一次 lftpd_string.c 已覆盖行 8 → 7(**代码少了一行**,不是
覆盖退化),已 --update-baseline 重登;现 22/22 套件、line=3003/3446(87.1%)、
func=236/259(91.1%)、tested_files=26、untested_files=118。

文档:主机单测覆盖率口径 §9 新增"三处死路径的处置"小节(裁定、改法、影响、钉住
它的用例一一对应),并把两处"等裁定"的登记改为已处置。

自查:coding-standard / test-cppcheck / id-check --strict(无台账外新账)/ doc-links
(35 处 = 存量)全过;八个改动文件与 HEAD 逐项比对 BOM/CR/U+FFFD/行尾无变化
(这几个文件本就不带 BOM,未被动过)。pre-commit --all-files 仍只有三条失败且与
本分支无关:mem-numbers / mem-budget(.map 与 docs/内存管理 受管快照漂移,增量见
上,按并行分支约定不在分支里 --write,等 Lead 合并后统一回填)与 ui-golden
(simulator/tests/ref/*.bin 在 .gitignore 内,新建 worktree 天生没有黄金参考)。
2026-10-23 23:44:48 +08:00

387 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} 只查一轮立即返回(**2026-10-23 修**:原先因
* `(timeout_ms > 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 表项不存在的 fd 计入异常集合(**2026-10-23 修**:判定原本排在
* continue 之后,exceptfds 是恒为空的死参数)
* TC-NETSEL-012b 未请求异常集时,缺失的 fd 不计数(不影响只查可读的调用方)
* TC-NETSEL-012c 异常位只落到被请求的那个集合
* 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,首轮即退");
/* 已修(2026-10-23):原来这里写的是 (timeout_ms > 0) && (已用时 >= timeout_ms),
* timeout = {0,0} 换算出 0 后**永不超时**,只能干等事件——而 POSIX 语义是
* "只查一轮立即返回"。调用方(netTask 在 IWDG required_mask 里)拿它当
* 非阻塞探测用就会卡死主循环。现在这条是正向断言。 */
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;
rc = net_select(1, &r, NULL, NULL, &tv);
CHECK((rc == 0) && (g_delay_calls == 0) && !NET_FD_ISSET(0, &r),
"TC-NETSEL-008 timeout={0,0} 只查一轮立即返回(与 poll 的 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 不作数)");
/* 已修(2026-10-23):异常判定原先排在 p_sock == NULL → continue 之后,而
* check_socket_error() 只有"表项为空 / fd 越界"才返回 1、越界的 i 又不在本循环
* 范围内 → exceptfds 是恒为空的死参数。现在把判定提到 continue 之前。 */
tv.tv_sec = 0;
tv.tv_usec = 1000;
reset_all();
g_sock_valid[0] = 0u;
NET_FD_ZERO(&r);
NET_FD_SET(0, &r);
rc = net_select(1, NULL, NULL, &r, &tv);
CHECK((rc == 1) && NET_FD_ISSET(0, &r),
"TC-NETSEL-012 表项不存在的 fd 计入异常集合");
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 计数(不误伤只查可读的调用方)");
reset_all();
g_sock_valid[0] = 0u;
NET_FD_ZERO(&r);
NET_FD_SET(0, &r); /* 请求可读:不该置位 */
NET_FD_ZERO(&w);
NET_FD_SET(0, &w); /* 请求异常:应该置位 */
rc = net_select(1, &r, NULL, &w, &tv);
CHECK((rc == 1) && NET_FD_ISSET(0, &w) && !NET_FD_ISSET(0, &r),
"TC-NETSEL-012c 异常位只落到被请求的那个集合,不污染可读集合");
}
/* ===================== 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;
}