Files
STM32F4-Base/test/host/soe_store/soe_store_test.c
T
wandering1 b623a406ad fix(栈·存储·UI 收尾): 断掉临界区↔断言幻影环回到 1,464,标定门控单点化,卷锁五态可观测
这四笔在主树里已经互相纠缠(同一次 @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 表逐项一致;本次运行不含任何未提交固件源码之外的新改动。
2026-10-26 23:31:05 +08:00

749 lines
34 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.
/*
* 模块名称: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;
}