test(host): P2 覆盖率批次,5 套主机单测与共享假 FatFs
P2 五个文件全部进分母(三名队友并行,写范围互斥、零越界):
net_socket 420 条 / 881-883=99.8%、52-52 函数(S1~S6 分片)
sys_log_mcu 75 条 / 310-313=99.0%、12-12
console_log 104 条 / 188-189=99.5%、15-15
cfg_file_store_mcu 34 条 / 97-97=100%、10-10
soe_store 26 条 / 78-79=98.7%、4-4
合计 659 条断言,P2 本体 1554-1561=99.6%、93-93 函数。
账:套件 28→33 全绿;files_tested 32→37;未触及 112→107(.c 34→29,
恒等式 29 = 0(P1剩) + 0(P2) + 29(P3) 咬合,§9 终点判据达成);分母
4684→6245 插桩行;line 85.8%→89.3%;func 90.1%→92.2%(这批 5 个文件
全都以 ≥98.7% 进分母,故未摊薄)。--update-baseline 前后逐文件比对:
下降 0 个文件。
基建:新建 test/host/common/fatfs_fake.{h,c}(以真实 ff.h 为声明源、只
定义三文件实际调用的 9 个 f_*,缺哪个链接期响;卷忙/短写/FR_DENIED
可注入;短名违规分档);扩展 ch395f_fake 为纯新增(g_fx/g_fxchip 另开
一份世界 + ext_reset 挂进既有复位,既有三套面一字不动),并按真头修正
两个错值常量(SOCKET_OPEN 0x01→0x05、TCP_ESTABLISHED 0x05→0x04),
另在 net_socket runner 加真头/假件同名常量文本核对闸(负例验证过)。
存量回归:net_listener 34、net_events 34、net_pending 43、net_select 22、
nand_port 149、lftpd_io 33、vol_lock 33、soe 32、cfg_file 151 全绿。
编号:补 TC-HOST-{NETSOCK,SYSLOG,CFGFST,SOESTORE,CONLOG} 五族(22→27,
先补族再引号);net_socket 模块码由自取的两字母 NS 正名为 NETSOCK;
id-check 增量拦截无台账外新账,可判定比例 83.27%(基线 82.6%)。
文档:覆盖率口径 §9 新增「P2 落地」(含两套分母口径差、10 条现状表、
12 条可复用手法与并发红线),§8 第 1 条改「清单已吃穿」,补变更记录。
本提交未改任何生产代码(App/Drivers/Src/Lib/MDK-ARM 零触碰)。
10 条现状已逐条登记在 §9(含 sys_log_mcu.c:509~537 卷忙整批丢日志、
net_socket.c 同文件两套 IP 字节序、UDP 两 API 绕过 netTask),已获指示
全部修复,另开一轮(含测试字面量统一纯 ASCII)。
gate: pre-commit run --all-files → 39 hooks, 38 Passed; only mem-numbers
Failed(本工作树从未 @build,无 .map 事实源,属分支期已知陷阱);
mem-budget Passed;test-coverage 与 id-check Passed。RAM/Flash 增量 = 0
(纯测试批次)。按仓内分支约定走 --no-verify,台账回填留待主树合并后。
This commit is contained in:
@@ -0,0 +1,666 @@
|
||||
/*
|
||||
* 模块名称:配置存储后端(cfg_file_store_mcu)主机侧单元测试(L0.5)
|
||||
* 模块功能:在 PC 上离线验证 App/cfg/cfg_file_store_mcu.c 的五个卷操作 + 三个堆操作:
|
||||
* cfg_store_read / cfg_store_write_atomic / cfg_store_stat /
|
||||
* cfg_store_remove / 两个路径 getter / alloc-free-heap_free
|
||||
* 重心放在**原子写**与它的 v1.2 修复(E-01)上:本 FatFs 移植的 f_rename
|
||||
* 对已存在目标返回 FR_EXIST、**不覆盖**,所以"覆盖写"必须先 f_unlink 目标
|
||||
* 再 f_rename——少了这一步,一切非首启保存(UI 改配置、模板重建)静默失败,
|
||||
* 而模拟器后端(POSIX rename 覆盖语义)照不出来。
|
||||
*
|
||||
* 被测对象:真实源文件 App/cfg/cfg_file_store_mcu.c(#include 原样纳入)。
|
||||
* 假件与桩:
|
||||
* test/host/common/fatfs_fake.{h,c} —— 假 FatFs 内存卷(真实 ff.h 声明;
|
||||
* 它的 f_rename **如实返回 FR_EXIST**,正是 E-01 的那颗雷)
|
||||
* stub/FreeRTOS.h —— 三个分配器原型(签名逐条对齐 portable.h:163/164/166)
|
||||
* 假卷锁 / 假堆(可注入分配失败)写在本文件里
|
||||
*
|
||||
* 为什么要主机侧单测(而不是只靠台架):
|
||||
* 1. **E-01 原子写**:unlink→rename 这个非原子窗口是"掉电时配置暂失、开机按首启
|
||||
* 重建工厂模板"的有意取舍。任何一次"顺手优化"(比如给 rename 加覆盖、或把
|
||||
* unlink 挪到 rename 之后)都会让配置保存静默失败或留下脏 TMP,而这在 POSIX
|
||||
* 后端上完全看不出来。→ 假卷把 FR_EXIST 原样喂进来。
|
||||
* 2. **失败不留半写**:TMP 写失败/短写必须 unlink TMP 且**不碰目标**——"目标文件
|
||||
* 保持不动"是函数头承诺,也是现场"改坏配置还能开机"的唯一保证。
|
||||
* 3. **卷锁 try 语义**:v1.3 把四个后端函数全部纳入 vol_lock_wait(0);写反了就是
|
||||
* "defaultTask 与 ftpTask 并发写 FTL → dhara journal 竞争"(实证过)或者
|
||||
* "UI 保存时死等卷 → 喂狗点阻塞 → 整机复位"。四个函数的 try/release 配对
|
||||
* 只能靠计数断言。
|
||||
* 4. **mtime 令牌的分钟粒度盲区**:cfg_file 的 poll 靠"令牌不等 = 可能变更",
|
||||
* 同一分钟内同长度重写令牌不变 → 变更检不出来。这是设计接受的限制,但必须
|
||||
* 钉在测试里,否则后人会以为令牌是内容哈希。
|
||||
* 5. **MicroLIB 512B C 堆**:cfg_store_alloc 是共享核心唯一的取内存入口
|
||||
* (优化调研 P0 第 1 条),分配失败必须如实返回 NULL;宿主 CRT 堆永远不会失败 → 只有假堆能测。
|
||||
*
|
||||
* 覆盖点(TC 号族 = CFGFST(三位序号)):
|
||||
* —— 读 ——
|
||||
* TC-CFGFST-001 读成功:内容与长度正确,打开模式恒为 FA_READ(不含写位)
|
||||
* TC-CFGFST-002 p_buf / p_nread 空指针 -> -1 且**不取卷锁**
|
||||
* TC-CFGFST-003 卷忙 -> -1、零 FatFs、不 release(try 失败没有锁可放)
|
||||
* TC-CFGFST-004 文件缺失 -> -1 **且已 release**
|
||||
* TC-CFGFST-005 cap 大于文件 -> br = 实际大小(部分读不是错误)
|
||||
* TC-CFGFST-006 f_read 失败 -> -1 + release + close 已配对
|
||||
* TC-CFGFST-007 短读(介质只回一部分)-> 返回 0 + nread 如实变小(现状钉住)
|
||||
* —— 原子写 ——
|
||||
* TC-CFGFST-010 首启写:TMP 建 -> 写 -> unlink(目标,FR_NO_FILE 容忍) -> rename
|
||||
* TC-CFGFST-011 **覆盖写必须先 unlink 目标**:假卷 rename 对已存在目标返 FR_EXIST,
|
||||
* 少了 unlink 就是 -1 且旧内容原封不动(E-01 的直接复现)
|
||||
* TC-CFGFST-012 rename 失败 -> -1 + TMP 清理 + 目标内容保持不动
|
||||
* TC-CFGFST-013 TMP 打开失败 -> -1、零 write、release
|
||||
* TC-CFGFST-014 写短(FR_OK + 字节数不足)-> -1 + TMP 被删 + 目标原内容不动
|
||||
* TC-CFGFST-015 len=0 -> 成功(清空配置是合法语义),目标变 0 字节
|
||||
* TC-CFGFST-016 卷忙 -> -1 且整条链零 FatFs
|
||||
* TC-CFGFST-017 p_buf NULL -> -1 且不取锁
|
||||
* TC-CFGFST-017b p_path NULL -> 由 s_build_tmp_path 的空指针守卫挡住
|
||||
* TC-CFGFST-018 临时名由目标推导:/config.ini -> /config.TMP;/cfg/a.ini ->
|
||||
* /cfg/config.TMP(同目录,避免跨目录改名)
|
||||
* TC-CFGFST-019 目标目录过长 -> s_build_tmp_path 返回 -1(守卫在取锁之前)
|
||||
* TC-CFGFST-020 TMP 的打开模式必须含 FA_WRITE(|FA_CREATE_ALWAYS)——陷阱一在本
|
||||
* 模块同样成立:只给 FA_CREATE_ALWAYS 的句柄 f_write 恒 FR_DENIED
|
||||
* —— 状态与删除 ——
|
||||
* TC-CFGFST-030 stat:size + mtime = (fdate<<16)|ftime,两次写之间令牌必变
|
||||
* TC-CFGFST-031 盲区:同一 dostime、同长度重写 -> 令牌不变(分钟粒度,非内容哈希)
|
||||
* TC-CFGFST-032 stat:文件缺失 / p_st NULL / 卷忙 三种 -1 与 release 配对
|
||||
* TC-CFGFST-033 remove:成功;缺失与卷忙的 -1 与 release 配对
|
||||
* —— 路径与 8.3 ——
|
||||
* TC-CFGFST-040 config_path / tmp_path 返回值在严格 8.3 卷上真的打得开
|
||||
* TC-CFGFST-041 短名违规分档:体>8 / 扩展>3 / 非法字符 / 点开头 -> FR_INVALID_NAME;
|
||||
* 含 ':' -> FR_INVALID_DRIVE(假件保真,也是"改名=静默失败"的防线)
|
||||
* TC-CFGFST-042 陷阱二:非法名只让本次操作失败,**不触发任何卷级动作**
|
||||
* (条目总数不变、目录不没、不留 TMP)
|
||||
* —— 堆 ——
|
||||
* TC-CFGFST-050 alloc/free 走 FreeRTOS 堆:字节数增减可观测、成对归还
|
||||
* TC-CFGFST-051 分配失败注入 -> cfg_store_alloc 返回 NULL(MicroLIB 512B 的形态)
|
||||
* TC-CFGFST-052 cfg_store_free(NULL) 安全(不计数、不崩)
|
||||
* TC-CFGFST-053 heap_free 原样转发 xPortGetFreeHeapSize
|
||||
*
|
||||
* 本套照出的现状(只登记、不改生产代码;进交 Lead 的报告):
|
||||
* cfg_file_store_mcu.c:50 的函数头写"卷已挂载(未挂载时 f_open 返回
|
||||
* FR_NO_SYSTEM,转 -1)"——**FRESULT 里根本没有 FR_NO_SYSTEM 这个码**
|
||||
* (Lib/FatFs/ff.h 的枚举是 FR_OK..FR_INVALID_PARAMETER 共 20 个,未挂载卷
|
||||
* 在本移植里实际返回 FR_NOT_READY / FR_NO_FILESYSTEM)。这条只写在注释里、
|
||||
* 不参与编译,所以一直没暴露;危害是后人照它写判据时会写出一个永不成立的
|
||||
* 分支(本模块实际也没写:它把所有非 FR_OK 一律转 -1,因此运行结果正确)。
|
||||
*
|
||||
* 故意没测(不留"看起来全覆盖"的错觉):
|
||||
* - **真掉电**:unlink 与 rename 之间断电图象只能靠"目标缺失 -> 上层按首启重建"
|
||||
* 的既有单测 + 真机验证;本套只钉"窗口存在"(TC-CFGFST-011 里 unlink 恰好 1 次)与
|
||||
* "失败路径不碰目标"。
|
||||
* - 目录不存在时的 f_open 行为差异(FR_NO_PATH)在 cfg 侧只体现在 -1,与缺失同
|
||||
* 出口,已并入 TC-CFGFST-004 的语义,不再单独造一条重复断言。
|
||||
* - 卷未挂载(真实 ff.c 的 FR_NOT_READY / FR_NO_FILESYSTEM):假卷不提供 f_mount
|
||||
* 概念——这三个模块的代码里也确实一次都没调 f_mount(挂载属 defaultTask),
|
||||
* 留给 default_task 那一层的集成验证。
|
||||
* - cfg_file.c 共享核心(928 行)不在本批:它有自己的存量单测口径。
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
*
|
||||
* 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2)
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ff.h"
|
||||
#include "fatfs_fake.h"
|
||||
#include "FreeRTOS.h" /* 桩:三个分配器原型 */
|
||||
#include "cfg_file_store.h" /* 真实头 */
|
||||
#include "vol_lock.h" /* 真实头 */
|
||||
|
||||
/* ===================== 断言框架 ===================== */
|
||||
|
||||
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)
|
||||
|
||||
#define P_CFG "/config.ini"
|
||||
#define P_TMP "/config.TMP"
|
||||
|
||||
/* ===================== 假件与生产源码纳入 ===================== */
|
||||
|
||||
#include "fatfs_fake.c"
|
||||
#include "cfg_file_store_mcu.c"
|
||||
|
||||
/* ===================== 假卷锁 ===================== */
|
||||
|
||||
static int s_lock_busy_left;
|
||||
static int s_lock_waits;
|
||||
static int s_lock_releases;
|
||||
static int s_lock_held;
|
||||
static int s_lock_seq_err;
|
||||
|
||||
static void locks_clear(void)
|
||||
{
|
||||
s_lock_busy_left = 0;
|
||||
s_lock_waits = 0;
|
||||
s_lock_releases = 0;
|
||||
s_lock_held = 0;
|
||||
s_lock_seq_err = 0;
|
||||
}
|
||||
|
||||
static void lock_busy_next(int n) { s_lock_busy_left = n; }
|
||||
|
||||
int vol_lock_init(void) { return 0; }
|
||||
|
||||
int vol_lock_wait(uint32_t timeout_ms)
|
||||
{
|
||||
int r;
|
||||
|
||||
(void)timeout_ms;
|
||||
if (s_lock_waits > 20000) {
|
||||
(void)fprintf(stderr, "[FATAL] vol_lock_wait guard tripped\n");
|
||||
exit(2);
|
||||
}
|
||||
r = (s_lock_busy_left > 0) ? 0 : 1;
|
||||
if (s_lock_busy_left > 0) {
|
||||
s_lock_busy_left--;
|
||||
}
|
||||
s_lock_waits++;
|
||||
if (r != 0) {
|
||||
s_lock_held = 1;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
void vol_lock_release(void)
|
||||
{
|
||||
s_lock_releases++;
|
||||
if (s_lock_held == 0) {
|
||||
s_lock_seq_err++; /* try 失败却 release = 释放了别人的锁 */
|
||||
}
|
||||
s_lock_held = 0;
|
||||
}
|
||||
|
||||
/* ===================== 假 FreeRTOS 堆(可注入失败) ===================== */
|
||||
|
||||
#define FAKE_HEAP_CAP 4096u
|
||||
|
||||
static uint8_t s_heap_pool[FAKE_HEAP_CAP];
|
||||
static uint32_t s_heap_used;
|
||||
static int s_heap_fail; /* 1 = 下次 pvPortMalloc 直接返回 NULL */
|
||||
static int s_heap_alloc_calls;
|
||||
static int s_heap_free_calls;
|
||||
static int s_heap_free_err; /* free 了指针之外的地址 = 分配器被写脏 */
|
||||
|
||||
static void heap_clear(void)
|
||||
{
|
||||
s_heap_used = 0u;
|
||||
s_heap_fail = 0;
|
||||
s_heap_alloc_calls = 0;
|
||||
s_heap_free_calls = 0;
|
||||
s_heap_free_err = 0;
|
||||
}
|
||||
|
||||
void *pvPortMalloc(size_t xWantedSize)
|
||||
{
|
||||
void *p;
|
||||
|
||||
if (s_heap_alloc_calls++ > 20000) {
|
||||
(void)fprintf(stderr, "[FATAL] pvPortMalloc guard tripped\n");
|
||||
exit(2);
|
||||
}
|
||||
if ((s_heap_fail != 0) || (xWantedSize == 0u)
|
||||
|| ((s_heap_used + (uint32_t)xWantedSize) > FAKE_HEAP_CAP)) {
|
||||
return NULL;
|
||||
}
|
||||
p = (void *)(&s_heap_pool[s_heap_used]);
|
||||
s_heap_used += (uint32_t)xWantedSize;
|
||||
return p;
|
||||
}
|
||||
|
||||
void vPortFree(void *pv)
|
||||
{
|
||||
if (s_heap_free_calls++ > 20000) {
|
||||
(void)fprintf(stderr, "[FATAL] vPortFree guard tripped\n");
|
||||
exit(2);
|
||||
}
|
||||
if (pv == NULL) {
|
||||
return;
|
||||
}
|
||||
if (((uint8_t *)pv < &s_heap_pool[0])
|
||||
|| ((uint8_t *)pv >= &s_heap_pool[FAKE_HEAP_CAP])) {
|
||||
s_heap_free_err++; /* 释放栈地址/野指针 */
|
||||
return;
|
||||
}
|
||||
/* 只支持栈式使用(本模块的实际用法:借一块、用完还) */
|
||||
s_heap_used = (uint32_t)((uint8_t *)pv - &s_heap_pool[0]);
|
||||
}
|
||||
|
||||
size_t xPortGetFreeHeapSize(void)
|
||||
{
|
||||
return (size_t)(FAKE_HEAP_CAP - s_heap_used);
|
||||
}
|
||||
|
||||
/* ===================== 复位 ===================== */
|
||||
|
||||
static void reset_all(void)
|
||||
{
|
||||
ff_fake_reset();
|
||||
locks_clear();
|
||||
heap_clear();
|
||||
ff_fake_set_dostime(0x5A21u, 0x6E40u);
|
||||
}
|
||||
|
||||
/* ===================== 读 ===================== */
|
||||
|
||||
static void tc_read(void)
|
||||
{
|
||||
uint8_t buf[64];
|
||||
size_t n = 999u;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-CFGFST-001..007 读 --\n");
|
||||
|
||||
/* 001 成功 + 只读模式 */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"net ip=192.168.1.100\n", 20u);
|
||||
ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 20u)
|
||||
&& (memcmp(buf, "net ip=192.168.1.100\n", 20u) == 0)
|
||||
&& (g_ff_obs.last_open_mode == (BYTE)FA_READ)
|
||||
&& (s_lock_waits == 1) && (s_lock_releases == 1) && (s_lock_seq_err == 0);
|
||||
CHECK(ok, "TC-CFGFST-001 读成功:内容与长度正确,打开模式就是 FA_READ(不带写位)");
|
||||
|
||||
/* 002 空指针守卫在取锁之前 */
|
||||
reset_all();
|
||||
ok = (cfg_store_read(P_CFG, NULL, sizeof(buf), &n) == -1)
|
||||
&& (cfg_store_read(P_CFG, buf, sizeof(buf), NULL) == -1);
|
||||
CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
|
||||
"TC-CFGFST-002 空指针 -> -1 且不取卷锁(守卫顺序正确)");
|
||||
|
||||
/* 003 卷忙 */
|
||||
reset_all();
|
||||
lock_busy_next(1);
|
||||
ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == -1);
|
||||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_lock_releases == 0)
|
||||
&& (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-003 卷忙 -> -1:零 FatFs、不 release(没拿到锁不放锁)");
|
||||
|
||||
/* 004 文件缺失:必须已放锁 */
|
||||
reset_all();
|
||||
ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == -1);
|
||||
CHECK(ok && (s_lock_releases == 1) && (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-004 文件缺失 -> -1 **且已释放卷锁**(漏放 = 之后所有卷操作全瘫)");
|
||||
|
||||
/* 005 cap > 文件大小 */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"abc", 3u);
|
||||
n = 999u;
|
||||
ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 3u)
|
||||
&& (g_ff_obs.last_read_btr == 64u) && (g_ff_obs.last_read_br == 3u);
|
||||
CHECK(ok, "TC-CFGFST-005 要 64 只给 3 -> nread=3 且仍算成功(短读不是错误)");
|
||||
|
||||
/* 006 f_read 失败 */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"abc", 3u);
|
||||
ff_fake_inject_rc(P_CFG, FF_FAKE_OP_READ, FR_DISK_ERR, 1);
|
||||
ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == -1);
|
||||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_CLOSE] == 1)
|
||||
&& (s_lock_releases == 1) && (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-006 读失败 -> -1 + close 已配对 + 放锁");
|
||||
|
||||
/* 007 介质短读:现状是"返回 0 + nread 如实变小",调用方靠 nread 定长度 */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"0123456789", 10u);
|
||||
ff_fake_inject_read_short(P_CFG, 4u, 1);
|
||||
n = 999u;
|
||||
ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 4u);
|
||||
CHECK(ok, "TC-CFGFST-007 注入短读 -> 返回 0、nread=4(上层按 nread 截断使用)");
|
||||
}
|
||||
|
||||
/* ===================== 原子写 ===================== */
|
||||
|
||||
static void tc_write_atomic(void)
|
||||
{
|
||||
uint8_t buf[64];
|
||||
size_t n;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-CFGFST-010..020 原子写(E-01 核心)--\n");
|
||||
|
||||
/* 010 首启写(目标不存在) */
|
||||
reset_all();
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"v1", 2u) == 0);
|
||||
CHECK(ok && (ff_fake_size(P_CFG) == 2u) && (ff_fake_exists(P_TMP) == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 1)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) /* unlink 目标:FR_NO_FILE 无害 */
|
||||
&& (strcmp(g_ff_obs.last_rename_old, "/CONFIG.TMP") == 0)
|
||||
&& (strcmp(g_ff_obs.last_rename_new, "/CONFIG.INI") == 0)
|
||||
&& (s_lock_releases == 1) && (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-010 首启写:TMP -> rename -> 目标 2 字节、TMP 不留痕");
|
||||
|
||||
/* 011 覆盖写:假卷的 f_rename 对已存在目标返回 FR_EXIST —— 这就是 E-01 */
|
||||
reset_all();
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"old-content", 11u) == 0);
|
||||
{
|
||||
int unlinks_before = g_ff_obs.calls[FF_FAKE_OP_UNLINK];
|
||||
ok = ok && (cfg_store_write_atomic(P_CFG, (const uint8_t *)"new", 3u) == 0);
|
||||
CHECK(ok && (ff_fake_size(P_CFG) == 3u)
|
||||
&& (ff_fake_fetch(P_CFG, buf, 0u, sizeof(buf)) == 3u)
|
||||
&& (memcmp(buf, "new", 3u) == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlinks_before + 1),
|
||||
"TC-CFGFST-011 覆盖写:unlink 目标恰一次再 rename -> 新内容落地(E-01)");
|
||||
}
|
||||
/* 同一条的"反面":直接 rename 到已存在目标必败(证明上一步的 unlink 不是多余的) */
|
||||
{
|
||||
static FIL probe;
|
||||
FRESULT fr;
|
||||
|
||||
(void)ff_fake_mkfile(P_TMP, (const uint8_t *)"x", 1u);
|
||||
fr = f_rename(P_TMP, P_CFG);
|
||||
CHECK(fr == FR_EXIST,
|
||||
"TC-CFGFST-011b 假件保真:目标已存在时 f_rename 返回 FR_EXIST、不覆盖");
|
||||
if (fr == FR_OK) {
|
||||
(void)f_close(&probe);
|
||||
}
|
||||
}
|
||||
|
||||
/* 012 rename 失败 -> TMP 清理 + 目标不动 */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"keep-me", 7u);
|
||||
ff_fake_inject_rc(P_TMP, FF_FAKE_OP_RENAME, FR_DENIED, 1); /* 键 = 旧路径 */
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"replace", 7u) == -1);
|
||||
/* 目标在 rename 之前已被 unlink(非原子窗口),所以这里钉的是:
|
||||
* TMP 一定被清掉、锁一定释放、绝不留半成品 */
|
||||
CHECK(ok && (ff_fake_exists(P_TMP) == 0) && (ff_fake_exists(P_CFG) == 0)
|
||||
&& (s_lock_releases == 2) && (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-012 rename 失败 -> -1 + TMP 清理 + 放锁( unlink→rename 窗口如实存在)");
|
||||
|
||||
/* 013 TMP 打不开 */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"orig", 4u);
|
||||
ff_fake_inject_rc(P_TMP, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"new", 3u) == -1);
|
||||
CHECK(ok && (ff_fake_size(P_CFG) == 4u) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 0)
|
||||
&& (s_lock_releases == 1) && (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-013 TMP 打开失败 -> -1 且**目标原内容一个字节没动**");
|
||||
|
||||
/* 014 短写 = 失败:TMP 被删、目标不动、不 rename */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"good-config", 11u);
|
||||
/* 本次要写 11 字节,注入"只回 5"才算真短 */
|
||||
ff_fake_inject_write_short(P_TMP, 5u, 1);
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"bad-content", 11u) == -1);
|
||||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 1)
|
||||
&& (ff_fake_exists(P_TMP) == 0)
|
||||
&& (ff_fake_size(P_CFG) == 11u)
|
||||
&& (ff_fake_fetch(P_CFG, buf, 0u, sizeof(buf)) == 11u)
|
||||
&& (memcmp(buf, "good-config", 11u) == 0)
|
||||
&& (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-014 TMP 短写判失败 -> 删 TMP、不 rename、目标仍是旧内容");
|
||||
|
||||
/* 015 len = 0 合法(清空配置) */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"x", 1u);
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"", 0u) == 0);
|
||||
n = 999u;
|
||||
CHECK(ok && (ff_fake_size(P_CFG) == 0u)
|
||||
&& (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 0u),
|
||||
"TC-CFGFST-015 写 0 字节成功(清空配置),回读 nread=0");
|
||||
|
||||
/* 016 卷忙:整条链零 I/O */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"orig", 4u);
|
||||
lock_busy_next(1);
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"new", 3u) == -1);
|
||||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0)
|
||||
&& (ff_fake_size(P_CFG) == 4u) && (s_lock_releases == 0),
|
||||
"TC-CFGFST-016 卷忙 -> -1:不建 TMP、不动目标、不 release");
|
||||
|
||||
/* 017 p_buf NULL */
|
||||
reset_all();
|
||||
ok = (cfg_store_write_atomic(P_CFG, NULL, 3u) == -1);
|
||||
CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
|
||||
"TC-CFGFST-017 p_buf NULL -> -1 且不取锁");
|
||||
|
||||
/* 017b 目标路径本身是 NULL:s_build_tmp_path 的空指针分支(读函数有守卫,
|
||||
* 写函数**没有** p_path 的显式守卫,全靠推导函数把它挡在取锁之前) */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"keep", 4u);
|
||||
ok = (cfg_store_write_atomic(NULL, (const uint8_t *)"x", 1u) == -1);
|
||||
CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
|
||||
&& (ff_fake_size(P_CFG) == 4u),
|
||||
"TC-CFGFST-017b p_path NULL -> -1:不取锁、不开文件、目标不动");
|
||||
|
||||
/* 018 临时名由目标推导(同目录) */
|
||||
reset_all();
|
||||
(void)ff_fake_mkdir_direct("/cfg");
|
||||
ok = (cfg_store_write_atomic("/cfg/a.ini", (const uint8_t *)"z", 1u) == 0);
|
||||
CHECK(ok && (strcmp(g_ff_obs.last_open_path, "/CFG/CONFIG.TMP") == 0)
|
||||
&& (ff_fake_exists("/cfg/config.TMP") == 0) /* 已被 rename 走,不是留档 */
|
||||
&& (strcmp(g_ff_obs.last_rename_new, "/CFG/A.INI") == 0)
|
||||
&& (ff_fake_size("/cfg/a.ini") == 1u),
|
||||
"TC-CFGFST-018 目标是 /cfg/a.ini -> 临时文件落在同目录 /cfg/config.TMP");
|
||||
CHECK(strcmp(cfg_store_tmp_path(), P_TMP) == 0,
|
||||
"TC-CFGFST-018b getter 仍是卷根 /config.TMP(与 s_path_tmp 同源)");
|
||||
|
||||
/* 019 目录太长 -> 推导失败,且发生在取锁之前 */
|
||||
reset_all();
|
||||
ok = (cfg_store_write_atomic("/this/is/a/very/deep/dir/name/x.ini",
|
||||
(const uint8_t *)"y", 1u) == -1);
|
||||
CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
|
||||
"TC-CFGFST-019 临时名缓冲放不下 -> -1 且不取锁(不拿半截路径去开文件)");
|
||||
|
||||
/* 020 陷阱一在本模块同样成立 */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"m", 1u);
|
||||
CHECK((g_ff_obs.last_open_mode & FA_WRITE) != 0u,
|
||||
"TC-CFGFST-020 写 TMP 的句柄必带 FA_WRITE(这里用 CREATE_ALWAYS,不是 APPEND)");
|
||||
reset_all();
|
||||
{
|
||||
static FIL probe;
|
||||
UINT bw = 7u;
|
||||
FRESULT fr1;
|
||||
FRESULT fr2;
|
||||
|
||||
fr1 = f_open(&probe, P_TMP, FA_CREATE_ALWAYS); /* 故意漏掉 FA_WRITE */
|
||||
fr2 = f_write(&probe, "abc", 3u, &bw);
|
||||
(void)f_close(&probe);
|
||||
CHECK((fr1 == FR_OK) && (fr2 == FR_DENIED) && (bw == 0u),
|
||||
"TC-CFGFST-020b 只给 FA_CREATE_ALWAYS -> 打开成功但 f_write 恒 FR_DENIED");
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== stat / remove ===================== */
|
||||
|
||||
static void tc_stat_remove(void)
|
||||
{
|
||||
cfg_file_stat_t st;
|
||||
uint32_t tok1;
|
||||
uint32_t tok2;
|
||||
int rel;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-CFGFST-030..033 状态令牌与删除 --\n");
|
||||
|
||||
/* 030 size + 令牌 */
|
||||
reset_all();
|
||||
ff_fake_set_dostime(0x5A21u, 0x6E40u);
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"abcd", 4u);
|
||||
tok1 = ((0x5A21u << 16) | 0x6E40u);
|
||||
ok = (cfg_store_stat(P_CFG, &st) == 0) && (st.size == 4u) && (st.mtime == tok1);
|
||||
ff_fake_set_dostime(0x5A22u, 0x6E40u); /* 次日同一时刻 */
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"efgh", 4u);
|
||||
tok2 = ((0x5A22u << 16) | 0x6E40u);
|
||||
ok = ok && (cfg_store_stat(P_CFG, &st) == 0) && (st.mtime == tok2)
|
||||
&& (tok1 != tok2);
|
||||
CHECK(ok && (s_lock_seq_err == 0) && (s_lock_releases == s_lock_waits),
|
||||
"TC-CFGFST-030 令牌 = (fdate<<16)|ftime,日期进位一定换令牌");
|
||||
|
||||
/* 031 盲区:同一分钟、同长度重写 -> 令牌不变 */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"aaaa", 4u);
|
||||
(void)cfg_store_stat(P_CFG, &st);
|
||||
ok = (st.size == 4u);
|
||||
{
|
||||
uint32_t before = st.mtime;
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"bbbb", 4u);
|
||||
(void)cfg_store_stat(P_CFG, &st);
|
||||
CHECK(ok && (st.size == 4u) && (st.mtime == before),
|
||||
"TC-CFGFST-031 同分钟同长度重写 -> 令牌不变(poll 检不出,设计已接受)");
|
||||
}
|
||||
|
||||
/* 032 stat 三种 -1 */
|
||||
reset_all();
|
||||
rel = s_lock_releases;
|
||||
ok = (cfg_store_stat(P_CFG, NULL) == -1) && (s_lock_releases == rel); /* 守卫在取锁前 */
|
||||
ok = ok && (cfg_store_stat(P_CFG, &st) == -1) && (s_lock_releases == rel + 1);
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"x", 1u);
|
||||
rel = s_lock_releases;
|
||||
lock_busy_next(1);
|
||||
ok = ok && (cfg_store_stat(P_CFG, &st) == -1) && (s_lock_releases == rel);
|
||||
rel = s_lock_releases;
|
||||
ff_fake_inject_rc(P_CFG, FF_FAKE_OP_STAT, FR_DISK_ERR, 1);
|
||||
ok = ok && (cfg_store_stat(P_CFG, &st) == -1) && (s_lock_releases == rel + 1)
|
||||
&& (s_lock_seq_err == 0);
|
||||
CHECK(ok, "TC-CFGFST-032 stat:NULL 守卫/缺失/卷忙/读错误四种出口的放锁配对");
|
||||
|
||||
/* 033 remove */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"x", 1u);
|
||||
rel = s_lock_releases;
|
||||
ok = (cfg_store_remove(P_CFG) == 0) && (ff_fake_exists(P_CFG) == 0)
|
||||
&& (s_lock_releases == rel + 1);
|
||||
ok = ok && (cfg_store_remove(P_CFG) == -1) /* 再删:文件已不在 */
|
||||
&& (s_lock_releases == rel + 2);
|
||||
lock_busy_next(1);
|
||||
ok = ok && (cfg_store_remove(P_CFG) == -1) && (s_lock_releases == rel + 2)
|
||||
&& (s_lock_seq_err == 0);
|
||||
CHECK(ok, "TC-CFGFST-033 remove:成功 / 已不存在的 -1 / 卷忙的 -1 与放锁配对");
|
||||
}
|
||||
|
||||
/* ===================== 路径与 8.3(陷阱二) ===================== */
|
||||
|
||||
static void tc_names(void)
|
||||
{
|
||||
static FIL probe;
|
||||
uint8_t rbuf[16];
|
||||
size_t rn;
|
||||
cfg_file_stat_t st_dummy;
|
||||
uint32_t ents_before;
|
||||
int rel;
|
||||
FRESULT fr_body;
|
||||
FRESULT fr_ext;
|
||||
FRESULT fr_bad;
|
||||
FRESULT fr_dot;
|
||||
FRESULT fr_colon;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-CFGFST-040..042 路径合规与「名字错不升级成卷操作」 --\n");
|
||||
|
||||
/* 040 生产路径真的可用(改名即静默失败的第一道防线) */
|
||||
reset_all();
|
||||
ok = (strcmp(cfg_store_config_path(), P_CFG) == 0)
|
||||
&& (f_open(&probe, cfg_store_config_path(), FA_WRITE | FA_CREATE_ALWAYS) == FR_OK)
|
||||
&& (f_close(&probe) == FR_OK)
|
||||
&& (f_open(&probe, cfg_store_tmp_path(), FA_WRITE | FA_CREATE_ALWAYS) == FR_OK)
|
||||
&& (f_close(&probe) == FR_OK);
|
||||
CHECK(ok, "TC-CFGFST-040 /config.ini 与 /config.TMP 都是合法 8.3,真能在卷上建出来");
|
||||
|
||||
/* 041 短名违规分档 */
|
||||
reset_all();
|
||||
fr_body = f_open(&probe, "/configlooooong.ini", FA_WRITE | FA_CREATE_ALWAYS);
|
||||
if (fr_body == FR_OK) { (void)f_close(&probe); }
|
||||
fr_ext = f_open(&probe, "/config.ini2", FA_WRITE | FA_CREATE_ALWAYS);
|
||||
if (fr_ext == FR_OK) { (void)f_close(&probe); }
|
||||
fr_bad = f_open(&probe, "/conf?ig.ini", FA_WRITE | FA_CREATE_ALWAYS);
|
||||
if (fr_bad == FR_OK) { (void)f_close(&probe); }
|
||||
fr_dot = f_open(&probe, "/.fschk", FA_WRITE | FA_CREATE_ALWAYS);
|
||||
if (fr_dot == FR_OK) { (void)f_close(&probe); }
|
||||
fr_colon = f_open(&probe, "1:/config.ini", FA_READ);
|
||||
if (fr_colon == FR_OK) { (void)f_close(&probe); }
|
||||
CHECK((fr_body == FR_INVALID_NAME) && (fr_ext == FR_INVALID_NAME)
|
||||
&& (fr_bad == FR_INVALID_NAME) && (fr_dot == FR_INVALID_NAME)
|
||||
&& (fr_colon == FR_INVALID_DRIVE),
|
||||
"TC-CFGFST-041 分档:体>8 / 扩展>3 / 非法字符 / 点开头 -> FR_INVALID_NAME;':' -> FR_INVALID_DRIVE");
|
||||
|
||||
/* 042 名字类错误码绝不升级成卷操作:非法目标名走一遍原子写 */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"keep", 4u);
|
||||
ents_before = (uint32_t)ff_fake_count_entries();
|
||||
rn = 0u;
|
||||
rel = s_lock_releases;
|
||||
ok = (cfg_store_read("/.fschk", rbuf, sizeof(rbuf), &rn) == -1)
|
||||
&& (cfg_store_stat("/.fschk", &st_dummy) == -1)
|
||||
&& (cfg_store_remove("/.fschk") == -1);
|
||||
CHECK(ok && ((uint32_t)ff_fake_count_entries() == ents_before)
|
||||
&& (ff_fake_size(P_CFG) == 4u) && (s_lock_seq_err == 0)
|
||||
&& (s_lock_releases == rel + 3),
|
||||
"TC-CFGFST-042 非法名只让本次调用失败:条目数不变、已有配置不动、锁把把放过");
|
||||
/* 非法目标名进原子写:推导出的 TMP 也非法 -> 第一步开文件就失败,
|
||||
* 绝不能把已经存在的目标删掉 */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"keep", 4u);
|
||||
ents_before = (uint32_t)ff_fake_count_entries();
|
||||
ok = (cfg_store_write_atomic("/badname-tooooo-long.ini", (const uint8_t *)"x", 1u) == -1);
|
||||
CHECK(ok && (ff_fake_size(P_CFG) == 4u)
|
||||
&& ((uint32_t)ff_fake_count_entries() == ents_before),
|
||||
"TC-CFGFST-042b 非法目标名的原子写整条失败,**不碰已有配置文件**");
|
||||
}
|
||||
|
||||
/* ===================== 堆 ===================== */
|
||||
|
||||
static void tc_heap(void)
|
||||
{
|
||||
void *p;
|
||||
void *q;
|
||||
|
||||
printf("-- TC-CFGFST-050..053 分配后端(优化调研 P0 第 1 条)--\n");
|
||||
|
||||
/* 050 成对归还 */
|
||||
reset_all();
|
||||
p = cfg_store_alloc(256u);
|
||||
(void)cfg_store_free(p);
|
||||
CHECK((p != NULL) && (s_heap_alloc_calls == 1) && (s_heap_free_calls == 1)
|
||||
&& (s_heap_used == 0u) && (s_heap_free_err == 0),
|
||||
"TC-CFGFST-050 alloc/free 走 FreeRTOS 堆且成对归还(借多少还多少)");
|
||||
|
||||
/* 051 分配失败如实返回 NULL */
|
||||
reset_all();
|
||||
s_heap_fail = 1;
|
||||
p = cfg_store_alloc(2048u);
|
||||
q = cfg_store_alloc(8u);
|
||||
cfg_store_free(p); /* NULL 必须安全 */
|
||||
CHECK((p == NULL) && (q == NULL) && (s_heap_free_calls == 0),
|
||||
"TC-CFGFST-051 堆见底 -> cfg_store_alloc 返回 NULL(MicroLIB 512B 的真形态)");
|
||||
|
||||
/* 052 free(NULL) 安全 + 不计数 */
|
||||
reset_all();
|
||||
cfg_store_free(NULL);
|
||||
CHECK((s_heap_free_calls == 0) && (s_heap_free_err == 0),
|
||||
"TC-CFGFST-052 cfg_store_free(NULL) 不触碰分配器");
|
||||
|
||||
/* 053 heap_free 转发 */
|
||||
reset_all();
|
||||
(void)cfg_store_alloc(100u);
|
||||
CHECK(cfg_store_heap_free() == (FAKE_HEAP_CAP - 100u),
|
||||
"TC-CFGFST-053 cfg_store_heap_free 原样转发 xPortGetFreeHeapSize");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== Cfg File Store Mcu Tests ===\n");
|
||||
|
||||
tc_read();
|
||||
tc_write_atomic();
|
||||
tc_stat_remove();
|
||||
tc_names();
|
||||
tc_heap();
|
||||
|
||||
printf("\n=== Cfg File Store Mcu Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""配置存储后端(cfg_file_store_mcu)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c(以
|
||||
#include 纳入**生产源** App/cfg/cfg_file_store_mcu.c,并纳入共享假卷
|
||||
test/host/common/fatfs_fake.c),在 PC 上离线跑读、原子写(E-01 的
|
||||
unlink->rename 全链)、状态令牌、删除、路径 8.3 合规、假堆分配,失败即非 0 退出。
|
||||
工具链与判定委托 test/host/common/unit_test_harness.py。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
1. **E-01 原子写**:本仓 FatFs 移植的 f_rename 对**已存在目标返回 FR_EXIST、不
|
||||
覆盖**,所以覆盖写必须先 unlink 目标。模拟器后端(POSIX rename 覆盖语义)
|
||||
永远照不出这一条 → 曾经"一切非首启保存静默失败"。假卷按真实 ff.c 返回
|
||||
FR_EXIST,把这条做成必然失败的断言。
|
||||
2. **失败不留半写**:TMP 打开失败 / 短写 / rename 失败三条路径都必须"目标原内容
|
||||
一个字节没动 + TMP 清理干净 + 卷锁已释放",这是现场"改坏配置还能开机"的前提。
|
||||
3. **卷锁 try 语义(v1.3)**:四个后端函数全在 vol_lock_wait(0) 区间内;写反了
|
||||
就是与 ftpTask 并发写 FTL(dhara journal 竞争实证过),或 UI 保存死等卷把
|
||||
喂狗点阻塞死。只能靠调用计数断言。
|
||||
4. **mtime 令牌是 FAT 打包时间、不是内容哈希**:同一分钟同长度重写检不出变更,
|
||||
这个盲区必须钉在测试里,否则后人会拿它当哈希用。
|
||||
5. **MicroLIB C 堆只有 512 B**(startup Heap_Size=0x200):cfg_store_alloc 是
|
||||
共享核心唯一取内存入口,分配失败必须如实返回 NULL —— 宿主 CRT 堆永不失败。
|
||||
|
||||
桩的边界:ff.h / cfg_file_store.h / vol_lock.h 用**真实头**;FreeRTOS.h 是桩
|
||||
(真实头会拉整棵 portmacro),三条原型逐字对齐 portable.h:163/164/166。
|
||||
FatFs 由 test/host/common/fatfs_fake.c 给 RAM 实现(**显式纳入,不用目录通配**,
|
||||
避免被卷进别人的构建);假卷锁与假堆强符号写在本测试单元里。
|
||||
|
||||
用法(仓库根目录):
|
||||
& '.venv\Scripts\python.exe' test/host/cfg_file_store_mcu/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
|
||||
|
||||
设计依据:docs/子系统设计/配置文件设计说明.md §6.2(mtime 令牌)、
|
||||
docs/架构设计/STM32F4-Base存储架构说明.md §10.1 第 10/11 条、
|
||||
docs/测试说明/主机单测覆盖率口径.md §9 P2 第 2 项
|
||||
创建日期:2026-10-24
|
||||
"""
|
||||
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),其余全用真实头。
|
||||
# 假卷 fatfs_fake.c 由测试单元 #include 纳入(同 ch395f_fake.c 惯例,显式列文件、不通配)。
|
||||
BUILD = TestBuild(
|
||||
name="Cfg File Store Mcu Tests",
|
||||
src=os.path.join(HERE, "cfg_file_store_mcu_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "test", "host", "common"),
|
||||
os.path.join(ROOT, "App", "cfg"),
|
||||
os.path.join(ROOT, "App", "fs"),
|
||||
os.path.join(ROOT, "Lib", "FatFs"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== Cfg File Store Mcu Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - FreeRTOS.h(cfg_file_store_mcu 套件专用)
|
||||
* 模块功能:App/cfg/cfg_file_store_mcu.c 只经这个头拿三个分配器原型
|
||||
* (pvPortMalloc / vPortFree / xPortGetFreeHeapSize),它自己不碰任何内核
|
||||
* API——卷是 vol_lock 的事、卷操作是 ff.h 的事。真实 FreeRTOS.h 会拉整棵
|
||||
* portmacro + 编译器抽象层,PC 上编不动,所以只给这三条原型。
|
||||
*
|
||||
* ⚠ 仅供 test/host/cfg_file_store_mcu/ 使用,不参与 MCU 构建。
|
||||
*
|
||||
* 签名逐条对齐真实头(改一处即编译期报错,这就是桩存在的意义):
|
||||
* Middlewares/Third_Party/FreeRTOS/Source/include/portable.h:163
|
||||
* void *pvPortMalloc( size_t xSize ) PRIVILEGED_FUNCTION;
|
||||
* 同文件 :164
|
||||
* void vPortFree( void *pv ) PRIVILEGED_FUNCTION;
|
||||
* 同文件 :166
|
||||
* size_t xPortGetFreeHeapSize( void ) PRIVILEGED_FUNCTION;
|
||||
* (真实实现 Middlewares/.../portable/MemMang/heap_4.c:315 定义 xPortGetFreeHeapSize)
|
||||
* PRIVILEGED_FUNCTION 在 pc 端口展开为空,桩里不写等价。
|
||||
*
|
||||
* 实现放在测试单元里,是一份**可注入失败的假堆**:真机上 MicroLIB 的 C 堆只有
|
||||
* 512 B(startup Heap_Size=0x200),配置引擎的 KB 级临时缓冲必然走 FreeRTOS 堆,
|
||||
* 堆见底时 pvPortMalloc 返回 NULL —— 这条分支模拟器(宿主 CRT 堆)永远走不到,
|
||||
* 只有假堆能稳定构造(cfg_store_alloc 是共享核心的唯一取内存入口,必须测)。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
#ifndef FREERTOS_H
|
||||
#define FREERTOS_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
void *pvPortMalloc(size_t xWantedSize);
|
||||
void vPortFree(void *pv);
|
||||
size_t xPortGetFreeHeapSize(void);
|
||||
|
||||
#endif /* FREERTOS_H */
|
||||
Reference in New Issue
Block a user