test(NET/lftpd): P1 增补 lftpd_io 模块(33 条),登记 FTP 域一处潜伏缺陷

新增 test/host/lftpd_io/:一个 TU 同时纳入生产源 lftpd_io.c 与 lftpd_string.c
(两者静态符号不冲突,省一套桩),33 条断言全部 MSVC 编译通过并 PASS。

钉住的契约(都是真机上偶发、日志看不出根因的那类):
- 路径规范化:**".." 上跳过头仍被钳在根内**("/a" + "../../x" = "/x")——FTP 客户端
  可发任意路径,这是安全属性;绝对名忽略 base、"." 丢弃、段间多余斜杠折叠、
  NULL 入参容忍、全空结果归 "/"。
- 分配失败:这个函数返回 pvPortMalloc 出来的指针(历史上正是它落在 C 库 malloc 上,
  RAM 方案 §4.2.1)。用**可注入失败的假堆**跑两条分支:首块失败 → NULL;结果缓冲
  失败 → NULL **且第一块已 vPortFree**(不泄漏堆)。模拟器宿主 CRT 永不失败,这条
  只有假堆能构造。
- 卷锁时机:open 成功**故意不释放**(整个传输期独占卷,由 close 释放),失败必须
  释放并留 ERROR;单文件槽守卫 s_file_open 命中时**不碰卷锁也不碰 FatFs**。
- 等卷锁循环每轮喂看门狗(lftpd_task_wd_beat):defaultTask 与 FTP 争卷时这里可能
  空转上百毫秒,漏喂就是整机复位。脚本化 vol_lock_wait 的返回序列直接构造 2 轮空等。
- lftpd_string_trim:行首空白由**返回指针**跳过、原缓冲不搬移(调用点 lftpd.c 用的
  正是返回值);全空白串返回空串。

**本批照出一处,未改语义、只登记**:lftpd_string_trim 的尾部循环写成
for (i = strlen(p); i >= 0 && isspace(p[i]); i--),i 起步那格是 NUL、isspace(0) 为假
→ 循环一次都不执行,**尾部空白与 CRLF 从未被裁掉**。今天不发作是因为 CRLF 在
lftpd_inet_read_line 里已用 strstr 截断、且唯一调用点只关心行首;风险在于"以后有人
拿它去尾部空白"不会生效。修法只要一行(i 从 strlen(p)-1 起),但那会改变 FTP 参数
(含空格的文件名/口令)的可见行为,需要 FTP 域一起过,**留给用户裁定**;当前用
TC-LFTSTR-005 把现状钉成"改了会响"(明确标注是现状桩、不是期望行为)。

未覆盖也写在文件头:lftpd_io_read/write/close/stat/unlink/opendir/readdir/closedir
要先把假 FIL/FILINFO 的字段语义(fptr、obj.objsize、fname 等)做实,否则断言会落在
假件自己写的规则上;这 8 个 f_* 只给了可链接的 FR_INT_ERR 实现,走进即失败不静默。

覆盖率:套件 20 → 21 全绿;files_tested 23 → 25;未触及 .c 43 → 41;分母 3,163 →
3,332 行;lftpd_io.c 85/159(53.5%)、lftpd_string.c 8/10。总体 line_rate 88.9% →
87.2% —— **这是摊薄不是回退**(新进来的文件只测了一半,分母涨得比分子快),已按
§5 口径写进口径文档 §9 的落地进度表;恒等式更新为 41 = 7(P1 剩) + 5(P2) + 29(P3)。

编号:按 B 层口径补建 2 族 TC-HOST-LFTPIO(游标 037)与 TC-HOST-LFTSTR(005),
TC-HOST 族现共 **15** 族;id_registry.json 仍只做增量插入(无重排)。

