Files
STM32F4-Base/test/host/net_socket/ns_s6_edges.c
T
super_agent_bot a4b8bee209
CI / 静态门禁 (pre-commit) (push) Has been cancelled
CI / 主机侧单测 (MSVC) (push) Has been cancelled
CI / 静态门禁 (pre-commit) (pull_request) Has been cancelled
CI / 主机侧单测 (MSVC) (pull_request) Has been cancelled
feat(net/udp): UDP 数据面并入 netMsgQueue 串行执行(#73)
前置来自 #57 SNTP 调研:旧 net_sendto / net_recvfrom 由调用方直调
SPI 且无限自旋收包,既违约「netTask 是芯片唯一访问者」,又把 IWDG
预算吃在自旋里。

改造面
- 新增 Drivers/BSP/NET/net_udp.c/.h(真机专属,模拟器不编译):
  bind/set_peer/sendto/recvfrom 四条执行体 + 挂起收包表 + tick;
  net_msg_type_t 加四型,公开 API 一律「入队 → 睡在通知上」。
- 源地址出参不搭 net_msg_t(按值拷贝,AGENTS 禁加回传字段):改住
  net_udp.c 的 per-fd 槽,netTask 就地写回调用方内存,通知值只带
  结果码;每条出口都清槽,泄漏 = 该 fd 永久 BUSY。
- 发送复用 net_send_locked,不写第二份分块逻辑;「会话是否已死」
  收成一条判据 net_send_state_down()(TCP 看 ESTABLISHED、UDP 看
  UDP_OPEN),原先 4 处各写一遍。
- 顺带修掉真缺陷(新条 TRAP-NET-22):旧 sendto 把 DesIP 写成具体
  IP,芯片静默切 UDP Client,而旧 recvfrom 无条件剥 8 字节 → SNTP
  这类 48 B 应答丢载荷前 8 字节并交出垃圾源地址。加 udp_mode(塞进
  原有 padding,sizeof 不变)、bind 恒 SERVER、显式对端 sendto 切
  CLIENT、set_peer(fd, NULL) 轻量回 SERVER,仅 SERVER 且 avail >= 8
  才剥头。
- 截断定案:一次调用交付一个数据报,buf 装不下的尾巴用 64 B 栈上
  scratch 排空(不用 clear_recv_buf——它连后面已到达的包一起丢)。
- 取消点接齐:net_init 重建 cancel_all、net_socket 分配 fd clear、
  net_close settle_close + clear、重 bind 前先结算等包者,
  net_process_messages 第 5 个 tick 位挂 net_udp_tick。
- 收紧两处契约(原实现无生产调用者):UDP 未 bind 一律 -5;
  net_recv / _nb / _to 拒 DGRAM(绕过剥头就是静默吃 8 字节);
  NET_ERR_NOT_IN_NETTASK(-12) 退役为历史常量并写明原因。

验证
- 主机单测 test/host/net_socket 全套 616/616 绿(改造前 470):新
  分片 ns_s7_udp.c 20 组,改写 440 / 555 / 560 组;覆盖率
  net_udp.c 234/236(99.2%)、net_socket.c 956/969(98.7%),
  scripts/test_coverage_baseline.json 随本提交登记新文件。
- 测试抓到 4 个被测码真 bug:自等守卫误用调用侧谓词、芯片长度被读
  两次、槽指针取在 fd 合法性检查之前、重 bind 静默作废等待者。
- Keil ARMCC 0 Error(s) 0 Warning(s);net_udp.o Code 1,358 / ZI 224
  (8 fd × 28 B,其余模块零增长),全仓 RW_IRAM1 72.2%;内存台账
  受管区按门禁提示用 --write / --update 机器回填,未手改数字。
- pre-commit 全量钩子(含 doc-links、coding-standard、mem-budget、
  generated-fresh、bsp-err、registration、nested-comment)全绿。

限度
- 本轮无板子:真机回归 5 条(两端互通、set_peer(NULL) 能否恢复收
  广播、截断排空后下一包完整性、挂起 recv 期间 IWDG 余量、bind 写
  DesIP 与 open 先后的真芯片效果)已写进覆盖率口径「台架必验」。
- scripts/stack_report.py 退出码 1 是 10-26 起挂在账上的 iec104Task
  栈债(margin 1,016 < 1,024,见 docs/代码规范/pre-commit使用说明.md:
  122);本轮只让 net_bind 的 UDP 分支多一层静态帧(1,544 → 1,600),
  netTask margin 1,888 与 timeSyncTask 1,152 不受影响。抬栈要过
  mem-budget 台账,不在本分支自行处理。
- 登记簿销项按仓库约定留给主工作树,本分支不动 docs/任务规划/**。
2026-11-03 18:08:27 +08:00

587 lines
29 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.
/*
* 分片 6:边界残支(公开入口的越界/队满、直调入口的参数支、等待环里的其余出口)
*
* 这一片不为「覆盖率好看」而写:每条都对应一条**能被改坏**的分支——越界检查挪到
* 入队之后,调用方就会在 netTask 里等到一个没人处理的 BADF;短写留痕被删掉,
* 上层就会像 2026-09-21 那次 UI 推流一样「对端永久等待剩余字节」而日志全白。
* 唯一一条刻意**不是**真实时序的场景是 TC-NETSOCK-530(一次轮询之间被压了 1s),
* 它在真机上的对应形态是 netTask 被更高优先级任务压住——用大步进时基表达,
* 并在注释里写明它考的是「停滞超时先于自旋预算生效」这条判定顺序。
*
* 创建日期:2026-10-24
*/
/* TC-NETSOCK-500 六个公开入口的越界形态:BADF 必须在**入队之前**由调用方侧挡下 */
static void tc_500_public_bounds(void)
{
struct net_sockaddr_in in;
char buf[8];
int addrlen;
bring_up();
(void)memset(&in, 0, sizeof(in));
addrlen = (int)sizeof(in);
CHECK(net_send(NET_MAX_SOCKETS, buf, 4, 0) == -1 && net_get_errno() == NET_ERR_BADF,
"TC-NETSOCK-500a net_send out of range -> BADF");
CHECK(net_recv(NET_MAX_SOCKETS, buf, 4, NET_MSG_DONTWAIT) == -1 &&
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500b net_recv out of range -> BADF");
CHECK(net_recv_to(NET_MAX_SOCKETS, buf, 4, 10U) == -1 &&
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500c net_recv_to out of range -> BADF");
CHECK(net_close(NET_MAX_SOCKETS) == -1 && net_get_errno() == NET_ERR_BADF,
"TC-NETSOCK-500d net_close out of range -> BADF");
CHECK(net_drop_conn(NET_MAX_SOCKETS) == -1 && net_get_errno() == NET_ERR_BADF,
"TC-NETSOCK-500e net_drop_conn out of range -> BADF");
CHECK(net_accept(NET_MAX_SOCKETS, (struct net_sockaddr *)&in, &addrlen) == -1 &&
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500f net_accept out of range -> BADF");
CHECK(net_connect(NET_MAX_SOCKETS, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 &&
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500g net_connect out of range -> BADF");
CHECK(net_sendto(NET_MAX_SOCKETS, buf, 4, 0, NULL, 0) == -1 &&
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500h net_sendto out of range -> BADF");
CHECK(net_recvfrom(NET_MAX_SOCKETS, buf, 4, NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500i net_recvfrom out of range -> BADF");
CHECK(g_ns_put_calls == 0,
"TC-NETSOCK-500j every out-of-range shape enqueues nothing (blocking after the enqueue hands netTask an ownerless request)");
reset_all();
}
/* TC-NETSOCK-501 队满时每个公开入口都要报 BUSY(不能有的报、有的静默) */
static void tc_501_all_entries_busy_on_full_queue(void)
{
struct net_sockaddr_in in;
int fd;
bring_up();
fd = mk_established(NET_SOCK_STREAM);
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
ns_rtos_q_fill((int)NS_Q_CAP);
CHECK(net_accept(fd, NULL, NULL) == -1 && net_get_errno() == NET_ERR_BUSY,
"TC-NETSOCK-501a net_accept on a full queue -> BUSY");
ns_rtos_q_fill((int)NS_Q_CAP);
(void)memset(&in, 0, sizeof(in));
CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 &&
net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-501b net_connect on a full queue -> BUSY");
ns_rtos_q_fill((int)NS_Q_CAP);
CHECK(net_recv_to(fd, (void *)"x", 1, 20U) == -1 && net_get_errno() == NET_ERR_BUSY,
"TC-NETSOCK-501c net_recv_to on a full queue -> BUSY");
ns_rtos_q_fill((int)NS_Q_CAP);
CHECK(net_recv(fd, (void *)"x", 1, NET_MSG_DONTWAIT) == -1 &&
net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-501d net_recv on a full queue -> BUSY");
ns_rtos_q_fill((int)NS_Q_CAP);
CHECK(net_send(fd, "x", 1, 0) == -1 && net_get_errno() == NET_ERR_BUSY,
"TC-NETSOCK-501e net_send on a full queue -> BUSY");
CHECK(g_ns_notify_calls == 0, "TC-NETSOCK-501f the full-queue path sends no notification (the caller leaves with the return value)");
reset_all();
}
/* TC-NETSOCK-502 fd 表用尽:NOMEM 且不影响已建立的会话 */
static void tc_502_socket_table_full(void)
{
int i;
int fd;
bring_up();
for (i = 0; i < NET_MAX_SOCKETS; i++) {
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
if (fd < 0) {
break;
}
}
CHECK(i == NET_MAX_SOCKETS, "TC-NETSOCK-502a all 8 slots are allocatable");
CHECK(net_socket(NET_AF_INET, NET_SOCK_STREAM) == -1 &&
net_get_errno() == NET_ERR_NOMEM, "TC-NETSOCK-502b the ninth -> NOMEM (no out-of-range table write)");
s_net_socks[3].state = NET_SOCK_STATE_ESTABLISHED;
CHECK(net_get_sock(3) != NULL && net_listener_armed(3) == 1,
"TC-NETSOCK-502c a full table does not disturb live sessions (alloc failure only returns NOMEM)");
reset_all();
}
/* TC-NETSOCK-503 net_wait_result 的两段式拾取:先零等待,已有通知就不必再睡 */
static void tc_503_zero_wait_pickup(void)
{
int fd;
int rc;
bring_up();
fd = mk_established(NET_SOCK_STREAM);
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
/* 预置一条**已挂起**的通知(真机上=上一条消息的回执还没被读走),
* 公开入口应先零等待拾取它,而不是再睡一次(睡过头就是 实测踩过的坑) */
ns_rtos_post_notify((TaskHandle_t)&g_ns_task_caller, (uint32_t)(int)4);
g_ns_block_waits = 0;
rc = net_send(fd, "abcd", 4, 0);
CHECK(rc == 4, "TC-NETSOCK-503a the zero-wait peek takes the notify value -> returns the result immediately");
CHECK(g_ns_block_waits == 0, "TC-NETSOCK-503b with a notification already pending it never enters the blocking branch");
CHECK(g_peers.send_reg_calls == 0 && g_chip.write_calls == 0,
"TC-NETSOCK-503c this call never touched the chip (the value peeked was the previous receipt, a shape we already hit in practice)");
reset_all();
}
/* TC-NETSOCK-504 直调入口的参数支(这些分支从公开入口不可达,只能直接钉) */
static void tc_504_locked_entry_branches(void)
{
struct net_sockaddr_in in;
int fd;
bring_up();
(void)memset(&in, 0, sizeof(in));
in.sin_port = net_htons(80);
in.sin_addr.s_addr = net_inet_addr("10.1.1.1");
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
CHECK(net_connect_locked(fd, NULL, (int)sizeof(in)) == -1 &&
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504a connect_locked with addr=NULL -> INVAL");
CHECK(net_connect_locked(fd, (const struct net_sockaddr *)&in, 1) == -1 &&
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504b connect_locked with a short addrlen -> INVAL");
s_net_socks[fd].in_use = 1U;
CHECK(net_send_locked(&s_net_socks[fd], "x", 1, NULL) == -1 &&
net_get_errno() == NET_ERR_NOTCONN, "TC-NETSOCK-504c direct send on a not-established block -> NOTCONN");
CHECK(net_recv_locked(&s_net_socks[fd], (void *)"x", 1, NET_MSG_DONTWAIT, 0U, NULL) == -1 &&
net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-504d direct recv, no data, non-blocking -> WOULDBLOCK");
CHECK(net_recv_locked(&s_net_socks[fd], NULL, 1, NET_MSG_DONTWAIT, 0U, NULL) == -1 &&
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504e direct recv with a NULL buf -> INVAL");
reset_all();
}
/* TC-NETSOCK-505 发送途中缓冲再也不腾出来:内层等待也要转挂起,并且末块不许登记 */
static void tc_505_inner_wait_and_last_chunk(void)
{
static char big[2100];
int fd;
int rc;
/* 第一块写得进、之后芯片不再报空闲 → 第二块的**内层**等待转挂起 */
bring_up();
fd = mk_established(NET_SOCK_STREAM);
g_ns_int_pin_set = 0;
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
g_fake.int_status[fd] = CH395F_SINT_STAT_SENBUF_FREE; /* 只给一次空闲 */
rc = net_send_locked(&s_net_socks[fd], big, 2100,
(TaskHandle_t)&g_ns_task_caller);
CHECK(rc == NET_PENDING_REGISTERED,
"TC-NETSOCK-505a the inner wait (after writing one chunk) also converts to pending past budget, no in-place busy wait");
CHECK(g_chip.write_calls == 2 && g_peers.last_snd_sent == 2048,
"TC-NETSOCK-505b registered progress = bytes written into the chip (1024+1024=2048), delivered bytes are never resent");
reset_all();
/* 末块:全部写完但芯片不回执 → 按完成返回,**不许**登记挂起
* (2026-10-14 台架回归 TC-NET-902 正是登记了却无人完成 → 永久挂死) */
bring_up();
fd = mk_established(NET_SOCK_STREAM);
rc = net_send_locked(&s_net_socks[fd], big, 1024,
(TaskHandle_t)&g_ns_task_caller);
CHECK(rc == 1024, "TC-NETSOCK-505c a single chunk finishing exactly -> returns 1024 (complete)");
CHECK(g_peers.send_reg_calls == 0,
"TC-NETSOCK-505d the last-chunk wait must not register pending (the sent>=len guard has to hold)");
reset_all();
}
/* TC-NETSOCK-506 UDP 发送卡住:两条出口都要留痕(2026-11-03 队列化改造后重写)
* 改造前这里钉的是"netTask 内自旋到 1s 停滞超时后短写"——那 1 秒全部花在 netTask
* 里,正是本次要消掉的形态。改造后 UDP 与 TCP 共用 net_send_locked 的预算机:
* ① 执行任务上下文:无人能完成挂起 → 拒绝自等 → -1(BUSY),且不写忙缓冲;
* ② 调用方上下文:超预算转挂起(登记进 net_send_pending,与 TCP 同一张表),
* 登记里带的是**已写进芯片的字节数**——续传从断点起,已交付的字节绝不重发
* (重发半截数据报 = 对端收到两个包,比丢包更难查)。
*/
static void tc_506_sendto_stall_and_short(void)
{
static char big[2100];
int fd;
int rc;
/* ① 执行任务上下文、发送缓冲一开始就不空闲 */
bring_up_netctx();
fd = udp_bind_ctx(8888);
s_net_socks[fd].send_ready = 0U;
g_ns_int_pin_set = 0;
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
/* 每轮 500ms:停在自旋预算(2ms)与停滞超时(1000ms)之间 → 走"转挂起被拒"这条口 */
g_ns_poll_tick_inc = 500U;
rc = net_sendto(fd, big, 2100, 0, NULL, 0);
CHECK(rc == -1 && net_get_errno() == NET_ERR_BUSY,
"TC-NETSOCK-506a a stall inside netTask now gives up with BUSY instead of spinning a full second and short-writing");
CHECK(g_chip.write_calls == 0,
"TC-NETSOCK-506b it never wrote into a busy buffer (no half datagram left in the chip FIFO)");
CHECK(g_peers.send_reg_calls == 1,
"TC-NETSOCK-506c the hand-over was really attempted (the refusal comes from the register side, not from us not trying to hand over)");
reset_all();
/* ② 调用方上下文:第一块写得进、之后芯片不再报空闲 → 第二块转挂起 */
bring_up();
fd = udp_bind_ctx(8888);
g_ns_int_pin_set = 0;
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
g_fake.int_status[fd] = CH395F_SINT_STAT_SENBUF_FREE; /* 只给一次空闲 */
g_ns_poll_tick_inc = 500U;
rc = net_send_locked(&s_net_socks[fd], big, 2100, (TaskHandle_t)&g_ns_task_caller);
CHECK(rc == NET_PENDING_REGISTERED,
"TC-NETSOCK-506d a UDP send past the spin budget converts to pending exactly like TCP (no in-place busy wait)");
CHECK(g_peers.send_reg_calls == 1 && g_peers.last_snd_sent == 1024,
"TC-NETSOCK-506e the pending entry carries the bytes already written (1024), so the resume starts at the break point");
CHECK(g_chip.write_calls == 1,
"TC-NETSOCK-506f exactly one chunk reached the chip before handing over");
reset_all();
}
/* TC-NETSOCK-530 停滞超时优先于自旋预算:考判定顺序,不考真实时长 */
static void tc_530_stall_timeout_before_spin(void)
{
static char buf[64];
int fd;
int rc;
/* 一次轮询之间被压了 2s(真机形态:netTask 被更高优先级任务压住),
* 停滞超时(NET_SEND_TIMEOUT_MS=1s)必须**先于**自旋预算(2ms)命中:
* 生产代码里两条判定的先后顺序就是这条断言的内容。 */
bring_up();
fd = mk_established(NET_SOCK_STREAM);
s_net_socks[fd].send_ready = 0U;
g_ns_poll_tick_inc = 2000U;
rc = net_send_locked(&s_net_socks[fd], buf, (int)sizeof(buf),
(TaskHandle_t)&g_ns_task_caller);
CHECK(rc == 0,
"TC-NETSOCK-530a the stall timeout returns the bytes sent so far, 0 (a short write, not -1)");
CHECK(g_peers.send_reg_calls == 0,
"TC-NETSOCK-530b the stall timeout wins over the spin budget: no pending registration (a swapped order would register)");
CHECK(net_get_errno() == NET_ERR_TIMEDOUT,
"TC-NETSOCK-530c errno = TIMEDOUT (lets the caller tell a stall from a disconnect)");
reset_all();
}
/* TC-NETSOCK-535 已经写出去一块之后的超时收口(内层等待的另一条出口) */
static void tc_535_inner_timeout_after_chunk(void)
{
static char big[2100];
int fd;
int rc;
/* 第一块写得进(send_ready 初值 1),第二块的**内层**等待直接撞上停滞超时
* (判定顺序:先超时后自旋,所以这里收口在超时支而不是登记挂起) */
bring_up();
fd = mk_established(NET_SOCK_STREAM);
g_ns_poll_tick_inc = 2000U;
rc = net_send_locked(&s_net_socks[fd], big, 2100,
(TaskHandle_t)&g_ns_task_caller);
CHECK(rc == 1024, "TC-NETSOCK-535a the already delivered first chunk still counts (1024), the rest is a short write");
CHECK(g_peers.send_reg_calls == 0,
"TC-NETSOCK-535b the stall timeout closes out BEFORE any pending registration (swapping the order parks a pending that never completes)");
CHECK(g_chip.write_calls == 1, "TC-NETSOCK-535c the chip was written exactly once: a delivered chunk must never be resent");
reset_all();
}
/* TC-NETSOCK-536 net_recv_to 的三条回传分支(-2 / -1+errno / >=0) */
static void tc_536_recv_to_three_exits(void)
{
char buf[16];
int fd;
/* >=0:正常拿到数据 */
bring_up();
fd = mk_established(NET_SOCK_STREAM);
s_net_socks[fd].readable = 1U;
ch395f_fake_push_recv_len((uint8_t)fd, 12U);
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
CHECK(net_recv_to(fd, buf, (int)sizeof(buf), 100U) == 12,
"TC-NETSOCK-536a data available -> returns the actual byte count (the >=0 branch)");
/* <0 且非 TIMEDOUT:分派侧 BADF → 恢复 errno 后返回 -1 */
reset_all();
bring_up();
(void)net_socket(NET_AF_INET, NET_SOCK_STREAM); /* 占掉 fd0,用未分配的 fd1 */
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
CHECK(net_recv_to(1, buf, (int)sizeof(buf), 100U) == -1 &&
net_get_errno() == NET_ERR_BADF,
"TC-NETSOCK-536b a real error -> -1 with errno restored from the notify value (strictly distinct from -2)");
/* ==TIMEDOUT:已在 TC-NETSOCK-224 钉过 -2;这里补一条「-2 不改写 errno」 */
reset_all();
bring_up();
fd = mk_established(NET_SOCK_STREAM);
(void)net_set_errno(NET_OK);
ns_peers_arm_pending_done(fd, NET_ERR_TIMEDOUT);
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
CHECK(net_recv_to(fd, buf, (int)sizeof(buf), 30U) == -2 &&
net_get_errno() == NET_OK,
"TC-NETSOCK-536c timeout is judged by the return code only and does NOT rewrite the errno slot (this branch sets none)");
reset_all();
}
/* TC-NETSOCK-537 fire_event:注册了回调才会被叫到(本层唯一的回调触发点在 SEND_OK) */
static void tc_537_event_callback_path(void)
{
int fd;
bring_up();
fd = mk_established(NET_SOCK_STREAM);
/* handle_send_event 是兄弟模块(net_events)的职责,本层只负责把中断交给它;
* 回调是否触发属 net_events 的断言面(其单测 TC-NETEV 已覆盖),此处只钉
* 「net_poll 不因回调未注册而跳过事件」。 */
s_net_socks[fd].event_cb = NULL;
g_ns_int_pin_set = 0;
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
g_fake.int_status[fd] = CH395F_SINT_STAT_SEND_OK;
CHECK(net_poll() == 1, "TC-NETSOCK-537 events are counted even with no callback registered (a null pointer never skips dispatch)");
reset_all();
}
/* TC-NETSOCK-540 close / drop_conn 的失败结果也要经通知值恢复精确 errno */
static void tc_540_negative_results(void)
{
net_msg_t msg;
int fd;
/* close 的负结果支:net_close_locked 自身恒返 0(复位没成也放行,见 TC-NETSOCK-320h),
* 所以 CLOSE 唯一的负值来自**分派侧**的 net_get_sock 判空 → 通知值 -2 →
* 调用方在自己的槽位里恢复成 BADF。范围合法但未分配的 fd 正好走这条。 */
bring_up();
fd = 3; /* 范围内、未分配 */
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
CHECK(net_close(fd) == -1 && net_get_errno() == NET_ERR_BADF,
"TC-NETSOCK-540a close on an unallocated fd -> -1 with errno restored to BADF from the notify value, not a generic NET_ERR");
CHECK(net_drop_conn(fd) == -1 && net_get_errno() == NET_ERR_BADF,
"TC-NETSOCK-540a2 drop_conn takes the same negative-value restoration branch");
reset_all();
/* drop_conn 同理:用未分配 fd 走分派侧 BADF 后再由调用方恢复 */
bring_up();
(void)memset(&msg, 0, sizeof(msg));
msg.type = NET_MSG_DROP_CONN;
msg.sockfd = 1;
msg.caller = (TaskHandle_t)&g_ns_task_net;
(void)ns_rtos_q_put_raw(&msg);
net_process_messages();
CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF,
"TC-NETSOCK-540b drop_conn BADF from the dispatch side travels back on the notify value");
/* RECV 的 BADF 支(分派侧 net_get_sock 判空) */
reset_all();
bring_up();
(void)memset(&msg, 0, sizeof(msg));
msg.type = NET_MSG_RECV;
msg.sockfd = 4;
msg.buf = (void *)"x";
msg.len = 1;
msg.caller = (TaskHandle_t)&g_ns_task_net;
(void)ns_rtos_q_put_raw(&msg);
net_process_messages();
CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF,
"TC-NETSOCK-540c recv BADF from the dispatch side likewise travels back on the notify value");
reset_all();
}
/* TC-NETSOCK-545 未绑队列时的 sendto/recvfrom/getpeername 等纯本地入口 */
static void tc_545_local_entries_without_queue(void)
{
struct net_sockaddr_in in;
int addrlen = (int)sizeof(in);
reset_all();
CHECK(net_getsockname(0, (struct net_sockaddr *)&in, &addrlen) == -1 &&
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-545a even uninitialised it safely reports BADF (no crash)");
CHECK(net_getpeername(0, (struct net_sockaddr *)&in, &addrlen) == -1,
"TC-NETSOCK-545b getpeername behaves the same");
CHECK(net_inet_addr("1.2.3.4") != 0U, "TC-NETSOCK-545c pure helpers do not depend on initialisation");
reset_all();
}
/* ============= TC-NETSOCK-550 s_addr 同族一致性(字节序统一后的收口断言) =============
*
* 这次字节序统一的实质不是「给 inet_addr 换个数值」,而是「全仓只剩一套 s_addr 约定」。
* 所以这组断言全部写在**跨函数的接缝**上:解析→存储→芯片下发→回读→打印,
* 每一环都拿同一句点分十进制做基准。任何一处又引入第二套顺位,这里必红。
*/
/* TC-NETSOCK-550 解析 → 存储 → 打印 闭合(getsockname / getpeername 同族可比对) */
static void tc_550_family_roundtrip(void)
{
struct net_sockaddr_in in;
struct net_sockaddr *sa = (struct net_sockaddr *)&in;
char txt[16];
int addrlen;
int fd;
bring_up();
/* 1) getsockname 的 s_addr 与 inet_addr 解析结果**直接相等**(旧实现差一次换端) */
(void)mk_established(NET_SOCK_STREAM); /* getsockname 要 in_use 的描述符 */
addrlen = (int)sizeof(in);
(void)memset(&in, 0, sizeof(in));
CHECK(net_getsockname(0, sa, &addrlen) == 0,
"TC-NETSOCK-550a getsockname precondition ok");
CHECK(in.sin_addr.s_addr == net_inet_addr("192.168.31.100"),
"TC-NETSOCK-550b getsockname s_addr is the same family as inet_addr (direct equality)");
CHECK(strcmp(net_inet_ntoa(in.sin_addr.s_addr, txt), "192.168.31.100") == 0,
"TC-NETSOCK-550c local address closes parse-store-print (old code printed 100.31.168.192)");
/* 2) connect 的对端簿记同样闭合(lftpd:864 与 104 显示走的就是这条链) */
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
(void)memset(&in, 0, sizeof(in));
in.sin_family = NET_AF_INET;
in.sin_port = net_htons(2404U);
in.sin_addr.s_addr = net_inet_addr("192.168.31.200");
g_ns_cur_task = (TaskHandle_t)&g_ns_task_net;
CHECK(net_connect_locked(fd, sa, (int)sizeof(in)) == 0,
"TC-NETSOCK-550d connect precondition ok");
addrlen = (int)sizeof(in);
(void)memset(&in, 0, sizeof(in));
CHECK(net_getpeername(fd, sa, &addrlen) == 0,
"TC-NETSOCK-550e getpeername ok (reads event cache, zero SPI)");
CHECK(in.sin_addr.s_addr == net_inet_addr("192.168.31.200"),
"TC-NETSOCK-550f getpeername peer equals the s_addr fed into net_connect");
CHECK(strcmp(net_inet_ntoa(in.sin_addr.s_addr, txt), "192.168.31.200") == 0,
"TC-NETSOCK-550g peer address prints right (lftpd connection-received log depends on it)");
CHECK(s_net_socks[fd].remote_ip_arr[0] == 192U &&
s_net_socks[fd].remote_ip_arr[3] == 200U,
"TC-NETSOCK-550h chip des_ip array stays wire order [192,168,31,200]: on-wire behaviour unchanged");
reset_all();
}
/* TC-NETSOCK-555 UDP 报文头里的源地址拆装(同族的第四个生产者:net_recvfrom) */
static void tc_555_udp_src_addr(void)
{
/* CH395F UDP 头(§9.2.4):[0-1] 总长(LE) [2-3] 源端口(LE) [4-7] 源 IP(线序) */
static const uint8_t hdr[8] = {
11U, 0U, /* 总长 = 8 头 + 3 数据 */
0x1FU, 0xCBU, /* 源端口 51999(小端 1F CB) */
239U, 1U, 2U, 3U /* 源 IP 线序:239.1.2.3 */
};
struct net_sockaddr_in in;
struct net_sockaddr *sa = (struct net_sockaddr *)&in;
char buf[16];
char txt[16];
int addrlen;
int fd;
bring_up_netctx();
/* 2026-11-03 队列化改造:剥头只发生在**服务器模式**,而服务器模式由 bind 建立
* (DesIP=0xFFFFFFFF)。改造前这段直接 net_socket 就能收,是因为它无条件剥头——
* 那正是本组要修的缺陷(客户端模式没有头,照剥就吃掉载荷前 8 字节)。 */
fd = udp_bind_ctx(8888);
ch395f_fake_push_recv_len((uint8_t)fd, (uint16_t)(8U + 3U));
ch395f_fake_set_recv_bytes(hdr, (uint16_t)sizeof(hdr));
addrlen = (int)sizeof(in);
(void)memset(&in, 0, sizeof(in));
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, sa, &addrlen) == 3,
"TC-NETSOCK-555a strips the 8-byte chip header and delivers only the 3-byte payload");
CHECK(in.sin_addr.s_addr == net_inet_addr("239.1.2.3"),
"TC-NETSOCK-555b source IP unpack matches the inet_addr family");
CHECK(in.sin_port == net_htons(51999U),
"TC-NETSOCK-555c source port read chip little-endian then stored in network order");
CHECK(addrlen == (int)sizeof(in) && net_ntohs(in.sin_port) == 51999U,
"TC-NETSOCK-555d addrlen filled and port round-trips to 51999");
CHECK(strcmp(net_inet_ntoa(in.sin_addr.s_addr, txt), "239.1.2.3") == 0,
"TC-NETSOCK-555e multicast source address prints correctly");
reset_all();
}
/* TC-NETSOCK-560 UDP 上下文契约(2026-11-03 队列化改造**反转**了本组的原命题)
* 改造前:非执行任务调用 net_sendto / net_recvfrom 一律 NET_ERR_NOT_IN_NETTASK(-12)
* ——那是 2026-10-24"先把危险面关掉"的临时守卫(SPI 竞争 TRAP-NET-14)。
* 改造后:任意任务都能调,芯片事务全部串行到 netTask 执行;守卫随之退役。
* 三条一起才成立,缺一条都不算把改造钉住:
* ① 非执行任务调用**成功**,且芯片事务发生在泵(=netTask 上下文)里,不在调用方
* ② 执行任务内直接执行,不投自己(板级用例照旧可在自己任务里裸调)
* ③ 参数/状态错误优先于上下文;-12 这个码在 UDP 路径上不再出现
*/
static void tc_560_udp_context_guard(void)
{
char buf[8];
int fd;
/* ① 非执行任务(bring_up 已把身份还原成 caller):改造前被拒,改造后必须成 */
bring_up();
fd = udp_bind_ctx(8888);
CHECK(fd >= 0, "UDP-560a0 bind from a caller task succeeds (queue path, no direct SPI)");
g_chip.write_calls = 0;
g_chip.read_calls = 0;
g_fxchip.set_des_ip_calls = 0;
g_ns_put_calls = 0;
g_ns_notify_calls = 0;
CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == 3,
"UDP-560a non-executing task's sendto now succeeds (it used to be refused with -12)");
CHECK(g_chip.write_calls == 1 && g_ns_put_calls >= 1 && g_ns_notify_calls >= 1,
"UDP-560b the write happened while the pump ran as netTask: the caller enqueued, blocked, and was notified (serialised, not forbidden)");
CHECK(net_get_errno() != NET_ERR_NOT_IN_NETTASK,
"UDP-560c the retired guard does not come back: -12 is never returned on a success path");
ch395f_fake_push_recv_len((uint8_t)fd, 8U + 3U);
s_net_socks[fd].readable = 1U;
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == 3,
"UDP-560d non-executing task's recvfrom goes the same route and returns the payload");
CHECK(g_chip.read_calls >= 2 && g_ns_block_waits >= 1,
"UDP-560e the caller slept on its own notification while netTask did the SPI reads (the old spin-in-caller shape is gone)");
/* ③ 参数检查排在上下文与状态之前(守卫退役不许顺手把错误优先级改掉) */
CHECK(net_sendto(-1, "abc", 3, 0, NULL, 0) == -1 &&
net_get_errno() == NET_ERR_BADF, "UDP-560f out-of-range fd still reports BADF first");
CHECK(net_sendto(fd, NULL, 3, 0, NULL, 0) == -1 &&
net_get_errno() == NET_ERR_INVAL, "UDP-560g NULL buf still reports INVAL");
CHECK(net_recvfrom(fd, NULL, 3, NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
net_get_errno() == NET_ERR_INVAL, "UDP-560h recvfrom keeps the same precedence");
reset_all();
/* ② 执行任务上下文:直接执行,不投自己(板级 testTask 形态) */
bring_up_netctx();
fd = udp_bind_ctx(8888);
g_ns_put_calls = 0;
CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == 3,
"UDP-560i netTask context sends directly");
CHECK(g_ns_put_calls == 0,
"UDP-560j the executing task never enqueues to itself (a self-post would have to wait for its own pump)");
reset_all();
/* ④ 队列未绑定(net_init 之前的早期上下文):与其他队列化接口同一口径 NET_ERR,
* 不是 BADF 也不是 -12——无从投递就是无从投递 */
reset_all();
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == NET_ERR &&
s_net_udp[fd].armed == 0U,
"UDP-560k before net_init (no queue bound) sendto returns NET_ERR and leaves no slot armed");
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == NET_ERR,
"UDP-560l the same for recvfrom (the armed out-param slot is handed back on that exit)");
reset_all();
/* ⑤ 执行任务内的**阻塞** UDP 收包仍被拒(改造没有打开自等死锁的门):
* 完成它的 tick 就跑在同一个任务里 */
bring_up_netctx();
fd = udp_bind_ctx(8888);
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == -1 &&
net_get_errno() == NET_ERR_BUSY,
"UDP-560m a blocking UDP receive inside netTask is refused with BUSY (self-wait = deadlock)");
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
net_get_errno() == NET_ERR_WOULDBLOCK,
"UDP-560n the same call with NET_MSG_DONTWAIT is the legal netTask form (WOULDBLOCK, no wait)");
reset_all();
}
static void ns_run_s6(void)
{
tc_500_public_bounds();
tc_501_all_entries_busy_on_full_queue();
tc_502_socket_table_full();
tc_503_zero_wait_pickup();
tc_504_locked_entry_branches();
tc_505_inner_wait_and_last_chunk();
tc_506_sendto_stall_and_short();
tc_530_stall_timeout_before_spin();
tc_535_inner_timeout_after_chunk();
tc_536_recv_to_three_exits();
tc_537_event_callback_path();
tc_540_negative_results();
tc_545_local_entries_without_queue();
tc_550_family_roundtrip();
tc_555_udp_src_addr();
tc_560_udp_context_guard();
}