Files
STM32F4-Base/test/host/soe_store/soe_store_test.c
T
wandering1 924ac3d428 test(host): 断言字面量统一纯 ASCII,30 文件 1535 行
用户裁定「统一」。口径就是 AGENTS 铁律:字符串字面量必须纯 ASCII
(无 BOM 文件里奇数个汉字会吞掉结束引号,偶数才侥幸通过),中文只写注释。
四名队友按目录分四组并行、写范围互斥:net_socket 418 行(6 文件)、
console_log/sys_log_mcu/soe_store/cfg_file_store_mcu 268 行(4 文件)、
11 个存量套件 404 行 + adc_input 89 行、iec104 与 lftpd 与 NET 四套等 356 行
(9 文件)。runner 的 .py 文档串不在口径内(Python 源不经 ARMCC 代码页)。

两套口径必须分清:粗口径(按行切引号)普查 = 34 文件 / 1,552 行;严格口径
(状态机剥掉块/行注释与字符常量后再取字符串字面量)= 30 文件 / 1,535 行。
差值那 4 个文件的命中全在注释里(ns_peers.c、net_socket/stub/ns_rtos.h、
common/ch395f_fake.h、common/fatfs_fake.c)——按口径一处都不该改,改了等于
把中文从注释里搬走、正好违背口径。粗口径绝不能当交付判据,否则要么白改
注释、要么留下「这行为什么没改」的悬案。

Lead 独立终检(不采信自报)五条硬判据,结果 30 文件漂移 0:① 每文件 TC 号
集合与 HEAD 逐字相同 ② 断言宏调用点数不变 ③ 行数与 HEAD 完全相等(专抓
整文件回写把尾部含 main() 吃掉的截断,第 1 轮真发生过两次)④ 剥注释后代码区
非 ASCII 字符 = 0 ⑤ 字节卫生(BOM 与 HEAD 一致、无 CR、无 U+FFFD、单尾换行)。
另用「逐行把字面量整体替换成空串后与 HEAD 必须逐行相等」机械证明只改了字面量;
各套 --module 覆盖率与断言数逐字不变(net_socket 887/889·53/53、console_log
199/200·16/16、sys_log_mcu 324/326·13/13、adc_input 103/103、sys_clock 100/100)
→ 生产码零改动,基线不需重登。

闸拦下一处真实回归(我的验收器没抓到):ns_s6_edges.c 翻译时把一个上一轮
已整改成平实描述的编号又写回了 CHECK 标签。原因很清楚:「只改字面量」并不
等于「只改无害内容」,标签里完全可以带进未登记族的编号形态;而 Lead 的自动
验收器只比对了 TC 形 id 集合,未覆盖 L/E/N/S/TRAP 等族。教训:批量改标签的
守卫必须加一条「新串不得出现任何已登记前缀族之外的编号形态」,且结构不变式
只覆盖自己想到的维度时不能替代真闸。
Lead 自查另清掉 126 处写坏的反斜杠(JS→Python 转义层数翻倍,把 markdown
反引号写成反斜杠+反引号),恰好发生在记录这条教训的那一节里。

文档:docs/测试说明/主机单测覆盖率口径.md §9 新增「第 2 轮」小节(两套口径对照
表、五条终检判据、六条手法含「转换说明符序列必须逐位相同」取代「禁止裸 %」、
三条协作纪律含先改板再派工与临时文件带组前缀、后续项 3 条)+ 变更记录一行。

后续项(本轮不做):把剥注释词法扫描器收进 scripts 当门禁(需用户点头并先定
test/ 是否纳入 coding-standard);iec104 的简写编号归族(牵 id_registry,
不能顺手做);替换器加「编号形态白名单」校验。

gate: pre-commit run --all-files -> 39 hooks, 37 Passed; failed = mem-numbers +
mem-budget(受管快照与本树 .map 的漂移,属第 1 轮生产码改动的遗留,非本批
增量)。本批只碰 test/host/**,不进 Keil 编译面也不进模拟器 CMake,**RAM/Flash
增量 = 0**;实测水位 RW_IRAM1 57.4%、RW_CCM 87.9%,均低于 92% 红线。
id-check 0 台账外新账(可判定 84.63% > 基线 82.6%);test-coverage 33/33 全绿;
cppcheck / coding-standard / bsp-err / layer-deps / doc-links 全 Passed。
按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
2026-10-24 21:00:17 +08:00

743 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 的 N-24 不同),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; /* 未持锁却 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;
}
int vol_lock_init(void) { return 0; }
int vol_lock_wait(uint32_t timeout_ms)
{
int r;
(void)timeout_ms;
/* 跑飞保险:调用数越界直接判死,不让死循环拖挂门禁 */
if (s_lock_waits > 20000) {
(void)fprintf(stderr, "[FATAL] vol_lock_wait 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),
"TC-SOESTORE-016 volume busy means defer: zero FatFs, zero release, no fault counted (feed duty)");
/* 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),
"TC-SOESTORE-018 queue path with busy volume: only the dequeued entry is lost, zero FatFs now");
/* 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;
}