test(net/udp): 主机单测补 UDP-25 两条 socket 并存的隔离(#73)
前 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 函数,模块级覆盖率无回退。
This commit is contained in:
@@ -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 只是归零**。
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user