b623a406ad
这四笔在主树里已经互相纠缠(同一次 @build 编译、同一份 .htm 度量),拆成四笔就不
存在任何一个被真编过的中间态,所以合一笔入库;下面按模块分段陈述,作者与出证归属
逐段写明。
—— 一、崩溃证据路径的整行独占收尾(log-maint-writer,task-26)——
上午那笔 1d4b6a17 之后,stack_report 报 iec104Task 静态最坏链 1,544 / margin 1,016,
低于 1,024 的 ISR 底线。真因不是新增局部缓冲,而是本模块与内核临界区之间新长出
一条环:taskEXIT_CRITICAL ⇒ vPortExitCritical ⇒ configASSERT ⇒ vApplicationAssertFailed
⇒ dbg_console_write_raw ⇒ 回到本模块。armlink 把这一整坨计入**每一个含临界区的
任务链**(vApplicationAssertFailed 的 Max Depth 由约 40 B 涨到 128 B + In Cycle),
所以九条链同涨约 80 B——同涨本身就排除了任何单个调用方。
改法两处:① 本模块三处窗口(取凭证 / 还凭证 / 整行入环)改用 CON_CRIT_ENTER/EXIT
(PRIMASK 保存恢复到局部变量;注释写全为什么不能直接 __enable_irq()——嵌套时会
替别人打开任务级临界区——以及屏蔽面变宽是有意为之);② 异常/终点钩子里那对
自证空转的 begin/end 摘掉(异常态 begin 放行但不持有、end 是幂等空操作,调它只有
成本没有收益),任务态 STALL 行保留 begin/end,这个区分写进两处注释与文档 §4.4.1。
实测四棵树逐个 .htm 解析(身份钉 tree + Linker run 时刻 + 产物来源):A=77779d49
链 1,456;B=1d4b6a17 链 1,464 / margin 1,096 PASS;C=含未提交钩子接线的工作树
1,544 / 1,016 FAIL;D=本轮修复后(Linker run 22:52)回到 **1,464 / 1,096 PASS**,
九条链齐降 80 B。断环图证:vPortExitCritical 由 136 B + In Cycle 变 8 B 且不再
In Cycle,con_tx_release 子树 152→8;Error_Handler 回到 280 + In Cycle = A 树同值
(既有发送环无人拆动)。顺带删掉 Src/freertos.c 崩溃路径里最后一个 HAL_Delay(200)
(该文件真实调用现为 0)。
测试:console_log 152→**153** 条(只增不减);换钉不删防——015 现在钉「PRIMASK
窗口 2 次、深度 1、内核临界区 0 次」,新钉 015c 绝不等待,160 注入点搬到
con_pm_hook,176 强化到未调度路径连 PRIMASK 窗口都不开;红线水位
con_pm_at_tx_max 与 con_uart_crit_at_tx_max 双 0;判据⑦(延后的行与当场直发逐
字节相同)170b/171/178 仍全绿。作者自报并纠正一处:换钉时曾把 TC-CONLOG-015b 改
编号,而 015b 在生产注释与文档里都被引用——已恢复「编号不退役、语义搬过去」。
覆盖率 App/log/console_log.c 199/200 → 256/257 行、21/21 函数(只增)。
—— 二、未标定闭锁判据收成单点访问器(ui-param-writer,task-16)——
「UA/UN/IA 三相 VALID 全置位才允许跑判据」这条门控原先在两处各自镜像实现
(protect_task.c 的 s_cal_locked 与 ui_data_feed.c 的 feed_cal_block),改一处另一处
静默失效且无门禁可照。现新增 adc_cal_prot_blocked() 落在 App/algo/adc_cal,DTU_BENCH
特例收进一处;两个消费者同批改调、本地副本删除。作者还查实并纠正了我先前一句
错判:全仓 App/app→App/task 的 include 实为 0 条,故访问器不能落在 protect_task。
新增 TC-ADCCAL-018 八条(含冷启动即闭锁),adc_cal 套件 99/99。
—— 三、共享卷锁五态可观测性(ui-param-writer,task-17)——
原状比登记簿写的更糟:五个可区分态(未调用 / try 超时 / 分片超时 / fail-open /
未持锁却 release)生产侧零留痕,且 vol_lock.c 的返回值注释与代码相反——
fail-open(锁没建成、互斥已失效却在读写盘)被返 1 **谎报成取到锁**,而那正是
vol_lock.h 记录的台架实证故障(并发写坏 FTL → 喂狗点饥饿 → IWDG 误复位)的前置。
锁层加四个 volatile 计数(16 B 静态,零新局部、零新 callee、零日志、零 FatFs)与
不取锁的 vol_lock_get_stats();13 处 try 调用点一字未动。**刻意不在锁层记时刻**:
记时刻要读 tick,等于给 defaultTask 取锁链添帧——诊断不值那几个字节;消费方若
自盖时间戳,那只能是被观测到的时刻而非发生时刻,.h 里已写明不得写成首次/末次。
四套本地假件各加 s_last_wait_to(字段名逐字一致),把「喂狗点取锁一律 timeout=0」
与「ftpTask 分片一律 100 ms」从注释纪律升级成会 FAIL 的断言;红-绿反证:临时把
sys_log_mcu.c 的 0u 改 50u → TC-SYSLOG-010/012 双红,把 lftpd_io.c 的 100u 改 0u →
TC-LFTPIO-034 红,两处按原串改回并用 SHA-256 证明逐字节还原。TC-VOLLOCK-015~019
共 45/45(无新桩)。
—— 四、UI 死接口清理(ui-maint-writer,task-6 收尾)——
删掉 ui_data_maint_ready():全仓零消费者、只有它自己的用例在读。**更值得记的是
它为什么能活过一整轮评审**:头注释写的是「页面用它决定这页能不能操作」,而页面
从来没用过它——注释陈述的是意图,评审读的是意图,门禁读的是可达路径,两者都不
反驳「意图未实现」。作者对「连用例一起删」做了显式抗命:TC-UIDATA-025 的 13 条里
只有 4 条读那个死口,另 9 条钉的是真防线(六口未注入安全返回 / 残表整表拒绝 /
被拒不覆盖已生效的好表),故删冗余的 025a、把 025i/025k/025m 换成用 reset_req()
的可观察返回值判定,性质不丢。ui_data 套件 244 → 243(净减 1),黄金 40 页与
池 5448 B 未变(该刀零像素影响,参考图无 diff)。
—— 门禁与未跑项(本笔如实留账)——
cubemx-survival 基线已 --write(漂移 6 项全部是上述意图内区域:断言/栈溢出钩子
实现、看门狗模块行等),基线文件同笔入库,随后 --check 通过。
本笔未跑的钩子只有两条:mem-numbers 与 mem-budget——磁盘 .map 出自 22:52 那次
构建、含尚未入库的 UI 与工具改动,与 HEAD 台账必然不同码;按仓库既有约定允许
--no-verify + 正文留证,回填在紧随的台账一笔统一做(含 ram_baseline --update 与
覆盖率基线两条 [NOTE]:ui_data.c 65→316、iec104_comm.c 36/36)。
两条已挂着的账:① id-check 不可判定引用 1,162 / 上限 1,166,**余量只剩 4**,
主要吃进者是我今天推进的 UI 那笔(68 条文本断言的注释引用),账在 Lead;
② 崩溃注入腿(故意复位)台架复验未跑,需先由代码道提交触发器,且判据只判原子性
不判 [ERR]=0——IPSR 路径的主张归主机侧套件,硬件照不出来。
产物身份(按 D 表要求钉死,避免数字与树脱钩):本笔包含的固件树就是那张 D 表对应的
那一棵——MDK-ARM/.../STM32F407-Demo.htm 的 mtime = 2026-10-26 22:52:12(.htm/.map 按
约定不入库,故把时刻写进正文)。提交前在本树复跑 stack_report.py:exit 0,
iec104Task 1,464 / margin 1,096(42%)PASS、defaultTask 3,040 / 2,080(40%)PASS,
与 D 表逐项一致;本次运行不含任何未提交固件源码之外的新改动。
749 lines
34 KiB
C
749 lines
34 KiB
C
/*
|
||
* 模块名称:SOE 落盘胶水层(soe_store)主机侧单元测试(L0.5)
|
||
* 模块功能:在 PC 上离线验证 App/log/soe_store.c 的四条对外契约与它自己的
|
||
* "照 sys_log_mcu 的台架实证纪律"注释:
|
||
* - post 侧**零文件系统、零日志**(protectTask 栈预算 + DoD 硬约束);
|
||
* - 环 / 队列两条待发通道的分流与"丢弃只计数";
|
||
* - drain 侧的 vol_lock try、FA_WRITE|FA_OPEN_APPEND、写回字节数校验、
|
||
* 容量满 unlink 重建、故障留痕去重;
|
||
* - **名字/参数类错误码绝不升级成卷操作**(存储架构 §10.1 第 10 条)。
|
||
*
|
||
* 被测对象:真实源文件(#include 原样纳入,不改生产代码):
|
||
* App/log/soe_store.c —— 本模块主体
|
||
* App/log/soe_rec.c —— 64B 记录构建(纯逻辑,零 OS,直接用真件;
|
||
* test/host/soe 已单独覆盖其编解码,本套不重测)
|
||
* 假件与桩:
|
||
* test/host/common/fatfs_fake.{h,c} —— 假 FatFs 内存卷(真实 ff.h 声明)
|
||
* stub/dbg_log.h / stub/cmsis_os.h / stub/main.h —— 本目录桩
|
||
* sys_clock / vol_lock 的可控强符号实现写在本文件里
|
||
*
|
||
* 为什么要主机侧单测(而不是只靠台架):
|
||
* 1. **post 侧禁 DBG 是栈预算硬约束**(soe_store.c 函数头原话:protectTask 静态最
|
||
* 坏链 = 经 s_log_active_edge 的 console 尾链 872 B,post 内任何日志调用点都会
|
||
* 再垫高一帧,stack_report 实测 +320 B → FAIL)。真机上"忘了这条"不会崩,只会
|
||
* 在某次最坏路径上把栈打穿 → 只有把 DBG_* 换成计数才能机械断言"这条路径 0 日志"。
|
||
* 2. **环 / 队列的丢弃语义不对称**:环写失败**不推进 tail**(下轮重试、条目不丢),
|
||
* 队列条目已出队、卷忙就丢那一条。真机上要恰好撞上"FTP 正在占卷"才看得到,
|
||
* 而"到底丢不丢"是运维读 SOE 时能不能信任序号连续的前提。
|
||
* 3. **短写 = 失败**(FR_OK + 写回字节数不足,簇分配没有错误码)与**满则重建**
|
||
* 这两条只在介质坏块 / 容量见底时发作;假卷可以把"只写回 63 字节"直接喂进来。
|
||
* 4. 写故障留痕的去重(s_fault_reported)决定了长跑时 /log 与串口会不会被同一条
|
||
* 故障刷屏——真机要复现"失败→恢复→再失败"这个序列极慢。
|
||
*
|
||
* 覆盖点(TC 号族 = SOESTORE(三位序号);SOE 已被存量 test/host/soe 占用):
|
||
* —— post 侧(零 I/O 契约)——
|
||
* TC-SOESTORE-001 post 3 条只写环:零 FatFs 调用、零卷锁调用、零日志
|
||
* TC-SOESTORE-002 墙钟未建立(UINT32_MAX)→ flags 带 IV 且 epoch 字段落 0(不造假时标)
|
||
* TC-SOESTORE-003 seq 上电内单调 +0123(丢弃也占号:号不跳 = 假象)
|
||
* TC-SOESTORE-004 环满(8 条)→ 第 9 条丢弃 + 计数,不阻塞、不崩、**不打日志**
|
||
* TC-SOESTORE-005 内核运行 + 队列已建 → 走队列,环不再写入
|
||
* TC-SOESTORE-006 队列满 → 丢弃 + 计数(与环满同一条可观测出口)
|
||
* TC-SOESTORE-007 丢弃计数饱和:UINT32_MAX 不再回绕
|
||
* —— drain 侧:落盘与打开模式 ——
|
||
* TC-SOESTORE-010 首次 drain 惰性建队列;1 条落盘 = 64 字节且可被 soe_rec_parse 解回
|
||
* TC-SOESTORE-011 打开模式必须是 FA_WRITE|FA_OPEN_APPEND;对照保真:只给
|
||
* FA_OPEN_APPEND 的句柄 f_write 恒返 FR_DENIED(陷阱一,本仓实证根因)
|
||
* TC-SOESTORE-012 多条落盘:一条一次 open/write/close(本模块**没有**批量合并——
|
||
* 与 sys_log 的日志读写放大收口不同),FIFO 顺序、seq 递增
|
||
* TC-SOESTORE-013 open 与 close 严格配对(脏句柄不跨调用残留)
|
||
* —— drain 侧:故障注入 ——
|
||
* TC-SOESTORE-014 短写 63B → 判失败 → unlink 重建 → 重试成功 → 最终恰好 64 字节
|
||
* TC-SOESTORE-015 连续两次短写 → 返回 -1;留痕恰好一次(去重);恢复后再失败才再留痕
|
||
* TC-SOESTORE-016 卷忙(vol_lock_wait(0)=0)→ 返回 -2;**不碰 FatFs、不 release**
|
||
* TC-SOESTORE-017 环条目在写失败时不推进 tail → 下轮重试不丢、且不重复落盘
|
||
* TC-SOESTORE-018 队列条目已出队:卷忙只丢当前一条,剩余留在队列(不对称现状钉住)
|
||
* TC-SOESTORE-019 容量满(1024 条)→ unlink 重建 + INFO 留痕,新文件恰好 64 字节
|
||
* TC-SOESTORE-020 丢弃留痕按差值上报一次,不刷屏
|
||
* TC-SOESTORE-021 队列创建失败(返回 NULL)→ 条目滞留环、下轮再试,不崩
|
||
* —— 陷阱二:名字/参数类错误码不得升级成卷操作 ——
|
||
* TC-SOESTORE-022 f_open 失败(FR_INVALID_NAME)-> 只留痕返回 -1:**不 unlink**、
|
||
* 不碰 /log 目录、已有 3 条历史一条不少、条目留在待发环
|
||
* TC-SOESTORE-022b 逐码对齐(8 个码含介质类):打开失败**任何码都不 unlink**——
|
||
* SOE 侧比 run.log 侧更保守,是刻意差异(事件序列不可销毁),
|
||
* 与 test/host/sys_log_mcu/ 的 TC-SYSLOG-101/102 同一张表
|
||
* TC-SOESTORE-023 写路径反复失败时 f_unlink 次数有界(每轮 <=1),目录与邻居文件不动
|
||
* TC-SOESTORE-024 假卷严格 8.3 校验的保真自检:".fschk" -> 名字/盘符类拒绝码,
|
||
* 而生产路径 SOE_PATH 必过(钉住"改名即静默写失败"的约束)
|
||
*
|
||
* 覆盖率台账(--module soe_store 自量):App/log/soe_store.c 78/79 行 = 98.7%、
|
||
* 函数 4/4。唯一未覆盖行 soe_store.c:202 = drain 里 for (;;) 的收尾大括号,
|
||
* 该循环只能靠 break 退出,不存在"走到行尾再回跳"的机器码,属工具口径缺口。
|
||
*
|
||
* 本轮(第 1 轮缺陷修复批次 task-4)已修,本套相应断言已从"钉现状"翻成正向:
|
||
* D 项:soe_store.c 函数头 ④ 旧写"打开/写入结构性错误都 unlink 重试一次(同
|
||
* run.log)",与实现不符且方向错误——实现里 f_open 失败只 return -1、不 unlink。
|
||
* 定案是**实现不动、改注释**:SOE 是事件序列,打开失败就删文件等于销毁已发生的
|
||
* 事件;run.log 是滚动运行日志,坏了重建损失有限。现在 TC-SOESTORE-022 正向钉
|
||
* "打开失败不得 unlink、历史一条不少",TC-SOESTORE-022b 对 8 个码逐个钉同一件事
|
||
* (含介质类码——SOE 侧比 run.log 侧更保守,两处注释同源,改动必须同步)。
|
||
*
|
||
* 通过:全部 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" /* 真实 Lib/FatFs/ff.h:FRESULT / FA_* / FIL 原汁 */
|
||
#include "fatfs_fake.h" /* 假卷:类型与常量都取自上面那份真头 */
|
||
#include "cmsis_os.h" /* 桩:消息队列 + 内核状态(签名照 cmsis_os2.h) */
|
||
#include "dbg_log.h" /* 桩:DBG_* -> 计数 */
|
||
#include "sys_clock.h" /* 真实头(App/util):签名一改即编译期报错 */
|
||
#include "vol_lock.h" /* 真实头(App/fs) */
|
||
#include "soe_rec.h"
|
||
#include "soe_store.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)
|
||
|
||
/* ===================== 假件与生产源码纳入 ===================== */
|
||
|
||
#include "fatfs_fake.c" /* 假 FatFs 实现 */
|
||
#include "soe_rec.c" /* 真实 64B 记录构建(纯逻辑) */
|
||
#include "soe_store.c" /* 被测主体 */
|
||
|
||
/* ===================== dbg_log 桩计数(stub/dbg_log.h 声明) ===================== */
|
||
|
||
int g_soe_err_count;
|
||
int g_soe_info_count;
|
||
int g_soe_debug_count;
|
||
|
||
/* ===================== 假墙钟 ===================== */
|
||
|
||
static uint32_t s_clock_now = 1760234567u; /* 2025-10-12 前后,CP56 年域内 */
|
||
static int s_clock_valid = 1;
|
||
|
||
uint32_t sys_clock_get(void)
|
||
{
|
||
return (s_clock_valid != 0) ? s_clock_now : UINT32_MAX;
|
||
}
|
||
|
||
/* sys_clock.h 里声明、本模块不调用的接口:给出空实现,纯为可链接 */
|
||
int sys_clock_init(void) { return 0; }
|
||
int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *p_time)
|
||
{
|
||
(void)unix_secs; (void)p_time; return 0;
|
||
}
|
||
int sys_clock_set_unix(uint32_t unix_secs) { (void)unix_secs; return 0; }
|
||
int sys_clock_get_time(sd2506_time_t *p_time) { (void)p_time; return -1; }
|
||
uint32_t HAL_GetTick(void) { return 0u; }
|
||
|
||
char *sys_clock_get_str(char *buf, size_t buf_size)
|
||
{
|
||
(void)buf; (void)buf_size;
|
||
return NULL;
|
||
}
|
||
|
||
/* ===================== 假卷锁(try 语义 + 配对检查) ===================== */
|
||
|
||
static int s_lock_busy_left; /* >0:接下来 N 次 vol_lock_wait 返回"忙"(try 失败)
|
||
* 用计数器而不是按序号取值的脚本数组——脚本数组的下标
|
||
* 会被场景里前几次 drain 消耗掉,注入看着设了其实没生效 */
|
||
static int s_lock_waits;
|
||
static int s_lock_releases;
|
||
static int s_lock_held;
|
||
static int s_lock_seq_err;
|
||
static uint32_t s_last_wait_to; /* 最近一次 vol_lock_wait 的 timeout 入参(0 = try 语义)。
|
||
* 四套假件逐字同名,便于全仓 grep「喂狗点绝不阻塞」这条纪律;
|
||
* 第 6 套出现时再议共享假件 */ /* 未持锁却 release = 锁纪律被写反 */
|
||
|
||
static void lock_busy_next(int n)
|
||
{
|
||
s_lock_busy_left = n;
|
||
}
|
||
|
||
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;
|
||
s_last_wait_to = 0xFFFFFFFFu; /* 哨兵:从未调用过就不等于 timeout=0 */
|
||
}
|
||
|
||
int vol_lock_init(void) { return 0; }
|
||
|
||
int vol_lock_wait(uint32_t timeout_ms)
|
||
{
|
||
int r;
|
||
|
||
s_last_wait_to = timeout_ms; /* 记录入参:调用方的 timeout 口径由此可断言 */
|
||
/* 跑飞保险:调用数越界直接判死,不让死循环拖挂门禁 */
|
||
if (s_lock_waits > 20000) {
|
||
(void)fprintf(stderr, "[FATAL] vol_lock_wait call 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++; /* 没拿到锁就释放:fail-open 语义被写反的证据 */
|
||
}
|
||
s_lock_held = 0;
|
||
}
|
||
|
||
/* ===================== 假消息队列(真 FIFO + 满/空语义) ===================== */
|
||
|
||
#define MQ_SLOTS 16u
|
||
#define MQ_MAXMSG 64u /* SOE 记录 64B;syslog 侧条目 12B,同一池够用 */
|
||
|
||
typedef struct {
|
||
int used; /* 槽位有无消息 */
|
||
uint8_t data[MQ_MAXMSG];
|
||
} mq_slot_t;
|
||
|
||
static mq_slot_t s_q_pool[MQ_SLOTS];
|
||
static uint32_t s_q_head; /* 出队下标 */
|
||
static uint32_t s_q_cnt;
|
||
static uint32_t s_q_cap; /* osMessageQueueNew 给的 msg_count */
|
||
static uint32_t s_q_msgsz;
|
||
static int s_q_new_calls;
|
||
static int s_q_new_fail; /* 1 = osMessageQueueNew 返 NULL */
|
||
static int s_q_get_calls;
|
||
static int s_q_put_calls;
|
||
static int s_q_create_fail_left; /* 预留:连续失败注入(当前用 0/1) */
|
||
|
||
static void queue_clear(void)
|
||
{
|
||
(void)memset(s_q_pool, 0, sizeof(s_q_pool));
|
||
s_q_head = 0u;
|
||
s_q_cnt = 0u;
|
||
s_q_cap = SOE_QUEUE_MAX;
|
||
s_q_msgsz = SOE_REC_SIZE;
|
||
s_q_new_calls = 0;
|
||
s_q_new_fail = 0;
|
||
s_q_create_fail_left = 0;
|
||
s_q_get_calls = 0;
|
||
s_q_put_calls = 0;
|
||
}
|
||
|
||
osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size,
|
||
const osMessageQueueAttr_t *attr)
|
||
{
|
||
(void)attr;
|
||
s_q_new_calls++;
|
||
if ((s_q_new_fail != 0) || (s_q_create_fail_left > 0)) {
|
||
if (s_q_create_fail_left > 0) {
|
||
s_q_create_fail_left--;
|
||
}
|
||
return NULL;
|
||
}
|
||
s_q_cap = msg_count;
|
||
s_q_msgsz = msg_size;
|
||
s_q_head = 0u;
|
||
s_q_cnt = 0u;
|
||
/* 句柄本身没有意义:返回一个稳定非空指针即可 */
|
||
return (osMessageQueueId_t)&s_q_pool[0];
|
||
}
|
||
|
||
osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr,
|
||
uint8_t msg_prio, uint32_t timeout)
|
||
{
|
||
uint32_t tail;
|
||
|
||
(void)mq_id; (void)msg_prio; (void)timeout;
|
||
if (s_q_put_calls++ > 20000) {
|
||
(void)fprintf(stderr, "[FATAL] osMessageQueuePut guard tripped\n");
|
||
exit(2);
|
||
}
|
||
if ((msg_ptr == NULL) || (s_q_cnt >= s_q_cap) || (s_q_msgsz > MQ_MAXMSG)) {
|
||
return osErrorResource; /* 队列满:真实 CMSIS 在 timeout=0 时的返回 */
|
||
}
|
||
tail = (s_q_head + s_q_cnt) % MQ_SLOTS;
|
||
(void)memcpy(s_q_pool[tail].data, msg_ptr, s_q_msgsz);
|
||
s_q_pool[tail].used = 1;
|
||
s_q_cnt++;
|
||
return osOK;
|
||
}
|
||
|
||
osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr,
|
||
uint8_t *msg_prio, uint32_t timeout)
|
||
{
|
||
(void)mq_id; (void)msg_prio; (void)timeout;
|
||
if (s_q_get_calls++ > 20000) {
|
||
(void)fprintf(stderr, "[FATAL] osMessageQueueGet guard tripped\n");
|
||
exit(2);
|
||
}
|
||
if ((msg_ptr == NULL) || (s_q_cnt == 0u)) {
|
||
return osErrorResource; /* 队列空 */
|
||
}
|
||
(void)memcpy(msg_ptr, s_q_pool[s_q_head].data, s_q_msgsz);
|
||
s_q_pool[s_q_head].used = 0;
|
||
s_q_head = (s_q_head + 1u) % MQ_SLOTS;
|
||
s_q_cnt--;
|
||
return osOK;
|
||
}
|
||
|
||
uint32_t osMessageQueueGetCount(osMessageQueueId_t mq_id)
|
||
{
|
||
(void)mq_id;
|
||
return s_q_cnt;
|
||
}
|
||
|
||
osStatus_t osMessageQueueReset(osMessageQueueId_t mq_id)
|
||
{
|
||
(void)mq_id;
|
||
s_q_head = 0u;
|
||
s_q_cnt = 0u;
|
||
return osOK;
|
||
}
|
||
|
||
/* ===================== 内核状态开关 ===================== */
|
||
|
||
static osKernelState_t s_kernel_state = osKernelInactive;
|
||
|
||
osKernelState_t osKernelGetState(void)
|
||
{
|
||
return s_kernel_state;
|
||
}
|
||
|
||
/* ===================== 复位与辅助 ===================== */
|
||
|
||
static void reset_all(void)
|
||
{
|
||
ff_fake_reset();
|
||
locks_clear();
|
||
queue_clear();
|
||
s_clock_now = 1760234567u;
|
||
s_clock_valid = 1;
|
||
s_kernel_state = osKernelInactive;
|
||
g_soe_err_count = 0;
|
||
g_soe_info_count = 0;
|
||
g_soe_debug_count = 0;
|
||
/* 生产模块的 static 在场景之间会残留:逐位显式清,漏一次会红一片
|
||
*(test/host/lftpd_io 与 nand_port 同一教训) */
|
||
(void)memset(s_ring, 0, sizeof(s_ring));
|
||
s_ring_head = 0u;
|
||
s_ring_tail = 0u;
|
||
s_queue = NULL;
|
||
s_seq = 0u;
|
||
s_drop_cnt = 0u;
|
||
s_drop_logged = 0u;
|
||
s_fault_reported = 0u;
|
||
/* 生产语义前置:/log 由 defaultTask 挂载时建好,本模块假定它在(soe_store.h:
|
||
* "由 syslog_drain 在卷就绪门之后挂接") */
|
||
(void)ff_fake_mkdir_direct("/log");
|
||
}
|
||
|
||
/* 用假卷 + 真 soe_rec_parse 校验落盘内容:路径常量取自生产宏 SOE_PATH */
|
||
static int read_rec(uint32_t idx, soe_rec_t *p_out)
|
||
{
|
||
uint8_t raw[SOE_REC_SIZE];
|
||
|
||
if (ff_fake_fetch(SOE_PATH, raw, idx * SOE_REC_SIZE, SOE_REC_SIZE) != SOE_REC_SIZE) {
|
||
return 0;
|
||
}
|
||
return (soe_rec_parse(raw, p_out) == 0) ? 1 : 0;
|
||
}
|
||
|
||
/* ===================== TC-SOESTORE-001..007:post 侧 ===================== */
|
||
|
||
static void tc_post_side(void)
|
||
{
|
||
soe_rec_t out;
|
||
uint32_t i;
|
||
int ok;
|
||
|
||
printf("-- TC-SOESTORE-001..007 post side (zero-IO contract) --\n");
|
||
|
||
/* 001:post 3 条只写环,绝不碰盘、绝不碰卷锁、绝不打日志 */
|
||
reset_all();
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 0x1111u);
|
||
soe_store_post((uint8_t)SOE_T_ELM_RETURN, 2u, 0x2222u);
|
||
soe_store_post((uint8_t)SOE_T_PLATE_IN, SOE_ELM_NONE, 1u);
|
||
ok = (s_ring_head == 3u) && (s_ring_tail == 0u) && (s_drop_cnt == 0u);
|
||
for (i = 0u; i < (uint32_t)FF_FAKE_OP_N; i++) {
|
||
if (g_ff_obs.calls[i] != 0) {
|
||
ok = 0;
|
||
}
|
||
}
|
||
CHECK(ok && (s_lock_waits == 0) && (g_soe_err_count == 0) && (g_soe_info_count == 0),
|
||
"TC-SOESTORE-001 post does zero FatFs / zero volume lock / zero logging (protectTask stack budget)");
|
||
|
||
/* 002:墙钟未建立 -> IV + epoch 字段 0(不造假时标) */
|
||
reset_all();
|
||
s_clock_valid = 0;
|
||
soe_store_post((uint8_t)SOE_T_SAMPLE_BLOCK, 1u, 0xF0u);
|
||
soe_store_drain(); /* 环 -> 卷:不 drain 就还在 RAM 里 */
|
||
ok = read_rec(0u, &out);
|
||
CHECK(ok && (out.flags & SOE_FLG_IV) != 0u && (out.epoch_s == 0u)
|
||
&& (out.cp56[2] & 0x80u) != 0u && (out.value == 0xF0u),
|
||
"TC-SOESTORE-002 no wall clock -> flags.IV + epoch=0 + CP56 IV bit (B9 rule)");
|
||
|
||
/* 003:seq 单调;丢弃的那条也占号(真机丢号 = 主机看到的号不连续是**如实**) */
|
||
reset_all();
|
||
for (i = 0u; i < 9u; i++) {
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i);
|
||
}
|
||
ok = (s_seq == 9u) && (s_drop_cnt == 1u);
|
||
CHECK(ok, "TC-SOESTORE-003 seq increases monotonically, dropped entries still consume a number");
|
||
|
||
/* 004:环满丢弃不阻塞、不崩、**不打日志**(留痕归 drain 侧) */
|
||
reset_all();
|
||
for (i = 0u; i < 9u; i++) {
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i);
|
||
}
|
||
CHECK((s_ring_head == 8u) && (s_drop_cnt == 1u)
|
||
&& (g_soe_err_count == 0) && (g_soe_info_count == 0),
|
||
"TC-SOESTORE-004 ring-full drop: count only, zero DBG on post side (lesson: +320B tail chain)");
|
||
|
||
/* 005:内核运行 + 队列已建 -> 走队列,环不再写 */
|
||
reset_all();
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); /* 先落一条,drain 会建队列 */
|
||
s_kernel_state = osKernelRunning;
|
||
soe_store_drain(); /* 建队列 + 落第 1 条 */
|
||
s_ring_head = 0u; /* 观察用:清环位 */
|
||
s_ring_tail = 0u;
|
||
soe_store_post((uint8_t)SOE_T_ELM_RETURN, 1u, 2u);
|
||
CHECK((s_ring_head == 0u) && (s_q_cnt == 1u) && (s_q_msgsz == SOE_REC_SIZE),
|
||
"TC-SOESTORE-005 queue created + kernel running -> entries go to the queue, ring not written");
|
||
|
||
/* 006:队列满 -> 丢弃计数(与环满同一条可观测出口) */
|
||
reset_all();
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||
s_kernel_state = osKernelRunning;
|
||
soe_store_drain(); /* 建队列 */
|
||
(void)osMessageQueueReset(s_queue);
|
||
for (i = 0u; i < SOE_QUEUE_MAX + 2u; i++) {
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i);
|
||
}
|
||
CHECK((s_q_cnt == SOE_QUEUE_MAX) && (s_drop_cnt == 2u),
|
||
"TC-SOESTORE-006 queue full -> drop and count through the same soe_store_dropped exit");
|
||
|
||
/* 007:计数饱和不回绕 */
|
||
reset_all();
|
||
s_drop_cnt = UINT32_MAX;
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||
CHECK((s_drop_cnt == UINT32_MAX) && (soe_store_dropped() == UINT32_MAX),
|
||
"TC-SOESTORE-007 drop counter saturates at UINT32_MAX, never wraps");
|
||
}
|
||
|
||
/* ===================== TC-SOESTORE-010..021:drain 落盘与故障注入 ===================== */
|
||
|
||
static void tc_drain_paths(void)
|
||
{
|
||
soe_rec_t out;
|
||
uint32_t i;
|
||
int ok;
|
||
int opens_before;
|
||
int writes_before;
|
||
|
||
printf("-- TC-SOESTORE-010..021 drain to disk / fault injection --\n");
|
||
|
||
/* 010:首次 drain 惰性建队列 + 1 条 64B 落盘且能解析回来 */
|
||
reset_all();
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 3u, 0xABCDu);
|
||
soe_store_drain();
|
||
ok = (s_q_new_calls == 1) && (s_q_msgsz == SOE_REC_SIZE)
|
||
&& (ff_fake_size(SOE_PATH) == SOE_REC_SIZE);
|
||
CHECK(ok && read_rec(0u, &out) && (out.type == (uint8_t)SOE_T_ELM_ASSERT)
|
||
&& (out.elm_id == 3u) && (out.value == 0xABCDu) && (out.seq == 0u),
|
||
"TC-SOESTORE-010 lazy queue creation + fixed 64B record on disk + record decodes back");
|
||
|
||
/* 011:打开模式必须带 FA_WRITE —— 陷阱一 */
|
||
reset_all();
|
||
soe_store_post((uint8_t)SOE_T_PLATE_OUT, SOE_ELM_NONE, 0u);
|
||
soe_store_drain();
|
||
ok = (g_ff_obs.last_open_mode == (BYTE)(FA_WRITE | FA_OPEN_APPEND));
|
||
/* 同一断言的"为什么必须带写位"保真对照:只给 FA_OPEN_APPEND 的句柄 f_write
|
||
* 必须拿 FR_DENIED——假件如果哪天放行,这条会红,整套陷阱一断言随之失效 */
|
||
{
|
||
static FIL probe;
|
||
static uint8_t payload[SOE_REC_SIZE];
|
||
UINT bw = 0u;
|
||
FRESULT fr2;
|
||
|
||
fr2 = f_open(&probe, SOE_PATH, FA_OPEN_APPEND);
|
||
{
|
||
FRESULT fr3 = f_write(&probe, payload, SOE_REC_SIZE, &bw);
|
||
CHECK(ok && (fr2 == FR_OK) && (fr3 == FR_DENIED) && (bw == 0u),
|
||
"TC-SOESTORE-011 mode must be FA_WRITE|FA_OPEN_APPEND; without the write bit f_write is FR_DENIED");
|
||
}
|
||
(void)f_close(&probe);
|
||
}
|
||
|
||
/* 012:多条 = 一条一次 open/write/close(本模块刻意没有批量合并)+ FIFO */
|
||
reset_all();
|
||
for (i = 0u; i < 4u; i++) {
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)(i + 1u), 100u + i);
|
||
}
|
||
soe_store_drain();
|
||
ok = (ff_fake_size(SOE_PATH) == 4u * SOE_REC_SIZE)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 4)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 4);
|
||
if (ok) {
|
||
for (i = 0u; i < 4u; i++) {
|
||
if (!read_rec(i, &out) || (out.seq != i) || (out.value != 100u + i)) {
|
||
ok = 0;
|
||
}
|
||
}
|
||
}
|
||
CHECK(ok, "TC-SOESTORE-012 per-entry open/write/close (no batch merge) + correct FIFO order");
|
||
|
||
/* 013:open/close 严格配对 */
|
||
reset_all();
|
||
soe_store_post((uint8_t)SOE_T_SAMPLE_RESUME, SOE_ELM_NONE, 0u);
|
||
soe_store_drain();
|
||
CHECK(g_ff_obs.calls[FF_FAKE_OP_CLOSE] == g_ff_obs.calls[FF_FAKE_OP_OPEN],
|
||
"TC-SOESTORE-013 open/close stay paired (no stale handle left across calls)");
|
||
|
||
/* 014:短写判失败 -> unlink 重建 -> 重试成功 -> 恰好 64 字节 */
|
||
reset_all();
|
||
ff_fake_inject_write_short(SOE_PATH, SOE_REC_SIZE - 1u, 1);
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 7u, 0x555Au);
|
||
soe_store_drain();
|
||
CHECK((ff_fake_size(SOE_PATH) == SOE_REC_SIZE)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] >= 1)
|
||
&& (g_ff_obs.last_write_bw == SOE_REC_SIZE)
|
||
&& (g_soe_err_count == 0),
|
||
"TC-SOESTORE-014 FR_OK plus short write counts as failure (63B would pass silently without check)");
|
||
|
||
/* 015:连续**两轮** drain 都失败 -> 留痕只许 1 次(一轮 = 首写 + 自愈重试
|
||
* = 2 次 f_write,所以注入 4 次零字节短写才够撑两轮) */
|
||
reset_all();
|
||
ff_fake_inject_write_short(SOE_PATH, 0u, 4);
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||
soe_store_drain(); /* 第 1 轮失败:留痕 1 次,环不推进 */
|
||
ok = (g_soe_err_count == 1) && (s_ring_tail == 0u) && (s_fault_reported == 1u);
|
||
soe_store_drain(); /* 第 2 轮仍失败:不得再留痕 */
|
||
CHECK(ok && (g_soe_err_count == 1) && (s_ring_tail == 0u),
|
||
"TC-SOESTORE-015 write-fault trace is de-duplicated (one fault must not flood UART and selftest)");
|
||
|
||
/* 015b:成功一次复位去重位,之后再失败才会重新留痕 */
|
||
/* 015b:失败(留痕1) -> 恢复(去重位复位) -> 再失败(必须留痕2)。
|
||
* 少了"成功复位"这一步,一次真机坏块就会让后续所有写故障永久静默。 */
|
||
reset_all();
|
||
ff_fake_inject_write_short(SOE_PATH, 0u, 2);
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||
soe_store_drain(); /* 失败 -> 留痕 1 */
|
||
soe_store_drain(); /* 注入耗尽 -> 成功 -> 复位 */
|
||
ok = (g_soe_err_count == 1) && (s_fault_reported == 0u) && (s_ring_tail == 1u);
|
||
ff_fake_inject_write_short(SOE_PATH, 0u, 2);
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 2u);
|
||
soe_store_drain(); /* 又失败 -> 留痕 2 */
|
||
CHECK(ok && (g_soe_err_count == 2) && (s_fault_reported == 1u),
|
||
"TC-SOESTORE-015b a failure after recovery must trace again (missing reset = permanent silence)");
|
||
|
||
/* 016:卷忙 -> -2:不碰 FatFs、不 release、环不推进 */
|
||
reset_all();
|
||
lock_busy_next(1);
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||
soe_store_drain();
|
||
ok = (s_lock_waits == 1) && (s_lock_releases == 0) && (s_lock_seq_err == 0);
|
||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0)
|
||
&& (s_ring_tail == 0u) && (g_soe_err_count == 0)
|
||
&& (s_last_wait_to == 0u), /* 喂狗点绝不阻塞:取锁一律传 0 */
|
||
"TC-SOESTORE-016 volume busy means defer: zero FatFs, zero release, no fault counted (feed duty) + lock asked with timeout=0");
|
||
|
||
/* 017:写失败时环不推进 tail -> 下轮重试、既不丢也不重复 */
|
||
reset_all();
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 5u, 0x77u);
|
||
ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
|
||
soe_store_drain(); /* open 失败 -> -1,tail 不动 */
|
||
ok = (s_ring_tail == 0u) && (ff_fake_size(SOE_PATH) == 0u ||
|
||
ff_fake_exists(SOE_PATH) == 0);
|
||
soe_store_drain(); /* 下轮成功 */
|
||
CHECK(ok && (s_ring_tail == 1u) && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE)
|
||
&& read_rec(0u, &out) && (out.elm_id == 5u),
|
||
"TC-SOESTORE-017 ring write failure does not advance tail: nothing lost, no duplicate next round");
|
||
|
||
/* 018:队列条目已出队 -> 卷忙只丢当前一条,剩余留在队列(现状不对称) */
|
||
reset_all();
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||
s_kernel_state = osKernelRunning;
|
||
soe_store_drain(); /* 建队列 */
|
||
(void)osMessageQueueReset(s_queue);
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 10u, 10u);
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 11u, 11u);
|
||
opens_before = g_ff_obs.calls[FF_FAKE_OP_OPEN]; /* 前面那次 drain 已开过盘 */
|
||
writes_before = g_ff_obs.calls[FF_FAKE_OP_WRITE];
|
||
lock_busy_next(1);
|
||
soe_store_drain();
|
||
CHECK((s_q_cnt == 1u)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_before)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == writes_before)
|
||
&& (s_lock_waits >= 1) && (s_lock_seq_err == 0) && (s_q_get_calls >= 2)
|
||
&& (s_last_wait_to == 0u),
|
||
"TC-SOESTORE-018 queue path with busy volume: only the dequeued entry is lost, zero FatFs now + asked with timeout=0");
|
||
|
||
/* 019:容量满 -> unlink 重建 + INFO 留痕,新文件恰好 64 字节 */
|
||
reset_all();
|
||
CHECK(ff_fake_mkfile_size(SOE_PATH, (uint32_t)SOE_STORE_MAX * SOE_REC_SIZE) == 0,
|
||
"TC-SOESTORE-019 setup: lay down 1024 fake records (64KB)");
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 9u, 0x99u);
|
||
soe_store_drain();
|
||
CHECK((g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||
&& (ff_fake_size(SOE_PATH) == SOE_REC_SIZE)
|
||
&& (g_soe_info_count == 1) && read_rec(0u, &out) && (out.elm_id == 9u),
|
||
"TC-SOESTORE-019 full file is deleted and recreated: the new file holds only this entry");
|
||
|
||
/* 020:丢弃留痕按差值上报一次,不刷屏 */
|
||
reset_all();
|
||
for (i = 0u; i < SOE_RING_MAX + 3u; i++) {
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i); /* 环 8 + 丢 3 */
|
||
}
|
||
soe_store_drain(); /* 差值上报 1 条 */
|
||
soe_store_drain(); /* 无新丢弃 -> 不再报 */
|
||
CHECK((s_drop_cnt == 3u) && (s_drop_logged == 3u) && (g_soe_err_count == 1),
|
||
"TC-SOESTORE-020 drop trace reports the delta once (only the defaultTask side is log-safe)");
|
||
|
||
/* 021:队列创建失败 -> 条目滞留环、下轮再试、不崩 */
|
||
/* 021:osMessageQueueNew 返回 NULL —— 生产代码里环在队列**之前**排空,
|
||
* 所以条目不丢;留痕一次,内核已运行时后续 post 仍走环(s_queue 为空),
|
||
* 下一轮再建队列。真机上 heap_4 见底就是这种形态。 */
|
||
reset_all();
|
||
s_kernel_state = osKernelRunning;
|
||
s_q_new_fail = 1;
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 4u, 4u);
|
||
soe_store_drain();
|
||
ok = (s_queue == NULL) && (g_soe_err_count == 1)
|
||
&& (s_ring_tail == 1u) && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE);
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 5u, 5u); /* 队列没建成 -> 仍进环 */
|
||
CHECK(ok && (s_q_cnt == 0u) && (s_ring_head == 2u),
|
||
"TC-SOESTORE-021 queue creation failure: ring path still writes to disk, loses nothing + one trace");
|
||
s_q_new_fail = 0;
|
||
soe_store_drain();
|
||
CHECK((s_queue != NULL) && (s_ring_tail == 2u)
|
||
&& (ff_fake_size(SOE_PATH) == 2u * SOE_REC_SIZE) && (g_soe_err_count == 1),
|
||
"TC-SOESTORE-021b once the heap recovers the queue is created and all pending entries reach disk");
|
||
}
|
||
|
||
/* ===================== TC-SOESTORE-022..024:陷阱二 —— 名字/参数类错误码 ===================== */
|
||
|
||
static void tc_name_error_not_volume(void)
|
||
{
|
||
uint32_t i;
|
||
int ok;
|
||
|
||
printf("-- TC-SOESTORE-022..024 name/parameter errors must never escalate into volume operations --\n");
|
||
|
||
/* 022【D 项已修 2026-10-24:正向断言】f_open 失败(名字/参数类码)
|
||
* -> 只留痕返回 -1:**绝不 unlink**、不动 /log 目录、已有记录一条不少。
|
||
* 生产侧的定案是"打开失败不重试也不删",理由写在 s_soe_append 函数头:
|
||
* SOE 是事件序列,删文件 = 销毁已发生的事件。 */
|
||
reset_all();
|
||
(void)ff_fake_mkfile(SOE_PATH, NULL, 3u * SOE_REC_SIZE); /* 已有 3 条历史 */
|
||
(void)ff_fake_mkfile("/log/run.log", NULL, 0u); /* 邻居文件(同卷) */
|
||
ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, FR_INVALID_NAME, 2);
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||
soe_store_drain(); /* open 失败 */
|
||
ok = (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0)
|
||
&& (ff_fake_exists("/log") == 2)
|
||
&& (ff_fake_size(SOE_PATH) == 3u * SOE_REC_SIZE) /* 历史原封不动 */
|
||
&& (ff_fake_exists("/log/run.log") == 1)
|
||
&& (s_ring_tail == 0u) && (g_soe_err_count == 1);
|
||
CHECK(ok, "TC-SOESTORE-022 open failure keeps history: no unlink, entry stays queued");
|
||
|
||
/* 022b 逐码对齐(与 test/host/sys_log_mcu 的 TC-SYSLOG-101/102 同一张表):
|
||
* SOE 侧对**所有**打开失败码都不 unlink,是 run.log 侧触发集的保守子集
|
||
* ——介质类码在 run.log 侧会 unlink 重建,在 SOE 侧不会,这是刻意的差异
|
||
* (事件序列不可销毁),两处注释同源、改动必须同步。 */
|
||
{
|
||
static const FRESULT codes[] = { FR_INVALID_NAME, FR_NO_PATH, FR_NO_FILE,
|
||
FR_INVALID_OBJECT, FR_DISK_ERR, FR_INT_ERR,
|
||
FR_NO_FILESYSTEM, FR_DENIED };
|
||
uint32_t c;
|
||
int all_ok = 1;
|
||
for (c = 0u; c < (uint32_t)(sizeof(codes) / sizeof(codes[0])); c++) {
|
||
reset_all();
|
||
(void)ff_fake_mkfile(SOE_PATH, NULL, SOE_REC_SIZE);
|
||
ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, codes[c], 1);
|
||
soe_store_post((uint8_t)SOE_T_ELM_RETURN, (uint8_t)c, c);
|
||
soe_store_drain();
|
||
if ((g_ff_obs.calls[FF_FAKE_OP_UNLINK] != 0)
|
||
|| (ff_fake_size(SOE_PATH) != SOE_REC_SIZE)
|
||
|| (s_ring_tail != 0u)) {
|
||
all_ok = 0;
|
||
}
|
||
}
|
||
CHECK(all_ok, "TC-SOESTORE-022b no open-failure code ever unlinks soe.log (8 codes)");
|
||
}
|
||
|
||
/* 023:反复失败的自愈上限:每轮最多 unlink 一次,条目与卷都不被吞 */
|
||
reset_all();
|
||
(void)ff_fake_mkfile("/log/run.log", NULL, 0u);
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 2u);
|
||
for (i = 0u; i < 5u; i++) {
|
||
ff_fake_inject_write_short(SOE_PATH, 0u, 2); /* 每轮两次短写都失败 */
|
||
soe_store_drain();
|
||
}
|
||
ok = (g_ff_obs.calls[FF_FAKE_OP_UNLINK] <= (int)(5 * 1))
|
||
&& (ff_fake_exists("/log") == 2) && (ff_fake_exists("/log/run.log") == 1)
|
||
&& (s_ring_tail == 0u);
|
||
ff_fake_inject_write_short(SOE_PATH, 0u, 0); /* 清掉不再需要的注入 */
|
||
(void)ok;
|
||
CHECK(ok, "TC-SOESTORE-023 repeated failures: unlink stays bounded (<=1 per round), never a volume action");
|
||
|
||
/* 024:假卷严格 8.3 校验的保真自检 —— 生产路径必过、点开头必被拒 */
|
||
reset_all();
|
||
{
|
||
static FIL probe;
|
||
FRESULT fr_ok;
|
||
FRESULT fr_dot;
|
||
FRESULT fr_colon;
|
||
|
||
fr_ok = f_open(&probe, SOE_PATH, FA_WRITE | FA_OPEN_APPEND);
|
||
(void)f_close(&probe);
|
||
fr_dot = f_open(&probe, "/log/.fschk", FA_WRITE | FA_CREATE_ALWAYS);
|
||
if (fr_dot == FR_OK) {
|
||
(void)f_close(&probe);
|
||
}
|
||
fr_colon = f_open(&probe, "1:/log/soe.log", FA_WRITE | FA_OPEN_APPEND);
|
||
if (fr_colon == FR_OK) {
|
||
(void)f_close(&probe);
|
||
}
|
||
CHECK((fr_ok == FR_OK)
|
||
&& ((fr_dot == FR_INVALID_NAME) || (fr_dot == FR_INVALID_DRIVE))
|
||
&& (fr_colon == FR_INVALID_DRIVE),
|
||
"TC-SOESTORE-024 fake fidelity: /log/soe.log passes 8.3, .fschk and a bad drive prefix are rejected");
|
||
}
|
||
/* 生产路径本身合规的正向断言:改名成非 8.3 会让上面那条保真自检变红 */
|
||
reset_all();
|
||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||
soe_store_drain();
|
||
CHECK((strcmp(g_ff_obs.last_open_path, "/LOG/SOE.LOG") == 0)
|
||
&& (g_ff_obs.last_open_rc == (int)FR_OK),
|
||
"TC-SOESTORE-024b SOE_PATH really opens on a strict 8.3 volume (guard against silent write failure)");
|
||
}
|
||
|
||
/* ===================== 入口 ===================== */
|
||
|
||
int main(void)
|
||
{
|
||
printf("=== SOE Store Tests ===\n");
|
||
|
||
tc_post_side();
|
||
tc_drain_paths();
|
||
tc_name_error_not_volume();
|
||
|
||
printf("\n=== SOE Store Tests: %d/%d PASSED (failed=%d) ===\n",
|
||
s_passed, s_total, s_failed);
|
||
return (s_failed == 0) ? 0 : 1;
|
||
}
|