From 1b5e5d424c4ded20549aec26b7a2e0389cf41878 Mon Sep 17 00:00:00 2001 From: super_agent_bot Date: Wed, 4 Nov 2026 01:28:01 +0800 Subject: [PATCH] =?UTF-8?q?test(net/udp):=20=E4=B8=BB=E6=9C=BA=E5=8D=95?= =?UTF-8?q?=E6=B5=8B=E8=A1=A5=20UDP-25=20=E4=B8=A4=E6=9D=A1=20socket=20?= =?UTF-8?q?=E5=B9=B6=E5=AD=98=E7=9A=84=E9=9A=94=E7=A6=BB=EF=BC=88#73?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前 24 组全都只用一条 UDP socket,漏掉的正是队列化真正要保的那半句承诺: **等待是一条 socket 自己的事**——串行的是命令,不是等待者。 UDP-25 用 13 条断言钉住两条并存 socket 的四件事:fdB 来包不得结算也不得通知 fdA 的 parked 收包(连它的调用缓冲都不许被写一个字节);fdA 自己的包到达时按 自己那一包的表头结算并恰好通知一次;fdB parked 时 fdA 自己 recvfrom 照常拿到 数据;关停在等的 fdB 必须当场结算、槽位与出参槽都不残留。 写这组时踩到假件的一个真坑,值得记下来:假件的 get_recv_len 是"每次读消耗一格、 最后一格永远重复"的脚本队列,本组前面给同一 fd 留过旧长度,不清就会拿**旧长度配 新内容**(现象是 UDP-25j 读出 12 字节而不是 7 字节),所以每条用例前用 ch395f_fake_reset_recv_len(fd) 归零再宣布——这是测法问题,不是被测代码的问题。 全套 667/667 绿(改造前 470,本次 +13);net_udp.c 仍 237/239 行 14/14 函数, net_socket.c 966/969 行 59/59 函数,模块级覆盖率无回退。 --- docs/测试说明/单元测试/主机单测覆盖率口径.md | 2 +- test/host/net_socket/ns_s7_udp.c | 100 +++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/docs/测试说明/单元测试/主机单测覆盖率口径.md b/docs/测试说明/单元测试/主机单测覆盖率口径.md index 851d82ae..ea349596 100644 --- a/docs/测试说明/单元测试/主机单测覆盖率口径.md +++ b/docs/测试说明/单元测试/主机单测覆盖率口径.md @@ -719,7 +719,7 @@ uint16_t ch395f_link_change_count(void); - **不写第二份分块逻辑**:UDP 发送复用 `net_send_locked`(1KB 分块 + 挂起 send 都在里面)。为此把"会话是否已死"收成一条判据 `net_send_state_down()`(`net_pending.c`):TCP 看 `ESTABLISHED`、UDP 看 `UDP_OPEN`;原先 `net_send_locked` 两处 + `net_send_pending_tick` + `net_pending_tick` 各写一遍,共 4 处。 - **顺带修掉的形态缺陷**(真隐患):`net_sendto` 为发一个包把 DesIP 从 `0xFFFFFFFF` 写成具体 IP,芯片静默切到 UDP Client 形态(收包**不带** 8 字节信息头),而旧 `recvfrom` 无条件剥 8 字节 → 请求-响应式协议(SNTP 48 B 应答)会丢载荷前 8 字节并交出垃圾源地址。**修法**:`net_sock_t` 加 `udp_mode`(塞进原有 2 字节 padding,`sizeof(net_sock_t)` 不变)、`bind` 恒 SERVER、显式对端 `sendto` 切 CLIENT、`net_udp_set_peer(fd, NULL)` 轻量回 SERVER(不重开 Socket 因此不清芯片缓冲)、只在 SERVER 且 `avail >= 8` 时剥头(残包不猜头,TC-NETSOCK-440o 契约不变)。 - **截断语义定案**:一次调用交付一个数据报,`buf` 装不下的尾巴**必须排空**——`ch395f_read_recv_buf` 是按长度推进读指针的消费式读,不排空则下一次 `recvfrom` 交出上一个包的残料。排空用 64 B 栈上 scratch 循环读,**不用** `ch395f_clear_recv_buf`:它清整个接收缓冲,会把后面已经到达的包一起丢。 -- **用例面**:新分片 `test/host/net_socket/ns_s7_udp.c` 共 24 组 UDP-1~24(bind 的 5 条芯片命令**顺序**、未 bind 一律 -5、服务器形态剥头逐字节、客户端形态不剥头、`sendto` 切形态、挂起收包在后续 poll 轮到达、预算超时按调用方 deadline 计、close 与重建结算不许静默丢通知、出参槽互斥与泄漏、截断排空、netTask 自等守卫、`net_init` 重建取消、流式 API 拒 `DGRAM`、非 netTask 调用零芯片事务、执行体与公开入口的参数守卫(越界 fd / `STREAM` / 未 bind / 零长 / 出参容量不足——拒时必须零芯片事务)、"队列未绑定"与"队列满"两条失败出口不许漏槽、`net_udp_set_peer` 轻/重两种回服务器模式(重开丢缓冲 vs 不重开)、挂起 send 由 `net_send_pending` 结算且登记时不许抢发第二份回执、`net_udp_tick` 的会话作废与 stale-readable 兜底、重 bind 必须把还在等包的调用方按 -8 结算、`bind` 遇满队列必须 BUSY 且不给 fd 投毒、**芯片 `open` 回 `ERR_BUSY` 时精确码必须沿通知值回到等待方**(真芯片会这么拒)、**队列里"别人的失败"排在我这条之前时不许污染我的回执**(同 545 在 TCP 侧的钉子,UDP bind 侧补一条)、`net_recvfrom_to` 的"真失败"与"超时"两条出口分开、**netTask 直调路径(板级用例走的那条)的 `set_peer` / `recvfrom` 正例与三种拒绝**、**UDP-24 = 240 轮定种子(`20261104`)随机长跑**:每轮随机载荷长度 1~112(112 = 假件收包流 128 B 上限减掉 8 字节信息头,不是魔数)、随机源端口与源 IP、随机 SERVER/CLIENT 形态、随机"显式对端 `sendto`"与"按记账对端 `sendto`",每 17 轮 `close` + 重 bind 一次;断言收在六族不变量上(读到的长度必须等于芯片宣布值、载荷既不吞信息头也不丢真字节、源地址按形态分别取自表头/对端记账、`udp_mode` 与命令严格一致、`close`/重 bind 后出参槽与挂起槽都不残留、每次 `sendto` 交给芯片的字节数恰好 = `len`),并要求 ≥90% 轮次端到端干净(实测 240/240)——**形态切换 × 挂起结算 × 关闭重建这三件事的交叉面,逐条用例排不出组合,只能靠随机扫**;改写 TC-NETSOCK-440(补 bind 前置 + 新增 440p~440u)、555(表头逐字节按形态分路)、560 组("拒在 netTask 外"→"经队列后非 netTask 仍零 SPI")、500h-i / 506 / 545。全套 **654/654 绿**(改造前 470)。 +- **用例面**:新分片 `test/host/net_socket/ns_s7_udp.c` 共 25 组 UDP-1~25(bind 的 5 条芯片命令**顺序**、未 bind 一律 -5、服务器形态剥头逐字节、客户端形态不剥头、`sendto` 切形态、挂起收包在后续 poll 轮到达、预算超时按调用方 deadline 计、close 与重建结算不许静默丢通知、出参槽互斥与泄漏、截断排空、netTask 自等守卫、`net_init` 重建取消、流式 API 拒 `DGRAM`、非 netTask 调用零芯片事务、执行体与公开入口的参数守卫(越界 fd / `STREAM` / 未 bind / 零长 / 出参容量不足——拒时必须零芯片事务)、"队列未绑定"与"队列满"两条失败出口不许漏槽、`net_udp_set_peer` 轻/重两种回服务器模式(重开丢缓冲 vs 不重开)、挂起 send 由 `net_send_pending` 结算且登记时不许抢发第二份回执、`net_udp_tick` 的会话作废与 stale-readable 兜底、重 bind 必须把还在等包的调用方按 -8 结算、`bind` 遇满队列必须 BUSY 且不给 fd 投毒、**芯片 `open` 回 `ERR_BUSY` 时精确码必须沿通知值回到等待方**(真芯片会这么拒)、**队列里"别人的失败"排在我这条之前时不许污染我的回执**(同 545 在 TCP 侧的钉子,UDP bind 侧补一条)、`net_recvfrom_to` 的"真失败"与"超时"两条出口分开、**netTask 直调路径(板级用例走的那条)的 `set_peer` / `recvfrom` 正例与三种拒绝**、**UDP-24 = 240 轮定种子(`20261104`)随机长跑**:每轮随机载荷长度 1~112(112 = 假件收包流 128 B 上限减掉 8 字节信息头,不是魔数)、随机源端口与源 IP、随机 SERVER/CLIENT 形态、随机"显式对端 `sendto`"与"按记账对端 `sendto`",每 17 轮 `close` + 重 bind 一次;断言收在六族不变量上(读到的长度必须等于芯片宣布值、载荷既不吞信息头也不丢真字节、源地址按形态分别取自表头/对端记账、`udp_mode` 与命令严格一致、`close`/重 bind 后出参槽与挂起槽都不残留、每次 `sendto` 交给芯片的字节数恰好 = `len`),并要求 ≥90% 轮次端到端干净(实测 240/240)——**形态切换 × 挂起结算 × 关闭重建这三件事的交叉面,逐条用例排不出组合,只能靠随机扫**、**UDP-25 = 两条 UDP socket 并存的隔离**(前 24 组全都只用一条 socket,漏掉的正是队列化真正要保的那半句:等待是**一条 socket 自己的事**——fdB 来包不得结算、不得通知 fdA 的 parked 收包,fdA 的调用缓冲一个字节都不能动;反过来 fdB parked 时 fdA 自己 `recvfrom` 必须照常拿到自己那一包,串行的是命令不是等待者;关停在等的那条必须当场结算,不许留槽);改写 TC-NETSOCK-440(补 bind 前置 + 新增 440p~440u)、555(表头逐字节按形态分路)、560 组("拒在 netTask 外"→"经队列后非 netTask 仍零 SPI")、500h-i / 506 / 545。全套 **667/667 绿**(改造前 470)。 - **假件新增(一律可加不可改)**:`g_fx.wlen[]` + `g_fx.last_write[]` 捕获 `ch395f_write_send_buf` 的**真实字节**(此前假件忽略 `p_data`,UDP 截断与排空无从断言);`ch395f_fake_set_write_delay(fd, n)` 让 SENBUF_FREE 第 n 次才到(钉"排空必须在下一次写之前完成");`g_chip.read_calls` 供"读了几个 scratch 块"断言。`g_fake` / `g_chip` 既有字段一字未动。 - **两条教训(都是被测码真 bug,不是测法自误)**:① **执行侧自等守卫不能复用调用侧谓词**——`net_udp_recvfrom_exec` 用 `net_ctx_is_owner_task()` 判自等,而它在 netTask 内恒真 → 每一条经队列投进来的正常 recvfrom 都被拒;正确判据是 `caller == xTaskGetCurrentTaskHandle()`(对齐 `net_pending.c` 的 `caller == s_owner_task`)。抓它的是 UDP-8a。② **芯片长度只能读一次并传参**——`net_udp_datagram_read` 内部又调一次 `ch395f_get_recv_len`,于是"剥几字节"与"读几字节"不同源;假件的 recv_len 队列**每次调用弹一个值**,这条设计当场把双读暴露出来。现由调用方读一次 `avail` 传入三个调用点。 - **补测阶段又抓到两条(同一批缺口分析顺手挖的)**:③ **槽指针必须在 fd 合法性检查之后取**——`net_udp_recvfrom_exec` 原先先算 `&s_net_udp[sockfd]` 再判 `p_sock == NULL`,越界 fd 立刻得到一个越界槽指针(哪怕当下没解引用,后面登记就是野写)。④ **作废不等于可以不吭声**——`net_udp_bind_exec` 重开芯片 Socket 会丢掉缓冲里的包,却只 `net_udp_clear()`:A 任务正阻塞在 `net_recvfrom`、B 任务重 bind 同一个 fd,A 就永远睡死。现在重开之前先按 `NET_ERR_CONNRESET` 结算并通知(UDP-20 钉住)。教训同 `net_pending`:**任何"取消挂起"的动作都必须走 complete 那条路,clear 只是归零**。 diff --git a/test/host/net_socket/ns_s7_udp.c b/test/host/net_socket/ns_s7_udp.c index edadcf56..75555bae 100644 --- a/test/host/net_socket/ns_s7_udp.c +++ b/test/host/net_socket/ns_s7_udp.c @@ -1456,6 +1456,105 @@ static void tc_udp24_random_soak(void) reset_all(); } +/* ================= UDP-25 两条 UDP socket 并存:挂起不串门 ================= */ + +/* 前面 1~24 组全都只用一条 UDP socket,唯独漏了队列化真正要保的东西: + * **等待是一条 socket 自己的事**。两条 socket 里一条 parked 时, + * 另一条来包不得惊动它,另一条自己收发也不得被它拖住—— + * 这是"串行执行、并行等待"这半句承诺的唯一形状。 */ +static void tc_udp25_two_socket_park_isolation(void) +{ + char buf_a[32]; + char buf_b[32]; + char buf_c[32]; + struct net_sockaddr_in sa; + net_msg_t msg; + int fd_a; + int fd_b; + int notifies_before; + int reads_before; + int alen; + int rc; + char nm[200]; + + bring_up_netctx(); + fd_a = udp_bind(123); + fd_b = udp_bind(456); + CHECK(fd_a >= 0 && fd_b >= 0 && fd_a != fd_b, + "UDP-25a two bound UDP sockets own different slots (per-fd state, not a module-wide flag)"); + if (fd_a < 0 || fd_b < 0 || fd_a == fd_b) { + reset_all(); + return; + } + + /* fdA 的收包登记成挂起:此刻两条 socket 的芯片缓冲都是空的 */ + (void)memset(buf_a, 0x5A, (int)sizeof(buf_a)); + ch395f_fake_push_recv_len((uint8_t)fd_a, 0U); + ch395f_fake_push_recv_len((uint8_t)fd_b, 0U); + udp_arm_and_enqueue(fd_a, buf_a, (int)sizeof(buf_a), 0U, &msg); + net_process_messages(); + CHECK(net_udp_pending(fd_a) == 1 && net_udp_pending(fd_b) == 0, + "UDP-25b only the socket someone actually waited on holds a pending slot"); + + /* fdB 来一个包:fdA 既不能被结算,也不能被通知,缓冲区一个字节都不能动 */ + notifies_before = g_ns_notify_calls; + reads_before = g_chip.read_calls; + s_net_socks[fd_b].readable = 1U; + ch395f_fake_push_recv_len((uint8_t)fd_b, 16U); + net_process_messages(); + CHECK(net_udp_pending(fd_a) == 1, + "UDP-25c another socket's datagram does not settle my parked receive"); + CHECK(g_ns_notify_calls == notifies_before, + "UDP-25d and does not wake the waiter (a stray notify turns the next real packet into a stale -2)"); + CHECK(buf_a[0] == (char)0x5A, + "UDP-25e the parked caller's buffer is untouched by the other socket's traffic"); + + /* 现在轮到 fdA 自己的包到:必须就地结算,源地址来自它自己那一包的头部。 + * 长度先把该 fd 的脚本队列清空再给一条:假件的长度是"每次读消耗一格、最后一格 + * 永远重复",本组前面给 fdA 留过 0/16 之类的旧值,不清就会拿旧长度配新内容。 */ + udp_stage_server_datagram("TIME12345678", 12, 51999U, "192.168.31.9"); + ch395f_fake_reset_recv_len((uint8_t)fd_a); + ch395f_fake_push_recv_len((uint8_t)fd_a, (uint16_t)(8 + 12)); + s_net_socks[fd_a].readable = 1U; + net_process_messages(); + CHECK(net_udp_pending(fd_a) == 0 && net_udp_armed(fd_a) == 0, + "UDP-25f the socket's own datagram settles its receive and frees both slots"); + CHECK(g_ns_notify_calls == notifies_before + 1 && + g_ns_notify_last_to == (TaskHandle_t)&g_ns_task_caller && + g_ns_notify_last_val == (uint32_t)12, + "UDP-25g exactly one notify to the waiter, carrying the byte count"); + CHECK(memcmp(buf_a, "TIME12345678", 12) == 0, + "UDP-25h the delivered bytes are this socket's own payload (header stripped, nothing off by eight)"); + + /* 先把 fdB 的芯片长度重新宣布成"空"(清掉上一段留的 16),否则它当场读走而不是挂起 */ + ch395f_fake_reset_recv_len((uint8_t)fd_b); + ch395f_fake_push_recv_len((uint8_t)fd_b, 0U); + udp_arm_and_enqueue(fd_b, buf_b, (int)sizeof(buf_b), 0U, &msg); + net_process_messages(); + CHECK(net_udp_pending(fd_b) == 1, "UDP-25i the second socket is now the parked one"); + udp_stage_server_datagram("FD-A-OK", 7, 52000U, "192.168.31.8"); + ch395f_fake_reset_recv_len((uint8_t)fd_a); + ch395f_fake_push_recv_len((uint8_t)fd_a, (uint16_t)(8 + 7)); + s_net_socks[fd_a].readable = 1U; + alen = (int)sizeof(sa); + (void)memset(&sa, 0, sizeof(sa)); + rc = net_recvfrom(fd_a, buf_c, (int)sizeof(buf_c), NET_MSG_DONTWAIT, + (struct net_sockaddr *)&sa, &alen); + (void)snprintf(nm, sizeof(nm), + "UDP-25j a parked socket never blocks another socket's own recvfrom (rc=%d want 7, parked still waiting=%d want 1)", + rc, net_udp_pending(fd_b)); + CHECK(rc == 7 && net_udp_pending(fd_b) == 1, nm); + CHECK(net_ntohs(sa.sin_port) == 52000U, + "UDP-25k and the source address still comes from the packet it actually read"); + CHECK(g_chip.read_calls > reads_before, + "UDP-25l the chip really was read for fdA (the group is not passing by doing nothing)"); + + (void)net_close_locked(&s_net_socks[fd_b]); + CHECK(net_udp_pending(fd_b) == 0 && net_udp_armed(fd_b) == 0, + "UDP-25m closing the parked socket settles it; nothing may outlive the fd"); + reset_all(); +} + /* ===================== 本分片入口 ===================== */ static void ns_run_s7(void) @@ -1484,4 +1583,5 @@ static void ns_run_s7(void) tc_udp22_set_peer_direct_ctx(); tc_udp23_recvfrom_direct_ctx(); tc_udp24_random_soak(); + tc_udp25_two_socket_park_isolation(); }