test(NET): net_pending 模块(43 条,覆盖 87.1%),钉住通知值回传契约

假 CH395F 的第二批消费者。新增 test/host/net_pending/,net_pending.c 覆盖
169/194 = 87.1%。共享假芯片补了**缓冲尺寸宏链的同构转抄**(RAM_BLOCK 512 /
SEND_BLOCKS 2 → SEND_BUF 1024;RECV_BLOCKS 4 → RECV_BUF 2048 → SPI_DMA_BUF 2052
→ SPI_DMA_MAX_PAYLOAD 2048),于是 net_pending.h 里的 NET_SEND_CHUNK_MAX 是**真值
1024**,分块断言测的是芯片上界而不是假件自己发明的数字。

本模块的核心契约是跨上下文回传:AGENTS 模块约定写着 net_* 的错误码经**任务通知值**
回传(net_msg_t 按值拷入队列,netTask 改的是自己那份副本,调用方永远看不到;也勿给
net_msg_t 加回传字段,否则调用方读到栈垃圾)。台架读不到通知值,所以把 xTaskNotify
做成可观测假件(记目标句柄与通知值)之后,这条契约才第一次被真正验证。

钉住的契约:
- 注册闸门:越界 fd 报 BADF;同 fd 已有挂起报 BUSY;caller 为空报 BUSY;**属主任务
  自己发起被拒**(自己等自己等于死锁 netTask);to_ms=0 记为无限等(不置 deadline)。
- 到达与截断:读量钳在调用方缓冲内(绝不越界写别人的 buf);readable 但芯片没数据时
  只清标志继续等,不误交 0 字节。
- 收尾四条分支:还有残留先把最后一包读走再收尾(**不丢尾包**);干净结束报 0 并按
  auto_relisten 请求重开监听(不自动重开的不被偷偷要求重建);released_abnormal 报
  CONNRESET 而不是 0(调用方要能分辨好坏收尾);session_dead 与 CLOSED 同路。
- settle_close:没挂起时直接返回不打扰芯片;有残留交还字节数;无残留报 CONNRESET。
- 挂起发送:参数校验(buf 空 / len<=0 / sent<0 / sent>=len 报 INVAL);单次写入钳在
  NET_SEND_CHUNK_MAX;**deadline 每写一轮就续期**(还在推进就不该被 1000ms 掐死,
  FTP 上传大文件最容易撞这条);停滞不前进才报 TIMEDOUT;槽位消失或会话塌掉都**交还
  已发字节数**(不是错误码,调用方据此算进度);cancel 留痕且幂等;**clear 不发通知**
  (与 recv 侧同一条纪律,不给调用方假答复)。

自查:42/43 首跑,唯一红的是**我的测试构造缺陷**——无限等那条没先把槽位置成在用,
被 in_use==0 兜底分支提前收口,测不到时间轴;补 mk_sock 后 43/43。reset_all 从上一
批的教训直接带过来(连被测模块的 static 挂起表与属主句柄一起复位),这次没再红。

覆盖率与台账:套件 23 → 24 全绿;files_tested 27 → 28;未触及 .c 39 → 38;分母
3,572 → 3,766 行;line=3297/3766(87.5%)、func=255/282(90.4%)。恒等式
38 = 4(P1 剩) + 5(P2) + 29(P3) 仍咬合。编号补建 TC-HOST-NETPEND(游标 040),
族数 17 → 18;注册表仍只做增量插入。

