/* * 模块名称: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 #include #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; }