自查:test-cppcheck Passed;coding-standard Passed;check_ids --strict 通过(无台账外
新账);check_doc_links --all = 35 处(= 存量,本分支贡献 0);新增/改动文件 BOM/CR/
U+FFFD/行尾卫生逐项净。**本增量零生产代码改动**(只加测试、注册表与文档),故未跑
@build。pre-commit --all-files 仍只有三条失败且与本分支无关:mem-numbers / mem-budget
(本 worktree 上次 @build 生成的 .map 与受管快照漂移,增量 ER_IROM1 +12B、RW_IRAM1
+8B、CCM 0,按并行分支约定不在分支里 --write,等 Lead 合并后统一回填)与 ui-golden
(simulator/tests/ref/*.bin 在 .gitignore 内,新 worktree 天生没有黄金参考)。
This commit is contained in:
2026-10-23 15:36:54 +08:00
parent b1291f1308
commit 6bdf063f50
7 changed files with 636 additions and 18 deletions
+427
View File
@@ -0,0 +1,427 @@
/*
* 模块名称:FTP 文件 IO 层(lftpd_io)+ FTP 字符串小工具主机侧单元测试
* 模块功能:在 PC 上离线验证 Drivers/BSP/NET/lftpd/lftpd_io.c 的
* **路径规范化(含越权上跳钳位)**、FatFs 路径映射、单文件槽守卫、
* 卷锁获取/释放时机、等锁期间喂狗,以及 lftpd_string.c 的 trim。
*
* 被测对象:真实源文件 ../../../../Drivers/BSP/NET/lftpd/lftpd_io.c 与
* ../../../../Drivers/BSP/NET/lftpd/lftpd_string.c(#include 原样纳入;
* 两个 .c 的静态符号不冲突,可共用一个 TU,省一套桩)。桩与假件:
* - 真实头:Lib/FatFs/ff.h(FRESULT/FIL/FA_* 一律原汁)、
* App/fs/vol_lock.h、lftpd.h、private/lftpd_io.h、private/lftpd_string.h;
* - 假 FatFs:本文件实现 f_open(记录 path/mode、返回可注入 FRESULT),
* 其余 f_* 未触及的分支本批不测(见下方"未覆盖");
* - 假堆:pvPortMalloc/vPortFree = 静态池 + **可注入分配失败**,
* 用来跑"堆不够 → 返回 NULL"这条真机上 MicroLIB/FreeRTOS 堆耗尽会走、
* 而模拟器(宿主 CRT)永远走不到的分支;
* - 假卷锁:vol_lock_wait 按脚本逐次返回,lftpd_task_wd_beat 计数。
*
* 为什么要主机侧单测(而不是只靠台架):
* 1. **越权钳位是安全属性**:FTP 客户端可以发任意带 ".." 的路径,规范化必须
* "在根上跳不动"("/a" + "../../x" = "/x",而不是逃出根目录)。真机上没有
* 第二个文件系统可逃,测试里却能逐段构造,一次钉死(TC-LFTPIO-010..013)。
* 2. 这个函数返回的是 **pvPortMalloc 出来的指针**——它正是历史上落在 C 库 malloc
* 上的那一个(RAM 方案 §4.2.1:lftpd_io.c 3 处 malloc 已改 pvPortMalloc)。
* 分配失败必须**返回 NULL 且不泄漏第一块**(否则 FTP 一次 LIST 就把堆啃穿)。
* 只有假堆能稳定构造这条路径。
* 3. **单文件槽守卫**(s_file_open)与**卷锁释放时机**:open 成功时故意**不**释放
* 卷锁(整个传输期间独占卷,由 close 释放),失败时必须释放。写反了就是
* "defaultTask 再也拿不到卷"或"传输中途别人能改卷",两者都只在真机偶发。
* 4. 等卷锁的循环里必须喂看门狗(lftpd_task_wd_beat)——defaultTask 与 FTP 争卷时
* 这里可能空转上百毫秒,漏喂就是整机复位(这条在真机上极难稳定复现)。
*
* 覆盖点:
* —— FatFs 路径映射(静态函数直测,TC-LFTPIO-001..004)——
* TC-LFTPIO-001 NULL / 空串 → "0:"(卷根)
* TC-LFTPIO-002 "/" → "0:"
* TC-LFTPIO-003 "/a/b.txt" → "a/b.txt"(去掉前导斜杠)
* TC-LFTPIO-004 非斜杠开头原样返回
* —— 路径规范化(TC-LFTPIO-010..022)——
* TC-LFTPIO-010 相对名拼到 base 下
* TC-LFTPIO-011 绝对名忽略 base
* TC-LFTPIO-012 ".." 回退一级
* TC-LFTPIO-013 **上跳过头也不逃出根**("/a" + "../../x" = "/x")
* TC-LFTPIO-014 "." 段被丢弃
* TC-LFTPIO-015 "base/.." 归一到 "/"
* TC-LFTPIO-016 连续斜杠与尾部斜杠被折叠
* TC-LFTPIO-017 base 为 NULL 视作空串
* TC-LFTPIO-018 name 为 NULL 视作空串
* TC-LFTPIO-019 两者皆 NULL → "/"
* TC-LFTPIO-020 根 + ".." → "/"
* TC-LFTPIO-021 第一块分配失败 → NULL(且不崩、不动第二块)
* TC-LFTPIO-022 第二块(结果缓冲)分配失败 → NULL **且第一块已释放**
* —— 打开文件:守卫、卷锁、错误通道(TC-LFTPIO-030..041)——
* TC-LFTPIO-030 open_read 成功返回 0,f_open 收到 FA_READ 与映射后的路径
* TC-LFTPIO-031 已有文件打开时 open_read 返回 -1 且**不再碰卷锁与 FatFs**
* TC-LFTPIO-032 open_read 失败返回 -1、留 ERROR、并**释放卷锁**
* TC-LFTPIO-033 open_read 成功后卷锁**不释放**(传输期独占,由 close 释放)
* TC-LFTPIO-034 卷未就绪时循环等待,每轮喂一次看门狗
* TC-LFTPIO-035 open_write 用 FA_WRITE|FA_CREATE_ALWAYS(覆盖写语义)
* TC-LFTPIO-036 open_write 同样受单文件槽守卫
* TC-LFTPIO-037 open_write 失败也释放卷锁
* —— trim(TC-LFTSTR-001..005)——
* TC-LFTSTR-001 行首空白由**返回指针**跳过,原缓冲不搬移(调用方用的就是返回值)
* TC-LFTSTR-002 无前导空白时返回同一指针、内容不变
* TC-LFTSTR-003 词中间空白保留
* TC-LFTSTR-004 全空白串 → 空串
* TC-LFTSTR-005 **现状桩**:尾部空白/CRLF 实际不裁(见下"本批照出的问题")
*
* 本批照出的问题(**未改语义**,等裁定):lftpd_string_trim 的尾部循环写成
* for (i = strlen(p); i >= 0 && isspace(p[i]); i--) —— i 起步那格是 '\0',
* isspace(0) 为假,循环一次都不执行,**尾部空白从未被裁掉**。今天不发作是因为
* FTP 命令行的 CRLF 在 lftpd_inet_read_line 里已用 strstr("\r\n") 截断,而唯一
* 调用点 lftpd.c:780 只关心行首。真正的风险是"以后有人拿它去尾部空白"——不会生效。
* 修法是一行(i 从 strlen(p)-1 起),但那会改变 FTP 参数(含空格的文件名/口令)
* 的可见行为,需与 FTP 域一起过;本批只用 TC-LFTSTR-005 把现状钉成"改了会响"。
*
* 未覆盖(诚实登记,下一批补):lftpd_io_read / write / close / stat / unlink /
* opendir / readdir / closedir —— 它们要先把假 FIL/FILINFO 的字段语义(fptr、
* obj.objsize、altname 等)做实,否则断言会落在假件自己写的规则上,失去意义。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-23
*/
#include <stdio.h>
#include <string.h>
#include "ff.h"
#include "private/lftpd_io.h"
#include "private/lftpd_string.h"
#include "vol_lock.h"
#include "lftpd.h"
/* 被测生产源(原样纳入;静态符号不冲突) */
#include "lftpd_io.c"
#include "lftpd_string.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)
/* ===================== dbg_log 桩计数 ===================== */
int g_lfio_info_count;
int g_lfio_error_count;
int g_lfio_debug_count;
static void logs_clear(void)
{
g_lfio_info_count = 0;
g_lfio_error_count = 0;
g_lfio_debug_count = 0;
}
/* ===================== 假堆(可注入分配失败) ===================== */
static uint8_t g_pool[512];
static size_t g_pool_used;
static int g_alloc_calls;
static int g_free_calls;
static int g_fail_at; /* >0:第 N 次 pvPortMalloc 开始失败;0 = 不失败 */
static void heap_reset(void)
{
g_pool_used = 0u;
g_alloc_calls = 0;
g_free_calls = 0;
g_fail_at = 0;
}
void *pvPortMalloc(size_t xWantedSize)
{
void *p;
g_alloc_calls++;
if ((g_fail_at > 0) && (g_alloc_calls >= g_fail_at)) {
return NULL;
}
if ((g_pool_used + xWantedSize + 8u) > sizeof(g_pool)) {
return NULL;
}
/* 8 字节对齐,模拟真实堆 */
g_pool_used = (g_pool_used + 7u) & ~(size_t)7u;
p = &g_pool[g_pool_used];
g_pool_used += xWantedSize;
return p;
}
void vPortFree(void *pv)
{
if (pv != NULL) {
g_free_calls++;
}
}
/* ===================== 假卷锁与喂狗 ===================== */
static int g_wait_seq[8]; /* 每次 vol_lock_wait 的返回值脚本(0=未就绪) */
static int g_wait_calls;
static int g_release_calls;
static int g_beat_calls;
int vol_lock_wait(uint32_t timeout_ms)
{
int r;
(void)timeout_ms;
r = (g_wait_calls < (int)(sizeof(g_wait_seq) / sizeof(g_wait_seq[0])))
? g_wait_seq[g_wait_calls] : 1;
g_wait_calls++;
return r;
}
void vol_lock_release(void)
{
g_release_calls++;
}
void lftpd_task_wd_beat(void)
{
g_beat_calls++;
}
int vol_lock_init(void) { return 0; }
/* ===================== 假 FatFs(只到本批用到的 f_open) ===================== */
static FRESULT g_open_rc;
static char g_open_path[64];
static BYTE g_open_mode;
static int g_open_calls;
FRESULT f_open(FIL *fp, const TCHAR *path, BYTE mode)
{
int i;
(void)fp;
g_open_calls++;
g_open_mode = mode;
for (i = 0; (path[i] != _T('\0')) && (i + 1 < (int)sizeof(g_open_path)); i++) {
g_open_path[i] = (char)path[i];
}
g_open_path[i] = '\0';
return g_open_rc;
}
/* 本批不测的 FatFs 入口:只为**可链接**而给出,一律返回 FR_INT_ERR——
* 真有测试走进来就会拿到 -1/-2 之类的意外返回值而断言失败,不会静默假绿。
* (下一批要把假 FIL/FILINFO 的字段语义做实再逐个展开) */
FRESULT f_close(FIL *fp) { (void)fp; return FR_INT_ERR; }
FRESULT f_read(FIL *fp, void *p_buff, UINT btr, UINT *br)
{
(void)fp; (void)p_buff; (void)btr;
if (br != NULL) { *br = 0u; }
return FR_INT_ERR;
}
FRESULT f_write(FIL *fp, const void *p_buff, UINT btw, UINT *bw)
{
(void)fp; (void)p_buff; (void)btw;
if (bw != NULL) { *bw = 0u; }
return FR_INT_ERR;
}
FRESULT f_stat(FILINFO *fno, const TCHAR *path) { (void)fno; (void)path; return FR_INT_ERR; }
FRESULT f_unlink(const TCHAR *path) { (void)path; return FR_INT_ERR; }
FRESULT f_opendir(DIR *dp, const TCHAR *path) { (void)dp; (void)path; return FR_INT_ERR; }
FRESULT f_readdir(DIR *dp, FILINFO *fno) { (void)dp; (void)fno; return FR_INT_ERR; }
FRESULT f_closedir(DIR *dp) { (void)dp; return FR_INT_ERR; }
/* ===================== 复位与辅助 ===================== */
static void reset_all(void)
{
heap_reset();
g_wait_calls = 0;
g_release_calls = 0;
g_beat_calls = 0;
g_open_calls = 0;
g_open_rc = FR_OK;
g_open_path[0] = '\0';
g_open_mode = 0;
g_wait_seq[0] = 1;
g_wait_seq[1] = 1;
g_wait_seq[2] = 1;
g_wait_seq[3] = 1;
s_file_open = 0;
logs_clear();
}
static char *canon(const char *base, const char *name)
{
char *p = lftpd_io_canonicalize_path(base, name);
static char keep[128];
if (p == NULL) {
return NULL;
}
(void)strncpy(keep, p, sizeof(keep) - 1u);
keep[sizeof(keep) - 1u] = '\0';
vPortFree(p);
return keep;
}
/* ===================== FatFs 路径映射 ===================== */
static void tc_fatfs_path(void)
{
printf("-- TC-LFTPIO-001..004 to_fatfs_path --\n");
CHECK((strcmp(to_fatfs_path(NULL), "0:") == 0) &&
(strcmp(to_fatfs_path(""), "0:") == 0), "TC-LFTPIO-001 NULL/空串 -> 0:");
CHECK(strcmp(to_fatfs_path("/"), "0:") == 0, "TC-LFTPIO-002 根 -> 0:");
CHECK(strcmp(to_fatfs_path("/a/b.txt"), "a/b.txt") == 0,
"TC-LFTPIO-003 去掉前导斜杠");
CHECK(strcmp(to_fatfs_path("a.txt"), "a.txt") == 0, "TC-LFTPIO-004 原样返回");
}
/* ===================== 路径规范化 ===================== */
static void tc_canon(void)
{
printf("-- TC-LFTPIO-010..022 canonicalize --\n");
reset_all();
CHECK(strcmp(canon("/dir", "f.txt"), "/dir/f.txt") == 0, "TC-LFTPIO-010 相对拼接");
CHECK(strcmp(canon("/dir", "/abs.txt"), "/abs.txt") == 0, "TC-LFTPIO-011 绝对忽略 base");
CHECK(strcmp(canon("/a/b", "../c"), "/a/c") == 0, "TC-LFTPIO-012 上跳一级");
CHECK(strcmp(canon("/a", "../../x"), "/x") == 0,
"TC-LFTPIO-013 上跳过头仍被钳在根内(安全属性)");
CHECK(strcmp(canon("/a", "."), "/a") == 0, "TC-LFTPIO-014 丢弃 . 段");
CHECK(strcmp(canon("/a", ".."), "/") == 0, "TC-LFTPIO-015 回退到根");
CHECK(strcmp(canon("/a//", "b"), "/a/b") == 0, "TC-LFTPIO-016 段间多余斜杠被折叠");
CHECK(strcmp(canon("/dir", "//x"), "/x") == 0,
"TC-LFTPIO-016b name 以斜杠开头按**绝对路径**处理(忽略 base,仍钳在根内)");
CHECK(strcmp(canon(NULL, "x"), "/x") == 0, "TC-LFTPIO-017 base NULL 视作空串");
CHECK(strcmp(canon("/a", NULL), "/a") == 0, "TC-LFTPIO-018 name NULL 视作空串");
CHECK(strcmp(canon(NULL, NULL), "/") == 0, "TC-LFTPIO-019 两个 NULL -> /");
CHECK(strcmp(canon("/", ".."), "/") == 0, "TC-LFTPIO-020 根上加 .. 仍是根");
reset_all();
g_fail_at = 1;
CHECK(lftpd_io_canonicalize_path("/a", "b") == NULL,
"TC-LFTPIO-021 首块分配失败返回 NULL");
CHECK(g_alloc_calls == 1, "TC-LFTPIO-021b 失败后不再分配第二块");
reset_all();
g_fail_at = 2;
CHECK(lftpd_io_canonicalize_path("/a", "b") == NULL,
"TC-LFTPIO-022 结果缓冲分配失败返回 NULL");
CHECK(g_free_calls == 1, "TC-LFTPIO-022b 第一块已释放(不泄漏堆)");
}
/* ===================== 打开文件:守卫 / 卷锁 / 错误通道 ===================== */
static void tc_open(void)
{
int rc;
printf("-- TC-LFTPIO-030..037 open_read / open_write --\n");
reset_all();
rc = lftpd_io_open_read("/a/b.txt");
CHECK((rc == 0) && (g_open_mode == FA_READ) &&
(strcmp(g_open_path, "a/b.txt") == 0),
"TC-LFTPIO-030 成功:FA_READ + 映射后的路径");
reset_all();
s_file_open = 1;
rc = lftpd_io_open_read("/x");
CHECK((rc == -1) && (g_open_calls == 0) && (g_wait_calls == 0),
"TC-LFTPIO-031 单文件槽守卫:已打开时不碰卷锁与 FatFs");
reset_all();
g_open_rc = FR_NO_FILE;
rc = lftpd_io_open_read("/x");
CHECK((rc == -1) && (g_release_calls == 1) && (g_lfio_error_count == 1),
"TC-LFTPIO-032 打开失败:返回 -1 + 留痕 + 释放卷锁");
reset_all();
rc = lftpd_io_open_read("/x");
CHECK((rc == 0) && (g_release_calls == 0) && (s_file_open == 1),
"TC-LFTPIO-033 成功后卷锁不释放(传输期独占)");
reset_all();
g_wait_seq[0] = 0; /* 第一轮:卷被 defaultTask 占着 */
g_wait_seq[1] = 0; /* 第二轮:仍未就绪 */
g_wait_seq[2] = 1; /* 第三轮:拿到 */
rc = lftpd_io_open_read("/x");
CHECK((rc == 0) && (g_beat_calls == 2) && (g_wait_calls == 3),
"TC-LFTPIO-034 等锁循环每轮喂看门狗(漏喂 = 整机复位)");
reset_all();
rc = lftpd_io_open_write("/up.bin");
CHECK((rc == 0) && (g_open_mode == (BYTE)(FA_WRITE | FA_CREATE_ALWAYS)),
"TC-LFTPIO-035 open_write 用覆盖写模式");
reset_all();
s_file_open = 1;
rc = lftpd_io_open_write("/up.bin");
CHECK((rc == -1) && (g_open_calls == 0), "TC-LFTPIO-036 open_write 同样有槽守卫");
reset_all();
g_open_rc = FR_DISK_ERR;
rc = lftpd_io_open_write("/up.bin");
CHECK((rc == -1) && (g_release_calls == 1), "TC-LFTPIO-037 open_write 失败也释放卷锁");
}
/* ===================== trim ===================== */
static void tc_trim(void)
{
char buf[32];
char *r;
printf("-- TC-LFTSTR-001..005 lftpd_string_trim --\n");
/* 实现是"返回一个跳过行首空白的指针",**不搬移缓冲**;调用方 lftpd.c:780
* 用的正是返回值。所以断言必须落在返回值上,落在 buf 上是错的。 */
(void)strcpy(buf, " PASS x");
r = lftpd_string_trim(buf);
CHECK((r != buf) && (strcmp(r, "PASS x") == 0) && (buf[0] == ' '),
"TC-LFTSTR-001 行首空白靠返回指针跳过,原缓冲不搬移");
(void)strcpy(buf, "NOOP");
r = lftpd_string_trim(buf);
CHECK((r == buf) && (strcmp(buf, "NOOP") == 0), "TC-LFTSTR-002 无前导空白返回同一指针");
(void)strcpy(buf, " USER admin ");
r = lftpd_string_trim(buf);
CHECK(strcmp(r, "USER admin ") == 0, "TC-LFTSTR-003 词中间空白保留");
(void)strcpy(buf, " \t\r\n");
r = lftpd_string_trim(buf);
CHECK(r[0] == '\0', "TC-LFTSTR-004 全空白 -> 空串");
/* 现状桩(不是期望行为):尾部循环从 strlen(p) 起步,那里是 '\0',
* isspace(0)=0 立刻退出 —— **尾部空白/CRLF 实际从未被裁**。今天无害是因为
* CRLF 在 lftpd_inet_read_line(lftpd_inet.c 的 strstr("\r\n"))里已截掉。
* 谁哪天把 trim 修对了,这条会红——那是提醒,不是回归。 */
(void)strcpy(buf, "LIST ");
r = lftpd_string_trim(buf);
CHECK(strcmp(r, "LIST ") == 0,
"TC-LFTSTR-005 现状:尾部空白不裁(潜伏缺陷已登记,修好它会让本条响)");
}
/* ===================== 入口 ===================== */
int main(void)
{
printf("=== LFTPD IO Tests ===\n");
tc_fatfs_path();
tc_canon();
tc_open();
tc_trim();
printf("\n=== LFTPD IO Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""FTP 文件 IO 层(lftpd_io + lftpd_string)的主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/lftpd_io/lftpd_io_test.c(同时纳入生产源
Drivers/BSP/NET/lftpd/lftpd_io.c 与 lftpd_string.c),在 PC 上离线验证路径规范化
(含 ".." 越权钳位)、FatFs 路径映射、单文件槽守卫、卷锁获取/释放时机、等锁期间
喂狗、分配失败返回 NULL 且不泄漏,以及 FTP 命令行 trim。失败即非 0 退出。
工具链与判定委托 test/host/common/unit_test_harness.py。
为什么要主机侧单测(而不是只用台架):
越权钳位是安全属性(FTP 客户端可发任意带 ".." 的路径),真机上没有第二个文件系统
可逃、测不出来;卷锁"成功不释放、失败必释放"写反了就是 defaultTask 永远拿不到卷
或传输中途卷被人改;等锁循环漏喂狗就是整机复位——三条都只在真机偶发、日志看不出
根因。分配失败分支更只有假堆能构造(模拟器宿主 CRT 永不失败)。
桩的边界:ff.h / vol_lock.h / lftpd.h / private/*.h 全用**真实头**(FRESULT、FA_*、
函数签名一律原汁);FreeRTOS.h 与 dbg_log.h 用本目录桩;f_open、卷锁、喂狗、
pvPortMalloc/vPortFree 由测试单元给可控实现(假堆可注入分配失败)。
read/write/close/stat/unlink/readdir 未列入本批:要先把假 FIL/FILINFO 的字段语义
做实,否则断言落在假件自己写的规则上(详见测试单元文件头"未覆盖")。
用法(仓库根目录):
python test/host/lftpd_io/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误。
设计依据:docs/内存管理/RAM使用分析与优化方案.md §4.2.1、docs/测试说明/主机单测覆盖率口径.md §9 P1
创建日期: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
# 顺序敏感:stub 必须最先(覆盖 FreeRTOS.h / dbg_log.h)。
# lftpd_io.c 里 include 的是 "private/lftpd_io.h" 与 "lftpd.h",故给出 lftpd 目录;
# vol_lock.h 在 App/fs,ff.h 在 Lib/FatFs(与 test/host/nand_port 同一份真实头)。
BUILD = TestBuild(
name="LFTPD IO Tests",
src=os.path.join(HERE, "lftpd_io_test.c"),
include_dirs=[
os.path.join(HERE, "stub"),
os.path.join(ROOT, "Drivers", "BSP", "NET", "lftpd"),
os.path.join(ROOT, "App", "fs"),
os.path.join(ROOT, "Lib", "FatFs"),
HERE,
],
# /D_CRT_SECURE_NO_WARNINGS:生产 lftpd_io.c 里的 strcpy/strcat/strncpy 在
# MSVC 下刷 C4996 弃用告警(ARMCC 不报,且 lftpd 不在 coding-standard 范围内),
# 这是纯主机侧噪音;关掉它才能让本模块的编译输出保持可读。
extra_flags=["/D_CRT_SECURE_NO_WARNINGS"],
summary_marker="=== LFTPD IO Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+23
View File
@@ -0,0 +1,23 @@
/*
* 模块名称:主机侧单测桩 - FreeRTOS.h(lftpd_io 模块专用)
* 模块功能:lftpd_io.c 只经这个头拿到 pvPortMalloc / vPortFree 的原型(它自己不
* 调内核 API,等卷锁是 vol_lock 的事)。真实 FreeRTOS.h 会拉整棵
* portmacro / 编译器抽象层,PC 上编不动,故这里只给这两个原型。
*
* ⚠ 仅供 test/host/lftpd_io/ 使用,不参与 MCU 构建。
* 签名必须与 FreeRTOS 的 portmacro 一致(size_t 入参 / void 返回的 free);
* 真实实现由测试单元给出**可注入失败的假堆**,用来跑"分配失败返回 NULL"的分支
* ——那正是真机上 MicroLIB 512B C 堆与 FreeRTOS 堆耗尽时的形态。
*
* 创建日期:2026-10-23
*/
#ifndef FREERTOS_H
#define FREERTOS_H
#include <stddef.h>
void *pvPortMalloc(size_t xWantedSize);
void vPortFree(void *pv);
#endif /* FREERTOS_H */
+19
View File
@@ -0,0 +1,19 @@
/*
* 模块名称:主机侧单测桩 - dbg_log.h(lftpd_io 模块专用)
* 模块功能:DBG_* 换成计数,让"打开失败必须留痕、等锁循环不打日志"可断言。
* ⚠ 仅供 test/host/lftpd_io/ 使用,不参与 MCU 构建。
* 创建日期:2026-10-23
*/
#ifndef DBG_LOG_H
#define DBG_LOG_H
extern int g_lfio_info_count;
extern int g_lfio_error_count;
extern int g_lfio_debug_count;
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_lfio_info_count++; } while (0)
#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_lfio_debug_count++; } while (0)
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_lfio_error_count++; } while (0)
#endif /* DBG_LOG_H */