Files
STM32F4-Base/test/host/net_select/net_select_test.c
T
wandering1 0f7b5a87fb chore(编号): 存量编号全量转语义短语,门禁识别面与语料同批重打
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号)
一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、
采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。

门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏
undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8
reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF;
id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。

test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字,
是本批唯一改变构建产物的文件集合。

为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged
部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、
不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。

实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01%
(基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 /
告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。

基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处
引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使
识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。

⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二
根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有),
今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。

固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0;
App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889
断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿;
check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带
BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。

.cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法:
Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。

同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%,
RW_CCM 57,376 B / 87.9%,阈值 92%。
2026-10-26 12:28:14 +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 poll runaway (>500 rounds without exit)\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 negative nfds -> -1");
CHECK(net_select(NET_MAX_SOCKETS + 1, &r, NULL, NULL, NULL) == -1,
"TC-NETSEL-003 nfds out of range -> -1 (must not scan past the table)");
}
/* ===================== 就绪位与集合回写 ===================== */
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 write-back keeps only the ready bits; not-ready fd0 is cleared");
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 the other sets may be 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 only the sets the caller actually requested get written back");
}
/* ===================== 超时算术 ===================== */
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 infinite wait wakes on a peer event");
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 integer truncation: 1999us counts as 1ms, exits on the first round");
/* 已修(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} checks one round and returns at once (aligned with poll timeout 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 listen state is not readable (accept semantics are not in the select layer)");
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 established + send_ready is what makes it writable");
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 connecting state is not writable (send_ready does not count)");
/* 已修(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 an fd with no table entry counts into the exception set");
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 a missing fd is not counted when the exception set was not requested");
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 the exception bit lands only in the requested set, never polluting the read set");
}
/* ===================== 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 invalid poll arguments -> -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 negative and out-of-range fd both set POLLNVAL and count as 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 sets 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 readable sets POLLIN without falsely setting 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 current state: POLLERR is never true (an empty entry is caught earlier by 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 returns at once (contrast with the matching select case)");
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 is cleared at the start of every round");
}
/* ===================== 入口 ===================== */
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;
}