Files
STM32F4-Base/test/host/lftpd_inet/lftpd_inet_test.c
T
wandering1 924ac3d428 test(host): 断言字面量统一纯 ASCII,30 文件 1535 行
用户裁定「统一」。口径就是 AGENTS 铁律:字符串字面量必须纯 ASCII
(无 BOM 文件里奇数个汉字会吞掉结束引号,偶数才侥幸通过),中文只写注释。
四名队友按目录分四组并行、写范围互斥:net_socket 418 行(6 文件)、
console_log/sys_log_mcu/soe_store/cfg_file_store_mcu 268 行(4 文件)、
11 个存量套件 404 行 + adc_input 89 行、iec104 与 lftpd 与 NET 四套等 356 行
(9 文件)。runner 的 .py 文档串不在口径内(Python 源不经 ARMCC 代码页)。

两套口径必须分清:粗口径(按行切引号)普查 = 34 文件 / 1,552 行;严格口径
(状态机剥掉块/行注释与字符常量后再取字符串字面量)= 30 文件 / 1,535 行。
差值那 4 个文件的命中全在注释里(ns_peers.c、net_socket/stub/ns_rtos.h、
common/ch395f_fake.h、common/fatfs_fake.c)——按口径一处都不该改,改了等于
把中文从注释里搬走、正好违背口径。粗口径绝不能当交付判据,否则要么白改
注释、要么留下「这行为什么没改」的悬案。

Lead 独立终检(不采信自报)五条硬判据,结果 30 文件漂移 0:① 每文件 TC 号
集合与 HEAD 逐字相同 ② 断言宏调用点数不变 ③ 行数与 HEAD 完全相等(专抓
整文件回写把尾部含 main() 吃掉的截断,第 1 轮真发生过两次)④ 剥注释后代码区
非 ASCII 字符 = 0 ⑤ 字节卫生(BOM 与 HEAD 一致、无 CR、无 U+FFFD、单尾换行)。
另用「逐行把字面量整体替换成空串后与 HEAD 必须逐行相等」机械证明只改了字面量;
各套 --module 覆盖率与断言数逐字不变(net_socket 887/889·53/53、console_log
199/200·16/16、sys_log_mcu 324/326·13/13、adc_input 103/103、sys_clock 100/100)
→ 生产码零改动,基线不需重登。

闸拦下一处真实回归(我的验收器没抓到):ns_s6_edges.c 翻译时把一个上一轮
已整改成平实描述的编号又写回了 CHECK 标签。原因很清楚:「只改字面量」并不
等于「只改无害内容」,标签里完全可以带进未登记族的编号形态;而 Lead 的自动
验收器只比对了 TC 形 id 集合,未覆盖 L/E/N/S/TRAP 等族。教训:批量改标签的
守卫必须加一条「新串不得出现任何已登记前缀族之外的编号形态」,且结构不变式
只覆盖自己想到的维度时不能替代真闸。
Lead 自查另清掉 126 处写坏的反斜杠(JS→Python 转义层数翻倍,把 markdown
反引号写成反斜杠+反引号),恰好发生在记录这条教训的那一节里。

文档:docs/测试说明/主机单测覆盖率口径.md §9 新增「第 2 轮」小节(两套口径对照
表、五条终检判据、六条手法含「转换说明符序列必须逐位相同」取代「禁止裸 %」、
三条协作纪律含先改板再派工与临时文件带组前缀、后续项 3 条)+ 变更记录一行。

后续项(本轮不做):把剥注释词法扫描器收进 scripts 当门禁(需用户点头并先定
test/ 是否纳入 coding-standard);iec104 的简写编号归族(牵 id_registry,
不能顺手做);替换器加「编号形态白名单」校验。

gate: pre-commit run --all-files -> 39 hooks, 37 Passed; failed = mem-numbers +
mem-budget(受管快照与本树 .map 的漂移,属第 1 轮生产码改动的遗留,非本批
增量)。本批只碰 test/host/**,不进 Keil 编译面也不进模拟器 CMake,**RAM/Flash
增量 = 0**;实测水位 RW_IRAM1 57.4%、RW_CCM 87.9%,均低于 92% 红线。
id-check 0 台账外新账(可判定 84.63% > 基线 82.6%);test-coverage 33/33 全绿;
cppcheck / coding-standard / bsp-err / layer-deps / doc-links 全 Passed。
按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
2026-10-24 21:00:17 +08:00

457 lines
17 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/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;
}