/* * 模块名称:运行/自检记录落盘后端(sys_log_mcu)主机侧单元测试(L0.5) * 模块功能:在 PC 上离线验证 App/log/sys_log_mcu.c 的四族契约—— * A 64B 定长记录格式(时间戳 / epoch ASCII / 描述三段 + 截断 + 占位) * B 写侧(单条 s_append_rec、批量 s_append_many、日志读写放大收口的"一批一次 * open/write/close"、容量轮转、L1 自愈、L2 留痕去重、卷锁 try) * C 排空(syslog_drain 的 preboot + 队列两条来源、换文件分批、 * 失败回投不丢条目、-3 退回慢路径) * D 读侧(日志读写放大收口的 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 语义 / 日志读写放大收口的读侧**:读窗口"命中即零 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 单条路径与批量路径产出的同内容记录**逐字节相同**(日志读写放大收口抽出 * 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 卷忙 -> 整批回投队列,一条不丢(A 项修复后的正向断言) * TC-SYSLOG-035a 回投后再排空:顺序与投递顺序一致,全程静默 * TC-SYSLOG-035b 慢路径遇卷忙 -> 尾部整批回投(参照物,A 项就是向它对齐) * TC-SYSLOG-035c 满批 8 条在卷忙时全部保住 * TC-SYSLOG-035d preboot 来源的条目卷忙时转投队列(不丢也不重复落盘) * 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 容量守卫:ts_len<=19 或 desc_len<=28 一律 -1 * TC-SYSLOG-057b 被拒的调用一个字节都不写调用方缓冲(C 项"宁可拒绝不截断") * 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 读侧不再截断(C 项已删死分支):拿到整 28 字符 + 唯一 NUL * TC-SYSLOG-099 p_rec / p_desc 空指针守卫(守卫在取锁之前) * TC-SYSLOG-100 last_epoch 读失败 -> -1 且 *p_epoch 清零 * —— I D 项:自愈触发集逐码(与 soe_store 侧同一张表)—— * TC-SYSLOG-101/101b 介质/结构类码(FR_DISK_ERR/INT_ERR/NO_FILESYSTEM/DENIED) * -> 单条与批量路径各 unlink 恰好一次 + 重开一次并落全(自愈成功 * 不留痕;文件被删是设计代价,所以 SOE 侧不允许这么做) * TC-SYSLOG-102 名字/参数类码(INVALID_NAME/NO_PATH/NO_FILE/INVALID_OBJECT/ * NOT_READY/NOT_ENABLED/TIMEOUT)连续 20 轮 -> 零 unlink、零重开、 * 邻居文件与 /log 目录不动、读侧代数照常自增 * TC-SYSLOG-103/103b s_is_selfheal_code() 分类表本身逐码钉死 * TC-SYSLOG-104 卷忙不计故障:恢复后第一次真失败仍立刻留痕(反证 -2 没置故障位) * —— J F 项:投递侧丢弃留痕节流(队列通道)—— * TC-SYSLOG-105 队列满后连投 12 条 -> 只报第一次(A 项回投让队列常驻满, * 不节流就按投递频率刷串口) * TC-SYSLOG-106 同一故障窗口内再投 5 条 -> 完全静默(丢弃语义不变,照丢) * TC-SYSLOG-107 复位点必须在 syslog_post 的成功分支:drain 只落盘、不复位 * TC-SYSLOG-107b 恢复后再灌满 -> 第二次故障重新留痕 * TC-SYSLOG-107c 队列通道与启动前数组通道的抑制位互不干扰 * —— 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 不可能失败, * 假件也不伪造返回值;为刷数字去伪造等于把断言写在假件规则上,宁可留空)。 * * 本套照出的四条现状 —— 已在 task-4(2026-10-24)全部处置,相应断言已翻成正向: * * A【已修】卷忙整批静默丢日志:syslog_drain 批量路径 done = n 后缺 rc==-2 分支 * → 回投循环空转,最多丢 8 条。修法 = 补 else if (rc == -2) { done = 0u; }, * 与 -3 慢路径遇 -2 的处置对齐(慢路径是参照物,没动它)。三条语义纪律 * ①不计故障 ②不打日志 ③回投保序 分别由 TC-SYSLOG-035 / 035a / 035d 钉住。 * B【已修】rc==-1 的留痕文案 "drop %lu entries" 与行为相反(done=0 → 其实整批 * 回投、一条没丢)。改成 "write fail, requeue N entries",并按"正常→故障" * 节流(新增文件级 s_requeue_reported[],成功一轮复位)——旧版持续故障时每 * 2s 刷一条。TC-SYSLOG-039 / 039b 钉住"节流一次"与"恢复后再失败要再报"。 * C【已修】syslog_get_record 的截断支永不可达:守卫已挡掉 desc_len <= * SYSLOG_DESC_MAX,而 dl 恒 <= 28。删掉死分支,容量要求写进函数头与 * sys_log.h(**宁可整条拒绝也不静默截断**:UI 拿到截断串会显示成一条被删短 * 过的日志,比读不到更难查)。TC-SYSLOG-057 / 057b / 098 钉住。 * D【已修】自愈触发集违反 §10.1 第 10 条:FR_INVALID_NAME / FR_NO_PATH / * FR_NO_FILE / FR_INVALID_OBJECT 都曾在 unlink 重建集合里,后果是名字/参数类 * 错误下**每轮删一次自己的历史日志且永远修不好**。现在按语义拆类,新增 * s_is_selfheal_code():触发集只留 FR_DISK_ERR / FR_INT_ERR / FR_NO_FILESYSTEM / * FR_DENIED,逐码依据(含 ff.c 行号)写在该函数头。TC-SYSLOG-101~103 逐码钉, * 与 test/host/soe_store/ 的 TC-SOESTORE-022b 是同一张表的两侧。 * * F【已修】投递侧丢弃留痕无节流:syslog_post 的两个丢弃分支(队列满 / 启动前数组 * 满)原来每条被丢的投递各打一行 DBG_ERROR。A 项修复让卷忙时整批回投 -> 队列 * 可长时间常驻满态 -> 此后每条业务投递都打一行,日志自己变成串口负载(比原 * 缺陷更常触发)。修法 = 与 B 同一套"正常→故障"节流纪律,但**另立两个抑制位** * (s_post_qfull_reported / s_post_pboot_full_reported,volatile 单字节,任意 * 上下文读写)——不复用 s_requeue_reported:那是 defaultTask 的 drain 侧"整批 * 回投",本处是任意任务的"这一条被丢了",恢复时机也不同(下一次投递成功)。 * **丢弃语义一个字没改**(满则丢仍照旧),变的只有留痕次数。 * TC-SYSLOG-041b/041d/041e/041f(数组通道)+ 105/106/107/107b/107c(队列通道)。 * * 顺手记一条容易误读的事实(不是缺陷):自愈成功 = **删掉整个日志文件重建**, * 所以一次打开失败会连带丢掉既有历史(TC-SYSLOG-101 明确断言文件只剩本次一条)。 * run.log 是滚动运行日志,这个代价可接受;soe.log 是事件序列,同样的动作被禁止 * (TC-SOESTORE-022 / 022b),两侧的差异是刻意的,注释也互指。 * * 故意没测(写在这里,不留"看起来全覆盖"的错觉): * - 真跨任务并发:读窗口用"对齐 32 位读写在 ARM 上原子"免锁,主机 x64 + 单线程 * 编不出真竞态;本套只断言单写者语义与代数失效逻辑本身。 * - f_close 内部 sync_fs -> disk_ioctl(CTRL_SYNC) -> ftl_flush_cache() 的掉电落盘 * 链:属 nand_port/GD5F 层,假卷里没有 FTL,"关文件即落盘"要靠真机 + 写回缓存空闲 flush 台账。 * - 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 #include #include #include #include #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 40u /* sizeof(syslog_entry_t) 在 x64 = 32(B 方案:条目内嵌 desc[29],只存指针时为 16),留余量。 * 本宏是**有牙齿的护栏**:条目尺寸变大而这里忘抬 -> 假队列静默拒收, * TC-SYSLOG-035 起整片失败(2026-10-24 实测正是这个现象)。 */ 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; /* B / F 项整改新增的留痕节流位:跨场景必须显式清(否则第二个故障场景看不到留痕) */ s_requeue_reported[SYSLOG_RUN] = 0u; s_requeue_reported[SYSLOG_SELFTEST] = 0u; s_post_qfull_reported = 0u; s_post_pboot_full_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 record format (fixed 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]space / [20..29]epoch / [30]space / [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 tail padded with NUL to 64B (basis of fixed-length parsing)"); /* 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 over-long desc truncated to 28B, never crossing the 64B boundary"); } /* 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 no wall clock -> placeholder timestamp + epoch all zero (never fake a time stamp)"); /* 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 same input formats byte-identical twice (premise of UI fixed-offset parsing)"); 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 batch path reuses the same s_format_rec (both equal the single-record result)"); } } /* ===================== B 写侧 ===================== */ static void tc_write_single(void) { uint8_t rec[SYSLOG_REC_SIZE]; int ok; printf("-- TC-SYSLOG-010..019 single-record write path --\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 single write ok: mode carries the write bit + open/write/close once each + lock released"); /* 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 alone -> f_write is always FR_DENIED (real root cause 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 volume busy means defer: no disk touch, no release, no fault counted (feed point never blocks)"); /* 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 + short write 63B treated as failure -> unlink and rebuild -> exactly 64 bytes"); /* 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 at 1024 records -> unlink first then write, new file holds only this record"); /* 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 failure during rotation -> -1 and **the volume lock is released** (leak = FTP never gets the volume)"); /* 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 returning FR_NO_FILE is harmless: no error branch, no trace, write continues"); /* 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 out of range / empty rec / empty desc -> -1 without touching the lock or the disk"); /* 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 each write bumps only this id's generation (the other file's window stays valid)"); /* 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 a persistent fault reports once; a failure after recovery must trace again"); } 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 batch write path (log I/O amplification wrap-up) --\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 a batch of 8 = one open + one 512B write + one close (old code needed 8 rounds)"); /* 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 batch argument guards run **before taking the volume lock** (bad n never occupies it)"); /* 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 batch does not fit -> -3 back to the slow path; the batch path **never** unlinks itself"); /* 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 batch with busy volume -> -2; short write (256 asked, 100 back) -> rebuild, whole batch lands"); /* 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 batch failure trace de-duplicated; generation still bumps (invalidate a window rather than read stale)"); (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 volume not ready -> queue created, entries stay in preboot, nothing written and no SOE call"); /* 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 the queue is created at the **first drain** (defaultTask context); creating objects pre-kernel kills boot"); /* 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 kernel not running: all 8 entries go to the preboot array"); 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 8 preboot entries of one file -> one merged 512B write (index inspected, not deleted)"); /* 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 a file change splits the batch (run split in two = 3 I/O rounds) + order preserved"); /* 035【A 项已修 2026-10-24:正向断言】卷忙 -> **整批回投队列**,一条不丢。 * 生产侧修法:syslog_drain 批量路径补 else if (rc == -2) { done = 0u; }, * 与 -3 慢路径遇 -2 的处置对齐。三条语义纪律各有一条断言: * ① 不计故障(无 DBG_ERROR) ② 不打日志(同一判据) ③ 回投保持原顺序 */ 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 -> 整批回投 */ ok = (syslog_count(SYSLOG_RUN) == 0u) && (s_q_cnt == 3u) && (g_sys_err_count == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0); CHECK(ok, "TC-SYSLOG-035 volume busy -> whole batch requeued, none lost"); /* ③ 顺序:回投后再排空,落盘顺序必须与投递顺序一致 */ syslog_drain(); ok = ok && (syslog_count(SYSLOG_RUN) == 3u) && (s_q_cnt == 0u); if (ok) { for (i = 0u; i < 3u; i++) { if (!raw_at(P_RUN, i, rec) || (memcmp(rec + SYSLOG_DESC_OFFS, "hold", 4u) != 0)) { ok = 0; } } } CHECK(ok && (g_sys_err_count == 0), "TC-SYSLOG-035a requeued batch keeps original order and stays silent"); /* 108【B 项整改 2026-10-24:投递即拷贝】投出去之后**改写调用方缓冲**, * 落盘内容必须仍是投递时的原文。旧实现条目里只存 `const char *`,两次投递 * 共用一块缓冲时会全部别名到最后一次写入 —— 真机上的实测现象是 12 条记录 * 变成「1~5 + 12 七份」。主机侧此前照不出来:用例传的都是字面量,静态存储期 * 无限,正好绕开这个坑(教训:断言里出现「生命周期」维度时才需要可变缓冲)。 */ reset_all(); ready_only(); s_kernel_state = osKernelRunning; syslog_drain(); { static char mut[24]; (void)strcpy(mut, "keepA"); syslog_post(SYSLOG_RUN, mut); (void)strcpy(mut, "zzzB"); /* 投递之后才改写:不得影响已投出去的条目 */ syslog_post(SYSLOG_RUN, mut); syslog_drain(); ok = (syslog_count(SYSLOG_RUN) == 2u) && raw_at(P_RUN, 0u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "keepA", 5u) == 0) && raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "zzzB", 4u) == 0); CHECK(ok, "TC-SYSLOG-108 post copies desc: caller buffer reuse cannot corrupt queued entries"); } /* 108b 拷贝路径上的长度钳位:超过 SYSLOG_DESC_MAX 的部分不得溢出条目缓冲, * 且落盘只剩前 28 字节(条目内缓冲 = 28 + 1,越界即踩相邻字段) */ reset_all(); ready_only(); s_kernel_state = osKernelRunning; syslog_drain(); syslog_post(SYSLOG_RUN, "0123456789012345678901234567890123456789"); syslog_drain(); ok = (syslog_count(SYSLOG_RUN) == 1u) && raw_at(P_RUN, 0u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "012345678901234567890123456789", 28u) == 0) && (rec[SYSLOG_DESC_OFFS + 28u] == 0); CHECK(ok, "TC-SYSLOG-108b over-long desc is clamped to SYSLOG_DESC_MAX inside the entry"); /* 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 slow path meets busy volume -> all 3 **requeued whole** (same code, opposite behaviour per path)"); /* 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 == SYSLOG_QUEUE_MAX) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0) && (g_sys_err_count == 0), "TC-SYSLOG-035c all 8 entries of a full batch survive a busy volume"); /* 035d preboot 来源的条目同样不丢:卷忙时它们从数组转投队列(数组侧删掉、 * 队列接手),既不是留在数组里被重复落盘、也不是被 memmove 吞掉 */ reset_all(); ready_only(); for (i = 0u; i < 4u; i++) { syslog_post(SYSLOG_RUN, "pb"); /* 内核未运行 -> preboot */ } s_kernel_state = osKernelRunning; lock_busy_next(1); syslog_drain(); ok = (s_preboot_cnt == 0u) && (s_q_cnt == 4u) && (syslog_count(SYSLOG_RUN) == 0u); syslog_drain(); CHECK(ok && (syslog_count(SYSLOG_RUN) == 4u) && (s_q_cnt == 0u), "TC-SYSLOG-035d preboot entries move to queue on busy volume (no loss, no dup)"); /* 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 requeue does not fit -> 5 hold-over drops + one drop trace and one wfail trace"); /* 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 -> per-record slow path: fill the rest -> full -> unlink rebuild -> continue, unlink exactly once"); /* 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 slow-path write failure: only **that one record** is lost, the last 2 land next round"); /* 039【B 项已修 2026-10-24:正向断言】rc == -1:整批回投 + 留痕文案如实 * (旧版这里打 "drop %lu entries" 但一条没丢,文案与行为相反)。 * 一轮失败 = s_append_many 自己那条写故障留痕 + drain 这条回投留痕 = 2; * 第二轮仍失败时两条都必须被各自的去重位压住(旧版 drain 那条每 2s 刷一次)。 */ 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, 4); /* 撑住两轮失败(每轮 2 次写) */ g_sys_err_count = 0; syslog_drain(); ok = (s_q_cnt == 3u) && (syslog_count(SYSLOG_RUN) == 0u) && (g_sys_err_count == 2); syslog_drain(); /* 第二轮:仍失败,必须静默 */ ok = ok && (s_q_cnt == 3u) && (g_sys_err_count == 2) && (s_requeue_reported[SYSLOG_RUN] == 1u); CHECK(ok, "TC-SYSLOG-039 write failure requeues whole batch; log throttled to first round"); ff_fake_inject_write_short(P_RUN, 0u, 0); /* no-op:清不掉已耗尽的注入 */ syslog_drain(); /* 注入耗尽 -> 成功 -> 复位 */ ok = ok && (syslog_count(SYSLOG_RUN) == 3u) && (s_q_cnt == 0u) && (s_requeue_reported[SYSLOG_RUN] == 0u); ff_fake_inject_write_short(P_RUN, 0u, 2); syslog_post(SYSLOG_RUN, "again"); g_sys_err_count = 0; syslog_drain(); /* 恢复后再失败 -> 必须重新留痕 */ CHECK(ok && (g_sys_err_count == 2), "TC-SYSLOG-039b recovery resets the throttle; a new fault is logged again"); /* 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 must call soe_store_drain (same writer, same volume-ready gate)"); /* 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 out of range / NULL desc -> silently dropped (does not occupy a pending slot)"); reset_all(); ready_only(); for (i = 0u; i < SYSLOG_QUEUE_MAX + 2u; i++) { syslog_post(SYSLOG_RUN, "pf"); } /* F 项已修(2026-10-24):旧实现每条被丢的投递都打一行,数组满 + 挂载慢的窗口 * 里按开机事件密度刷屏。现在只报转入故障那一次;**丢弃语义不变**(照样丢 2 条)。 */ CHECK((s_preboot_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1), "TC-SYSLOG-041b preboot full: 2 entries dropped but logged only once"); /* 节流复位靠"数组重新收得下第一条":先排空、再投一条成功的,抑制位才归零。 * 注意排空本身不复位它(复位点在 syslog_post 的成功分支里)。 */ syslog_drain(); ok = (s_preboot_cnt == 0u) && (s_post_pboot_full_reported == 1u); s_kernel_state = osKernelInactive; /* 让 post 走数组而不是队列 */ syslog_post(SYSLOG_RUN, "recovered"); /* 成功收下第一条 -> 复位 */ ok = ok && (s_preboot_cnt == 1u) && (s_post_pboot_full_reported == 0u); CHECK(ok, "TC-SYSLOG-041d recovery (one accepted entry) clears the throttle bit"); /* 重新灌满:8 槽 + 2 条溢出 -> 只报一次(证明抑制位真的重新武装了) */ g_sys_err_count = 0; for (i = 0u; i < SYSLOG_QUEUE_MAX + 1u; i++) { syslog_post(SYSLOG_RUN, "pf2"); } CHECK((s_preboot_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1), "TC-SYSLOG-041e second fault is logged again, then stays silent"); g_sys_err_count = 0; for (i = 0u; i < 3u; i++) { syslog_post(SYSLOG_RUN, "pf3"); /* 故障期间继续静默 */ } CHECK((g_sys_err_count == 0) && (s_post_pboot_full_reported == 1u), "TC-SYSLOG-041f drops inside the same fault window log nothing"); 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 kernel running -> queue path; queue full drops with a trace"); } /* ===================== 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 read side (log I/O amplification wrap-up sliding window) --\n"); /* 050 count */ reset_all(); ready_only(); CHECK(syslog_count(SYSLOG_RUN) == 0u, "TC-SYSLOG-050a missing file -> 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), no content read); id out of range -> 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 window hit -> the second read makes **zero FatFs calls** (UI paging no longer opens per record)"); /* 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 once the writer lands the generation changes -> window refills, never returns stale content"); /* 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 crosses the window -> one open + lseek(8*64) + read(1 record) (window aligned to 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 out-of-range idx / empty file -> -1 (count checked before and after taking the lock)"); /* 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 busy volume while reading -> -1 (try semantics: uiTask never blocks for the lock, retry next refresh)"); /* 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 short read -> window invalidated (a half window treated as valid = UI shows half a stale record)"); /* 057 get_record 守卫与截断 */ reset_all(); ready_only(); (void)s_append(SYSLOG_RUN, "01234567890123456789012345678901234567"); /* 拒绝路径必须一个字节都不写调用方缓冲:先填满 0x7A 当哨兵 */ (void)memset(desc, 0x7A, sizeof(desc)); (void)memset(ts, 0x7A, sizeof(ts)); 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 guards: ts_len<=19 or desc_len<=28 always -1; only 29 reads the full 28 characters"); /* C 项整改(2026-10-24):容量不足一律拒绝,且**不得**碰调用方缓冲—— * 上面每次被拒的调用都用了同一对缓冲,哨兵还在就说明没写进去 */ reset_all(); ready_only(); (void)s_append(SYSLOG_RUN, "sentinel-check"); (void)memset(desc, 0x7A, sizeof(desc)); (void)memset(ts, 0x7A, sizeof(ts)); ok = (syslog_get_record(SYSLOG_RUN, 0u, ts, SYSLOG_TS_LEN, desc, sizeof(desc)) == -1) && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, SYSLOG_DESC_MAX) == -1); { uint32_t k; int untouched = 1; for (k = 0u; k < sizeof(desc); k++) { if (desc[k] != (char)0x7A) { untouched = 0; } } for (k = 0u; k < sizeof(ts); k++) { if (ts[k] != (char)0x7A) { untouched = 0; } } CHECK(ok && (untouched != 0), "TC-SYSLOG-057b rejected call writes nothing into caller buffers"); } /* 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: no record -> -1; with a record parse the 10 ASCII digits"); } /* ===================== E mounted / 上电重启分类 ===================== */ static void tc_mounted(void) { uint8_t rec[SYSLOG_REC_SIZE]; int ok; printf("-- TC-SYSLOG-060..064 post-mount handling (idempotent mount + power/reboot classification) --\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 exists -> write only the one power/reboot record"); /* 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 idempotent re-entry: no mkdir, no second power-on record (the 2s recheck must not flood)"); } (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 failure: not ready, trace once (no 2s flooding), third round self-heals and resets the flag"); /* 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 busy volume -> return at once (a failed try must not release; retry after the next 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 last epoch 300s(<600) before now -> desc is \"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 gap >=600s -> \"power on\" (over ten minutes off counts as a fresh power-up)"); 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 wall clock invalid (UINT32_MAX) -> conservatively \"power on\", never judge a reboot from a fake delta"); /* 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 with /log present f_stat short-circuits, f_mkdir is never called"); } /* ===================== 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 forensics channel) --\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 writes a variable-length line (with CRLF) plus a separate run.log evidence record"); /* 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 the lock is taken twice and released twice (calling s_append while holding it would deadlock)"); /* 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 beyond 8KB -> unlink first then write (a long run must not grow without bound)"); /* 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 busy volume -> -1 and **neither write happened** (feed point never blocks for the lock)"); /* 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 a wdev write failure does not block the second run.log record (the intent of extra evidence)"); } /* ===================== 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 name class split and never escalate --\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 classes: body>8 / ext>3 / bad char / leading dot -> 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 production paths /log, run.log, selftest.log, wdev.log are all valid 8.3 names"); /* 082 名字类错误码不得升级成卷操作:反复失败也只动自己的文件 */ reset_all(); ready_only(); (void)ff_fake_mkfile(P_SELFTEST, NULL, SYSLOG_REC_SIZE); /* 邻居文件 */ (void)ff_fake_mkfile("/config.ini", NULL, 12u); /* 同卷别的东西 */ /* D 项整改后:名字/参数类码**一次都不许 unlink**(旧实现每轮删一次自己的 * 历史日志、且永远修不好)。留痕仍受 L2 去重,只 1 条。 */ 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] == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 20) /* 也没有"重开一次" */ && (g_sys_err_count == 1); CHECK(ok, "TC-SYSLOG-082 20 rounds of FR_INVALID_NAME: zero unlink, zero reopen, one log"); } /* ===================== 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 remaining branch coverage --\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 one batch open failure -> unlink and reopen -> whole batch of 2 lands, no trace"); /* 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 batch open keeps failing -> -1 + one trace + the volume lock released"); /* 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 second count after the lock fails -> release the lock and return -1 (leak = FTP waits forever)"); /* 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 read open failure -> release lock and -1; next round refills the window after recovery"); /* 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 no wall clock -> wdev.log gets a placeholder timestamp (forensic lines must stay parseable)"); /* 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 a wdev short write fails with -1 while the second run.log evidence record still lands"); /* 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 queue not created -> nothing is drained (no silent loss); after the heap recovers it empties into 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 queue-side file change requeues: all three land, order inside each file kept (requeue is not a drop)"); /* 098【C 项已修 2026-10-24】原来的 "if (dl >= desc_len) dl = desc_len - 1" * 是永不可达的死分支(守卫已挡掉 desc_len <= SYSLOG_DESC_MAX,而 dl 恒 * <= SYSLOG_DESC_MAX),已从生产码删除。本条改为正向钉住"读侧不再截断": * desc_len = 29 时拿到的是整 28 字符,且 desc[28] 是唯一的 NUL。 * 若有人把守卫放宽(例如改成 desc_len == 0),本条会因拿到 27 字符而红。 */ 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 && (desc[SYSLOG_DESC_MAX - 1u] != '\0') && (desc[SYSLOG_DESC_MAX] == '\0'), "TC-SYSLOG-098 no truncation on read side: full 28 chars with one NUL"); /* 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 NULL guards (guards run before taking the lock, so nothing is released)"); /* 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 read failure -> -1 and *p_epoch cleared (never leave the caller's old value)"); (void)i; } /* ===================== I D 项整改:自愈触发集逐码 ===================== */ /* 打开失败时"该不该 unlink 重建"逐码判定。断言的是**语义归属**,不是假件规则: * 介质/结构类(能靠删文件修好)→ 必须恰好 unlink 一次并再开一次; * 名字/参数类(删了也修不好)→ 必须零 unlink、零重开。 */ static void tc_selfheal_codes(void) { static const FRESULT heal_codes[] = { FR_DISK_ERR, FR_INT_ERR, FR_NO_FILESYSTEM, FR_DENIED }; /* FR_NO_FILE 也在"不进"这一列:本模块用 FA_OPEN_APPEND(含 FA_OPEN_ALWAYS), * 文件不存在会被直接建出来;真返回 FR_NO_FILE 只可能是同名对象是目录 *(ff.c:3854),unlink 一个目录修不好它,反而违反"自愈只针对自己的文件"。 */ static const FRESULT noheal_codes[] = { FR_INVALID_NAME, FR_NO_PATH, FR_NO_FILE, FR_INVALID_OBJECT, FR_NOT_READY, FR_NOT_ENABLED, FR_TIMEOUT }; static uint8_t one[SYSLOG_REC_SIZE]; uint32_t c; uint32_t round; uint32_t gen_seed; int ok; (void)memset(one, 0x11, sizeof(one)); printf("-- TC-SYSLOG-101..104 self-heal trigger set, per code (fix D) --\n"); /* 101 介质/结构类:单条与批量路径都必须"unlink 恰好一次 + 重开一次", * 并最终把这条/这批落下去,且不留痕(自愈成功不该报故障) */ for (c = 0u; c < (uint32_t)(sizeof(heal_codes) / sizeof(heal_codes[0])); c++) { reset_all(); ready_only(); (void)s_append(SYSLOG_RUN, "seed"); /* 先让文件存在 */ { int opens_seed = g_ff_obs.calls[FF_FAKE_OP_OPEN]; ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, heal_codes[c], 1); ok = (s_append(SYSLOG_RUN, "single") == 0) && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_seed + 2) /* 自愈的前提就是删文件:seed 那条随之消失,文件只剩本次这一条。 * (SOE 侧绝不这么做,理由见 TC-SOESTORE-022:事件序列不能销毁) */ && (ff_fake_size(P_RUN) == 1u * SYSLOG_REC_SIZE) && (g_sys_err_count == 0) && (s_lock_seq_err == 0); } CHECK(ok, "TC-SYSLOG-101 single path heals one media/structure code"); reset_all(); ready_only(); (void)s_append(SYSLOG_RUN, "seed"); { int opens_seed = g_ff_obs.calls[FF_FAKE_OP_OPEN]; static uint8_t batch[2u * SYSLOG_REC_SIZE]; (void)memset(batch, 0x22, sizeof(batch)); ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, heal_codes[c], 1); ok = (s_append_many(SYSLOG_RUN, batch, 2u) == 0) && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_seed + 2) && (ff_fake_size(P_RUN) == 2u * SYSLOG_REC_SIZE) /* 旧的 seed 被删掉 */ && (g_sys_err_count == 0) && (s_lock_seq_err == 0); } CHECK(ok, "TC-SYSLOG-101b batch path heals the same code once"); } /* 102 名字/参数类:连续 20 轮都必须零 unlink、零重开、只留痕一次 */ for (c = 0u; c < (uint32_t)(sizeof(noheal_codes) / sizeof(noheal_codes[0])); c++) { reset_all(); ready_only(); (void)s_append(SYSLOG_RUN, "seed"); (void)ff_fake_mkfile(P_SELFTEST, NULL, SYSLOG_REC_SIZE); /* 邻居 */ { int opens_seed = g_ff_obs.calls[FF_FAKE_OP_OPEN]; gen_seed = s_rd_gen[SYSLOG_RUN]; ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, noheal_codes[c], 200); for (round = 0u; round < 20u; round++) { ok = 1; if (s_append(SYSLOG_RUN, "no-heal") != -1) { ok = 0; } /* 批量路径也必须走同一条判定:给它一整条 64B 记录的缓冲 */ if (s_append_many(SYSLOG_RUN, one, 1u) != -1) { ok = 0; } } /* 代数每轮 +2(单条与批量各自都失效一次读窗口):宁多失效也不读脏窗 */ CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_seed + 40) && (ff_fake_exists(P_SELFTEST) == 1) && (ff_fake_exists("/log") == 2) && (s_rd_gen[SYSLOG_RUN] == gen_seed + 40u) && (s_lock_seq_err == 0), "TC-SYSLOG-102 name/param codes never unlink or reopen (20 rounds)"); } } /* 103 s_is_selfheal_code() 直测:把分类表本身钉死(写死的期望表,不读生产逻辑) */ ok = (s_is_selfheal_code(FR_DISK_ERR) == 1) && (s_is_selfheal_code(FR_INT_ERR) == 1) && (s_is_selfheal_code(FR_NO_FILESYSTEM) == 1) && (s_is_selfheal_code(FR_DENIED) == 1); CHECK(ok, "TC-SYSLOG-103 trigger set = {DISK_ERR, INT_ERR, NO_FILESYSTEM, DENIED}"); ok = (s_is_selfheal_code(FR_INVALID_NAME) == 0) && (s_is_selfheal_code(FR_NO_PATH) == 0) && (s_is_selfheal_code(FR_NO_FILE) == 0) && (s_is_selfheal_code(FR_INVALID_OBJECT) == 0) && (s_is_selfheal_code(FR_NOT_READY) == 0) && (s_is_selfheal_code(FR_NOT_ENABLED) == 0) && (s_is_selfheal_code(FR_TIMEOUT) == 0) && (s_is_selfheal_code(FR_OK) == 0) && (s_is_selfheal_code(FR_EXIST) == 0) && (s_is_selfheal_code(FR_WRITE_PROTECTED) == 0); CHECK(ok, "TC-SYSLOG-103b name/param/volume-gate codes all excluded"); /* 104 卷忙不计故障这条纪律(A 项要求①)在写路径同样成立:-2 不产生任何留痕, * 也不自增故障位(s_fault_reported 是 s_append_rec 的函数级 static, * 测不到,只能从"恢复后第一次真失败仍立刻留痕"反证它没被置位) */ reset_all(); ready_only(); /* s_append_rec 的 L2 去重位是函数级 static,外部清不了:先用一次成功写复位 */ ok = (s_append(SYSLOG_RUN, "reset-dedup") == 0); g_sys_err_count = 0; lock_busy_next(1); ok = ok && (s_append(SYSLOG_RUN, "busy") == -2) && (g_sys_err_count == 0); ff_fake_inject_write_short(P_RUN, 0u, 2); ok = ok && (s_append(SYSLOG_RUN, "real-fault") == -1); CHECK(ok && (g_sys_err_count == 1), "TC-SYSLOG-104 busy is not a fault: first real failure still logs immediately"); } /* ===================== J F 项:投递侧丢弃留痕节流(队列通道) ===================== */ static void tc_post_throttle(void) { uint32_t i; int ok; printf("-- TC-SYSLOG-105..107 post-side drop-log throttle (queue channel) --\n"); /* 105 队列灌满后持续投递 -> 只报第一次。这正是 A 项修复引出的新场景: * 卷被 ftpTask 长期占用 -> drain 把整批回投 -> 队列常驻满 -> 每条业务 * 投递都会"队列满 + 打一行",串口按投递频率刷屏。 */ reset_all(); ready_only(); s_kernel_state = osKernelRunning; syslog_drain(); /* 建队列(8 槽) */ g_sys_err_count = 0; for (i = 0u; i < SYSLOG_QUEUE_MAX + 4u; i++) { syslog_post(SYSLOG_RUN, "flood"); } CHECK((s_q_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1) && (s_post_qfull_reported == 1u), "TC-SYSLOG-105 queue full: 4 drops but only the first one logged"); /* 106 同一故障窗口内继续投递 -> 完全静默(丢弃语义不变,仍照丢) */ g_sys_err_count = 0; for (i = 0u; i < 5u; i++) { syslog_post(SYSLOG_RUN, "flood-more"); } CHECK((g_sys_err_count == 0) && (s_q_cnt == SYSLOG_QUEUE_MAX), "TC-SYSLOG-106 later drops in the same window log nothing"); /* 107 恢复(排空腾出槽位 + 下一条被收下)-> 抑制位复位;再灌满必须重新报一次。 * 复位点必须在 syslog_post 的成功分支,不是 drain 里——drain 只负责落盘。 */ ok = (s_post_qfull_reported == 1u); syslog_drain(); /* 队列清空、条目落盘 */ ok = ok && (s_q_cnt == 0u) && (s_post_qfull_reported == 1u); /* drain 不该复位 */ syslog_post(SYSLOG_RUN, "recovered"); ok = ok && (s_q_cnt == 1u) && (s_post_qfull_reported == 0u); CHECK(ok, "TC-SYSLOG-107 recovery via a successful post clears the bit (drain does not)"); g_sys_err_count = 0; for (i = 0u; i < SYSLOG_QUEUE_MAX + 1u; i++) { syslog_post(SYSLOG_RUN, "again"); } CHECK((s_q_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1), "TC-SYSLOG-107b a second fault logs again (throttle re-armed)"); /* 顺带确认两个通道的抑制位互不干扰:数组通道此前未触发过 */ CHECK(s_post_pboot_full_reported == 0u, "TC-SYSLOG-107c the two channels keep independent throttle bits"); } /* ===================== 入口 ===================== */ 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(); tc_selfheal_codes(); tc_post_throttle(); printf("\n=== Sys Log Mcu Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; }