Files
STM32F4-Base/test/host/soe_store/soe_store_test.c
T
wandering1 04997e48aa refactor(编号): test/host 与 App/Drivers 注释去测试层级号(73 站点 / 64 文件)
同族第二道:代码注释面。41 个 run_unit_test.py 文件头与引用行、common harness 与两份
README、4 个 *_test.c 文件头、App/** 14 处(含 BOM 文件 App/cfg/cfg_file.c)、
GD5F nand_ftl.c 1 处,共 73 站点 / 64 文件;措辞取「主机侧纯逻辑单元测试运行器
(纯标准库)」避免同行「主机侧」重复。

零行为改动(机器证据,不是口头):
- 17 个 .c/.h 剥掉注释后与 HEAD **逐字节相同**;
- 42 个 .py 走 ast.dump 比较、仅归一 docstring(真实字符串字面量参与比较,故旗标或
  输出文本被动过必然报红)→ 全部相等;behaviour_changed=0。
- py_compile 42/42;抽跑 watchdog 47/47、freq 46/46(动手前先取基线,改后同数);
  CR 字节 0、EF BF BD 0、BOM 与 HEAD 逐一一致、check_bom_drift rc=0。

刻意未动:6 处字符串字面量(FTP 应答 "215 UNIX Type: L8"、lftpd_test.c SYST 断言、
console_log_test.c 的 "L1\r\n"、phasor_test.c 的 "L2-in ring" 文案与 printf 标题)——
改了就是改测试输出/行为;UI 设计令牌层 L0(ui_tokens.h、ui_content_page.c)与
test/host/README.md 的算法层号是**另外的族**。

两条 SyntaxWarning(ftp_cfg / net_cfg 文件头 \S 转义)**HEAD 同样存在**,已对 HEAD 副本
复现证明非本次引入,未顺手改。
2026-10-28 17:52:55 +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)主机侧纯逻辑单元测试
* 模块功能:在 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;
}