Files
STM32F4-Base/test/host/lftpd_inet/lftpd_inet_test.c
T
wandering1 8aaf9bbb0e test(NET/lftpd): lftpd_inet 模块(28 条,127/127 = 100%)
FTP 的 socket 适配层吃满——全仓第一个 **100% 行覆盖**的文件(127/127)。
新增 test/host/lftpd_inet/,28 条断言全部 MSVC 编译通过并 PASS。

钉住的契约:
- 端口字节序:bind 前必须过 net_htons(sin_port 的注释就写着"网络字节序"),问名字
  回来要 ntohs 还原;问不到名字报 -1,不拿脏端口当结果。
- 失败路径一律关 socket:建口失败不 bind;bind 失败关口不 listen;listen 失败关口
  并各留一条 ERROR(漏关就是白占一个号,CH395F 只有 8 个)。
- accept 每轮失败都让出 CPU(同一段里也喂狗),直到成功;**网络代次一变立刻撤掉
  accept**——否则改了 IP 之后监听还挂在旧代次上,端口在新地址上根本不通。这是现场
  最常见的自愈路径,台架上要凑"对面正连着我顺手改 IP"极难。
- 带超时的 accept:监听门撤防时**立即放弃**,不对着死监听耗满窗口;对端一直不开
  数据通道就按 100ms 步进等满再 INFO 收场。
- read_line:CRLF 就地截成结束符(FTP 命令行的边界);跨包分片要能拼出整行;读到 0
  (对端关闭)立刻失败;-2 表示这一片超时继续等;其它负值**原样返回**(错误码不许被
  吞成 -1);等待按 200ms 分片、30 s 总预算到点退出(分片写错就饿死看门狗);缓冲
  填满仍无行尾要失败且**不越界写**(最后一格留给结束符)。
- 短写续传:net_send 可以只写一部分,续传必须从正确偏移接着写(少半条响应就是协议
  错乱);写 0 字节按失败处理;二进制写成功时回请求长度。

桩的口径:private/lftpd_inet.h 与 net_types.h 用真实头;net_socket.h 是桩但函数原型
与真实头逐字一致,并**照做真实头对 FreeRTOS.h / task.h 的 include**(lftpd_inet.c 的
TickType_t / vTaskDelay / pdMS_TO_TICKS 就是从那条链上拿的,桩少这一行就是 C2065)。
lftpd_task_wd_beat 是 __weak 空实现、主机侧替换不掉,因此用同段代码里的"睡 100ms"
次数当喂狗轮数的代理(写进桩注释,不假装能直接数到喂狗)。accept 循环带 100 轮跑飞
保险(net_select 批次被挂死教训换来的)。

覆盖率与台账:套件 25 → 26 全绿;files_tested 29 → 30;未触及 .c 37 → 36;分母
3,945 → 4,072 行;line=3599/4072(**88.4%**,连涨三批)、func=272/299(91.0%)。
恒等式 36 = 2(P1 剩:lftpd.c 与 iec104_serv.c) + 5(P2) + 29(P3) 仍咬合。编号补建
TC-HOST-LFTINET(游标 070),族数 19 → 20。

自查:test-cppcheck Passed;check_ids --strict 通过;doc-links 35 处(= 存量);
9 个新增/改动文件 BOM/CR/U+FFFD/行尾逐项净。零生产代码改动,未跑 @build。
--all-files 仍只有 mem-numbers / mem-budget(.map 台账漂移,等 Lead 合并后统一回填)
与 ui-golden(ref/*.bin 不入库,新 worktree 天生没有黄金参考)三条失败,与本分支无关。
2026-10-23 23:44:52 +08:00

457 lines
16 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称: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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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/等待循环跑飞(>100 轮未退出)\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 建 socket 失败即返回,不再 bind");
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 bind 失败要把 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 listen 失败同样关socket 并留痕");
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 流式监听口,且**端口过了 htons**(sin_port 是网络序)");
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 问不到名字就报 -1(不拿脏端口当结果)");
net_reset(); logs_clear();
g_net.getsock_port_net = net_htons(8888u);
CHECK(lftpd_inet_get_socket_port(3) == 8888,
"TC-LFTINET-006 端口回来要 ntohs 还原成宿主序");
}
/* ===================== 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 一击即中不睡不等");
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 每轮失败都让出一次 CPU(同段代码里也喂狗),直到成功");
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 **代次一变立刻撤掉 accept**(否则新 IP 生效后监听还挂在旧代次)");
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 超时窗内等到数据通道");
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 监听门撤防立即放弃,不对着死监听耗满 5 s");
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 对端一直不开数据通道:按 100ms 步进等满后 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 一次到位:CRLF 就地截成结束符(FTP 命令行的边界)");
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 分两片到达也要拼出整行(跨包命令行)");
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 读到 0(对端关闭)立刻失败,不进死循环");
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 表示这一片超时,继续等而不是当错误");
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 其它负值**原样返回**(错误码不许被吞成 -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 30 s 总预算按 200ms 分片累加,到点退出(饿不到看门狗)");
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 缓冲填满仍无行尾:失败且**不越界写**(留最后一格给结束符)");
}
/* ===================== 写与读 ===================== */
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 一次写完");
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 短写后续传从**正确的偏移**接着写(少半条响应就是协议错乱)");
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 写 0 字节按失败处理(不是当成成功写了 0 字节)");
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 写失败原样回 -1");
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 二进制写也走短写续传,成功时回**请求长度**");
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 读到数据立刻交还");
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 直接失败返回");
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 其它负值继续等,直到 30 s 预算耗尽");
net_reset(); logs_clear();
CHECK((lftpd_inet_close(3) == 0) && (g_net.close_calls == 1),
"TC-LFTINET-070 close 就是转 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;
}