Files
STM32F4-Base/test/host/sys_log_mcu/sys_log_mcu_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

1535 lines
68 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.
/*
* 模块名称:运行/自检记录落盘后端(sys_log_mcu)主机侧单元测试(L0.5)
* 模块功能:在 PC 上离线验证 App/log/sys_log_mcu.c 的四族契约——
* A 64B 定长记录格式(时间戳 / epoch ASCII / 描述三段 + 截断 + 占位)
* B 写侧(单条 s_append_rec、批量 s_append_many、N-24 的"一批一次
* open/write/close"、容量轮转、L1 自愈、L2 留痕去重、卷锁 try)
* C 排空(syslog_drain 的 preboot + 队列两条来源、换文件分批、
* 失败回投不丢条目、-3 退回慢路径)
* D 读侧(N-24 的 8 条滑动窗口 + 写者代数失效 + 越界/忙/短读处置)
* 外加 syslog_mcu_mounted 的 挂载幂等与上电/重启分类、sys_log_evidence
* 的 wdev.log 取证通道(8KB 护栏 + 第二条写必须在释放卷锁之后)。
*
* 被测对象:真实源文件 App/log/sys_log_mcu.c(#include 原样纳入,不改生产代码)。
* 假件与桩:
* test/host/common/fatfs_fake.{h,c} —— 假 FatFs 内存卷(真实 ff.h 声明)
* soe_rec.c 不纳入;soe_store_drain() 用**计数桩**(B9 挂接点本身由
* test/host/soe_store/ 那套测,本套只断言"每次 drain 各调一次")
* stub/{cmsis_os,dbg_log,main}.h 本目录桩;sys_clock_* / vol_lock_* /
* osMessageQueue* 的可控强符号实现写在本文件里
*
* 为什么要主机侧单测(而不是只用台架)——本文件的四条坑全是"日志看不出来"型的:
* 1. **FA_OPEN_APPEND 不含 FA_WRITE**(§10.1 第 11 条):漏写位时 f_open 会成功
* (文件甚至被创建出来),只有 f_write 恒返 FR_DENIED(7)。真机表现是
* "/log 写失败 x5/开机",2026-09-20 才定位到。→ 用假卷把这条做成必然失败的断言。
* 2. **f_write 簇分配失败 = FR_OK + 短写**:只判 fr != FR_OK 就等于把丢记录当成功。
* 3. **挂载幂等**:syslog_mcu_mounted 曾在建 /log 失败后永久 no-op(运行期日志与自检
* 全部滞留),且 2s 重试会刷屏 → 幂等 + 留痕去重都要注入 mkdir 失败才测得到。
* 4. **读窗口 try 语义 / N-24 读侧**:读窗口"命中即零 I/O"与"写者一落地就必须失效重填"是同一
* 枚硬币的两面——只做前者会把陈旧读当正确,只做后者会退化成逐条 open/seek/close
* 的旧实现(UI 翻 20 条 = 20 次 FAT 往返)。两者都靠假卷的调用计数才断言得了。
*
* 覆盖点(TC 号族 SYSLOG,形如 TC-SYSLOG-001,族内按功能分档;缺口在小节末尾如实登记):
* —— A 记录格式 ——
* TC-SYSLOG-001 布局:[0..18] 时间戳、[19]=' '、[20..29]=epoch "%010u"、[30]=' '、
* [31..] 描述、尾部 NUL 填充到 64B
* TC-SYSLOG-002 描述超长截断到 SYSLOG_DESC_MAX,且不越 64B 边界
* TC-SYSLOG-003 墙钟未建立:get_str 返 NULL -> 占位 "1970-01-01 08:00:00";
* get() 返 UINT32_MAX -> epoch 字段落 "0000000000"
* TC-SYSLOG-004 单条路径与批量路径产出的同内容记录**逐字节相同**(N-24 抽出
* s_format_rec 的全部理由:UI 按定长字段偏移解析)
* —— B 写侧 ——
* TC-SYSLOG-010 单条落盘成功 + 打开模式必须 FA_WRITE|FA_OPEN_APPEND;close 配对
* TC-SYSLOG-011 只给 FA_OPEN_APPEND 的对照保真:f_write 恒 FR_DENIED(陷阱一)
* TC-SYSLOG-012 卷忙(try 失败)-> 返回 -2、零 FatFs、不计故障、不 release
* TC-SYSLOG-013 FR_OK + 短写 -> 判失败 -> unlink 重建重试 -> 恰好 64 字节
* TC-SYSLOG-014 容量满(1024 条)-> 先 unlink 再写;新文件只剩本次一条
* TC-SYSLOG-015 满且 unlink 失败(真错误码)-> 返回 -1 并释放卷锁
* TC-SYSLOG-016 满且 unlink 返 FR_NO_FILE -> 视为无害,继续写
* TC-SYSLOG-017 无效 id / 空指针守卫 -> -1
* TC-SYSLOG-018 写成功 -> 读侧代数 s_rd_gen 自增(窗口失效钩子)
* TC-SYSLOG-019 连续两轮写失败只留痕一次;成功一次复位后再失败才再留痕
* TC-SYSLOG-020 批量:8 条 = 一次 open + 一次 512B write + 一次 close
* TC-SYSLOG-021 批量入参守卫(id 越界 / n=0 / n>8 / 空指针)-> -1
* TC-SYSLOG-022 剩余容量装不下一批 -> 返回 -3 且**不 unlink**(轮转只在慢路径)
* TC-SYSLOG-023 批量卷忙 -> -2;批量短写 -> unlink 重试成功
* TC-SYSLOG-024 批量持续失败 -> -1 + 留痕去重(与单条同一判据)
* —— C 排空 ——
* TC-SYSLOG-030 卷未就绪 -> drain 直接返回,preboot 条目滞留不丢
* TC-SYSLOG-031 首次 drain 惰性建队列(内核对象在 osKernelStart 之后)
* TC-SYSLOG-032 preboot 8 条同文件 -> 一批 512B 落盘 + preboot 计数清零
* TC-SYSLOG-033 两个文件混排 -> 按文件分批,顺序不乱、条目不丢
* TC-SYSLOG-034 换文件那条不消耗(preboot 下标回退 / 队列回投)
* TC-SYSLOG-035 卷忙 -> 整批回投队列,下一轮全部落盘(旧实现 -2 直接丢一条)
* TC-SYSLOG-036 队列满回投失败 -> "hold-over drop" 留痕,不崩
* TC-SYSLOG-037 -3 -> 逐条慢路径,容量轮转只发生一次
* TC-SYSLOG-038 慢路径中途写失败 -> 该条丢弃留痕 + 尾部回投
* TC-SYSLOG-039 批量整体 -1 -> 整批丢弃留痕一次并结束本轮
* TC-SYSLOG-040 drain 尾部各调一次 soe_store_drain()(B9 挂接点)
* TC-SYSLOG-041 post:内核未运行走 preboot、满则丢弃留痕;内核运行走队列、满则留痕;
* id 越界/NULL 描述静默丢弃
* —— D 读侧 ——
* TC-SYSLOG-050 count = filesize/64(O(1));文件缺失/未挂载 -> 0
* TC-SYSLOG-051 窗口命中 -> 第二次读**零 FatFs 调用**
* TC-SYSLOG-052 写者落地 -> 代数变化 -> 窗口重填并读到新内容
* TC-SYSLOG-053 窗口按 8 条对齐:跨窗读 idx=8 -> 一次 open + lseek(512) + read(64)
* TC-SYSLOG-054 越界 idx -> -1;空文件 -> -1
* TC-SYSLOG-055 读时卷忙 -> -1(读窗口 try 语义:UI 每 100ms 喂心跳,绝不阻塞)
* TC-SYSLOG-056 短读 -> 作废窗口(s_rd_win_cnt=0),不把半窗当有效命中
* TC-SYSLOG-057 get_record:缓冲容量守卫 + 描述按 desc_len 截断
* TC-SYSLOG-058 last_epoch:无记录 -1;有记录解析 10 位 ASCII
* —— E mounted / 上电重启分类 ——
* TC-SYSLOG-060 /log 不存在 -> f_mkdir 成功 + 幂等(第二次不再建目录)
* TC-SYSLOG-061 f_mkdir 失败 -> 不就绪、留痕去重、后续重试成功并复位
* TC-SYSLOG-062 卷忙 -> 本轮顺延、不置就绪
* TC-SYSLOG-063 末条 epoch 距 now < 600s -> "reboot";>=600s 或墙钟无效 -> "power on"
* TC-SYSLOG-064 f_stat /log 已存在 -> 不再调 f_mkdir(幂等靠 stat,不是靠状态位)
* —— F 取证通道 sys_log_evidence ——
* TC-SYSLOG-070 写 /log/wdev.log 变长行("ts 描述\\r\\n")+ 随后补一条 run.log
* TC-SYSLOG-071 卷锁顺序硬约束:第二条写在 release **之后**(非递归锁,反了自死锁)
* TC-SYSLOG-072 >8192B -> 先 unlink 再写(防长跑无界增长)
* TC-SYSLOG-073 卷忙 -> 返回 -1 且 wdev.log 零字节(不阻塞喂狗点)
* TC-SYSLOG-074 wdev 写失败 -> 返回 -1,但第二条 run.log 仍写(独立证据语义)
* —— H 补齐分支(自愈 / 锁后二次校验 / 回投 / 占位时标 / 空指针)——
* TC-SYSLOG-090 批量 open 失败一次 -> unlink 重开 -> 整批落全、不留痕
* TC-SYSLOG-091 批量 open 持续失败 -> -1 + 留痕一次 + 已放锁
* TC-SYSLOG-092 取锁后二次 count 失败 -> 放锁返回 -1(漏放 = FTP 永久等不到卷)
* TC-SYSLOG-093 读时 f_open 失败 -> 放锁 -1;下轮恢复后窗口正常重填
* TC-SYSLOG-094 无墙钟 -> wdev.log 落占位时标(取证行也必须可解析)
* TC-SYSLOG-095 wdev 短写 -> 返回 -1,第二条独立证据仍落 run.log
* TC-SYSLOG-096 队列创建失败 -> 一条都不排(不静默丢),堆恢复后排空并接上 SOE
* TC-SYSLOG-097 队列侧换文件那条原样回投 -> 三条全落、各文件内顺序不乱
* TC-SYSLOG-098 desc_len=29 时截断分支不可达(守卫取值导致,登记死分支)
* TC-SYSLOG-099 p_rec / p_desc 空指针守卫(守卫在取锁之前)
* TC-SYSLOG-100 last_epoch 读失败 -> -1 且 *p_epoch 清零
* —— G 陷阱二:名字/参数类错误码严禁升级成卷操作 ——
* TC-SYSLOG-080 短名违规分档保真:体>8 / 扩展>3 / 非法字符 / 以点开头(.fschk)
* 各自落 FR_INVALID_NAME;含 ':' 落 FR_INVALID_DRIVE
* TC-SYSLOG-081 生产路径全过严格 8.3:/log /run.log /selftest.log /wdev.log /soe.log
* TC-SYSLOG-082 FR_INVALID_NAME 反复失败 20 轮:unlink 次数有界、/log 目录与
* 邻居文件一根毛没动(自愈只针对自己的文件)
*
* 覆盖率台账(--module sys_log_mcu 自量):App/log/sys_log_mcu.c 310/313 行 =
* 99.0%、函数 12/12。未覆盖 3 行:L628(下面第三条死分支)、L727~728
* (sys_log_evidence 里 snprintf 返回 <=0 的分支——宿主 snprintf 不可能失败,
* 假件也不伪造返回值;为刷数字去伪造等于把断言写在假件规则上,宁可留空)。
*
* 本套照出的现状(**只钉现状、不改生产代码**;三条都进交 Lead 的报告):
*
* 【疑似缺陷 A:卷忙时整批日志被静默丢弃】sys_log_mcu.c:509~537 的批量路径里
* done 先被置成 n,只有 rc==-3 与 rc==-1 两个分支会改它;**rc==-2(卷忙)没有
* 任何分支**,于是紧随其后的 "for (i = done; i < n; i++) osMessageQueuePut(...)"
* 一次都不执行 —— 整批(最多 8 条)既没落盘也没回投,且不计故障、不打日志。
* 而同一段代码自己的注释写的是"rc == -2(卷忙):整批回投队列,静默顺延",
* 函数头 465~470 行与设计文档 §4.4 又都写明"**条目不丢**(旧实现 -2 卷忙会
* 直接丢掉那一条)"。⇒ 现状比旧实现更糟:旧实现丢 1 条,现在一次丢 8 条。
* 触发条件极常见:卷锁 v1.2 之后 ftpTask 传文件期间 defaultTask 每 2s 排空
* 一次,每次撞上卷忙就吞一批。对照:逐条慢路径(-3 分支)遇 -2 会正确回投
* 尾部 —— 同一个错误码在两条路径上行为相反(TC-SYSLOG-035 与 035b 成对钉住)。
*
* 【疑似缺陷 B:rc==-1 的留痕文案与实际行为相反】sys_log_mcu.c:527~529 打
* DBG_ERROR("drop %lu entries id=%d"),但该分支把 done 置 0 -> 整批**回投队列**,
* 一条都没丢(下一轮重试)。排障时按"日志说丢了"去找会走错方向;而真正会丢的
* 是上面那个没有任何日志的 -2 分支。另外写故障持续时每轮 drain 都会重投重
* 试一遍(s_append_many 内部的 L2 去重只压得住它自己那条,压不住这条)。
*
* 【死分支:syslog_get_record 的截断不可达】sys_log_mcu.c:616 的守卫是
* desc_len <= SYSLOG_DESC_MAX 直接 -1,而函数里 dl 恒 <= SYSLOG_DESC_MAX,
* 于是 627~628 的 "if (dl >= desc_len) dl = desc_len - 1" 永不成立。
* 无害(宁可拒绝也不越界),但"传小缓冲就能拿到截断内容"的直觉是错的(TC-SYSLOG-098)。
*
* 故意没测(写在这里,不留"看起来全覆盖"的错觉):
* - 真跨任务并发:读窗口用"对齐 32 位读写在 ARM 上原子"免锁,主机 x64 + 单线程
* 编不出真竞态;本套只断言单写者语义与代数失效逻辑本身。
* - f_close 内部 sync_fs -> disk_ioctl(CTRL_SYNC) -> ftl_flush_cache() 的掉电落盘
* 链:属 nand_port/GD5F 层,假卷里没有 FTL,"关文件即落盘"要靠真机 + N-22 台账。
* - snprintf 失败(n<=0)分支:宿主 snprintf 不会失败,且假件不提供注入——
* 为了刷覆盖率去伪造返回值等于把断言写在假件规则上,宁可留空。
* - DBG 文本内容:桩只计数(真实宏展开成 snprintf + 192B 行缓冲)。
*
* 通过:全部 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 <time.h>
#include "ff.h"
#include "fatfs_fake.h"
#include "cmsis_os.h"
#include "dbg_log.h"
#include "sys_clock.h"
#include "vol_lock.h"
#include "sys_log.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)
/* 路径常量:与生产 s_paths[] / sys_log_evidence() 里的字面量一致(写死在测试里
* 是为了"改生产路径名必须同时改测试",改名静默失效正是 §10.1 第 10 条的老账) */
#define P_RUN "/log/run.log"
#define P_SELFTEST "/log/selftest.log"
#define P_WDEV "/log/wdev.log"
/* ===================== 假件与生产源码纳入 ===================== */
#include "fatfs_fake.c"
#include "sys_log_mcu.c"
/* ===================== dbg_log 桩计数 ===================== */
int g_sys_err_count;
int g_sys_info_count;
int g_sys_debug_count;
/* ===================== soe_store 挂接点计数桩 ===================== */
static int s_soe_drain_calls;
void soe_store_drain(void)
{
if (s_soe_drain_calls++ > 20000) {
(void)fprintf(stderr, "[FATAL] soe_store_drain guard tripped\n");
exit(2);
}
}
void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value)
{
(void)type; (void)elm_id; (void)value;
}
uint32_t soe_store_dropped(void)
{
return 0u;
}
/* ===================== 假墙钟 ===================== */
static uint32_t s_now = 1760234567u;
static int s_clock_ok = 1;
uint32_t sys_clock_get(void)
{
return (s_clock_ok != 0) ? s_now : UINT32_MAX;
}
char *sys_clock_get_str(char *buf, size_t buf_size)
{
struct tm gmt;
time_t t;
size_t need = SYSLOG_TS_LEN + 1u;
if ((s_clock_ok == 0) || (buf == NULL) || (buf_size < need)) {
return NULL;
}
t = (time_t)s_now;
#if defined(_WIN32)
if (gmtime_s(&gmt, &t) != 0) {
return NULL;
}
#else
gmt = *gmtime(&t);
#endif
(void)strftime(buf, buf_size, "%Y-%m-%d %H:%M:%S", &gmt);
return buf;
}
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; }
/* ===================== 假卷锁(try 语义 + 配对检查) ===================== */
static int s_lock_busy_left;
static int s_lock_busy_skip; /* 先放行 N 次再开始忙(同一次 drain 里多把锁的场景) */
static int s_lock_waits;
static int s_lock_releases;
static int s_lock_held;
static int s_lock_seq_err;
static void lock_busy_next(int n)
{
s_lock_busy_skip = 0;
s_lock_busy_left = n;
}
/* 前 skip 次 vol_lock_wait 正常放行,接下来 busy 次返回"忙"。
* 需要它是因为同一次 syslog_drain() 里会连拿两把锁:批量路径那把必须先成功,
* 才谈得上"退回逐条慢路径后第二把忙"。 */
static void lock_busy_after(int skip, int busy)
{
s_lock_busy_skip = skip;
s_lock_busy_left = busy;
}
static void locks_clear(void)
{
s_lock_busy_left = 0;
s_lock_busy_skip = 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 guard tripped\n");
exit(2);
}
if (s_lock_busy_skip > 0) {
s_lock_busy_skip--;
r = 1;
} else {
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++;
}
s_lock_held = 0;
}
/* ===================== 假消息队列(真 FIFO + 满/空语义) ===================== */
#define MQ_SLOTS 16u
#define MQ_MAXMSG 24u /* sizeof(syslog_entry_t) 在 x64 = 16,留余量 */
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;
static uint32_t s_q_msgsz;
static int s_q_new_calls;
static int s_q_new_fail;
static int s_q_get_calls;
static int s_q_put_calls;
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 = SYSLOG_QUEUE_MAX;
s_q_msgsz = (uint32_t)sizeof(syslog_entry_t);
s_q_new_calls = 0;
s_q_new_fail = 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) {
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;
}
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_now = 1760234567u;
s_clock_ok = 1;
s_kernel_state = osKernelInactive;
g_sys_err_count = 0;
g_sys_info_count = 0;
g_sys_debug_count = 0;
s_soe_drain_calls = 0;
/* 生产模块的 static 残留:逐位显式清(漏一个就会"上一场景的尾巴"咬下一场景) */
(void)memset(s_preboot, 0, sizeof(s_preboot));
s_preboot_cnt = 0u;
(void)memset(s_batch_src, 0, sizeof(s_batch_src));
s_queue = NULL;
s_fs_ready = 0u;
s_mount_fail_reported = 0u;
(void)memset(s_rd_win, 0, sizeof(s_rd_win));
s_rd_win_id = -1;
s_rd_win_base = 0u;
s_rd_win_cnt = 0u;
s_rd_win_gen = 0u;
s_rd_gen[SYSLOG_RUN] = 0u;
s_rd_gen[SYSLOG_SELFTEST] = 0u;
/* 卷状态:/log 由 defaultTask 挂载时建好;本套多数场景直接假定已建,
* "没建 -> 补建" 那条路径单独用 TC-SYSLOG-060/061 测 */
(void)ff_fake_mkdir_direct("/log");
}
/* 就绪但**不**走 mounted()(避免上电那条记录混进断言) */
static void ready_only(void)
{
s_fs_ready = 1u;
}
/* 用生产 API 落一条记录(时间戳/epoch 由假墙钟决定) */
static void put_rec(syslog_id_t id, const char *desc)
{
ready_only();
(void)s_append(id, desc);
}
/* 从假卷直读第 i 条原始记录 */
static int raw_at(const char *path, uint32_t i, uint8_t *p_out)
{
return (ff_fake_fetch(path, p_out, i * SYSLOG_REC_SIZE, SYSLOG_REC_SIZE)
== SYSLOG_REC_SIZE) ? 1 : 0;
}
/* ===================== A 记录格式 ===================== */
static void tc_format(void)
{
uint8_t rec[SYSLOG_REC_SIZE];
uint8_t b[SYSLOG_REC_SIZE];
int ok;
uint32_t i;
printf("-- TC-SYSLOG-001..004 记录格式(64B 定长)--\n");
/* 001 布局 */
reset_all();
s_now = 1760234567u;
s_clock_ok = 1;
s_format_rec(SYSLOG_RUN, "hello", rec);
ok = (memcmp(rec, "2025-10-12 02:02:47", SYSLOG_TS_LEN) == 0)
&& (rec[SYSLOG_TS_LEN] == ' ')
&& (memcmp(rec + SYSLOG_EPOCH_OFFS, "1760234567", SYSLOG_EPOCH_LEN) == 0)
&& (rec[SYSLOG_EPOCH_OFFS - 1u] == ' ')
&& (memcmp(rec + SYSLOG_DESC_OFFS, "hello", 5u) == 0)
&& (rec[SYSLOG_DESC_OFFS + 5u] == '\0');
CHECK(ok, "TC-SYSLOG-001 [0..18]ts / [19]空格 / [20..29]epoch / [30]空格 / [31..]desc");
ok = 1;
for (i = SYSLOG_DESC_OFFS + 6u; i < SYSLOG_REC_SIZE; i++) {
if (rec[i] != 0u) {
ok = 0;
}
}
CHECK(ok, "TC-SYSLOG-001b 尾部 NUL 填充到 64B(定长解析的地基)");
/* 002 描述截断 */
reset_all();
{
char big[SYSLOG_DESC_MAX + 16u];
(void)memset(big, 'A', sizeof(big) - 1u);
big[sizeof(big) - 1u] = '\0';
s_format_rec(SYSLOG_RUN, big, rec);
ok = 1;
for (i = 0u; i < SYSLOG_DESC_MAX; i++) {
if (rec[SYSLOG_DESC_OFFS + i] != (uint8_t)'A') {
ok = 0;
}
}
CHECK(ok && (rec[SYSLOG_DESC_OFFS + SYSLOG_DESC_MAX] == 0u)
&& (rec[SYSLOG_REC_SIZE - 1u] == 0u),
"TC-SYSLOG-002 超长描述截到 28B,不越 64B 边界");
}
/* 003 墙钟未建立 */
reset_all();
s_clock_ok = 0;
s_format_rec(SYSLOG_RUN, "x", rec);
CHECK((memcmp(rec, "1970-01-01 08:00:00", SYSLOG_TS_LEN) == 0)
&& (memcmp(rec + SYSLOG_EPOCH_OFFS, "0000000000", SYSLOG_EPOCH_LEN) == 0),
"TC-SYSLOG-003 无墙钟 -> 占位时间戳 + epoch 全 0(不造假时标)");
/* 004 单条 vs 批量逐字节相同 */
reset_all();
s_now = 1700000000u;
s_format_rec(SYSLOG_RUN, "same-content", rec);
ready_only();
(void)memset(b, 0, sizeof(b));
s_format_rec(SYSLOG_RUN, "same-content", b);
CHECK(memcmp(rec, b, SYSLOG_REC_SIZE) == 0,
"TC-SYSLOG-004 同一输入两次格式化逐字节一致(UI 定长偏移解析的前提)");
reset_all();
s_now = 1700000000u;
ready_only();
s_format_rec(SYSLOG_RUN, "ab", rec);
{
uint8_t batch[SYSLOG_REC_SIZE * 2u];
(void)memset(batch, 0, sizeof(batch));
s_format_rec(SYSLOG_RUN, "ab", batch);
s_format_rec(SYSLOG_RUN, "ab", batch + SYSLOG_REC_SIZE);
CHECK((memcmp(rec, batch, SYSLOG_REC_SIZE) == 0)
&& (memcmp(rec, batch + SYSLOG_REC_SIZE, SYSLOG_REC_SIZE) == 0),
"TC-SYSLOG-004b 批量路径复用同一 s_format_rec(两条都等于单条)");
}
}
/* ===================== B 写侧 ===================== */
static void tc_write_single(void)
{
uint8_t rec[SYSLOG_REC_SIZE];
int ok;
printf("-- TC-SYSLOG-010..019 单条写路径 --\n");
/* 010 成功 + 打开模式 */
reset_all();
ready_only();
ok = (s_append(SYSLOG_RUN, "boot") == 0)
&& (g_ff_obs.last_open_mode == (BYTE)(FA_WRITE | FA_OPEN_APPEND))
&& (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 1)
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1)
&& (g_ff_obs.calls[FF_FAKE_OP_CLOSE] == 1)
&& (s_lock_releases == 1) && (s_lock_seq_err == 0)
&& (g_sys_err_count == 0);
CHECK(ok, "TC-SYSLOG-010 单条落盘成功:模式带写位 + open/write/close 各一次 + 放锁");
/* 011 陷阱一保真对照 */
reset_all();
{
static FIL probe;
UINT bw = 99u;
FRESULT fr1;
FRESULT fr2;
fr1 = f_open(&probe, P_RUN, FA_OPEN_APPEND);
fr2 = f_write(&probe, rec, SYSLOG_REC_SIZE, &bw);
(void)f_close(&probe);
CHECK((fr1 == FR_OK) && (fr2 == FR_DENIED) && (bw == 0u),
"TC-SYSLOG-011 只给 FA_OPEN_APPEND -> f_write 恒 FR_DENIED(2026-09-20 真根因)");
}
/* 012 卷忙 */
reset_all();
ready_only();
lock_busy_next(1);
ok = (s_append(SYSLOG_RUN, "x") == -2);
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0)
&& (s_lock_releases == 0) && (s_lock_seq_err == 0) && (g_sys_err_count == 0),
"TC-SYSLOG-012 卷忙 = 顺延:不碰盘、不 release、不计故障(喂狗点绝不阻塞)");
/* 013 短写 -> 自愈 */
reset_all();
ready_only();
ff_fake_inject_write_short(P_RUN, SYSLOG_REC_SIZE - 1u, 1);
ok = (s_append(SYSLOG_RUN, "short") == 0);
CHECK(ok && (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE)
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
&& (g_ff_obs.last_write_bw == SYSLOG_REC_SIZE),
"TC-SYSLOG-013 FR_OK+短写 63B 被判失败 -> unlink 重建 -> 恰好 64 字节");
/* 014 容量满 -> 删除重建 */
reset_all();
ready_only();
(void)ff_fake_mkfile_size(P_RUN, (uint32_t)SYSLOG_RUN_MAX * SYSLOG_REC_SIZE);
ok = (s_append(SYSLOG_RUN, "wrap") == 0);
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
&& (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE)
&& (syslog_count(SYSLOG_RUN) == 1u),
"TC-SYSLOG-014 满 1024 条 -> 先 unlink 再写,新文件只剩本次一条");
/* 015 满 + unlink 真失败 */
reset_all();
ready_only();
(void)ff_fake_mkfile_size(P_RUN, (uint32_t)SYSLOG_RUN_MAX * SYSLOG_REC_SIZE);
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_DISK_ERR, 1);
ok = (s_append(SYSLOG_RUN, "x") == -1);
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
&& (s_lock_releases == 1) && (s_lock_seq_err == 0),
"TC-SYSLOG-015 容量轮转时 unlink 失败 -> 返回 -1 且**已释放卷锁**(漏放 = FTP 永久拿不到卷)");
/* 016 满 + unlink 返 FR_NO_FILE 无害 */
reset_all();
ready_only();
(void)ff_fake_mkfile_size(P_RUN, (uint32_t)SYSLOG_RUN_MAX * SYSLOG_REC_SIZE);
/* 注入只替换返回码、不真的删条目(真机上 FR_NO_FILE 的含义恰恰是"条目已不在"),
* 所以这里钉的是**处置**:该码不进错误分支、流程继续把这条写下去。
* 文件大小断言留给 TC-SYSLOG-014(那里 unlink 是真删)。 */
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_NO_FILE, 1);
ok = (s_append(SYSLOG_RUN, "gone") == 0);
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1) && (g_sys_err_count == 0),
"TC-SYSLOG-016 unlink 返 FR_NO_FILE 被当无害:不进错误分支、不留痕、继续写");
/* 017 入参守卫 */
reset_all();
ready_only();
ok = (s_append((syslog_id_t)SYSLOG_COUNT, "x") == -1)
&& (s_append_rec(SYSLOG_RUN, NULL) == -1)
&& (s_append(SYSLOG_RUN, NULL) == -1)
&& (s_append_rec((syslog_id_t)9u, rec) == -1);
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
"TC-SYSLOG-017 id 越界 / 空 rec / 空 desc -> -1 且不碰卷锁与盘");
/* 018 写成功 -> 读侧代数自增 */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "a");
ok = (s_rd_gen[SYSLOG_RUN] == 1u) && (s_rd_gen[SYSLOG_SELFTEST] == 0u);
(void)s_append(SYSLOG_SELFTEST, "b");
CHECK(ok && (s_rd_gen[SYSLOG_SELFTEST] == 1u),
"TC-SYSLOG-018 每次落盘只自增本 id 的代数(另一文件的窗口不被误失效)");
/* 019 留痕去重 */
reset_all();
ready_only();
ff_fake_inject_write_short(P_RUN, 0u, 4);
ok = (s_append(SYSLOG_RUN, "e1") == -1);
(void)s_append(SYSLOG_RUN, "e2"); /* 第二轮仍失败 */
ok = ok && (g_sys_err_count == 1);
(void)s_append(SYSLOG_RUN, "ok"); /* 注入耗尽 -> 成功 -> 复位 */
ff_fake_inject_write_short(P_RUN, 0u, 2);
(void)s_append(SYSLOG_RUN, "e3"); /* 再失败 -> 再留痕 */
CHECK(ok && (g_sys_err_count == 2),
"TC-SYSLOG-019 持续故障只报第一次;恢复后再失败必须重新留痕");
}
static void tc_write_many(void)
{
uint8_t batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE];
uint32_t i;
int ok;
printf("-- TC-SYSLOG-020..024 批量写路径(N-24)--\n");
/* 020 8 条一次 I/O */
reset_all();
ready_only();
(void)memset(batch, 0x5A, sizeof(batch));
ok = (s_append_many(SYSLOG_RUN, batch, SYSLOG_BATCH_MAX) == 0);
CHECK(ok && (ff_fake_size(P_RUN) == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 1)
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1)
&& (g_ff_obs.last_write_btw == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE)
&& (g_ff_obs.last_write_bw == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE)
&& (g_ff_obs.calls[FF_FAKE_OP_CLOSE] == 1),
"TC-SYSLOG-020 一批 8 条 = 一次 open + 一次 512B write + 一次 close(旧实现 8 轮)");
/* 021 入参守卫 */
reset_all();
ready_only();
ok = (s_append_many(SYSLOG_RUN, batch, 0u) == -1)
&& (s_append_many(SYSLOG_RUN, batch, SYSLOG_BATCH_MAX + 1u) == -1)
&& (s_append_many(SYSLOG_RUN, NULL, 1u) == -1)
&& (s_append_many((syslog_id_t)SYSLOG_COUNT, batch, 1u) == -1);
CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
"TC-SYSLOG-021 批量入参守卫在**取卷锁之前**(越界 n 不占用卷)");
/* 022 容量不足 -> -3 且不 unlink */
reset_all();
ready_only();
(void)ff_fake_mkfile_size(P_RUN, ((uint32_t)SYSLOG_RUN_MAX - 3u) * SYSLOG_REC_SIZE);
ok = (s_append_many(SYSLOG_RUN, batch, SYSLOG_BATCH_MAX) == -3);
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
&& (s_lock_releases == 1) && (s_lock_seq_err == 0)
&& (ff_fake_size(P_RUN) == ((uint32_t)SYSLOG_RUN_MAX - 3u) * SYSLOG_REC_SIZE),
"TC-SYSLOG-022 装不下一批 -> -3 交回慢路径;批量路径**绝不**自己 unlink 重建");
/* 023 批量卷忙 / 短写自愈 */
reset_all();
ready_only();
lock_busy_next(1);
ok = (s_append_many(SYSLOG_RUN, batch, 4u) == -2);
ff_fake_inject_write_short(P_RUN, 100u, 1); /* 本批 4 条 = 256 B,100 才算真短 */
ok = ok && (s_append_many(SYSLOG_RUN, batch, 4u) == 0);
CHECK(ok && (ff_fake_size(P_RUN) == 4u * SYSLOG_REC_SIZE)
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
&& (g_sys_err_count == 0),
"TC-SYSLOG-023 批量卷忙 -> -2;短写(256 只回 100)-> 自愈重建后整批落全");
/* 024 批量持续失败 + 去重 */
reset_all();
ready_only();
ff_fake_inject_write_short(P_RUN, 0u, 4);
ok = (s_append_many(SYSLOG_RUN, batch, 2u) == -1);
(void)s_append_many(SYSLOG_RUN, batch, 2u);
CHECK(ok && (g_sys_err_count == 1) && (s_rd_gen[SYSLOG_RUN] == 2u),
"TC-SYSLOG-024 批量持续失败留痕去重;代数照样自增(宁多失效一轮也不读脏窗)");
(void)i;
}
/* ===================== C 排空 ===================== */
static void tc_drain(void)
{
uint8_t rec[SYSLOG_REC_SIZE];
uint32_t i;
int ok;
printf("-- TC-SYSLOG-030..041 syslog_drain 排空 --\n");
/* 030 卷未就绪:条目滞留 preboot,drain 直接返回。
* 注意两条容易被读漏的事实:
* a) 队列还没建时(s_queue == NULL)即使内核已运行,post 也走 preboot 数组
* (syslog_post 的条件是 s_queue != NULL && 内核运行,两个条件都要满足);
* b) drain 里"惰性建队列"在 s_fs_ready 判定**之前**,所以卷没就绪也会先把
* 队列建出来 —— 于是就绪前的条目一律滞留 preboot、就绪后一次排空。 */
reset_all();
s_kernel_state = osKernelRunning;
syslog_post(SYSLOG_RUN, "early");
ok = (s_preboot_cnt == 1u) && (s_q_cnt == 0u) && (s_queue == NULL);
syslog_drain();
CHECK(ok && (s_q_new_calls == 1) && (s_preboot_cnt == 1u)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_soe_drain_calls == 0),
"TC-SYSLOG-030 卷未就绪 -> 队列先建成、条目滞留 preboot、不落盘也不调 SOE");
/* 031 惰性建队列 */
reset_all();
ready_only();
ok = (s_queue == NULL);
syslog_drain();
CHECK(ok && (s_queue != NULL) && (s_q_new_calls == 1) && (s_q_msgsz == sizeof(syslog_entry_t)),
"TC-SYSLOG-031 队列在**首次 drain**(defaultTask 上下文)才创建(内核前建对象 = 启动失败)");
/* 032 preboot 8 条一批落 */
reset_all();
ready_only();
for (i = 0u; i < SYSLOG_QUEUE_MAX; i++) {
syslog_post(SYSLOG_RUN, "pre");
}
CHECK(s_preboot_cnt == SYSLOG_QUEUE_MAX, "TC-SYSLOG-032a 内核未运行:8 条全进 preboot 数组");
syslog_drain();
ok = (s_preboot_cnt == 0u) && (syslog_count(SYSLOG_RUN) == SYSLOG_BATCH_MAX)
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1)
&& (g_ff_obs.last_write_btw == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE);
CHECK(ok, "TC-SYSLOG-032b preboot 8 条同文件 -> 一次 512B 合并写(数组下标看而不删)");
/* 033 + 034 两文件混排:换文件那条不消耗 */
reset_all();
ready_only();
syslog_post(SYSLOG_RUN, "r1");
syslog_post(SYSLOG_RUN, "r2");
syslog_post(SYSLOG_SELFTEST, "s1");
syslog_post(SYSLOG_RUN, "r3");
syslog_drain();
ok = (syslog_count(SYSLOG_RUN) == 3u) && (syslog_count(SYSLOG_SELFTEST) == 1u);
if (ok) {
ok = raw_at(P_RUN, 0u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "r1", 2u) == 0)
&& raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "r2", 2u) == 0)
&& raw_at(P_RUN, 2u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "r3", 2u) == 0)
&& raw_at(P_SELFTEST, 0u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "s1", 2u) == 0);
}
/* selftest 插在 run 中间,于是 run.log 被切成"r1r2"与"r3"两批:
* 合计 3 次 open/write/close,而不是 2 次 —— 换文件处即分批处。 */
CHECK(ok && (s_preboot_cnt == 0u)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3)
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 3),
"TC-SYSLOG-033/034 换文件即分批(run 被切成两批 = 共 3 轮 I/O)+ 顺序不乱");
/* 035【现状桩 = 疑似生产缺陷,见文件头"本套照出的现状"】
* sys_log_mcu.c:509~537 的批量路径里 done 先被置成 n,只有 -3 / -1 分支会
* 把它改回 0;**rc == -2(卷忙)没有任何分支**,于是紧随其后的
* "for (i = done; i < n; i++) osMessageQueuePut(...)" 一次都不执行 ——
* 整批(最多 8 条)**静默丢失**,而同一段代码上方的注释写的是
* "rc == -2(卷忙):整批回投队列,静默顺延",设计文档 §4.4 也写明
* "队列条目在落盘失败(-2 卷忙 / -1 写失败 / -3 容量不足)时回投队列——
* 条目不丢(旧实现 -2 卷忙会直接丢掉那一条)"。
* ⇒ 本条按**当前实现**断言(丢了就是丢了),并显式命名为 DEFECT,
* 生产侧一旦按注释补上 done=0,这条会红,提醒同步翻转成"不丢"的正向断言。 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain(); /* 建队列 */
for (i = 0u; i < 3u; i++) {
syslog_post(SYSLOG_RUN, "hold");
}
lock_busy_next(1);
syslog_drain(); /* 批量 -2 */
CHECK((syslog_count(SYSLOG_RUN) == 0u) && (s_q_cnt == 0u)
&& (g_sys_err_count == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
"TC-SYSLOG-035[DEFECT 现状] 卷忙 -> 整批 3 条既没落盘也没回投(静默丢失)");
/* 035b 对照组:慢路径(-3 分支内)遇到 -2 时**会**把尾部回投 —— 同一卷忙
* 在两条路径上行为不一致,这就是"批量路径漏了一个分支"最直接的证据 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain();
for (i = 0u; i < 3u; i++) {
syslog_post(SYSLOG_RUN, "slow-hold");
}
/* 让批量路径先返回 -3(容量只剩 1 条),从而进入逐条慢路径,再让卷忙 */
(void)ff_fake_mkfile_size(P_RUN, ((uint32_t)SYSLOG_RUN_MAX - 1u) * SYSLOG_REC_SIZE);
lock_busy_after(1, 1); /* 批量那把必须拿到,慢路径那把才忙 */
/* 批量路径先拿锁 -> 发现装不下一批 -> 返回 -3(此时一条都没写);逐条慢路径
* 的第二把锁遇到卷忙 -> break -> done=0 -> 整批 3 条全部回投队列。 */
g_ff_obs.calls[FF_FAKE_OP_OPEN] = 0;
syslog_drain();
CHECK((s_q_cnt == 3u) && (syslog_count(SYSLOG_RUN) == SYSLOG_RUN_MAX - 1u)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
"TC-SYSLOG-035b 慢路径遇卷忙 -> 3 条**整批回投**(同一码在两条路径上行为相反)");
/* 035c 卷忙丢掉的是"整批"而不是"一条":8 条一批全丢 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain();
for (i = 0u; i < SYSLOG_QUEUE_MAX; i++) {
syslog_post(SYSLOG_RUN, "eight");
}
lock_busy_next(1);
syslog_drain();
CHECK((s_q_cnt == 0u) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0),
"TC-SYSLOG-035c 卷忙一次吞掉一整批(8 条),不是旧实现的 1 条");
/* 036 回投溢出(hold-over drop):批量里混了 preboot 与队列两个来源时,
* 回投的目标队列已经被"没被取走的那些"占着 -> 装不下的部分才真被丢掉。
* 队列 8 槽占满 + preboot 5 条 = 一批取 8 条(5 preboot + 3 队列),
* 失败后 done=0 整批回投 -> 队列只剩 3 个空位 -> 必然触发 5 次留痕。 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain(); /* 建队列 */
for (i = 0u; i < SYSLOG_QUEUE_MAX; i++) {
syslog_post(SYSLOG_RUN, "q8"); /* 队列 8 槽占满 */
}
s_kernel_state = osKernelInactive;
for (i = 0u; i < 5u; i++) {
syslog_post(SYSLOG_RUN, "p5"); /* preboot 再放 5 条 */
}
ok = (s_q_cnt == SYSLOG_QUEUE_MAX) && (s_preboot_cnt == 5u);
ff_fake_inject_write_short(P_RUN, 0u, 2); /* 整批写失败 */
g_sys_err_count = 0;
s_kernel_state = osKernelRunning;
syslog_drain();
CHECK(ok && (s_q_cnt == SYSLOG_QUEUE_MAX) && (s_preboot_cnt == 0u)
&& (g_sys_err_count == 1 + 5 + 1),
"TC-SYSLOG-036 回投装不下 -> 5 次 hold-over drop + drop/wfail 各 1 条留痕");
/* 037 -3 退回慢路径:容量轮转只发生一次 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain();
for (i = 0u; i < 4u; i++) {
syslog_post(SYSLOG_RUN, "slow");
}
(void)ff_fake_mkfile_size(P_RUN, ((uint32_t)SYSLOG_RUN_MAX - 2u) * SYSLOG_REC_SIZE);
syslog_drain();
ok = (syslog_count(SYSLOG_RUN) == 2u)
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] >= 4);
CHECK(ok && (s_q_cnt == 0u),
"TC-SYSLOG-037 -3 -> 逐条慢路径:装满剩位 -> 满 -> unlink 重建 -> 继续,全程 unlink 恰一次");
/* 038 慢路径中途写失败 -> 该条丢 + 尾部回投 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain();
for (i = 0u; i < 3u; i++) {
syslog_post(SYSLOG_RUN, "mid");
}
(void)ff_fake_mkfile_size(P_RUN, ((uint32_t)SYSLOG_RUN_MAX - 1u) * SYSLOG_REC_SIZE);
ff_fake_inject_write_short(P_RUN, 0u, 2); /* 第一条就失败 */
g_sys_err_count = 0;
syslog_drain();
ok = (s_q_cnt == 2u) && (ff_fake_size(P_RUN) == 0u) && (g_sys_err_count >= 2);
syslog_drain(); /* 之后成功 */
CHECK(ok && (s_q_cnt == 0u) && (syslog_count(SYSLOG_RUN) == 2u),
"TC-SYSLOG-038 慢路径写失败:只丢**当前那一条**,尾部 2 条下轮补齐");
/* 039【现状桩 = 第二个不一致】rc == -1 分支把 done 置 0 并打
* DBG_ERROR("drop %lu entries id=%d"),但 done=0 意味着紧随其后的循环把
* **整批全部回投队列** —— 日志说"丢了多少条",实际一条没丢(下轮还会重试)。
* 措辞与行为相反,排障时会把人往错方向带;本条按现状钉住。 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain();
for (i = 0u; i < 3u; i++) {
syslog_post(SYSLOG_RUN, "wfail");
}
ff_fake_inject_write_short(P_RUN, 0u, 2);
g_sys_err_count = 0;
syslog_drain();
CHECK((s_q_cnt == 3u) && (syslog_count(SYSLOG_RUN) == 0u) && (g_sys_err_count == 2),
"TC-SYSLOG-039[DEFECT 现状] 批量 -1:日志报 drop,条目却整批回投(3 条仍在队列)");
syslog_drain(); /* 注入耗尽 -> 这次真的落下去 */
CHECK((syslog_count(SYSLOG_RUN) == 3u) && (s_q_cnt == 0u),
"TC-SYSLOG-039b 回投的 3 条下一轮补齐(与 035 的卷忙整批丢失形成对照)");
/* 040 B9 挂接:每次 drain 尾部各调一次 soe_store_drain */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain();
ok = (s_soe_drain_calls == 1);
syslog_drain();
CHECK(ok && (s_soe_drain_calls == 2),
"TC-SYSLOG-040 drain 尾部必调 soe_store_drain(同写者、同卷就绪门)");
/* 041 post 分流与守卫 */
reset_all();
ready_only();
syslog_post((syslog_id_t)SYSLOG_COUNT, "bad-id");
syslog_post(SYSLOG_RUN, NULL);
CHECK((s_preboot_cnt == 0u) && (s_q_cnt == 0u),
"TC-SYSLOG-041a id 越界 / NULL 描述 -> 静默丢弃(不占待发位)");
reset_all();
ready_only();
for (i = 0u; i < SYSLOG_QUEUE_MAX + 2u; i++) {
syslog_post(SYSLOG_RUN, "pf");
}
CHECK((s_preboot_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 2),
"TC-SYSLOG-041b preboot 满(8 槽)-> 丢弃 + 每条一次留痕(日志不得反压业务)");
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
s_q_cap = 2u; /* 造一个很短的队列 */
s_q_msgsz = (uint32_t)sizeof(syslog_entry_t);
s_queue = (osMessageQueueId_t)&s_q_pool[0];
syslog_post(SYSLOG_RUN, "q1");
syslog_post(SYSLOG_RUN, "q2");
syslog_post(SYSLOG_RUN, "q3");
CHECK((s_q_cnt == 2u) && (g_sys_err_count == 1),
"TC-SYSLOG-041c 内核运行 -> 走队列;队列满丢弃 + 留痕");
}
/* ===================== D 读侧 ===================== */
static void tc_read(void)
{
char ts[SYSLOG_TS_LEN + 1u];
char desc[SYSLOG_DESC_MAX + 1u];
uint32_t epoch;
uint32_t i;
int ok;
printf("-- TC-SYSLOG-050..058 读侧(N-24 滑动窗口)--\n");
/* 050 count */
reset_all();
ready_only();
CHECK(syslog_count(SYSLOG_RUN) == 0u, "TC-SYSLOG-050a 文件缺失 -> count 0");
for (i = 0u; i < 3u; i++) {
(void)s_append(SYSLOG_RUN, "c");
}
ok = (syslog_count(SYSLOG_RUN) == 3u) && (syslog_count((syslog_id_t)7u) == 0u);
CHECK(ok, "TC-SYSLOG-050b count = filesize/64(O(1),不读盘内容);id 越界 -> 0");
/* 051 窗口命中零 I/O */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "w0");
(void)s_append(SYSLOG_RUN, "w1");
{
int opens_base = g_ff_obs.calls[FF_FAKE_OP_OPEN];
int reads_base = g_ff_obs.calls[FF_FAKE_OP_READ];
int opens_after;
ok = (syslog_get_record(SYSLOG_RUN, 1u, ts, sizeof(ts), desc, sizeof(desc)) == 0);
opens_after = g_ff_obs.calls[FF_FAKE_OP_OPEN];
ok = ok && (strcmp(desc, "w1") == 0)
&& (opens_after == opens_base + 1) /* 填窗这一次 I/O */
&& (g_ff_obs.calls[FF_FAKE_OP_READ] == reads_base + 1);
ok = ok && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
&& (strcmp(desc, "w0") == 0)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_after) /* 命中 -> 零 I/O */
&& (g_ff_obs.calls[FF_FAKE_OP_READ] == reads_base + 1);
}
CHECK(ok, "TC-SYSLOG-051 窗口命中 -> 第二次读**零 FatFs 调用**(UI 翻页不再逐条开合)");
/* 052 写者代数失效 */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "old");
ok = (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
&& (strcmp(desc, "old") == 0);
(void)s_append(SYSLOG_RUN, "new"); /* 代数 +1 -> 窗口必须重填 */
ok = ok && (syslog_get_record(SYSLOG_RUN, 1u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
&& (strcmp(desc, "new") == 0);
CHECK(ok, "TC-SYSLOG-052 写者一落地代数就变 -> 窗口重填,绝不返回陈旧内容");
/* 053 跨窗口 */
reset_all();
ready_only();
for (i = 0u; i < 9u; i++) {
(void)s_append(SYSLOG_RUN, "x");
}
(void)syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc));
{
int opens_before = g_ff_obs.calls[FF_FAKE_OP_OPEN];
ok = (syslog_get_record(SYSLOG_RUN, 8u, ts, sizeof(ts), desc, sizeof(desc)) == 0);
ok = ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_before + 1)
&& (g_ff_obs.last_lseek_ofs == (FSIZE_t)(SYSLOG_RD_WINDOW * SYSLOG_REC_SIZE))
&& (g_ff_obs.last_read_btr == SYSLOG_REC_SIZE);
}
CHECK(ok, "TC-SYSLOG-053 idx=8 跨窗 -> 一次 open + lseek(8*64) + read(1 条)(窗口按 8 对齐)");
/* 054 越界 */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "only");
ok = (syslog_get_record(SYSLOG_RUN, 1u, ts, sizeof(ts), desc, sizeof(desc)) == -1)
&& (s_read_raw(SYSLOG_RUN, 5u, (uint8_t *)desc) == -1);
reset_all();
ready_only();
ok = ok && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == -1);
CHECK(ok, "TC-SYSLOG-054 越界 idx / 空文件 -> -1(等锁前后各校验一次 count)");
/* 055 读时卷忙 */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "busy");
(void)syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)); /* 填窗 */
/* 换到另一 id 迫使真读盘,再让卷忙 */
(void)s_append(SYSLOG_SELFTEST, "s");
lock_busy_next(1);
ok = (syslog_get_record(SYSLOG_SELFTEST, 0u, ts, sizeof(ts), desc, sizeof(desc)) == -1)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] >= 1) && (s_lock_seq_err == 0);
CHECK(ok, "TC-SYSLOG-055 读时卷忙 -> -1(读窗口 try 语义:uiTask 也绝不阻塞等锁,下轮刷新重读)");
/* 056 短读作废窗口 */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "half");
(void)s_append(SYSLOG_RUN, "half2");
s_rd_win_id = -1;
s_rd_win_cnt = 0u;
ff_fake_inject_read_short(P_RUN, 1u, 1);
ok = (s_read_raw(SYSLOG_RUN, 0u, (uint8_t *)desc) == -1)
&& (s_rd_win_cnt == 0u) && (s_rd_win_id == -1);
CHECK(ok, "TC-SYSLOG-056 短读 -> 窗口作废(半窗当有效命中 = UI 显示半条脏记录)");
/* 057 get_record 守卫与截断 */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "01234567890123456789012345678901234567");
ok = (syslog_get_record(SYSLOG_RUN, 0u, NULL, 32u, desc, sizeof(desc)) == -1)
&& (syslog_get_record(SYSLOG_RUN, 0u, ts, SYSLOG_TS_LEN, desc, sizeof(desc)) == -1)
&& (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), NULL, sizeof(desc)) == -1)
/* 守卫是 desc_len <= SYSLOG_DESC_MAX(不是 0),所以 28 及以下整条被拒 */
&& (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, SYSLOG_DESC_MAX) == -1)
&& (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, SYSLOG_DESC_MAX + 1u) == 0)
/* 唯一"能读"的边界是 desc_len = 29:这时函数里 dl >= desc_len 那条截断
* 永远不成立(dl 由记录内容决定,恒 <= SYSLOG_DESC_MAX = 28),所以拿到的是
* 整 28 字符而不是截断结果 —— 即"传刚够的缓冲会被拒、传更小的也被拒,
* 从来没有截断"。登记为死分支,见文件头"本套照出的现状"。 */
&& (strlen(desc) == (size_t)SYSLOG_DESC_MAX);
CHECK(ok, "TC-SYSLOG-057 守卫:ts_len<=19 或 desc_len<=28 一律 -1;29 才读得到整 28 字符");
/* 058 last_epoch */
reset_all();
ready_only();
ok = (syslog_last_epoch(SYSLOG_RUN, &epoch) == -1) && (epoch == 0u);
s_now = 1700000123u;
(void)s_append(SYSLOG_RUN, "e");
ok = ok && (syslog_last_epoch(SYSLOG_RUN, &epoch) == 0) && (epoch == 1700000123u);
CHECK(ok, "TC-SYSLOG-058 last_epoch:无记录 -1;有记录解析 10 位 ASCII");
}
/* ===================== E mounted / 上电重启分类 ===================== */
static void tc_mounted(void)
{
uint8_t rec[SYSLOG_REC_SIZE];
int ok;
printf("-- TC-SYSLOG-060..064 挂载后处理(挂载幂等 + 上电/重启分类)--\n");
/* 060 /log 不在 -> 建目录 + 写上电记录;第二次调用完全静默 */
reset_all();
s_fs_ready = 0u;
s_kernel_state = osKernelInactive;
ok = (ff_fake_exists("/log") == 2);
syslog_post(SYSLOG_RUN, "pre-mount"); /* 就绪前排队的条目也应当被带出来 */
syslog_mcu_mounted();
ok = ok && (s_fs_ready == 1u) && (syslog_count(SYSLOG_RUN) == 1u);
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == 0) && (g_sys_err_count == 0),
"TC-SYSLOG-060a /log 已存在 -> 只写上电/重启记录一条");
/* 060b 幂等:再来一次既不建目录也不写第二条 */
reset_all();
s_fs_ready = 0u;
syslog_mcu_mounted(); /* 第一次:写一条上电/重启记录 */
{
uint32_t cnt1 = syslog_count(SYSLOG_RUN);
int mkdirs1 = g_ff_obs.calls[FF_FAKE_OP_MKDIR];
syslog_mcu_mounted(); /* 第二次:应完全静默 */
CHECK((s_fs_ready == 1u) && (syslog_count(SYSLOG_RUN) == cnt1)
&& (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == mkdirs1),
"TC-SYSLOG-060b 幂等再调用:不再建目录、不再写上电记录(2s 复查不刷屏)");
}
(void)rec;
/* 061 建目录失败:不就绪 + 留痕去重 + 后续重试成功 */
reset_all();
s_fs_ready = 0u;
ff_fake_inject_rc("/log", FF_FAKE_OP_STAT, FR_DISK_ERR, 3); /* 逼它每次都走 mkdir 分支 */
ff_fake_inject_rc("/log", FF_FAKE_OP_MKDIR, FR_DENIED, 2);
g_sys_err_count = 0;
syslog_mcu_mounted(); /* 失败 1 -> 留痕 */
ok = (s_fs_ready == 0u) && (g_sys_err_count == 1);
syslog_mcu_mounted(); /* 失败 2 -> 不再留痕 */
ok = ok && (g_sys_err_count == 1) && (s_lock_releases == 2) && (s_lock_seq_err == 0);
syslog_mcu_mounted(); /* 注入耗尽 -> 成功 */
CHECK(ok && (s_fs_ready == 1u) && (s_mount_fail_reported == 0u),
"TC-SYSLOG-061 mkdir 失败:不就绪、留痕一次(防 2s 刷屏)、第三轮自愈并复位标志");
/* 062 卷忙:本轮顺延,不置就绪、不碰盘 */
reset_all();
s_fs_ready = 0u;
lock_busy_next(1);
syslog_mcu_mounted();
CHECK((s_fs_ready == 0u) && (g_ff_obs.calls[FF_FAKE_OP_STAT] == 0)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_lock_releases == 0),
"TC-SYSLOG-062 卷忙 -> 直接返回(try 失败不 release,下轮 2s 后再试)");
/* 063 上电 / 重启分类:阈值 SYSLOG_REBOOT_WINDOW_S 两侧 + 墙钟无效 */
reset_all();
s_fs_ready = 0u;
s_now = 1760234567u;
(void)ff_fake_mkfile_size(P_RUN, SYSLOG_REC_SIZE); /* 有一条末条记录可解析 */
/* 末条 epoch 造在 5 分钟前 -> reboot */
{
uint8_t one[SYSLOG_REC_SIZE];
(void)memset(one, 0, sizeof(one));
(void)memcpy(one, "2025-10-12 01:57:47", SYSLOG_TS_LEN);
one[SYSLOG_TS_LEN] = ' ';
one[SYSLOG_EPOCH_OFFS - 1u] = ' ';
(void)memcpy(one + SYSLOG_EPOCH_OFFS, "1760234267", SYSLOG_EPOCH_LEN); /* -300s */
(void)ff_fake_mkfile(P_RUN, one, SYSLOG_REC_SIZE);
}
syslog_mcu_mounted();
ok = raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "reboot", 6u) == 0)
&& (rec[SYSLOG_DESC_OFFS + 6u] == '\0');
CHECK(ok, "TC-SYSLOG-063a 末条 epoch 距 now 300s(<600) -> 描述落 \"reboot\"");
reset_all();
s_fs_ready = 0u;
{
uint8_t one[SYSLOG_REC_SIZE];
(void)memset(one, 0, sizeof(one));
(void)memcpy(one, "2025-10-12 08:00:00", SYSLOG_TS_LEN);
one[SYSLOG_TS_LEN] = ' ';
one[SYSLOG_EPOCH_OFFS - 1u] = ' ';
(void)memcpy(one + SYSLOG_EPOCH_OFFS, "1760200000", SYSLOG_EPOCH_LEN); /* -9.5h */
(void)ff_fake_mkfile(P_RUN, one, SYSLOG_REC_SIZE);
}
syslog_mcu_mounted();
ok = raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "power on", 8u) == 0);
CHECK(ok, "TC-SYSLOG-063b 间隔 >=600s -> \"power on\"(掉电超过 10 分钟算重新上电)");
reset_all();
s_fs_ready = 0u;
s_clock_ok = 0;
{
uint8_t one[SYSLOG_REC_SIZE];
(void)memset(one, 0, sizeof(one));
(void)memcpy(one + SYSLOG_EPOCH_OFFS, "1760234560", SYSLOG_EPOCH_LEN); /* 距 now 7s */
(void)ff_fake_mkfile(P_RUN, one, SYSLOG_REC_SIZE);
}
syslog_mcu_mounted();
ok = raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "power on", 8u) == 0);
CHECK(ok, "TC-SYSLOG-063c 墙钟无效(UINT32_MAX)-> 保守判 \"power on\",不拿假差值判重启");
/* 064 幂等靠 stat,不靠状态位:/log 已在就不再 mkdir */
reset_all();
s_fs_ready = 0u;
syslog_mcu_mounted();
ok = (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == 0) && (g_ff_obs.calls[FF_FAKE_OP_STAT] >= 1);
CHECK(ok, "TC-SYSLOG-064 /log 已存在时靠 f_stat 短路,f_mkdir 一次都不调");
}
/* ===================== F 取证通道 sys_log_evidence ===================== */
static void tc_evidence(void)
{
uint8_t raw[8u * SYSLOG_REC_SIZE];
int ok;
printf("-- TC-SYSLOG-070..074 sys_log_evidence(wdev.log 取证通道)--\n");
/* 070 变长行 + 第二条 run.log */
reset_all();
s_now = 1760234567u;
ok = (sys_log_evidence("task stall declared") == 0);
{
uint32_t n = ff_fake_fetch(P_WDEV, raw, 0u, sizeof(raw));
ok = ok && (n > 0u)
&& (memcmp(raw, "2025-10-12 02:02:47 task stall declared\r\n", 41u) == 0)
&& (syslog_count(SYSLOG_RUN) == 1u)
&& raw_at(P_RUN, 0u, raw)
&& (memcmp(raw + SYSLOG_DESC_OFFS, "wd stall declared", 17u) == 0);
}
CHECK(ok, "TC-SYSLOG-070 wdev.log 写变长行(带 CRLF),并补一条 run.log 独立证据");
/* 071 卷锁顺序:第二条写必须在释放之后(非递归锁,反了 = 自死锁) */
reset_all();
(void)sys_log_evidence("order probe");
CHECK((s_lock_waits == 2) && (s_lock_releases == 2) && (s_lock_seq_err == 0)
&& (s_lock_held == 0),
"TC-SYSLOG-071 取锁两次各放一次(在持锁状态下调 s_append 会永远拿不到锁)");
/* 072 >8192B 护栏 */
reset_all();
(void)ff_fake_mkfile_size(P_WDEV, 8193u);
ok = (sys_log_evidence("after rotate") == 0);
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
&& (ff_fake_size(P_WDEV) > 0u) && (ff_fake_size(P_WDEV) < 200u),
"TC-SYSLOG-072 超过 8KB -> 先 unlink 再写(长跑不被无界撑爆)");
/* 073 卷忙 */
reset_all();
lock_busy_next(1);
ok = (sys_log_evidence("busy probe") == -1);
CHECK(ok && (ff_fake_exists(P_WDEV) == 0) && (syslog_count(SYSLOG_RUN) == 0u)
&& (s_lock_releases == 0) && (s_lock_seq_err == 0),
"TC-SYSLOG-073 卷忙 -> -1 且**两条写都没发生**(喂狗点绝不阻塞等锁)");
/* 074 wdev 写失败:返回 -1,但第二条仍写("独立证据"语义) */
reset_all();
ff_fake_inject_rc(P_WDEV, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
ok = (sys_log_evidence("second write still") == -1);
CHECK(ok && (syslog_count(SYSLOG_RUN) == 1u) && (g_sys_err_count == 0),
"TC-SYSLOG-074 wdev 写失败不阻断第二条 run.log(多一条独立证据的原始意图)");
}
/* ===================== G 陷阱二:名字/参数类错误码严禁升级成卷操作 ===================== */
static void tc_name_rules(void)
{
static FIL probe;
FRESULT fr_body;
FRESULT fr_ext;
FRESULT fr_bad;
FRESULT fr_dot;
FRESULT fr_colon;
int ok;
uint32_t i;
printf("-- TC-SYSLOG-080..082 陷阱二分档与「绝不升级」 --\n");
/* 080 短名违规**分档**:每条都落到 FR_INVALID_NAME;':' 落到 FR_INVALID_DRIVE */
reset_all();
fr_body = f_open(&probe, "/log/selftestt.log", FA_WRITE | FA_CREATE_ALWAYS);
if (fr_body == FR_OK) { (void)f_close(&probe); }
fr_ext = f_open(&probe, "/log/run.logbak", FA_WRITE | FA_CREATE_ALWAYS);
if (fr_ext == FR_OK) { (void)f_close(&probe); }
fr_bad = f_open(&probe, "/log/ru*n.log", FA_WRITE | FA_CREATE_ALWAYS);
if (fr_bad == FR_OK) { (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/run.log", FA_WRITE | FA_OPEN_APPEND);
if (fr_colon == FR_OK) { (void)f_close(&probe); }
CHECK((fr_body == FR_INVALID_NAME) && (fr_ext == FR_INVALID_NAME)
&& (fr_bad == FR_INVALID_NAME) && (fr_dot == FR_INVALID_NAME)
&& (fr_colon == FR_INVALID_DRIVE),
"TC-SYSLOG-080 分档:体>8 / 扩展>3 / 非法字符 / 点开头 -> FR_INVALID_NAME;':' -> FR_INVALID_DRIVE");
/* 081 生产实际用的每个名字都必须过严格 8.3(改名 = 静默写失败的第一道防线) */
reset_all();
ok = (f_open(&probe, P_RUN, FA_WRITE | FA_OPEN_APPEND) == FR_OK) && (f_close(&probe) == FR_OK);
ok = ok && (f_open(&probe, P_SELFTEST, FA_WRITE | FA_OPEN_APPEND) == FR_OK)
&& (f_close(&probe) == FR_OK);
ok = ok && (f_open(&probe, P_WDEV, FA_WRITE | FA_OPEN_APPEND) == FR_OK)
&& (f_close(&probe) == FR_OK);
ok = ok && (f_stat("/log", NULL) == FR_OK);
CHECK(ok, "TC-SYSLOG-081 生产路径 /log、run.log、selftest.log、wdev.log 全部 8.3 合规");
/* 082 名字类错误码不得升级成卷操作:反复失败也只动自己的文件 */
reset_all();
ready_only();
(void)ff_fake_mkfile(P_SELFTEST, NULL, SYSLOG_REC_SIZE); /* 邻居文件 */
(void)ff_fake_mkfile("/config.ini", NULL, 12u); /* 同卷别的东西 */
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_INVALID_NAME, 200);
for (i = 0u; i < 20u; i++) {
(void)s_append(SYSLOG_RUN, "bad-name");
}
ok = (ff_fake_exists("/log") == 2)
&& (ff_fake_size(P_SELFTEST) == SYSLOG_REC_SIZE)
&& (ff_fake_size("/config.ini") == 12u)
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] <= (int)(2u * 20u))
&& (g_sys_err_count == 1);
CHECK(ok, "TC-SYSLOG-082 反复拿 FR_INVALID_NAME 也只 unlink 自己的文件:目录/邻居不动、留痕一次");
}
/* ===================== H 补齐分支(自愈 / 二次校验 / 回投 / 占位时标) ===================== */
static void tc_extra_branches(void)
{
char ts[SYSLOG_TS_LEN + 1u];
char desc[SYSLOG_DESC_MAX + 1u];
uint8_t raw[SYSLOG_REC_SIZE];
uint32_t epoch;
uint32_t i;
int ok;
printf("-- TC-SYSLOG-090..100 补齐分支 --\n");
/* 090 批量路径的 L1 自愈:open 失败一次 -> unlink + 重开 -> 整批照常落盘
* (这条钉的是 s_append_many 里那段"和单条同一份自愈表";漏了它,
* FTP 侧删过文件之后的第一批日志就会整批丢) */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "seed"); /* 先让文件存在 */
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
{
static uint8_t batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE];
(void)memset(batch, 0x33, sizeof(batch));
ok = (s_append_many(SYSLOG_RUN, batch, 2u) == 0);
}
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3) /* 单条 1 + 批量首开 1 + 自愈重开 1 */
&& (ff_fake_size(P_RUN) == 2u * SYSLOG_REC_SIZE) && (g_sys_err_count == 0),
"TC-SYSLOG-090 批量 open 失败一次 -> unlink 重开 -> 整批 2 条落全、不留痕");
/* 091 批量 open 永久失败 -> -1(走的是"打开失败"那条 else,不是写失败) */
reset_all();
ready_only();
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 99);
{
static uint8_t batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE];
ok = (s_append_many(SYSLOG_RUN, batch, 3u) == -1);
}
CHECK(ok && (ff_fake_exists(P_RUN) == 0) && (g_sys_err_count == 1)
&& (s_lock_releases == 1) && (s_lock_seq_err == 0),
"TC-SYSLOG-091 批量打开持续失败 -> -1 + 留痕一次 + 已释放卷锁");
/* 092 取锁后二次校验:等锁期间写者可能已把文件删掉(容量轮转/取证重建)
* -> 释放锁再返回 -1,绝不能拿锁前的旧 total 去 lseek */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "one");
ff_fake_inject_rc_skip(P_RUN, FF_FAKE_OP_STAT, FR_DISK_ERR, 1, 1);
/* 前面那次 s_append 已经取放锁各一次,所以这里的基线是 1,不是 0 */
ok = (s_lock_releases == 1);
ok = ok && (s_read_raw(SYSLOG_RUN, 0u, raw) == -1)
&& (s_lock_releases == 2) && (s_lock_seq_err == 0);
CHECK(ok && (s_rd_win_cnt == 0u),
"TC-SYSLOG-092 锁后二次 count 失败 -> 放锁返回 -1(漏放 = FTP 永久等不到卷)");
/* 093 读时 f_open 失败 -> 放锁返回 -1(另一条提前退出路径) */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "one");
s_rd_win_id = -1;
s_rd_win_cnt = 0u;
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
ok = (s_read_raw(SYSLOG_RUN, 0u, raw) == -1)
&& (s_lock_releases == 2) && (s_lock_seq_err == 0);
/* 第二次读(注入耗尽)应成功,证明窗口没被留在半初始化状态 */
ok = ok && (s_read_raw(SYSLOG_RUN, 0u, raw) == 0) && (s_rd_win_cnt == 1u);
CHECK(ok, "TC-SYSLOG-093 读打开失败 -> 放锁 -1;下轮恢复后窗口正常重填");
/* 094 墙钟未建立时 evidence 用占位时标(不得写半截或未初始化缓冲) */
reset_all();
s_clock_ok = 0;
ok = (sys_log_evidence("no clock") == 0);
if (ok) {
(void)ff_fake_fetch(P_WDEV, raw, 0u, SYSLOG_TS_LEN);
ok = (memcmp(raw, "1970-01-01 08:00:00", SYSLOG_TS_LEN) == 0);
}
CHECK(ok, "TC-SYSLOG-094 无墙钟 -> wdev.log 落占位时标(取证行也必须可解析)");
/* 095 wdev 短写:FR_OK + 字节数不足 -> -1,但第二条 run.log 照写 */
reset_all();
ff_fake_inject_write_short(P_WDEV, 3u, 1);
ok = (sys_log_evidence("short line") == -1);
CHECK(ok && (ff_fake_size(P_WDEV) == 3u) && (syslog_count(SYSLOG_RUN) == 1u)
&& (g_sys_err_count == 0),
"TC-SYSLOG-095 wdev 短写判失败返回 -1,第二条独立证据仍落 run.log");
/* 096 队列创建失败:条目滞留 preboot、留痕一次、下轮建成后排空 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
s_q_new_fail = 1;
syslog_post(SYSLOG_RUN, "nq");
syslog_drain();
ok = (s_queue == NULL) && (g_sys_err_count == 1) && (s_preboot_cnt == 1u)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_soe_drain_calls == 0);
s_q_new_fail = 0;
syslog_drain();
CHECK(ok && (s_queue != NULL) && (syslog_count(SYSLOG_RUN) == 1u)
&& (s_preboot_cnt == 0u) && (s_soe_drain_calls == 1),
"TC-SYSLOG-096 队列没建成 -> 一条都不排(不静默丢),堆恢复后排空并接上 SOE");
/* 097 换文件那条来自队列 -> 原样回投队列(不是 preboot 下标回退)
* 队列里 RUN/SELFTEST/RUN 交替:三条必须全落且各自文件内顺序不乱 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain(); /* 建队列 */
syslog_post(SYSLOG_RUN, "qa");
syslog_post(SYSLOG_SELFTEST, "qb");
syslog_post(SYSLOG_RUN, "qc");
ok = (s_q_cnt == 3u);
syslog_drain();
if (ok) {
ok = (syslog_count(SYSLOG_RUN) == 2u) && (syslog_count(SYSLOG_SELFTEST) == 1u)
&& raw_at(P_RUN, 0u, raw) && (memcmp(raw + SYSLOG_DESC_OFFS, "qa", 2u) == 0)
&& raw_at(P_RUN, 1u, raw) && (memcmp(raw + SYSLOG_DESC_OFFS, "qc", 2u) == 0)
&& raw_at(P_SELFTEST, 0u, raw) && (memcmp(raw + SYSLOG_DESC_OFFS, "qb", 2u) == 0)
&& (s_q_cnt == 0u);
}
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3),
"TC-SYSLOG-097 队列侧换文件回投:三条全落、各文件内顺序不乱(回投不是丢弃)");
/* 098 dl >= desc_len 的截断分支:守卫已挡掉 desc_len <= SYSLOG_DESC_MAX,
* 而 dl 由记录内容决定、恒 <= SYSLOG_DESC_MAX -> 该分支在**当前守卫取值下
* 永不可达**。这条断言把"不可达"这件事本身钉住:一旦有人把守卫放宽成
* desc_len == 0,本条就会因为读到截断结果而红,提醒补截断用例。 */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "abcdefghijklmnopqrstuvwxyz0123"); /* 34 字符 -> 截到 28 */
ok = (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, SYSLOG_DESC_MAX + 1u) == 0)
&& (strlen(desc) == (size_t)SYSLOG_DESC_MAX)
&& (desc[SYSLOG_DESC_MAX] == '\0');
CHECK(ok, "TC-SYSLOG-098 desc_len=29 时截断分支不可达(拿到整 28 字符,非 28-1)");
/* 099 空指针守卫:s_read_raw 的 p_rec 与 evidence 的 p_desc */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "g");
ok = (s_read_raw(SYSLOG_RUN, 0u, NULL) == -1)
&& (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
&& (sys_log_evidence(NULL) == -1);
CHECK(ok && (s_lock_seq_err == 0),
"TC-SYSLOG-099 p_rec / p_desc 空指针守卫(守卫顺序在取锁之前,不放锁)");
/* 100 last_epoch:有记录但读失败 -> -1 且把 *p_epoch 清 0
* (调用方 syslog_mcu_mounted 直接拿这个值判上电/重启,留旧值就会误判) */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "e");
s_rd_win_id = -1;
s_rd_win_cnt = 0u;
epoch = 0xA5A5A5A5u;
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
ok = (syslog_last_epoch(SYSLOG_RUN, &epoch) == -1) && (epoch == 0u);
CHECK(ok, "TC-SYSLOG-100 last_epoch 读失败 -> -1 且 *p_epoch 清零(不留调用方旧值)");
(void)i;
}
/* ===================== 入口 ===================== */
int main(void)
{
printf("=== Sys Log Mcu Tests ===\n");
tc_format();
tc_write_single();
tc_write_many();
tc_drain();
tc_read();
tc_mounted();
tc_evidence();
tc_name_rules();
tc_extra_branches();
printf("\n=== Sys Log Mcu Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}