自查门禁:test-cppcheck Passed;check_ids --strict 通过;doc-links 35 处(= 存量);
12 个新增/改动文件 BOM/CR/U+FFFD/行尾逐项净。零生产代码改动,未跑 @build。
--all-files 仍只有 mem-numbers / mem-budget(.map 台账漂移,等 Lead 合并后统一回填)
与 ui-golden(ref/*.bin 不入库)三条失败,与本分支改动无关。
This commit is contained in:
2026-10-23 22:40:00 +08:00
parent 0a3b932395
commit b4441f0e1e
12 changed files with 702 additions and 17 deletions
@@ -232,9 +232,13 @@
四道时间闸门全钉住(30ms 退避 / 400 与 100ms 两级沉降 / 800ms 上限只留痕 / 2000ms
预算交对账器),另有接收区 256B 钳位与 16 轮上限、"清接收→关断→读即清→读状态"的
调用顺序、五种静默放弃条件 |
| NET 剩下两块(`net_pending`/`net_events`) | ⬜ 未开始 | 假芯片已就绪,直接接即可 |
`net_pending` 要 `net_listener_request` 的记录型假件;`net_events` 依赖最多
(`net_sock_raw`/`net_listener_settle`/`net_pending_cancel`/`xTaskGetTickCount`)放最后 |
| NET 三块之二:`net_pending.c` | ✅ 完成(**169/194 = 87.1%**) | `test/host/net_pending`(43 条) |
跨上下文契约(**错误码走任务通知值**,靠可观测的 `xTaskNotify` 假件才测得到)、
收尾四条分支(不丢尾包 / 干净报 0 / 异常释放报 CONNRESET / 槽位没了报 CONNRESET)、
发送分块钳在 `NET_SEND_CHUNK_MAX=1024`(真值,非假件自造)、`deadline` 每轮续期
(长文件不被 1000ms 掐死)、停滞超时、取消与 `clear` 不发通知的纪律 |
| NET 最后一块(`net_events.c`) | ⬜ 未开始 | 假芯片与假件已就绪;它依赖最多
(`net_sock_raw`/`net_listener_settle`/`net_pending_cancel`/`xTaskGetTickCount`) |
| 104 服务层 | ⬜ 未开始 | — | 桩最省(复用 `cmsis_os2.h` + `net_socket.h`) |
本批照出的两处**现状**(只登记、**未改语义**;全库唯一调用点 `test/board/net_test_task.c`
+22
View File
@@ -1768,6 +1768,28 @@
"subitem_ok": true,
"owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);2026-10-23 随覆盖率口径 §9 假 CH395F 立项第一批补登记",
"cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌"
},
{
"name": "TC-HOST-NETPEND",
"class": "TC",
"title": "主机侧单测用例号:NET 层挂起 recv 与挂起 send(test/host/net_pending)",
"pattern": "\\bTC-NETPEND-[0-9]{1,4}[a-z]?\\b",
"digit_width": [
2,
4
],
"owner_docs_glob": [
"test/host/net_pending/*"
],
"scope": "global",
"frozen": false,
"cursor": "TC-NETPEND-040",
"cursor_owner_only": true,
"check_dangling": true,
"allow_orphans": true,
"subitem_ok": true,
"owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);2026-10-23 随覆盖率口径 §9 假 CH395F 第二批补登记",
"cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌"
}
],
"unmanaged_bare": {
+26 -14
View File
@@ -192,6 +192,14 @@
"lines_covered": 125,
"lines_valid": 126
},
"Drivers/BSP/NET/net_pending.c": {
"func_rate": 0.75,
"funcs_covered": 12,
"funcs_valid": 16,
"line_rate": 0.8711,
"lines_covered": 169,
"lines_valid": 194
},
"Drivers/BSP/NET/net_select.c": {
"func_rate": 1.0,
"funcs_covered": 5,
@@ -307,6 +315,13 @@
"lines_total": 126,
"ok": true
},
"net_pending": {
"files": {
"Drivers/BSP/NET/net_pending.c": 194
},
"lines_total": 194,
"ok": true
},
"net_select": {
"files": {
"Drivers/BSP/NET/net_select.c": 117
@@ -426,22 +441,23 @@
"Drivers/BSP/NET/lftpd/lftpd_io.c",
"Drivers/BSP/NET/lftpd/lftpd_string.c",
"Drivers/BSP/NET/net_listener.c",
"Drivers/BSP/NET/net_pending.c",
"Drivers/BSP/NET/net_select.c",
"Drivers/BSP/NETCFG/ftp_cfg.c",
"Drivers/BSP/NETCFG/net_cfg.c"
],
"totals": {
"files_generated_excluded": 5,
"files_tested": 27,
"files_untested": 117,
"func_rate": 0.9135,
"funcs_covered": 243,
"funcs_valid": 266,
"line_rate": 0.8757,
"lines_covered": 3128,
"lines_valid": 3572,
"modules_ok": 23,
"modules_total": 23
"files_tested": 28,
"files_untested": 116,
"func_rate": 0.9043,
"funcs_covered": 255,
"funcs_valid": 282,
"line_rate": 0.8755,
"lines_covered": 3297,
"lines_valid": 3766,
"modules_ok": 24,
"modules_total": 24
},
"untested_sources": [
{
@@ -476,10 +492,6 @@
"code_lines": 300,
"path": "Drivers/BSP/UI/ui_content_page.c"
},
{
"code_lines": 287,
"path": "Drivers/BSP/NET/net_pending.c"
},
{
"code_lines": 281,
"path": "App/task/default_task.c"
+14
View File
@@ -49,6 +49,20 @@
#define CH395F_TCP_LISTEN 0x01U
#define CH395F_TCP_ESTABLISHED 0x05U
/* 发送/接收缓冲尺寸宏链:**与真实 ch395f.h 同构转抄**(2026-10-23 逐条核对)。
* net_pending.h 里的 NET_SEND_CHUNK_MAX 由这两个宏推导,是"一次最多往芯片写多少"
* 的真实上界,假件不能自己发明数字(否则分块测试就是在测假件的常量):
* RAM_BLOCK_SIZE 512 / SEND_BLOCKS 2 → SEND_BUF 1024
* RECV_BLOCKS 4 → RECV_BUF 2048 → SPI_DMA_BUF 2052 → SPI_DMA_MAX_PAYLOAD 2048
* ⇒ NET_SEND_CHUNK_MAX = min(1024, 2048) = 1024 */
#define CH395F_RAM_BLOCK_SIZE 512U
#define CH395F_SEND_BLOCKS 2U
#define CH395F_SEND_BUF_SIZE_BYTES (CH395F_SEND_BLOCKS * CH395F_RAM_BLOCK_SIZE)
#define CH395F_RECV_BLOCKS 4U
#define CH395F_RECV_BUF_SIZE_BYTES (CH395F_RECV_BLOCKS * CH395F_RAM_BLOCK_SIZE)
#define CH395F_SPI_DMA_BUF_SIZE (CH395F_RECV_BUF_SIZE_BYTES + 4U)
#define CH395F_SPI_DMA_MAX_PAYLOAD (CH395F_SPI_DMA_BUF_SIZE - 4U)
#define CH395F_FAKE_SOCK_MAX 8 /* 与 NET_MAX_SOCKETS 同量级,测试自用 */
#define CH395F_FAKE_QUEUE_LEN 24
#define CH395F_FAKE_LOG_LEN 96
+467
View File
@@ -0,0 +1,467 @@
/*
* 模块名称:NET 层挂起 recv / 挂起 send(net_pending)主机侧单元测试
* 模块功能:在 PC 上离线验证 Drivers/BSP/NET/net_pending.c 的两条挂起队列:注册闸门
* (越界/重复/执行任务自己发起)、**错误码经任务通知值回传**这条跨上下文
* 契约、收数据到达与截断、超时、会话塌掉的四种收尾分支(不丢尾包 / 干净
* 结束报 0 / 异常释放报 CONNRESET / 槽位消失报 CONNRESET)、settle_close 的
* 收尾语义,以及发送侧的分块上限、停滞判定、deadline 续期与取消语义。
*
* 被测对象:真实源文件 ../../../Drivers/BSP/NET/net_pending.c(#include 原样纳入)。
* net_pending.h / net_types.h / net_config.h / net_listener.h 全用**真实头**
* (TaskHandle_t 入参、NET_PENDING_REGISTERED、NET_SEND_TIMEOUT_MS、
* net_listener_req_t 枚举一改就编译期报错)。假件:
* - 假 CH395F 芯片(test/host/common/ch395f_fake.h/.c,含与真实头同构转抄
* 的缓冲尺寸宏链,NET_SEND_CHUNK_MAX 因此是真值 1024 而非假件自造);
* - **可观测的 xTaskNotify**(记目标句柄与通知值)与固定句柄的
* xTaskGetCurrentTaskHandle;
* - 记录型 net_listener_request;可控时基;可控 socket 表数组(tick 入参);
* 可控"芯片此刻还有多少字节可读";dbg_log 计数;net_set_errno 记录。
*
* 为什么要主机侧单测(而不是只靠台架):
* 1. AGENTS 模块约定写着:net_* 公开 API 的错误码经**任务通知值**回传,因为 net_msg_t
* 是**按值拷入队列**的,netTask 改的是自己那份副本,调用方永远看不到;也**勿给
* net_msg_t 加回传字段**(加了只会让调用方读到栈垃圾)。这条契约在真机上表现为
* "偶发拿到 0 或正数却不知道为什么",日志无痕;在 PC 上必须把 xTaskNotify 做成
* 可观测的才谈得上验证——台架根本读不到通知值。
* 2. 挂起 recv 的收尾有**四条**分支(还有尾包 / 干净结束报 0 / 异常释放报 CONNRESET /
* 槽位没了报 CONNRESET),差一条就是"对端已断而我方任务还挂着"或"最后一包数据
* 被丢掉"。真机要凑齐"关闭瞬间芯片接收区还剩字节"这种窗口极难。
* 3. 发送侧分块上限 = min(芯片发送缓冲, SPI DMA 上界):一次写太多会踩芯片 FIFO,
* 表现为"大包发送偶发数据错位";而 deadline 每写一轮就续期,写错就会把**长文件
* 在传输中途掐死**(FTP 上传大文件最容易撞)。两者都要能精确数着字节测。
*
* 覆盖点:TC-NETPEND-001..006 注册闸门;007..018 到达/截断/超时/四条收尾/取消与清理;
* 019..021 settle_close;030..040 挂起发送(参数校验、分块、续期、停滞、取消)。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-23
*/
#include <stdio.h>
#include <string.h>
#include "net_pending.h"
#include "net_listener.h"
#include "net_socket.h"
#include "ch395f.h"
#include "ch395f_fake.c"
#include "net_pending.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)
int g_npd_info_count;
int g_npd_error_count;
int g_npd_debug_count;
/* ============ 可观测的任务通知(本模块最重要的一条契约靠它才测得到) ============ */
static int g_owner_obj;
static int g_caller_obj;
static int g_notify_n;
static void *g_notify_target[16];
static uint32_t g_notify_value[16];
TaskHandle_t xTaskGetCurrentTaskHandle(void)
{
return (TaskHandle_t)&g_owner_obj;
}
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction)
{
(void)eAction;
if (g_notify_n < (int)(sizeof(g_notify_target) / sizeof(g_notify_target[0]))) {
g_notify_target[g_notify_n] = (void *)xTaskToNotify;
g_notify_value[g_notify_n] = ulValue;
}
g_notify_n++;
return 1;
}
/* ============ 记录型监听请求 ============ */
static int g_lis_req_n;
static int g_lis_req_sock;
static int g_lis_req_reason;
void net_listener_request(int sockfd, net_listener_req_t reason)
{
g_lis_req_n++;
g_lis_req_sock = sockfd;
g_lis_req_reason = (int)reason;
}
void net_listener_settle(int sockfd, net_listener_settle_t level) { (void)sockfd; (void)level; }
void net_listener_clear(int sockfd) { (void)sockfd; }
void net_listener_tick(void) { }
/* ============ 可控时基与 errno ============ */
static uint32_t g_now;
static int g_errno_last;
static int g_errno_calls;
uint32_t HAL_GetTick(void) { return g_now; }
int net_set_errno(int err)
{
g_errno_last = err;
g_errno_calls++;
return err;
}
/* ============ 可控 socket 表(tick 的入参) ============ */
static net_sock_t g_socks[NET_MAX_SOCKETS];
static uint8_t g_rbuf[64];
static uint8_t g_wbuf[4096];
static void mk_sock(int fd, net_sock_state_t st)
{
g_socks[fd].in_use = 1u;
g_socks[fd].state = st;
g_socks[fd].type = NET_SOCK_STREAM;
g_socks[fd].auto_relisten = 1u;
g_socks[fd].local_port = 2404u;
}
static void reset_all(void)
{
int i;
(void)memset(g_socks, 0, sizeof(g_socks));
(void)memset(g_rbuf, 0, sizeof(g_rbuf));
g_now = 1000u;
g_errno_last = 0;
g_errno_calls = 0;
g_notify_n = 0;
g_lis_req_n = 0;
g_lis_req_sock = -1;
g_lis_req_reason = -1;
g_npd_info_count = 0;
g_npd_error_count = 0;
g_npd_debug_count = 0;
ch395f_fake_reset();
/* 被测模块自己的 static 必须复位:两张挂起表 + 属主句柄
(net_listener 批次在这里红过 9 条,同样的坑不踩第二遍)。 */
for (i = 0; i < NET_MAX_SOCKETS; i++) {
s_net_pending[i].active = 0u;
s_net_send[i].active = 0u;
}
s_owner_task = (TaskHandle_t)&g_owner_obj;
}
static void tc_recv_register(void)
{
int rc;
printf("-- TC-NETPEND-001..006 recv register --\n");
reset_all();
rc = net_pending_register(-1, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj);
CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-001 下越界 fd 报 BADF");
rc = net_pending_register(NET_MAX_SOCKETS, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj);
CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-001b 上越界同样拦下");
reset_all();
rc = net_pending_register(0, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj);
CHECK(rc == NET_PENDING_REGISTERED, "TC-NETPEND-005 注册成功回 0x70000000 标记");
rc = net_pending_register(0, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj);
CHECK((rc == -1) && (g_errno_last == NET_ERR_BUSY),
"TC-NETPEND-002 同 fd 已有挂起就拒绝(一条 socket 只挂一个 recv)");
reset_all();
rc = net_pending_register(0, g_rbuf, 10, 100u, NULL);
CHECK((rc == -1) && (g_errno_last == NET_ERR_BUSY),
"TC-NETPEND-003 caller 为空拒绝(没有回传目标就别挂起)");
rc = net_pending_register(0, g_rbuf, 10, 100u, (TaskHandle_t)&g_owner_obj);
CHECK((rc == -1) && (g_errno_last == NET_ERR_BUSY),
"TC-NETPEND-004 属主任务自己发起被拒(自己等自己 = 死锁 netTask)");
reset_all();
/* 必须先把槽位置成"在用且已建立":否则 tick 第一条兜底(in_use == 0 →
* CONNRESET)就会把挂起收掉,测的就不是时间轴了。 */
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
(void)net_pending_register(0, g_rbuf, 10, 0u, (TaskHandle_t)&g_caller_obj);
CHECK((s_net_pending[0].infinite == 1u) && (s_net_pending[0].deadline == 0u),
"TC-NETPEND-006 to_ms=0 记为无限等(不置 deadline)");
g_now = 999999u;
net_pending_tick(g_socks);
CHECK((s_net_pending[0].active == 1u) && (g_notify_n == 0),
"TC-NETPEND-006b 无限等不被时间掐掉(也不会偷偷发通知)");
}
static void tc_recv_paths(void)
{
printf("-- TC-NETPEND-007..018 recv completion --\n");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
g_socks[0].readable = 1u;
(void)net_pending_register(0, g_rbuf, 10, 500u, (TaskHandle_t)&g_caller_obj);
ch395f_fake_push_recv_len(0, 5u);
net_pending_tick(g_socks);
CHECK((g_notify_n == 1) && (g_notify_value[0] == 5u) &&
(g_notify_target[0] == (void *)&g_caller_obj) &&
(g_socks[0].recv_bytes == 5u) && (g_socks[0].readable == 0u) &&
(s_net_pending[0].active == 0u),
"TC-NETPEND-007 数据到达:字节数经通知值回传给发起任务");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
g_socks[0].readable = 1u;
(void)net_pending_register(0, g_rbuf, 10, 500u, (TaskHandle_t)&g_caller_obj);
ch395f_fake_push_recv_len(0, 100u);
net_pending_tick(g_socks);
CHECK((g_notify_value[0] == 10u) && (g_chip.last_read_len == 10u),
"TC-NETPEND-008 读量钳在调用方缓冲内(绝不越界写别人的 buf)");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
g_socks[0].readable = 1u;
(void)net_pending_register(0, g_rbuf, 10, 500u, (TaskHandle_t)&g_caller_obj);
net_pending_tick(g_socks);
CHECK((g_notify_n == 0) && (g_socks[0].readable == 0u) &&
(s_net_pending[0].active == 1u),
"TC-NETPEND-009 空 readable:清标志继续等,不误交 0 字节");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
(void)net_pending_register(0, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj);
g_now = 1100u;
net_pending_tick(g_socks);
CHECK((g_notify_value[0] == (uint32_t)NET_ERR_TIMEDOUT) &&
(g_errno_last == NET_ERR_TIMEDOUT) && (s_net_pending[0].active == 0u),
"TC-NETPEND-010 超时:错误码同时进通知值与 errno(跨上下文拿得到)");
reset_all();
(void)net_pending_register(0, g_rbuf, 10, 500u, (TaskHandle_t)&g_caller_obj);
g_socks[0].in_use = 0u;
net_pending_tick(g_socks);
CHECK(g_notify_value[0] == (uint32_t)NET_ERR_CONNRESET,
"TC-NETPEND-011 槽位消失报 CONNRESET(不能让任务熬到超时才发现)");
reset_all();
mk_sock(0, NET_SOCK_STATE_CLOSED);
(void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj);
ch395f_fake_push_recv_len(0, 12u);
net_pending_tick(g_socks);
CHECK((g_notify_value[0] == 12u) && (g_socks[0].recv_bytes == 12u) &&
(g_lis_req_n == 0),
"TC-NETPEND-012 关闭但有残留:先把最后一包读走再收尾(不丢尾包)");
reset_all();
mk_sock(0, NET_SOCK_STATE_CLOSED);
(void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj);
net_pending_tick(g_socks);
CHECK((g_notify_value[0] == 0u) && (g_lis_req_n == 1) && (g_lis_req_sock == 0) &&
(g_lis_req_reason == (int)NET_LISTENER_REQ_PENDING),
"TC-NETPEND-013 干净结束:报 0 并请求重开监听(单连接 socket 必须回 LISTEN)");
reset_all();
mk_sock(0, NET_SOCK_STATE_CLOSED);
g_socks[0].auto_relisten = 0u;
(void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj);
net_pending_tick(g_socks);
CHECK((g_notify_value[0] == 0u) && (g_lis_req_n == 0),
"TC-NETPEND-013b 不自动重开的 socket 不会被偷偷要求重建监听");
reset_all();
mk_sock(0, NET_SOCK_STATE_CLOSED);
g_socks[0].released_abnormal = 1u;
(void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj);
net_pending_tick(g_socks);
CHECK(g_notify_value[0] == (uint32_t)NET_ERR_CONNRESET,
"TC-NETPEND-014 异常释放报 CONNRESET 而不是 0(调用方要能分辨好坏收尾)");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
g_socks[0].session_dead = 1u;
(void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj);
net_pending_tick(g_socks);
CHECK((g_notify_n == 1) && (g_lis_req_n == 1),
"TC-NETPEND-015 session_dead 与 CLOSED 同路(状态滞后时仍能收口)");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
(void)net_pending_register(0, g_rbuf, 10, 500u, (TaskHandle_t)&g_caller_obj);
net_pending_cancel(0, NET_ERR_CONNRESET);
CHECK((g_notify_value[0] == (uint32_t)NET_ERR_CONNRESET) &&
(g_errno_last == NET_ERR_CONNRESET) && (s_net_pending[0].active == 0u),
"TC-NETPEND-016 取消:错误码经通知值送达并解除挂起");
net_pending_cancel(0, NET_ERR_CONNRESET);
CHECK(g_notify_n == 1, "TC-NETPEND-016b 重复取消幂等(不给同一任务发两次通知)");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
mk_sock(2, NET_SOCK_STATE_ESTABLISHED);
(void)net_pending_register(0, g_rbuf, 10, 500u, (TaskHandle_t)&g_caller_obj);
(void)net_pending_register(2, g_rbuf, 10, 500u, (TaskHandle_t)&g_caller_obj);
net_pending_cancel_all(NET_ERR_CONNRESET);
CHECK((g_notify_n == 2) && (s_net_pending[0].active == 0u) &&
(s_net_pending[2].active == 0u),
"TC-NETPEND-017 cancel_all 只通知挂着的那两个(网络整体复位时用)");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
(void)net_pending_register(0, g_rbuf, 10, 500u, (TaskHandle_t)&g_caller_obj);
net_pending_clear(0);
CHECK((s_net_pending[0].active == 0u) && (g_notify_n == 0),
"TC-NETPEND-018 clear 只解除挂起、不发通知(不给调用方假答复)");
}
static void tc_settle_close(void)
{
int before;
printf("-- TC-NETPEND-019..021 settle_close --\n");
reset_all();
mk_sock(0, NET_SOCK_STATE_CLOSED);
before = g_chip.read_calls + g_chip.get_status_calls;
net_pending_settle_close(0, &g_socks[0]);
CHECK((g_chip.read_calls + g_chip.get_status_calls) == before,
"TC-NETPEND-019 没挂起时直接返回(不打扰芯片)");
reset_all();
mk_sock(0, NET_SOCK_STATE_CLOSED);
(void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj);
ch395f_fake_push_recv_len(0, 7u);
net_pending_settle_close(0, &g_socks[0]);
CHECK((g_notify_value[0] == 7u) && (g_socks[0].recv_bytes == 7u) &&
(g_socks[0].readable == 0u),
"TC-NETPEND-020 收尾时把剩余字节读走并交还字节数(收口不丢数据)");
reset_all();
mk_sock(0, NET_SOCK_STATE_CLOSED);
(void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj);
net_pending_settle_close(0, &g_socks[0]);
CHECK(g_notify_value[0] == (uint32_t)NET_ERR_CONNRESET,
"TC-NETPEND-021 无残留则报 CONNRESET(关闭收尾不该被当成读到 0 字节)");
}
static void tc_send_paths(void)
{
int rc;
printf("-- TC-NETPEND-030..040 send pending --\n");
reset_all();
rc = net_send_pending_register(-1, g_wbuf, 100, 0, (TaskHandle_t)&g_caller_obj);
CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-030 越界 fd 报 BADF");
rc = net_send_pending_register(0, NULL, 100, 0, (TaskHandle_t)&g_caller_obj);
CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030b buf 为空报 INVAL");
rc = net_send_pending_register(0, g_wbuf, 0, 0, (TaskHandle_t)&g_caller_obj);
CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030c len 非正报 INVAL");
rc = net_send_pending_register(0, g_wbuf, 100, -1, (TaskHandle_t)&g_caller_obj);
CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030d sent 为负报 INVAL");
rc = net_send_pending_register(0, g_wbuf, 100, 100, (TaskHandle_t)&g_caller_obj);
CHECK((g_errno_last == NET_ERR_INVAL) && (s_net_send[0].active == 0u),
"TC-NETPEND-030e sent>=len 报 INVAL(已发完的不必挂)");
rc = net_send_pending_register(0, g_wbuf, 100, 50, NULL);
CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031 caller 为空报 BUSY");
rc = net_send_pending_register(0, g_wbuf, 100, 50, (TaskHandle_t)&g_owner_obj);
CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031b 属主自己发报 BUSY");
rc = net_send_pending_register(0, g_wbuf, 100, 50, (TaskHandle_t)&g_caller_obj);
CHECK(rc == NET_PENDING_REGISTERED, "TC-NETPEND-032 挂起发送注册成功");
rc = net_send_pending_register(0, g_wbuf, 100, 50, (TaskHandle_t)&g_caller_obj);
CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031c 同 fd 只允许一条挂起发送");
reset_all();
(void)net_send_pending_register(0, g_wbuf, 3000, 700, (TaskHandle_t)&g_caller_obj);
g_socks[0].in_use = 0u;
net_send_pending_tick(g_socks);
CHECK((g_notify_n == 1) && (g_notify_value[0] == 700u),
"TC-NETPEND-033 槽位消失回**已发字节数**(不是错误码,调用方据此算进度)");
reset_all();
mk_sock(0, NET_SOCK_STATE_CLOSED);
(void)net_send_pending_register(0, g_wbuf, 3000, 700, (TaskHandle_t)&g_caller_obj);
net_send_pending_tick(g_socks);
CHECK((g_npd_error_count == 1) && (g_errno_last == NET_ERR_CONNRESET) &&
(g_notify_value[0] == 700u),
"TC-NETPEND-034 会话塌了:留 ERROR + 报 CONNRESET,但仍把已发字节交回去");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
g_socks[0].send_ready = 1u;
(void)net_send_pending_register(0, g_wbuf, 3000, 0, (TaskHandle_t)&g_caller_obj);
net_send_pending_tick(g_socks);
CHECK((g_chip.write_calls == 1) &&
(g_chip.last_read_len == (uint16_t)NET_SEND_CHUNK_MAX) &&
(g_socks[0].send_bytes == (uint32_t)NET_SEND_CHUNK_MAX) &&
(g_socks[0].send_ready == 0u) && (s_net_send[0].active == 1u),
"TC-NETPEND-035 单次写入钳在 NET_SEND_CHUNK_MAX(芯片发送缓冲上界)");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
(void)net_send_pending_register(0, g_wbuf, 3000, 0, (TaskHandle_t)&g_caller_obj);
g_socks[0].send_ready = 1u;
net_send_pending_tick(g_socks);
g_socks[0].send_ready = 1u;
net_send_pending_tick(g_socks);
g_socks[0].send_ready = 1u;
net_send_pending_tick(g_socks);
CHECK((g_notify_value[0] == 3000u) && (s_net_send[0].active == 0u) &&
(g_chip.write_calls == 3),
"TC-NETPEND-036 分三块发完 3000 字节后交还总长度");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
(void)net_send_pending_register(0, g_wbuf, 3000, 0, (TaskHandle_t)&g_caller_obj);
g_now = 1900u;
g_socks[0].send_ready = 1u;
net_send_pending_tick(g_socks);
g_now = 2800u;
g_socks[0].send_ready = 1u;
net_send_pending_tick(g_socks);
CHECK((g_npd_error_count == 0) && (s_net_send[0].active == 1u) &&
(g_chip.write_calls == 2),
"TC-NETPEND-038 每写一轮续期 deadline:还在推进就不该被 1000ms 掐死");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
(void)net_send_pending_register(0, g_wbuf, 3000, 0, (TaskHandle_t)&g_caller_obj);
g_now = 2000u;
net_send_pending_tick(g_socks);
CHECK((g_npd_error_count == 1) && (g_errno_last == NET_ERR_TIMEDOUT) &&
(g_notify_value[0] == 0u),
"TC-NETPEND-037 停滞超时报 TIMEDOUT(对端不收时不能永久占着发送通道)");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
(void)net_send_pending_register(0, g_wbuf, 100, 40, (TaskHandle_t)&g_caller_obj);
net_send_pending_cancel(0, NET_ERR_CONNRESET);
CHECK((g_npd_error_count == 1) && (g_errno_last == NET_ERR_CONNRESET) &&
(g_notify_value[0] == 40u) && (s_net_send[0].active == 0u),
"TC-NETPEND-039 取消挂起发送:留痕 + 交还已发字节");
net_send_pending_cancel(0, NET_ERR_CONNRESET);
CHECK(g_notify_n == 1, "TC-NETPEND-039b 重复取消幂等");
reset_all();
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
(void)net_send_pending_register(0, g_wbuf, 100, 40, (TaskHandle_t)&g_caller_obj);
net_send_pending_clear(0);
CHECK((s_net_send[0].active == 0u) && (g_notify_n == 0),
"TC-NETPEND-040 clear 不发通知(与 recv 侧同一条纪律)");
}
int main(void)
{
printf("=== NET Pending Tests ===\n");
tc_recv_register();
tc_recv_paths();
tc_settle_close();
tc_send_paths();
printf("\n=== NET Pending Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""NET 层挂起 recv / 挂起 send(net_pending)主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/net_pending/net_pending_test.c(以 #include 纳入生产源
Drivers/BSP/NET/net_pending.c 与共享假芯片 test/host/common/ch395f_fake.c),在 PC
上离线验证两条挂起队列:注册闸门、错误码经任务通知值回传、收数据到达/截断/超时、
会话塌掉的四条收尾分支、settle_close 语义、发送侧分块上限与停滞判定与 deadline
续期。工具链与判定委托 test/host/common/unit_test_harness.py。
为什么要主机侧单测(而不是只用台架):
本模块的核心契约是"跨上下文回传"——net_msg_t 按值拷入队列,netTask 改的是自己那份
副本,错误码只能搭任务通知值回来(也绝不能给 net_msg_t 加回传字段,否则调用方读到
栈垃圾)。台架根本读不到通知值,所以这条契约在真机上只表现为"偶发拿到 0 或正数却
不知道为何"。另外:收尾四条分支少一条就是"对端已断而我方任务还挂着"或"最后一包数据
被丢掉";发送分块上限踩错会写爆芯片 FIFO;deadline 续期写错会把 FTP 大文件在传输
中途掐死——这些都要能精确数着字节与毫秒测。
桩的边界:net_pending.h / net_types.h / net_config.h / net_listener.h 用真实头
(TaskHandle_t 入参与 NET_PENDING_REGISTERED、NET_SEND_TIMEOUT_MS、net_listener_req_t
一改即编译期报错);net_socket.h 是桩(net_set_errno 与 NET_ERR_RETURN_1 照抄真实头
同名宏);ch395f.h 只转发到共享假芯片(缓冲尺寸宏链与真实头同构转抄,
NET_SEND_CHUNK_MAX 因此是真值 1024);FreeRTOS.h / task.h 是宿主等价壳,但把
xTaskNotify 做成**可观测**假件——没有它,本模块最重要的那条契约根本无法验证。
用法(仓库根目录):
python test/host/net_pending/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误。
设计依据:AGENTS.md 模块约定(错误码经任务通知值回传)、
docs/网络/Net层阻塞模型架构设计.md、docs/测试说明/主机单测覆盖率口径.md §9
创建日期:2026-10-23
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
BUILD = TestBuild(
name="NET Pending Tests",
src=os.path.join(HERE, "net_pending_test.c"),
include_dirs=[
os.path.join(HERE, "stub"),
os.path.join(ROOT, "test", "host", "common"),
os.path.join(ROOT, "Drivers", "BSP", "NET"),
HERE,
],
summary_marker="=== NET Pending Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+28
View File
@@ -0,0 +1,28 @@
/*
* 模块名称:主机侧单测桩 - FreeRTOS.h(net_pending 模块专用)
* 模块功能:net_pending 的整个设计就是"错误码经**任务通知值**回传",所以它必须拿到
* TaskHandle_t / BaseType_t / eNotifyAction 这三个类型才能编译——真实
* net_pending.h 的原型里就带着它们(绕不开,且绕开就丢掉接口真实性)。
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。三个宿主等价物与 FreeRTOS 一致:句柄是
* void*,BaseType_t 是 portBASE_TYPE(最小整型),通知动作是三值枚举。
* 创建日期:2026-10-23
*/
#ifndef FREERTOS_H
#define FREERTOS_H
#include <stddef.h>
#include <stdint.h>
typedef void *TaskHandle_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;
typedef enum {
eNoAction = 0,
eSetBits = 1,
eIncrement = 2,
eSetValueWithOverwrite = 3,
eSetValueWithoutOverwrite = 4
} eNotifyAction;
#endif /* FREERTOS_H */
+10
View File
@@ -0,0 +1,10 @@
/*
* 模块名称:主机侧单测桩 - ch395f.h(net_pending 模块入口)
* 模块功能:只做转发,真身在 test/host/common/ch395f_fake.h/.c(NET 域共享假芯片)。
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
* 创建日期:2026-10-23
*/
#ifndef CH395F_H
#define CH395F_H
#include "ch395f_fake.h"
#endif /* CH395F_H */
+18
View File
@@ -0,0 +1,18 @@
/*
* 模块名称:主机侧单测桩 - dbg_log.h(net_pending 模块专用)
* 模块功能:DBG_* 换成计数,让"会话塌了必须留 ERROR""停滞超时必须留 ERROR"可断言。
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
* 创建日期:2026-10-23
*/
#ifndef DBG_LOG_H
#define DBG_LOG_H
extern int g_npd_info_count;
extern int g_npd_error_count;
extern int g_npd_debug_count;
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_npd_info_count++; } while (0)
#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_npd_debug_count++; } while (0)
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_npd_error_count++; } while (0)
#endif /* DBG_LOG_H */
+11
View File
@@ -0,0 +1,11 @@
/*
* 模块名称:主机侧单测桩 - main.h(net_pending 模块专用)
* 模块功能:只给 HAL_GetTick(recv 超时窗与 send 停滞窗的时基,由测试显式设定)。
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
* 创建日期:2026-10-23
*/
#ifndef MAIN_H
#define MAIN_H
#include <stdint.h>
uint32_t HAL_GetTick(void);
#endif /* MAIN_H */
+18
View File
@@ -0,0 +1,18 @@
/*
* 模块名称:主机侧单测桩 - net_socket.h(NET 域共享形态,见 net_listener 同名文件)
* 模块功能:net_pending.c 需要 net_set_errno() 与 NET_ERR_RETURN_1()(写法照抄真实
* net_socket.h:528/532);socket 状态与错误码常量取**真实** net_types.h。
* 本模块**不用** net_get_sock——tick 收的是 net_sock_t 表数组(AGENTS 模块
* 约定:net_get_sock 对未用槽位返回 NULL,而 tick 必须读 in_use==0 兜底)。
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
* 创建日期:2026-10-23
*/
#ifndef NET_SOCKET_H
#define NET_SOCKET_H
#include "net_types.h"
int net_set_errno(int err);
#define NET_ERR_RETURN_1(err) (net_set_errno(err), -1)
#endif /* NET_SOCKET_H */
+22
View File
@@ -0,0 +1,22 @@
/*
* 模块名称:主机侧单测桩 - task.h(net_pending 模块专用)
* 模块功能:只声明 net_pending 真正用到的两个内核入口,实现由测试单元给出**可观测**
* 的假件:xTaskNotify 把 (目标句柄, 通知值) 记进日志,
* xTaskGetCurrentTaskHandle 返回一个固定的"属主任务"句柄。
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
* 为什么这两个假件是重点:AGENTS 模块约定写着"net_msg_t 是按值拷入队列的,
* netTask 改的是自己那份副本,调用方永远看不到 → 错误码必须搭通知值回来",
* 还写着"勿给 net_msg_t 加回传字段"。这条契约只有把 xTaskNotify 做成可观测的,
* 才能在主机侧被验证(否则"调用方拿到错误码"这件事在 PC 上根本无法发生)。
* 创建日期:2026-10-23
*/
#ifndef TASK_H
#define TASK_H
#include "FreeRTOS.h"
TaskHandle_t xTaskGetCurrentTaskHandle(void);
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue,
eNotifyAction eAction);
#endif /* TASK_H */