/* * 模块名称:运行/自检记录落盘后端(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 #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 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; }