/* * 模块名称:FTP 的 socket 适配层(lftpd_inet)主机侧单元测试 * 模块功能:在 PC 上离线验证 Drivers/BSP/NET/lftpd/lftpd_inet.c 的监听/接受/读写 * 适配:端口必须过 htons、回来的端口要 ntohs 还原;accept 循环每轮喂狗并 * 让出 CPU、**网络代次一变就立刻撤掉 accept**(否则新 IP 生效后监听还挂在 * 旧代次上);带超时的 accept 在监听门撤防时立即放弃;read_line 的 CRLF * 截断、分片拼接、-2 继续等、其它负值原样返回、200ms 分片与 30 s 总预算; * 短写续传(FTP 响应少半条就是协议错乱)。 * * 被测对象:真实源文件 ../../../../Drivers/BSP/NET/lftpd/lftpd_inet.c(#include 纳入)。 * private/lftpd_inet.h 用真实头;net_types.h 用**真实头**(struct * net_sockaddr_in 的 sin_port 注释就写着"网络字节序",htons/ntohs 的契约 * 靠它才钉得住)。假的是 socket 层本身:accept 序列、bind/listen 返回码、 * 代次变化点、监听门开关、recv 的返回序列 + 字节流、send 的短写序列, * 外加可控 tick 与 vTaskDelay 计数(带跑飞保险)。 * * 为什么要主机侧单测(而不是只靠台架): * 1. 代次撤 accept 与"监听门撤防就放弃"这两条只在**异常时刻**成立(网络重建、 * 客户端连上控制口却迟迟不开数据通道)。台架上要凑"对面正连着我顺手改 IP" * 这种时序极难,而真机上它是现场最常见的自愈路径。 * 2. 200ms 分片 + 30 s 总预算是 FTP 控制信道的等待结构:分片写错会把看门狗饿到 * 复位(每轮都要喂),预算写错会让 ftpTask 永久卡在一条命令上。两者都需要 * "按毫秒数着"才能验证。 * 3. 短写续传:net_send 可以只写一部分;如果适配层按一次写完处理,FTP 响应就会 * 少半条(客户端表现为协议错乱、卡住),而这种短写在真机上取决于当时缓冲余量。 * 4. lftpd_task_wd_beat 在本文件是 __weak 空实现(真机由 lftpd.c 覆盖),主机侧 * 替换不掉,所以用同一段里的"睡 100ms"次数当喂狗轮数的代理(已在桩里注明)。 * * 覆盖点:TC-LFTINET-001..004 监听;005/006 端口还原;010..012 accept 与代次; * 020..022 带超时 accept 与撤防;030..036 read_line;040..043 write_string; * 050 write;060..062 read;070 close。 * * 通过:全部 TC PASS(main 返回 0)。 * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 * * 创建日期:2026-10-23 */ #include #include #include #include "private/lftpd_inet.h" #include "net_types.h" #include "net_socket.h" #include "lftpd_inet.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_lfi_info_count; int g_lfi_error_count; int g_lfi_debug_count; /* ===================== 可控 socket 层 ===================== */ typedef struct { int socket_rc; int bind_rc; int listen_rc; int getsock_rc; uint16_t getsock_port_net; int accept_seq[16]; int accept_n; int accept_i; int accept_calls; int gen; int gen_flip_after; int armed; int recv_seq[24]; int recv_n; int recv_i; int recv_calls; uint32_t last_recv_timeout; char rx[512]; int rx_pos; int send_seq[8]; int send_n; int send_i; int send_calls; int send_off[8]; const char *send_base; int socket_calls; int bind_calls; int listen_calls; int close_calls; int last_domain; int last_type; uint16_t last_bind_port_net; int delay_calls; TickType_t tick; } lfi_net_t; static lfi_net_t g_net; static void net_reset(void) { (void)memset(&g_net, 0, sizeof(g_net)); g_net.socket_rc = 3; g_net.bind_rc = 0; g_net.listen_rc = 0; g_net.getsock_rc = 0; g_net.armed = 1; g_net.gen = 7; g_net.tick = 0u; } uint32_t net_get_generation(void) { return (uint32_t)g_net.gen; } int net_listener_armed(int sockfd) { (void)sockfd; return g_net.armed; } uint16_t net_htons(uint16_t hostshort) { return (uint16_t)((uint16_t)((hostshort & 0xFFU) << 8) | (uint16_t)(hostshort >> 8)); } uint16_t net_ntohs(uint16_t netshort) { return net_htons(netshort); } int net_socket(int domain, int type) { g_net.socket_calls++; g_net.last_domain = domain; g_net.last_type = type; return g_net.socket_rc; } int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { const struct net_sockaddr_in *a = (const struct net_sockaddr_in *)addr; (void)sockfd; (void)addrlen; g_net.bind_calls++; g_net.last_bind_port_net = a->sin_port; return g_net.bind_rc; } int net_listen(int sockfd) { (void)sockfd; g_net.listen_calls++; return g_net.listen_rc; } int net_close(int sockfd) { (void)sockfd; g_net.close_calls++; return 0; } int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) { int r; (void)sockfd; (void)addr; (void)addrlen; g_net.accept_calls++; if ((g_net.gen_flip_after > 0) && (g_net.accept_calls >= g_net.gen_flip_after)) { g_net.gen++; /* 模拟网络在这期间重建(代次 +1) */ } if (g_net.accept_i < g_net.accept_n) { r = g_net.accept_seq[g_net.accept_i++]; } else { r = (g_net.accept_n > 0) ? g_net.accept_seq[g_net.accept_n - 1] : -1; } return r; } int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen) { struct net_sockaddr_in *a = (struct net_sockaddr_in *)addr; (void)sockfd; (void)addrlen; a->sin_port = g_net.getsock_port_net; return g_net.getsock_rc; } int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { int r; int i; (void)sockfd; g_net.recv_calls++; g_net.last_recv_timeout = timeout_ms; /* 每次调用当作耗掉一个 200ms 片:让 30 s 总预算的到点路径可测 */ g_net.tick += 200u; if (g_net.recv_i < g_net.recv_n) { r = g_net.recv_seq[g_net.recv_i++]; } else { r = (g_net.recv_n > 0) ? g_net.recv_seq[g_net.recv_n - 1] : -2; } if (r > 0) { for (i = 0; (i < r) && (i < len) && (g_net.rx_pos < (int)sizeof(g_net.rx)); i++) { ((char *)buf)[i] = g_net.rx[g_net.rx_pos++]; } } return r; } int net_send(int sockfd, const void *buf, int len, int flags) { int r; (void)sockfd; (void)flags; if (g_net.send_base == NULL) { g_net.send_base = (const char *)buf; } g_net.send_calls++; if (g_net.send_i < 8) { g_net.send_off[g_net.send_i] = (int)((const char *)buf - g_net.send_base); } if (g_net.send_i < g_net.send_n) { r = g_net.send_seq[g_net.send_i++]; } else { r = (g_net.send_n > 0) ? g_net.send_seq[g_net.send_n - 1] : len; } return r; } TickType_t xTaskGetTickCount(void) { return g_net.tick; } void vTaskDelay(TickType_t xTicksToDelay) { (void)xTicksToDelay; g_net.delay_calls++; if (g_net.delay_calls > 100) { /* 跑飞保险:本模块每条用例都必须在有限轮内退出(accept 序列要么成功、 * 要么撤防、要么代次变化)。真跑飞就退出,绝不把挂死留给 pre-commit。 */ printf(" ABORT accept/wait loop runaway (>100 rounds without exit)\n"); fflush(stdout); exit(2); } } static void logs_clear(void) { g_lfi_info_count = 0; g_lfi_error_count = 0; g_lfi_debug_count = 0; } /* ===================== 监听与端口还原 ===================== */ static void tc_listen(void) { printf("-- TC-LFTINET-001..006 listen / port --\n"); net_reset(); logs_clear(); g_net.socket_rc = -1; CHECK((lftpd_inet_listen(21) == -1) && (g_net.bind_calls == 0) && (g_lfi_error_count == 1), "TC-LFTINET-001 a socket creation failure returns at once, no bind attempt"); net_reset(); logs_clear(); g_net.bind_rc = -1; CHECK((lftpd_inet_listen(21) == -1) && (g_net.close_calls == 1) && (g_net.listen_calls == 0), "TC-LFTINET-002 a bind failure must close the socket"); net_reset(); logs_clear(); g_net.listen_rc = -1; CHECK((lftpd_inet_listen(21) == -1) && (g_net.close_calls == 1) && (g_lfi_error_count == 1), "TC-LFTINET-003 a listen failure also closes the socket and logs it"); net_reset(); logs_clear(); CHECK((lftpd_inet_listen(21) == 3) && (g_net.last_domain == NET_AF_INET) && (g_net.last_type == NET_SOCK_STREAM) && (g_net.last_bind_port_net == net_htons(21u)) && (g_net.listen_calls == 1), "TC-LFTINET-004 a stream listener whose port went through htons (sin_port is network order)"); net_reset(); logs_clear(); g_net.getsock_rc = -1; CHECK((lftpd_inet_get_socket_port(3) == -1) && (g_lfi_error_count == 1), "TC-LFTINET-005 a failing getsockname returns -1 (never report a stale port as the result)"); net_reset(); logs_clear(); g_net.getsock_port_net = net_htons(8888u); CHECK(lftpd_inet_get_socket_port(3) == 8888, "TC-LFTINET-006 the port comes back through ntohs into host order"); } /* ===================== accept:喂狗、代次、撤防 ===================== */ static void tc_accept(void) { printf("-- TC-LFTINET-010..012 / 020..022 accept --\n"); net_reset(); logs_clear(); g_net.accept_seq[0] = 9; g_net.accept_n = 1; CHECK((lftpd_inet_accept(3) == 9) && (g_net.delay_calls == 0), "TC-LFTINET-010 an immediate hit neither sleeps nor waits"); net_reset(); logs_clear(); g_net.accept_seq[0] = -1; g_net.accept_seq[1] = -1; g_net.accept_seq[2] = 9; g_net.accept_n = 3; CHECK((lftpd_inet_accept(3) == 9) && (g_net.delay_calls == 2) && (g_lfi_info_count == 0), "TC-LFTINET-011 every failed round yields the CPU (and feeds the watchdog in the same block) until it succeeds"); net_reset(); logs_clear(); g_net.accept_seq[0] = -1; g_net.accept_seq[1] = -1; g_net.accept_seq[2] = -1; g_net.accept_n = 3; g_net.gen_flip_after = 3; /* 第 3 轮时网络重建 */ CHECK((lftpd_inet_accept(3) == -1) && (g_lfi_info_count == 1) && (g_net.accept_calls == 3), "TC-LFTINET-012 a generation change aborts accept immediately (otherwise the listener stays on the old generation after the new IP takes effect)"); net_reset(); logs_clear(); g_net.accept_seq[0] = -1; g_net.accept_seq[1] = 9; g_net.accept_n = 2; CHECK((lftpd_inet_accept_timeout(3, 5000) == 9) && (g_net.delay_calls == 1), "TC-LFTINET-020 the data channel is accepted inside the timeout window"); net_reset(); logs_clear(); g_net.accept_seq[0] = -1; g_net.accept_seq[1] = -1; g_net.accept_n = 2; g_net.armed = 0; /* 监听门被撤(会话已放弃) */ CHECK((lftpd_inet_accept_timeout(3, 5000) == -1) && (g_net.accept_calls == 1) && (g_lfi_error_count == 1), "TC-LFTINET-021 lowering the listener gate gives up at once instead of burning the full 5 s on a dead listener"); net_reset(); logs_clear(); g_net.accept_seq[0] = -1; g_net.accept_n = 1; CHECK((lftpd_inet_accept_timeout(3, 300) == -1) && (g_net.delay_calls == 3) && (g_lfi_info_count == 1), "TC-LFTINET-022 the peer never opens the data channel: step by 100ms to the limit and finish as INFO"); } /* ===================== read_line:分片、截断、预算 ===================== */ static void tc_read_line(void) { char b[32]; int rc; printf("-- TC-LFTINET-030..036 read_line --\n"); net_reset(); logs_clear(); (void)strcpy(g_net.rx, "USER admin\r\n"); g_net.recv_seq[0] = 12; g_net.recv_n = 1; rc = lftpd_inet_read_line(3, b, sizeof(b)); CHECK((rc == 0) && (strcmp(b, "USER admin") == 0), "TC-LFTINET-030 one shot: CRLF is cut into the terminator in place (the FTP command line boundary)"); net_reset(); logs_clear(); (void)strcpy(g_net.rx, "USER admin\r\n"); g_net.recv_seq[0] = 4; g_net.recv_seq[1] = 8; g_net.recv_n = 2; rc = lftpd_inet_read_line(3, b, sizeof(b)); CHECK((rc == 0) && (strcmp(b, "USER admin") == 0), "TC-LFTINET-031 two arriving halves are stitched into one line (a command line spanning packets)"); net_reset(); logs_clear(); g_net.recv_seq[0] = 0; g_net.recv_n = 1; CHECK(lftpd_inet_read_line(3, b, sizeof(b)) == -1, "TC-LFTINET-032 reading 0 (peer closed) fails at once and must not spin"); net_reset(); logs_clear(); (void)strcpy(g_net.rx, "NOOP\r\n"); g_net.recv_seq[0] = -2; g_net.recv_seq[1] = 6; g_net.recv_n = 2; rc = lftpd_inet_read_line(3, b, sizeof(b)); CHECK((rc == 0) && (strcmp(b, "NOOP") == 0), "TC-LFTINET-033 -2 means this slice timed out: keep waiting instead of treating it as an error"); net_reset(); logs_clear(); g_net.recv_seq[0] = -5; g_net.recv_n = 1; CHECK(lftpd_inet_read_line(3, b, sizeof(b)) == -5, "TC-LFTINET-034 any other negative value is returned verbatim (an error code must not be swallowed into -1)"); net_reset(); logs_clear(); g_net.recv_seq[0] = -2; g_net.recv_n = 1; /* 一直超时,每轮耗 200ms */ rc = lftpd_inet_read_line(3, b, sizeof(b)); CHECK((rc == -1) && (g_net.recv_calls == 150) && (g_net.last_recv_timeout <= 200u), "TC-LFTINET-035 the 30 s total budget accumulates in 200ms slices and exits on time (the watchdog never starves)"); net_reset(); logs_clear(); (void)memset(g_net.rx, 'A', 40); /* 没有 CRLF 的长行 */ g_net.recv_seq[0] = 7; g_net.recv_n = 1; (void)memset(b, 0, sizeof(b)); rc = lftpd_inet_read_line(3, b, 8); CHECK((rc == -1) && (b[7] == '\0'), "TC-LFTINET-036 buffer full with no line end: fail without writing out of bounds (the last byte is reserved for the terminator)"); } /* ===================== 写与读 ===================== */ static void tc_write_read(void) { char b[16]; const char msg[] = "220 ready\r\n"; printf("-- TC-LFTINET-040..070 write / read / close --\n"); net_reset(); logs_clear(); g_net.send_seq[0] = 12; g_net.send_n = 1; CHECK((lftpd_inet_write_string(3, msg) == 0) && (g_net.send_calls == 1), "TC-LFTINET-040 everything written in a single call"); net_reset(); logs_clear(); g_net.send_seq[0] = 5; g_net.send_seq[1] = 7; g_net.send_n = 2; CHECK((lftpd_inet_write_string(3, msg) == 0) && (g_net.send_calls == 2) && (g_net.send_off[1] == 5), "TC-LFTINET-041 after a short write the continuation resumes at the correct offset (half a response is a protocol break)"); net_reset(); logs_clear(); g_net.send_seq[0] = 0; g_net.send_n = 1; CHECK((lftpd_inet_write_string(3, msg) == -1) && (g_lfi_error_count == 1), "TC-LFTINET-042 writing 0 bytes counts as a failure (not a successful zero-byte write)"); net_reset(); logs_clear(); g_net.send_seq[0] = -1; g_net.send_n = 1; CHECK(lftpd_inet_write(3, msg, 12) == -1, "TC-LFTINET-043 a write failure returns -1 as is"); net_reset(); logs_clear(); g_net.send_seq[0] = 4; g_net.send_seq[1] = 6; g_net.send_n = 2; CHECK((lftpd_inet_write(3, msg, 10) == 10) && (g_net.send_calls == 2), "TC-LFTINET-050 binary writes also resume after short writes and return the requested length on success"); net_reset(); logs_clear(); (void)strcpy(g_net.rx, "STOR\r\n"); g_net.recv_seq[0] = 4; g_net.recv_n = 1; CHECK(lftpd_inet_read(3, b, sizeof(b)) == 4, "TC-LFTINET-060 data read is handed back immediately"); net_reset(); logs_clear(); g_net.recv_seq[0] = -1; g_net.recv_n = 1; CHECK(lftpd_inet_read(3, b, sizeof(b)) == -1, "TC-LFTINET-061 -1 fails and returns directly"); net_reset(); logs_clear(); g_net.recv_seq[0] = -3; g_net.recv_n = 1; CHECK((lftpd_inet_read(3, b, sizeof(b)) == -1) && (g_net.recv_calls > 1), "TC-LFTINET-062 other negative values keep waiting until the 30 s budget is exhausted"); net_reset(); logs_clear(); CHECK((lftpd_inet_close(3) == 0) && (g_net.close_calls == 1), "TC-LFTINET-070 close only forwards to net_close"); } int main(void) { printf("=== LFTPD INET Tests ===\n"); tc_listen(); tc_accept(); tc_read_line(); tc_write_read(); printf("\n=== LFTPD INET Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; }