diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index b992609e..42f02b3d 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -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` diff --git a/scripts/id_registry.json b/scripts/id_registry.json index 44d2ca1f..020777ec 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -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": { diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index 5ddf8060..563836d2 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -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" diff --git a/test/host/common/ch395f_fake.h b/test/host/common/ch395f_fake.h index 8f612326..cfd0a516 100644 --- a/test/host/common/ch395f_fake.h +++ b/test/host/common/ch395f_fake.h @@ -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 diff --git a/test/host/net_pending/net_pending_test.c b/test/host/net_pending/net_pending_test.c new file mode 100644 index 00000000..eef73a0c --- /dev/null +++ b/test/host/net_pending/net_pending_test.c @@ -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 +#include + +#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; +} diff --git a/test/host/net_pending/run_unit_test.py b/test/host/net_pending/run_unit_test.py new file mode 100644 index 00000000..9e212c54 --- /dev/null +++ b/test/host/net_pending/run_unit_test.py @@ -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)) + diff --git a/test/host/net_pending/stub/FreeRTOS.h b/test/host/net_pending/stub/FreeRTOS.h new file mode 100644 index 00000000..14694d3b --- /dev/null +++ b/test/host/net_pending/stub/FreeRTOS.h @@ -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 +#include + +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 */ diff --git a/test/host/net_pending/stub/ch395f.h b/test/host/net_pending/stub/ch395f.h new file mode 100644 index 00000000..d88d6ba1 --- /dev/null +++ b/test/host/net_pending/stub/ch395f.h @@ -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 */ diff --git a/test/host/net_pending/stub/dbg_log.h b/test/host/net_pending/stub/dbg_log.h new file mode 100644 index 00000000..aed034f2 --- /dev/null +++ b/test/host/net_pending/stub/dbg_log.h @@ -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 */ diff --git a/test/host/net_pending/stub/main.h b/test/host/net_pending/stub/main.h new file mode 100644 index 00000000..4c84aa67 --- /dev/null +++ b/test/host/net_pending/stub/main.h @@ -0,0 +1,11 @@ +/* + * 模块名称:主机侧单测桩 - main.h(net_pending 模块专用) + * 模块功能:只给 HAL_GetTick(recv 超时窗与 send 停滞窗的时基,由测试显式设定)。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-23 + */ +#ifndef MAIN_H +#define MAIN_H +#include +uint32_t HAL_GetTick(void); +#endif /* MAIN_H */ diff --git a/test/host/net_pending/stub/net_socket.h b/test/host/net_pending/stub/net_socket.h new file mode 100644 index 00000000..5a70dfeb --- /dev/null +++ b/test/host/net_pending/stub/net_socket.h @@ -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 */ diff --git a/test/host/net_pending/stub/task.h b/test/host/net_pending/stub/task.h new file mode 100644 index 00000000..768f78ba --- /dev/null +++ b/test/host/net_pending/stub/task.h @@ -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 */