0f7b5a87fb
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号) 一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、 采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。 门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏 undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8 reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF; id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。 test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字, 是本批唯一改变构建产物的文件集合。 为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged 部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、 不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。 实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01% (基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 / 告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。 基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处 引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使 识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。 ⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二 根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有), 今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。 固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0; App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889 断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿; check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带 BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。 .cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法: Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。 同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%, RW_CCM 57,376 B / 87.9%,阈值 92%。
150 lines
9.1 KiB
C
150 lines
9.1 KiB
C
/*
|
||
* 模块名称:假 FatFs 内存卷(存储域主机侧单测共享件)
|
||
* 模块功能:给 App/log 与 App/cfg 的主机侧单测一个可脚本化的假文件系统——
|
||
* 类型与常量取自**真实 Lib/FatFs/ff.h**(FRESULT / FIL / FILINFO / FA_*
|
||
* 一字不改),本件在 fatfs_fake.c 里给出九个 f_* 原型的 RAM 实现:
|
||
* f_open / f_read / f_write / f_lseek / f_close / f_stat / f_unlink /
|
||
* f_mkdir / f_rename。
|
||
*
|
||
* 仅供 test/host/** 使用:不参与 MCU 构建,也不参与 simulator 构建。
|
||
*
|
||
* 为什么共享一份(照 test/host/common/ch395f_fake.{h,c} 的先例):
|
||
* sys_log_mcu / soe_store / cfg_file_store_mcu 三块的成败**全部**由
|
||
* 「这次 f_open 拿到什么码、f_write 写回多少字节」决定。三份假件各写一遍,
|
||
* 漂移 = 「假件与被测代码同时错而无人报错」。签名与真实头同源 → 真实 API 一改
|
||
* 就编译期报错(同 test/host/nand_port 的「真头 + 假 diskio」路子)。
|
||
*
|
||
* —— 必须复刻的三条本仓实证语义(docs/架构设计/STM32F4-Base存储架构说明.md §10.1)——
|
||
* 1) FA_OPEN_APPEND(0x30) **不含 FA_WRITE(0x02)**:本移植 FA_OPEN_APPEND =
|
||
* FA_OPEN_ALWAYS(0x10) | FA_SEEKEND(0x20),而 FA_SEEKEND 只定义在 ff.c:74、
|
||
* ff.h 里没有。所以只给 FA_OPEN_APPEND 打开的文件**能打开、能创建**,但
|
||
* f_write 恒返 FR_DENIED(7)——2026-09-20 台架「/log 写失败 x5/开机」的真根因
|
||
* (§10.1 第 11 条)。假件按真实 ff.c 一样把 mode 原值存进 fp->flag 再判
|
||
* flag & FA_WRITE,不做任何「便利放行」。
|
||
* 2) f_write 簇分配失败 = **FR_OK + 短写**(无错误码,判失败只能校验写回字节数):
|
||
* 真实 ff.c:4102 的 if (clst == 0) break; → 循环带着已写计数退出 → FR_OK。
|
||
* 假件把「每文件固定后备簇池」当介质:写到装不下就返回 FR_OK + 截断字节数;
|
||
* 另给定点注入,让「恰好短一字节」这种真机要撞坏块才有的形态可复现。
|
||
* 3) FF_USE_LFN=0 → **严格 8.3 短名**:主名 <=8、扩展 <=3、非法字符集
|
||
* * + ; < = > [ ] | 与 0x7F(ff.c create_name 非 LFN 分支逐字模拟)。
|
||
* 「.fschk」的真实死法 = 开头那个点把游标直接推进扩展域、随后 5 个字符撑爆
|
||
* 扩展域上限 3 → FR_INVALID_NAME(6);路径里任何位置出现 ':' 被 get_ldnumber
|
||
* 当盘符前缀吃下 → FR_INVALID_DRIVE(11)。两者都是**纯 RAM 里的名字判定、
|
||
* 不碰盘**(§10.1 第 10 条,含「测试用例须同时接受这两个码」的口径)。
|
||
*
|
||
* —— 其余与真实 ff.c 对齐的路径/错误码细则 ——
|
||
* - 前导分隔符被剥掉、恒从卷根开始(FF_FS_RPATH=0,见 ff.c follow_path);
|
||
* - 中间目录段不存在 → FR_NO_PATH;末段不存在且未给创建位 → FR_NO_FILE;
|
||
* - 对目录本身 f_open(无创建位)→ FR_NO_FILE;
|
||
* - FA_CREATE_NEW 撞上已存在 → FR_EXIST;FA_CREATE_ALWAYS → 截零 + 刷新时间戳;
|
||
* - 路径剥完分隔符为空 → FR_INVALID_NAME(ff.c create_name 的 i==0 分支);
|
||
* - f_stat:父目录在而对象不在 → FR_NO_FILE;父目录也不在 → FR_NO_PATH;
|
||
* - f_unlink 不存在 → FR_NO_FILE;f_mkdir 已存在 → FR_EXIST;
|
||
* - **f_rename 撞已存在的新名 → FR_EXIST、不覆盖**(ff.c「new name already in
|
||
* use」,2026-09-22 台架暴露的那条:缺了它,一切「目标存在的覆盖写」
|
||
* 必然失败,而模拟器端 POSIX rename 的覆盖语义照不出来)。不复刻本条 =
|
||
* cfg_store_write_atomic 里「先 unlink 再 rename」那一步永远测不出必要性;
|
||
* - f_lseek 越过文件尾:只读句柄**钳到 objsize 且不报错**(ff.c 正常寻址分支),
|
||
* 可写句柄才扩展文件;
|
||
* - f_open 失败即作废句柄(ff.c「fp->obj.fs = 0」)→ 后续 f_write 拿
|
||
* FR_INVALID_OBJECT,不会拿着脏句柄继续写;
|
||
* - f_close 成功即释放句柄,并把打开期间的变更落成新的 fdate/ftime
|
||
* (真实 ff.c 在 close 里用 get_fattime 回写目录项)。
|
||
*
|
||
* —— 脚本化模型(沿用本仓既有假件风格:nand_port 的 RAM 假芯片 / lftpd_io 的假堆)——
|
||
* - 注入:按路径 + op 预置「接下来 N 次返回某码 / 只写回 n 字节 / 只读回 n 字节」;
|
||
* path 传 NULL = 不限路径;
|
||
* - 观测:逐 op 调用计数 + 最近一次入参(路径、mode、btw/bw、btr/br、lseek 偏移),
|
||
* 让「这轮压根不该碰盘」与「打开模式必须带写位」成为可断言的事;
|
||
* - 卷:条目直读直写(ff_fake_mkfile / ff_fake_mkfile_size / ff_fake_fetch)用来
|
||
* 铺前置状态——「run.log 已经有 1024 条记录」不可能靠真写 65536 字节铺出来;
|
||
* - 假时钟:ff_fake_set_dostime 控制 fdate/ftime,FAT 分钟粒度导致
|
||
* 「同分钟内同长度重写不可检出」这件事只有它能测(cfg_store_stat 的 mtime 令牌);
|
||
* - ff_fake_reset() 一次全清(卷 + 注入 + 观测 + 时钟)。
|
||
*
|
||
* 有意**不提供** f_mount / f_mkfs / f_opendir / f_readdir:三个被测模块一次都不该
|
||
* 调它们,真调了就是链接期报错——比运行时断言更硬,也不会静默通过。
|
||
*
|
||
* 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2)
|
||
*/
|
||
|
||
#ifndef FATFS_FAKE_H
|
||
#define FATFS_FAKE_H
|
||
|
||
#include <stdint.h>
|
||
|
||
/* 真实头在 Lib/FatFs/;本文件不重定义任何 FatFs 类型或宏 */
|
||
#include "ff.h"
|
||
|
||
#define FF_FAKE_MAX_ENT 16u /* 卷内条目上限(文件 + 目录) */
|
||
#define FF_FAKE_PATH_MAX 24u /* 归一化后路径上限("/log/selftest.log" = 17) */
|
||
/* 每文件的后备簇池:必须**大于生产侧单文件容量上限**(run.log / soe.log
|
||
* = 1024 条 x 64 B = 65536 B,见 sys_log.h:SYSLOG_RUN_MAX 与 soe_store.h:
|
||
* SOE_STORE_MAX),否则测"容量满 unlink 重建"时追加位落在池外,f_write 恒
|
||
* 返回 0 字节短写,轮转场景根本跑不到。写满仍然如实返回 FR_OK + 短写
|
||
*(真实 ff.c 簇分配失败的同一形态)。16 条目 x 66 KB ≈ 1 MB 静态,主机侧无压力。 */
|
||
#define FF_FAKE_CLUSTER 67584u
|
||
#define FF_FAKE_INJ_MAX 8u /* 注入槽位数 */
|
||
#define FF_FAKE_GUARD 50000 /* 跑飞保险:单个 f_* 调用数超此值直接 exit(2) */
|
||
|
||
/* 注入与计数的操作编号(fatfs_fake.c 内部同一套) */
|
||
enum {
|
||
FF_FAKE_OP_OPEN = 0,
|
||
FF_FAKE_OP_READ,
|
||
FF_FAKE_OP_WRITE,
|
||
FF_FAKE_OP_LSEEK,
|
||
FF_FAKE_OP_CLOSE,
|
||
FF_FAKE_OP_STAT,
|
||
FF_FAKE_OP_UNLINK,
|
||
FF_FAKE_OP_MKDIR,
|
||
FF_FAKE_OP_RENAME,
|
||
FF_FAKE_OP_N
|
||
};
|
||
|
||
/* 注入动作 */
|
||
enum {
|
||
FF_FAKE_ACT_RC = 0, /* 直接返回指定 FRESULT,不做任何数据搬运 */
|
||
FF_FAKE_ACT_WSHORT, /* 仅 f_write:FR_OK 但只写回 n 字节 */
|
||
FF_FAKE_ACT_RSHORT /* 仅 f_read :FR_OK 但只读回 n 字节 */
|
||
};
|
||
|
||
typedef struct {
|
||
int calls[FF_FAKE_OP_N]; /* 逐 op 调用次数(含失败的调用) */
|
||
BYTE last_open_mode; /* 最近一次 f_open 的 mode 原值(断言 FA_WRITE 位) */
|
||
char last_open_path[FF_FAKE_PATH_MAX];
|
||
char last_unlink_path[FF_FAKE_PATH_MAX];
|
||
char last_mkdir_path[FF_FAKE_PATH_MAX];
|
||
char last_rename_old[FF_FAKE_PATH_MAX];
|
||
char last_rename_new[FF_FAKE_PATH_MAX];
|
||
UINT last_write_btw; /* 请求写的字节数 */
|
||
UINT last_write_bw; /* 实际写回的字节数(短写的断言面) */
|
||
UINT last_read_btr;
|
||
UINT last_read_br;
|
||
FSIZE_t last_lseek_ofs;
|
||
int last_open_rc; /* 最近一次 f_open 的返回码 */
|
||
int last_write_rc; /* 最近一次 f_write 的返回码 */
|
||
} ff_fake_obs_t;
|
||
|
||
extern ff_fake_obs_t g_ff_obs;
|
||
|
||
/* —— 世界控制 —— */
|
||
void ff_fake_reset(void); /* 卷 + 注入 + 观测 + 时钟一次全清 */
|
||
void ff_fake_set_dostime(uint16_t fdate, uint16_t ftime); /* 假 get_fattime() 的两个域 */
|
||
|
||
/* —— 故障注入(times = 生效次数;path = NULL 表示不限路径)—— */
|
||
void ff_fake_inject_rc(const char *path, int op, FRESULT rc, int times);
|
||
void ff_fake_inject_write_short(const char *path, UINT n, int times);
|
||
void ff_fake_inject_read_short(const char *path, UINT n, int times);
|
||
|
||
/* —— 卷内容直读直写(绕开打开语义,用来铺前置状态)—— */
|
||
int ff_fake_exists(const char *path); /* 0 无 / 1 文件 / 2 目录 */
|
||
uint32_t ff_fake_size(const char *path); /* 无此文件返回 0xFFFFFFFF */
|
||
uint32_t ff_fake_time_token(const char *path); /* (fdate<<16)|ftime,同 cfg_store_stat */
|
||
uint32_t ff_fake_fetch(const char *path, uint8_t *p_buf, uint32_t off, uint32_t cap);
|
||
int ff_fake_mkfile(const char *path, const uint8_t *p_data, uint32_t len);
|
||
int ff_fake_mkfile_size(const char *path, uint32_t size); /* 只给逻辑长度(造容量满) */
|
||
int ff_fake_mkdir_direct(const char *path); /* 无条件建目录 */
|
||
int ff_fake_count_entries(void); /* 卷内条目数(含目录) */
|
||
|
||
#endif /* FATFS_FAKE_H */
|