Files
STM32F4-Base/test/host/soe_store/soe_store_test.c
T
wandering1 794906cdc8 test(host): P2 覆盖率批次,5 套主机单测与共享假 FatFs
P2 五个文件全部进分母(三名队友并行,写范围互斥、零越界):
  net_socket 420 条 / 881-883=99.8%、52-52 函数(S1~S6 分片)
  sys_log_mcu 75 条 / 310-313=99.0%、12-12
  console_log 104 条 / 188-189=99.5%、15-15
  cfg_file_store_mcu 34 条 / 97-97=100%、10-10
  soe_store 26 条 / 78-79=98.7%、4-4
合计 659 条断言,P2 本体 1554-1561=99.6%、93-93 函数。

账:套件 28→33 全绿;files_tested 32→37;未触及 112→107(.c 34→29,
恒等式 29 = 0(P1剩) + 0(P2) + 29(P3) 咬合,§9 终点判据达成);分母
4684→6245 插桩行;line 85.8%→89.3%;func 90.1%→92.2%(这批 5 个文件
全都以 ≥98.7% 进分母,故未摊薄)。--update-baseline 前后逐文件比对:
下降 0 个文件。

基建:新建 test/host/common/fatfs_fake.{h,c}(以真实 ff.h 为声明源、只
定义三文件实际调用的 9 个 f_*,缺哪个链接期响;卷忙/短写/FR_DENIED
可注入;短名违规分档);扩展 ch395f_fake 为纯新增(g_fx/g_fxchip 另开
一份世界 + ext_reset 挂进既有复位,既有三套面一字不动),并按真头修正
两个错值常量(SOCKET_OPEN 0x01→0x05、TCP_ESTABLISHED 0x05→0x04),
另在 net_socket runner 加真头/假件同名常量文本核对闸(负例验证过)。
存量回归:net_listener 34、net_events 34、net_pending 43、net_select 22、
nand_port 149、lftpd_io 33、vol_lock 33、soe 32、cfg_file 151 全绿。

编号:补 TC-HOST-{NETSOCK,SYSLOG,CFGFST,SOESTORE,CONLOG} 五族(22→27,
先补族再引号);net_socket 模块码由自取的两字母 NS 正名为 NETSOCK;
id-check 增量拦截无台账外新账,可判定比例 83.27%(基线 82.6%)。

文档:覆盖率口径 §9 新增「P2 落地」(含两套分母口径差、10 条现状表、
12 条可复用手法与并发红线),§8 第 1 条改「清单已吃穿」,补变更记录。
本提交未改任何生产代码(App/Drivers/Src/Lib/MDK-ARM 零触碰)。

10 条现状已逐条登记在 §9(含 sys_log_mcu.c:509~537 卷忙整批丢日志、
net_socket.c 同文件两套 IP 字节序、UDP 两 API 绕过 netTask),已获指示
全部修复,另开一轮(含测试字面量统一纯 ASCII)。

gate: pre-commit run --all-files → 39 hooks, 38 Passed; only mem-numbers
Failed(本工作树从未 @build,无 .map 事实源,属分支期已知陷阱);
mem-budget Passed;test-coverage 与 id-check Passed。RAM/Flash 增量 = 0
(纯测试批次)。按仓内分支约定走 --no-verify,台账回填留待主树合并后。
2026-10-24 17:54:02 +08:00

709 lines
31 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 目录、条目全部保留(自愈只针对自己的文件)
* 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 退出,不存在"走到行尾再回跳"的机器码,属工具口径缺口。
*
* 本套照出的现状(只登记、不改生产代码;详见交 Lead 的报告):
* soe_store.c 的函数头第 ④ 条写"打开/写入结构性错误 → unlink 重试一次(同 run.log)",
* 但实现里 **f_open 失败直接 return -1、不 unlink 重试**(只有 f_write 失败才重试)。
* run.log 侧(sys_log_mcu.c s_append_rec)则对 FR_NO_FILE/FR_NO_PATH/FR_INVALID_NAME
* 等一整套码做 unlink+重开。两套并不一致 —— 是注释夸大了实现,还是实现漏了一半,
* 需生产侧裁定;本套只用 TC-SOESTORE-022 把现状钉成"改了会响"。
*
* 通过:全部 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 侧(零 I/O 契约)--\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 零 FatFs / 零卷锁 / 零日志(protectTask 栈预算)");
/* 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 无墙钟 -> flags.IV + epoch=0 + CP56 IV 位(B9 口径)");
/* 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 单调递增,丢弃也占号(号不跳才是假象)");
/* 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 环满丢弃:只计数,post 侧零 DBG(栈尾链 +320B 教训)");
/* 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 队列已建 + 内核运行 -> 条目进队列、环不再写");
/* 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 队列满 -> 丢弃并计数(soe_store_dropped 同一出口)");
/* 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 丢弃计数饱和:UINT32_MAX 不回绕");
}
/* ===================== 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 落盘 / 故障注入 --\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 惰性建队列 + 64B 定长落盘 + 记录可解回");
/* 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 模式必为 FA_WRITE|FA_OPEN_APPEND;缺写位 f_write 恒 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 逐条 open/write/close(无批量合并)+ FIFO 顺序正确");
/* 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 配对(脏句柄不跨调用残留)");
/* 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+短写被当失败处理(不校验字节数就会静默收下 63B)");
/* 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 写故障留痕去重(防串口与 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 恢复后再失败必须重新留痕(漏复位 = 永久静默)");
/* 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 卷忙 = 顺延:零 FatFs、零 release、不计故障(喂狗点纪律)");
/* 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 环条目写失败不推进 tail:不丢、下轮也不重复落");
/* 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 队列路径卷忙:只丢已出队那一条、本轮零 FatFs(与环路径不对称)");
/* 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 前置:铺 1024 条(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 满则删除重建:新文件只剩本次这一条");
/* 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 丢弃留痕按差值上报一次(defaultTask 侧才是日志安全区)");
/* 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 队列创建失败:环路径照常落盘不丢条目 + 留痕一次");
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 堆恢复后队列补建、滞留条目全部落盘");
}
/* ===================== TC-SOESTORE-022..024:陷阱二 —— 名字/参数类错误码 ===================== */
static void tc_name_error_not_volume(void)
{
uint32_t i;
int ok;
printf("-- TC-SOESTORE-022..024 名字/参数类错误码严禁升级成卷操作 --\n");
/* 022:FR_INVALID_NAME 只让本条失败,**不得** unlink 别人、不得动目录 */
reset_all();
(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 失败 -> -1 */
ok = (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0)
&& (ff_fake_exists("/log") == 2)
&& (ff_fake_exists("/log/run.log") == 1)
&& (s_ring_tail == 0u) && (g_soe_err_count == 1);
CHECK(ok, "TC-SOESTORE-022 FR_INVALID_NAME -> 只留痕失败;不 unlink、目录与邻居文件不动");
/* 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 长跑反复失败:unlink 有界(<=1/轮),绝不升级为整卷动作");
/* 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 假件保真:/log/soe.log 过 8.3,.fschk 与坏盘符被拒");
}
/* 生产路径本身合规的正向断言:改名成非 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 在严格 8.3 卷上确实打得开(改名=静默写失败的防线)");
}
/* ===================== 入口 ===================== */
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;
}