Files
STM32F4-Base/test/host/sys_log_mcu/sys_log_mcu_test.c
T
wandering1 e4d70f9fcb docs(真值): 勘查 13 条文档/代码不一致全部处置(10 改 3 核实)+ 补 3 处
证据源 = 主树 ci-artifacts/archify/architecture-dtu-overview-20261030-095132/
evidence.md §11。该目录不入库,故把每条的判据与实测源抄进正文,别让证据只活在
一台机器上。处置口径:能改的改成与代码同源,核实后确认文档没问题的明确写「不改」。

判停点与就绪屏障(条 1、条 3)
- AGENTS 原写 `protect_task.c:98` 直接停走判据:`:98` 是 `s_prev_cal_lock` 变量,
  判停点是 `App/task/protect_task.c:268 if (adc_cal_prot_blocked() != 0u)`,
  判定本体在标定层 `adc_cal_prot_blocked()`、protectTask 只是消费者 → 改引用,
  同步 `ui_task.c:240` 页面注释与 `docs/任务规划/数据可信.md` 的同一处行号。
- `SYS_EVT_FTP_READY` 有生产者(`Src/freertos.c:490`,注释自陈预留)无消费者;
  `SYS_EVT_TIMESYNC_READY` 全仓无生产者也无消费者(只有定义与测试桩)。
  AGENTS、`sys_events.h` 位注释、屏障配图(生成器 + 产物)三处从「四个就绪位
  并列」改成「bit0/bit1 在用 + bit2/bit3 预留」,并指向
  `docs/架构设计/项目启动顺序管理.md` §3.2 的逐位对照表。ftpTask 实际等的是
  `NET|STORAGE`(`Src/freertos.c:477-479`,WaitAll|NoClear,100 ms 分片喂狗)。

/log 不是「三本」(条 5)
- `App/log/sys_log_mcu.c` 的 `s_paths[]` = run / selftest / accid / alarm /
  start / oper / yk 七本 64B 定长文本 + `soe.log`(64B 定长二进制)+ `wdev.log`
  (变长取证行,`App/fs/vol_lock` 侧投,不走定长格式)。
  → AGENTS 两处、总览 §2.1 与 §4 的「三本」改成「七本 + SOE(+ wdev 取证行)」。

主机单测 44 套 → 47 套(条 2,两把机器尺互证)
- `scripts/check_test_coverage.py --list` 打印 47 行;`ls -d test/host/*/` = 48 个
  一级目录,唯一没有 `run_unit_test.py` 的是 `common/` ⇒ 47 套。
- 差额三名 = 屏板链路套件:`sb_proto`、`sb_link`(2026-10-29 入库)、
  `sb_led_map`(2026-10-30 入库)。专属钩子仍是 22 根 ⇒ 无专属钩子的从 22 套变 25 套。
- 同步面:AGENTS、`docs/门禁说明/README.md`、`host-unit-tests.md` §6 台账(名单补
  三名)、`docs/代码规范/pre-commit使用说明.md`、`主机单测覆盖率口径.md`(含耗时句)、
  `scripts/README.md`、总览目录树、`.pre-commit-config.yaml` 三处注释、
  `test/host/sys_log_mcu/sys_log_mcu_test.c` 一处注释。
- 带日期戳的历史测量值按纪律冻结不动:28 套、41 套 ~105 s、40 套/18 套那三处。

本仓 LVGL 版本(条 6)
- vendored 版本 = `Lib/lvgl/lv_version.h:9-11` 的 9.3.0;总览两处「LVGL v9.2」与
  `gen_arch_svg.py` 图内文字改 v9.3 并重跑产物(脚本与产物同一笔,`generated-fresh`
  逐字节核)。
- **刻意不动**:`docs/内存管理/RAM使用分析与优化方案.md` 两处「LVGL v9.2 文档」+
  GitHub `release/v9.2` 分支 URL ——那是对外游某分支文档的引用,不是本仓版本声明。

触发口径与栈值(条 4、7、9、10)
- ADC 映射 §1「ADC1 为软件触发」是旧口径:`Src/adc.c:53` 实为
  `ADC_EXTERNALTRIGCONV_T3_TRGO` → 改成 TIM3 TRGO,并补一句「TIM3 与 TPAFE 的
  CONVST 之间没有硬件耦合,仍不构成共同采样触发」,缺陷结论本身不变。
- 同文档 adcTask 栈 512 → 640 words(`Src/freertos.c:159 .stack_size = 640 * 4`)。
- `docs/UI设计/UI_设计架构.md` 的 uiTask 栈「16384 words」是单位错:
  实为 4096 words = 16384 B(`Src/freertos.c:218`),顺手补上行号。
- `App/app/app_main.c:197` 注释「DE = PD0」陈旧:`:198` 实传 `ST_DIR4_*`
  (V0.1 实板 PA15,V0.0 才是 PD0)→ 注释改指 V0.1 网表报告的引脚迁移表。
- 两处已退役编号 `N-18`(ADC 映射、V0.1 网表报告)换成登记簿条目名
  「双转换器无共同采样触发」。

核实后确认「文档没问题」的三条(条 8、12、13)
- 黄金参考「菜单 8 状态 + 内容页 66 张」:`simulator/tests/ref/` 实测 66 个
  `shot_*.bin`,与 `ui-golden` 钩子显示名一致 → 不改。
- `App/task/rs485_task.c` 存在但 `Src/freertos.c` 不创建它(grep 无 rs485):
  AGENTS 与总览都已写明「尚未接线 / 暂缓」,图侧画成「实现待接线」→ 不改。
- 104 端口 `:2404` 是工厂默认、可经 `/config.ini` `[comm]` 改 → 总览索引表补
  「默认」二字;「uiTask 10 ms 节拍」核实 = `ui_tokens.h:65 UI_TASK_TICK_MS 10` ✓。

并行勘查会话的同族发现(它扫「文档声明数 vs 真值」,逐处独立复验后采纳 3 处)
- `App/protocol/iec104/iec104_link.h` 头注释写「端口:TCP 2404(§1.5 B0 定义)」,
  同文件下方已写「服务端口真源已迁 [comm] 段」= 自相矛盾 → 补「工厂默认」并指回
  [comm] 与「下次重 bind 即生效」。
- `scripts/gen_task_table.py` 的 iec104Task 描述「:2404 监听」→「工厂默认 :2404
  监听」,`--write` 重生成 README 任务表(AGENTS 表无描述列,实测未被改写)。
- `App/log/README.md` 五行写入点行号全漂:事故 181→188、告警 184→191、启动
  176→183(`App/task/protect_task.c`);操作 `iec104_pts.c` 574/602→582/610、
  `default_task.c` 242→266、`sys_log_mcu.c` 646→682;遥控 573/576/582/586/596/
  598/609 → 581/584/590/594/604/606/617(宿主 `sim_syslog.c:245` 原值正确、未动)。
- **撤销一条**:我早先记的「`docs/算法/继电保护功能复现规划.md:225` 声称记录本已
  有消费者」与原文不符——:225 实为规划表里「动作/SOE 记录 | 复用 `sys_log`」一行,
  属能力复用口径、不是当前态断言 → 不改。教训:**改引用前逐处读现文,别照自己
  上一轮的笔记盲替**(这条比改中的每一处都值钱)。
- 同一批勘查还报了约 12 处「`2404` 缺『工厂默认』限定词」(AGENTS、`App/protocol/
  README.md`、`Src/freertos.c` 注释、5 篇 docs、`test/host` 工具,含一张配图生成器
  要连产物同批重跑)与 `测试开发标准流程.md`「像素层 60 内容页」、
  `.pre-commit-config.yaml` uinav 注释「快检档 18 张黄金参考」(后者写时为真、
  描述的却是钩子现有行为):**本笔刻意不做**,另开任务空间处理——那批要同时动
  AGENTS.md 与 `.pre-commit-config.yaml` 两处单写者面,与并行会话撞车的代价更高。
- 端口生效语义(用户裁定 2026-10-30):产品基线写「公共地址与端口可配…改后需重启
  生效」,代码 `App/task/iec104_serv.c` 是「ca 改后要重启;port 下次重 bind 才生效」
  ——裁定**按代码语义要改**处理(改了端口应立刻重 bind),属固件行为变更,另开任务
  且需真机验证;`.serialtest/config.json` 写死 `i104: listening on 2404` 必检锚点
  是同题(现场改端口会让串口回归假失败),一并留给那条任务。

自查(任务树内)
- `check_doc_links.py --sections-strict`:12 个改动 .md 逐个 rc=0;
  `check_mermaid.py` 改动 .md 全 rc=0;`check_generated_fresh.py` PASS
  (25 个生成器 / 21 一致 / 4 存量基线);`check_nested_comment.py --all` PASS
  (449 文件,新发 0、存量 2);`check_bom_drift.py --all` 99 个名单文件无漂移。
- 编译:`.dsh/skills/keil-build` 退出码 0(0 错误 0 警告),
  Program Size Code=246720 RO=29836 RW=2052 ZI=149708 —— 与本树凭证同码,
  证明这批改的确实只有注释与文档。
- 字节体检:23 个文件无 CR / U+FFFD / 行尾空格 / 缺末行换行;`ui_task.c` 被编辑后
  丢的 3 字节 BOM 已补回(bom-guard 复验通过);5 处「三连空行」逐文件与
  HEAD 比对确认是存量(Python 生成器的 PEP8 双空行与个别 C 文件原有空行)。
- 一次性工具 `ci-artifacts/fix_docs2.py`:断言式规则表,按文件读一次、在演化文本上
  逐条替换、一次写盘。第一版写成「每条规则各自从启动时快照起算」,同一文件后一条
  把前一条整体覆盖,只有每文件最后一条活下来——靠「改完再全仓复扫旧形态」当场抓到,
  这条判据本身已值得写进流程。
- 待办:`git add` 后需重跑 `ram_baseline.py --update`(体积凭证按 git 索引内容生成,
  未 staged 的文件不在凭证里,`check_size_gate.py --local` 现在因此报红)。
- 未 commit、未 push(AGENTS.md 边界:提交属 Ask first)。
2026-10-30 14:19:35 +08:00

2526 lines
127 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:运行/自检记录落盘后端(sys_log_mcu)主机侧纯逻辑单元测试
* 模块功能:在 PC 上离线验证 App/log/sys_log_mcu.c 的四族契约——
* A 64B 定长记录格式(时间戳 / epoch ASCII / 描述三段 + 截断 + 占位)
* B 写侧(单条 s_append_rec、批量 s_append_many、日志读写放大收口的"一批一次
* open/write/close"、容量轮转、文件级自愈、留痕去重、卷锁 try)
* C 排空(syslog_drain 的 preboot + 队列两条来源、换文件分批、
* 失败回投不丢条目、-3 退回慢路径)
* D 读侧(日志读写放大收口的 8 条滑动窗口 + 写者代数失效 + 越界/忙/短读处置)
* 外加 syslog_mcu_mounted 的 挂载幂等与上电/重启分类、sys_log_evidence
* 的 wdev.log 取证通道(8KB 护栏 + 第二条写必须在释放卷锁之后),
* 以及「清除记录」的请求 / 落地分离与四条判据(K 族,2026-10-26)。
*
* 被测对象:真实源文件 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 往返)。两者都靠假卷的调用计数才断言得了。
* 5. **「清除记录」的请求 / 落地分离**(2026-10-26 加,K 族):最顺手的写法是在 UI
* 按键回调里直接 f_unlink —— 文件确实删了,台架当场也看不出毛病,代价是把 FAT
* 目录项回写挂进 uiTask 每 100 ms 一次的喂狗节拍,并与唯一写者自己的"容量满
* unlink 重建"交错。这条"谁在什么上下文碰盘"的边界,只有离线数调用才钉得住:
* 请求侧必须零 FatFs / 零卷锁(TC-SYSLOG-110),删除必须恰好一次 unlink 且
* 发生在排空之后(TC-SYSLOG-111/117)。真机上这条写错的表现为偶发丢一段日志
* 或一次误复位,串口里没有任何一行会指着这处代码。
* 6. **一张表的下标序 = 枚举序**(2026-10-26 加,L 族):记录类型就是"哪个文件",
* 于是 s_paths[]/s_caps[] 与 syslog_id_t 的**逐项同序**成了正确性本身。少写一项
* 编译期就报,写错序却什么都不报错——事故记录会安静地写进告警文件,屏上一切
* 正常,只有查记录的人被误导。这类"顺序即语义"的耦合只能靠逐 id 断言"我投的
* 这句出现在哪个路径、别的文件里没有"来钉(TC-SYSLOG-130),容量同理(131)。
* quiet 投递通道也在这一族:它存在的唯一理由是"投递侧不许带日志行缓冲",
* 所以必须断言**丢弃时零留痕**(132/135),否则退化回 syslog_post 也没人发现。
*
* 覆盖点(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 目录与
* 邻居文件一根毛没动(自愈只针对自己的文件)
* —— K 记录删除(「清除记录」前置,2026-10-26)——
* TC-SYSLOG-110 请求侧只登记:零卷锁、零 FatFs、记录还在盘上(uiTask 唯一被
* 允许做的事就是发这个请求)
* TC-SYSLOG-111 落地在 syslog_drain(defaultTask)内:恰好一次 unlink、零 open、
* count 归零、卷锁配对、邻居文件不动、零留痕
* TC-SYSLOG-112 删后重建可继续写:清除后下一条记录把文件建回来、打开模式带写位、
* 64/64 写全、读侧从 idx 0 读到新内容
* TC-SYSLOG-113 FR_NO_FILE 幂等返 0:无留痕、卷锁配对、读侧代数照常自增
* TC-SYSLOG-114 卷占用返 -1:零盘触点、不 release(没拿到锁)、不计故障、原文件一字节不少
* TC-SYSLOG-115 请求路径遇卷忙 = 静默顺延(WAIT 不是 FAIL、请求挂回、零留痕),
* 下一轮卷空闲自己落地(用户不必重按)
* TC-SYSLOG-116 unlink 真失败 -> -1 + 留痕一次 + 文件完好;请求侧判 FAIL 且
* **绝不自动重试**(反复删自己的日志 = 写侧刚修掉的坑,别再造一遍)
* TC-SYSLOG-117 drain 内顺序本身也是判据:本轮排空出来的旧记录一起被清掉,
* 删除之后新投的条目仍落盘(清除不等于把日志关掉)
* TC-SYSLOG-118 同 id 重复请求合并为一次 unlink(请求是标志,不是计数器)
* TC-SYSLOG-119 只删自己:清 RUN 不碰 SELFTEST,也不碰取证文件 wdev.log
* TC-SYSLOG-120 三处入口的入参守卫都在取锁之前;坏 id 不污染合法 id 的状态
* TC-SYSLOG-121 删除成功 -> 读侧代数自增(屏上"已清除"而窗口还装着旧记录 = 错数据);
* 删除失败 -> 代数不变、缓存记录仍可读(盘上什么都没动)
* TC-SYSLOG-122 /log 目录不在 -> FR_NO_PATH **不算**幂等成功:-1 + 一次留痕 +
* 零 f_mkdir(名字/参数类码不得升级成卷级动作);目录回来后 FAIL 不粘
*
* —— L 五类记录与 quiet 投递通道(道F task-10,2026-10-26)——
* TC-SYSLOG-130 七类记录各进各的文件、各带各的描述:**id -> 文件的表序就是语义**
* TC-SYSLOG-130b 卷里恰好七个记录文件,没有第八个拼错名字被顺手建出来的
* TC-SYSLOG-130c 七条生产路径逐条过严格 8.3(主名<=8 / 扩展=3 / 不以点开头)
* TC-SYSLOG-131 accid 到自己那格(512)轮转,邻居 alarm 一字节没动
* TC-SYSLOG-131b yk 用的是自己那格(256)——三档上限互不相同,能照出 s_caps[] 串格
* TC-SYSLOG-132 quiet 通道队列满:计数前进、投递侧**零留痕**、drain 侧按差值报一次
* TC-SYSLOG-133 清除记录 = 一次运维动作 -> 落一条操作记录点名被清的那份;被清那份仍归零
* TC-SYSLOG-133b 清 oper.log 自己时**不投**操作记录(否则刚删空又变 1 条,砸掉归零判据)
* TC-SYSLOG-134 quiet 的入参守卫:坏 id / NULL 既不投也不计丢弃(参数错 != 容量故障)
* TC-SYSLOG-135 启动前数组满同样计数、同样静默(另一条腿,与 F 项的两个通道分区一致)
* TC-SYSLOG-136 同内容在七类之间**逐字节相同**:记录体不编码类型 = "文件即类型"的机器表达
* TC-SYSLOG-137 卷锁 fail-open(零互斥那一态)第一次前进:补投**一条**自检记录、字段带累计值、
* 且 g_sys_err_count 恒 0(不打 DBG 错级、不打 [ERR]——道G task-40)
* TC-SYSLOG-137b 同一 episode(一次上电)内该计数继续前进:不再投第二条(诊断不许变成
* 负载:每轮一条 = 每轮一次 NAND 写,与 s_quiet_drop 那条纪律同源)
* TC-SYSLOG-137c 计数没前进:多轮 drain 一条都不投(边沿判据,不是电平判据)
* TC-SYSLOG-137d 重新上电(reset_all)后计数再前进:又投一条——证明 latch 是"每 boot
* 一条"而不是"永远只有一条",否则故障复发就永久静默
*
* 覆盖率台账(`scripts/check_test_coverage.py --module sys_log_mcu` 本机实跑):
* App/log/sys_log_mcu.c **403/406 行 = 99.3%、函数 22/22**。
* 上一轮(K 族)是 377/380 行、函数 18/18 —— L 族新增 4 枚函数
* (s_entry_fill / s_put / syslog_post_quiet / syslog_post_dropped)与 26 行
* 可执行代码**全部**有断言跑到:覆盖行 +26、函数 +4,未覆盖仍是同一批 3 行老账
* (一条死分支 + sys_log_evidence 里 snprintf<=0),**一条没涨**。
* ⚠ 全量门禁(47 套并集 + 基线只增不减)由提交前那一次统一跑,本行只是自量记录。
* L 族变异实测(2026-10-26,三处同时注入后逐字节还原):
* ① s_paths[] 里 accid 与 alarm 两格**对调**(就是"表序错"那个静默错法);
* ② 给 quiet 的丢弃分支补一条 `DBG_ERROR`(= 悄悄退化成 syslog_post);
* ③ 去掉"清 oper.log 自己不投操作记录"那半枚守卫。
* → 137 掉到 132:**TC-SYSLOG-130 / 131**(①)、**132 / 135**(②)、
* **133b**(③)各自转红且互不牵连——每条新纪律都被单独钉住,不是靠一锅大断言。
* App/log/sys_log_mcu.c **377/380 行 = 99.2%、函数 18/18**。
* 本轮(K 族)之前是 328/330 行、函数 13/13 —— 新增 5 个函数
* (s_clear_try / s_clear_service / syslog_clear_apply / syslog_request_clear /
* syslog_clear_state)与 50 行新增可执行代码(已覆盖 +49):新增行**全部**有断言
* 跑到,未覆盖仍是 3 行,一条没涨。
* 仍未覆盖的 3 行是同一批老账:一条死分支 + sys_log_evidence 里 snprintf 返回
* <=0 的分支——宿主 snprintf 不可能失败,假件也不伪造返回值;为刷数字去伪造
* 等于把断言写在假件规则上,宁可留空。
* 变异实测("断言有牙齿"的证据,2026-10-26,改完已逐字节还原):
* ① 在排空循环**之前**多调一次 `s_clear_service()`(净效果 = 删除抢在条目落盘之前
* 执行,尾部那次因请求已销账而空转);② 把 `FR_NO_PATH` 也算成幂等成功。
* 两处一起改 → TC-SYSLOG-115 / 115b / 117 / 117b / 122 **五条转红**(126 → 121),
* 还原后复绿 126/126。也就是说"删除必须排在条目排空之后落地"与"名字/参数类码
* 不算幂等成功"这两条本来只写在注释里的纪律,现在真的会被测试拦住。
*
* 本套照出的四条现状 —— 已在 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),两侧的差异是刻意的,注释也互指。
*
* 故意没测(写在这里,不留"看起来全覆盖"的错觉):
* - 真机上 uiTask 发请求与 defaultTask 落地的并发:主机单线程编不出真竞态,本套只
* 钉"请求侧零盘触点 + 落地只在 drain 里"这两条形状;两个 volatile 单字节交接的
* 最坏竞态(读到上一次的结果 / 失败被新请求清掉)写在 sys_log.h 的函数头里。
* - quiet 通道与 post 通道在**同一轮**里交错时的抑制位归属(quiet 成功不复位 F 项
* 那两个位):这是刻意的设计(那两个位描述 post 自己的历史),TC-SYSLOG-107c 已
* 钉住两位互不干扰;"quiet 投成功把 post 的故障静默位清了"这种错向,本套没有
* 单独一条——要钉它得先给两个通道各造一套满态时序,收益不抵这套的复杂度。
* - 五类记录各自的**业务触发时序**(哪个元件边沿算事故、哪条 104 命令算操作)不在
* 本套:那是判据与协议侧的事,分别由 test/host/prot_elm 的 TC-PROTELEM-015 与
* test/host/iec104 的 TC-IEC104-030 钉;本套只管"投进来以后存储对不对"。
* - 删除之后"下一次上电会不会多写一条 power on":那是 syslog_mcu_mounted() 的
* 上电分类逻辑(E 族已测),与删除路径无关,两边各测各的。
* - 真跨任务并发:读窗口用"对齐 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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "ff.h"
#include "fatfs_fake.h"
#include "cmsis_os.h"
#include "dbg_log.h"
#include "sys_clock.h"
#include "vol_lock.h"
#include "sys_log.h"
#include "soe_store.h"
/* ===================== 断言框架 ===================== */
static int s_total;
static int s_passed;
static int s_failed;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
/* 路径常量:与生产 s_paths[] / sys_log_evidence() 里的字面量一致(写死在测试里
* 是为了"改生产路径名必须同时改测试",改名静默失效正是 §10.1 第 10 条的老账) */
#define P_RUN "/log/run.log"
#define P_SELFTEST "/log/selftest.log"
#define P_WDEV "/log/wdev.log"
/* 五类新记录(task-10,L 族):生产用的就是这七条路径,改名必须同步这里 */
#define P_ACCID "/log/accid.log"
#define P_ALARM "/log/alarm.log"
#define P_START "/log/start.log"
#define P_OPER "/log/oper.log"
#define P_YK "/log/yk.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 uint32_t s_last_wait_to; /* 最近一次 vol_lock_wait 的 timeout 入参(0 = try 语义)。
* 四套假件逐字同名,便于全仓 grep「喂狗点绝不阻塞」这条纪律;
* 第 6 套出现时再议共享假件 */
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;
s_last_wait_to = 0xFFFFFFFFu; /* 哨兵:从未调用过就不等于 timeout=0 */
}
/* 卷锁观测计数假件(道G task-40):生产侧 syslog_drain() 每轮读一次,判"边沿"。
* failopen 由测试自己抬(模拟 osMutexNew 失败那一支),其余三态给固定值——本卡
* 只消费 fail-open 那一态,其余三态各有属主(App/fs/vol_lock.h 的四态语义不得合并)。 */
static uint32_t s_vl_failopen_fake;
int vol_lock_init(void) { return 0; }
void vol_lock_get_stats(vol_lock_stats_t *p_out)
{
if (p_out == NULL) {
return;
}
p_out->acquired = 11u;
p_out->try_timeout = 2u;
p_out->blk_timeout = 0u;
p_out->failopen = s_vl_failopen_fake;
}
int vol_lock_wait(uint32_t timeout_ms)
{
int r;
s_last_wait_to = timeout_ms; /* 记录入参:调用方的 timeout 口径由此可断言 */
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 项整改新增的留痕节流位:跨场景必须显式清(否则第二个故障场景看不到留痕)。
* ⚠ 按 SYSLOG_COUNT 循环清,不要逐 id 手写——task-10 把 id 从 2 扩到 7 之后,
* 任何"逐枚点名"的清理都会静默漏掉新增的那几格,症状是"单独跑绿、连跑偶红"。 */
(void)memset(s_requeue_reported, 0, sizeof(s_requeue_reported));
s_post_qfull_reported = 0u;
s_post_pboot_full_reported = 0u;
/* quiet 投递通道(L 族):丢弃计数与 drain 侧已上报水位都要归零,
* 否则上一个场景的丢弃会让本场景第一条留痕被当成"已经报过" */
s_quiet_drop = 0u;
s_quiet_reported = 0u;
/* 卷锁 fail-open 留痕(道G task-40):latch 与水位、假件计数都得归零,
* 否则上一个场景"已经报过"会让本场景的边沿静默(同上面那条教训) */
s_vl_failopen_seen = 0u;
s_vl_reported = 0u;
s_vl_failopen_fake = 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;
(void)memset(s_rd_gen, 0, sizeof(s_rd_gen)); /* 同上:按数组清,扩 id 不会漏 */
/* 「清除记录」的请求 / 结果交接(本套新增,K 族):两枚都必须显式清,
* 否则上一个场景留下的 WAIT/FAIL 会让下一个场景第一眼就读到错状态 */
/* 「清除记录」的请求 / 结果交接(K 族):两枚按数组清,理由同 s_rd_gen——
* 五类新记录每一类都可被清除,漏清一格就是上一场景的 WAIT/FAIL 咬下一场景。 */
(void)memset(s_clear_req, 0, sizeof(s_clear_req));
(void)memset(s_clear_st, 0, sizeof(s_clear_st)); /* 0 == SYSLOG_CLEAR_NONE */
/* 卷状态:/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)
&& (s_last_wait_to == 0u) /* 喂狗点绝不阻塞:取锁一律传 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 + lock asked with timeout=0 (feed point never blocks)");
/* 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)
&& (s_last_wait_to == 0u),
"TC-SYSLOG-012 volume busy means defer: no disk touch, no release, no fault counted (feed point never blocks) + defer path also asked with timeout=0");
/* 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**(旧实现每轮删一次自己的
* 历史日志、且永远修不好)。留痕仍受去重约束,只 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 批量路径的文件级自愈: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 的去重位是函数级 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");
}
/* ===================== K 记录删除(「清除记录」前置) ===================== */
/*
* 这一族钉的是"谁在什么上下文把文件删掉",加上删除本身的四条判据:
* 计数归零(111)、文件不在时幂等返 0(113)、卷占用/真失败返 -1 且不误删原文件
* (114/116)、删后重建可继续写(112)。
* 为什么值得离线测:最顺手的写法是在 UI 按键回调里直接 f_unlink —— 台架上当场看不出
* 毛病,代价是把 FAT 目录项回写挂进 uiTask 每 100 ms 一次的喂狗节拍,并与唯一写者
* 自己的"容量满 unlink 重建"交错。请求/落地分开之后,这条边界只能靠"数请求侧的盘
* 触点"和"数删除发生在哪一轮"钉住(110/111/117 干的就是这件事)。
* 一处限度说清楚:本目录的卷锁假件不记录超时入参,所以"绝不阻塞"只能断言成
* "try 失败 = 零盘触点 + 不 release + 不计故障"(114),测不到传进去的超时值到底
* 是不是 0;要测那一层得给共用的卷锁假件加一列观测,不属本套的范围。
*/
static void tc_clear(void)
{
char ts[SYSLOG_TS_LEN + 1u];
char desc[SYSLOG_DESC_MAX + 1u];
uint8_t rec[SYSLOG_REC_SIZE];
uint32_t i;
int ok;
printf("-- TC-SYSLOG-110..121 record clear (request / land split) --\n");
/* 110 请求侧:登记就是登记 —— 一把锁都不取、一个 FatFs 调用都没有、记录还在盘上。
* 这是"UI 只能发请求"的全部技术含义:按键回调在 uiTask,那侧不许碰存储。 */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "old1");
(void)s_append(SYSLOG_RUN, "old2");
{
int open_base = g_ff_obs.calls[FF_FAKE_OP_OPEN];
int unlink_base = g_ff_obs.calls[FF_FAKE_OP_UNLINK];
int lock_base = s_lock_waits;
int rel_base = s_lock_releases;
ok = (syslog_request_clear(SYSLOG_RUN) == 0)
&& (syslog_count(SYSLOG_RUN) == 2u)
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_WAIT)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == open_base)
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlink_base)
&& (s_lock_waits == lock_base)
&& (s_lock_releases == rel_base);
CHECK(ok, "TC-SYSLOG-110 request only books the ask: zero volume lock, zero FatFs, records still on the volume");
}
/* 111 落地发生在 drain(defaultTask)里:恰好一次 unlink(不 open)、计数归零、
* 锁成对、零留痕,且邻居文件 selftest.log 一字节没动 */
(void)s_append(SYSLOG_SELFTEST, "neighbour");
{
int open_base = g_ff_obs.calls[FF_FAKE_OP_OPEN];
syslog_drain();
ok = (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK)
&& (syslog_count(SYSLOG_RUN) == 0u)
&& (ff_fake_exists(P_RUN) == 0)
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
&& (strcmp(g_ff_obs.last_unlink_path, "/LOG/RUN.LOG") == 0) /* 观测位存的是归一化后的 8.3 大写名 */
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == open_base)
&& (s_lock_seq_err == 0)
&& (g_sys_err_count == 0)
&& (ff_fake_exists(P_SELFTEST) == 1)
&& (ff_fake_size(P_SELFTEST) == SYSLOG_REC_SIZE);
}
CHECK(ok, "TC-SYSLOG-111 drain lands it: one unlink and zero open, count 0, lock paired, no trace, neighbour intact");
/* 112 判据"删后重建可继续写":文件被删之后,下一条记录必须把文件建回来并写全。
* 陷阱一在这里才有牙齿 —— 只有"文件不存在"这一条路才走 f_open 的创建分支,
* 少了 FA_WRITE 位,平时写得好好的,清除之后从此每条静默写不进去。 */
ok = (s_append(SYSLOG_RUN, "after-clear") == 0)
&& (ff_fake_exists(P_RUN) == 1)
&& (syslog_count(SYSLOG_RUN) == 1u)
&& (g_ff_obs.last_open_mode == (BYTE)(FA_WRITE | FA_OPEN_APPEND))
&& (g_ff_obs.last_write_bw == SYSLOG_REC_SIZE);
CHECK(ok, "TC-SYSLOG-112 after a clear the next record recreates the file with the write bit set and lands 64/64 bytes");
ok = ok && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
&& (strcmp(desc, "after-clear") == 0);
CHECK(ok, "TC-SYSLOG-112b the recreated file is readable from index 0 (cleared history never comes back)");
/* 113 幂等:文件本来就不存在 -> 返 0、只留一次 unlink、零留痕、卷锁照样配对 */
reset_all();
ready_only();
{
int rel_base = s_lock_releases;
uint32_t gen = s_rd_gen[SYSLOG_RUN];
ok = (ff_fake_exists(P_RUN) == 0)
&& (syslog_clear_apply(SYSLOG_RUN) == 0)
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
&& (g_sys_err_count == 0)
&& (s_lock_releases == rel_base + 1)
&& (s_lock_seq_err == 0)
&& (s_rd_gen[SYSLOG_RUN] == gen + 1u);
}
CHECK(ok, "TC-SYSLOG-113 FR_NO_FILE is an idempotent success: 0 back, no trace, lock released, window bumped");
/* 114 卷占用:公开 API 返 -1,一个盘操作都没有、不 release(没拿到锁)、不计故障、
* 原文件一字节不少。"try 失败不许 release"是漏锁 = FTP 永久等不到卷的老账。 */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "keepme");
{
int open_base = g_ff_obs.calls[FF_FAKE_OP_OPEN];
int unlink_base = g_ff_obs.calls[FF_FAKE_OP_UNLINK];
int rel_base = s_lock_releases;
lock_busy_next(1);
ok = (syslog_clear_apply(SYSLOG_RUN) == -1)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == open_base)
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlink_base)
&& (s_lock_releases == rel_base)
&& (s_lock_seq_err == 0)
&& (g_sys_err_count == 0)
&& (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE)
&& (syslog_count(SYSLOG_RUN) == 1u);
}
CHECK(ok, "TC-SYSLOG-114 busy volume -> -1 with zero disk touch, no release, no fault counted, original file intact");
/* 115 走请求路径时卷占用 = 静默顺延,不是失败:状态仍是 WAIT、请求挂回、零留痕;
* 下一轮卷空闲后同一个请求自己落地(用户不必重按一次按钮)。 */
reset_all();
ready_only();
syslog_drain(); /* 建队列,本轮空转 */
(void)s_append(SYSLOG_RUN, "keepme");
(void)syslog_request_clear(SYSLOG_RUN);
lock_busy_next(1);
g_sys_err_count = 0;
syslog_drain(); /* 这一轮的清除拿锁失败 */
ok = (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_WAIT)
&& (ff_fake_exists(P_RUN) == 1)
&& (g_sys_err_count == 0)
&& (s_lock_seq_err == 0);
CHECK(ok, "TC-SYSLOG-115 busy while landing defers: ask kept, state WAIT (not FAIL), file untouched, silent");
syslog_drain(); /* 卷空闲 -> 落地 */
ok = ok && (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK)
&& (syslog_count(SYSLOG_RUN) == 0u);
CHECK(ok, "TC-SYSLOG-115b the deferred ask lands on the next round without a second press");
/* 116 真失败(非 FR_NO_FILE):apply 返 -1 + 留痕一次;请求侧判 FAIL 且绝不自动
* 重试 —— 反复 unlink 自己的日志文件是本项目在写侧刚修掉的坑(名字/参数类
* 错误码同名同因,重试只会每轮删一次历史),删除路径不许再造一遍。 */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "precious");
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_DISK_ERR, 4);
ok = (syslog_clear_apply(SYSLOG_RUN) == -1)
&& (g_sys_err_count == 1)
&& (ff_fake_exists(P_RUN) == 1)
&& (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE);
CHECK(ok, "TC-SYSLOG-116 a real unlink error -> -1, one trace, the log file itself is still there");
reset_all();
ready_only();
syslog_drain();
(void)s_append(SYSLOG_RUN, "precious");
(void)syslog_request_clear(SYSLOG_RUN);
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_DISK_ERR, 4);
syslog_drain();
{
int unlink_base = g_ff_obs.calls[FF_FAKE_OP_UNLINK];
ok = (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_FAIL)
&& (ff_fake_exists(P_RUN) == 1);
syslog_drain(); /* 不许自己再删一次 */
ok = ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlink_base)
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_FAIL);
}
CHECK(ok, "TC-SYSLOG-116b a failed clear latches FAIL and is never retried on its own (re-press is the retry)");
/* 117 drain 内的顺序是判据的一部分:本轮排空出来的旧记录必须一起被清掉
* (先删后写 = 白清,count 不归零);而删除**之后**投的条目要留下
* (否则"清除记录"变成了"关掉日志")。 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain(); /* 建队列 */
for (i = 0u; i < 3u; i++) {
syslog_post(SYSLOG_RUN, "old");
}
(void)syslog_request_clear(SYSLOG_RUN);
syslog_drain(); /* 排空 3 条 -> 然后删除 */
ok = (syslog_count(SYSLOG_RUN) == 0u) && (ff_fake_exists(P_RUN) == 0)
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK);
CHECK(ok, "TC-SYSLOG-117 the clear runs after the entry drain: the queued backlog does not survive it");
syslog_post(SYSLOG_RUN, "new");
syslog_drain();
ok = ok && (syslog_count(SYSLOG_RUN) == 1u)
&& (raw_at(P_RUN, 0u, rec) != 0)
&& (memcmp(rec + SYSLOG_DESC_OFFS, "new", 3u) == 0);
CHECK(ok, "TC-SYSLOG-117b a record posted after the clear still lands: clearing is not disabling logging");
/* 118 同一 id 连点两次只登记一次(合并成一次删除,不是计数器) */
reset_all();
ready_only();
ok = (syslog_request_clear(SYSLOG_RUN) == 0) && (syslog_request_clear(SYSLOG_RUN) == 0)
&& (s_clear_req[SYSLOG_RUN] == 1u);
syslog_drain();
ok = ok && (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK)
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
&& (s_clear_req[SYSLOG_RUN] == 0u);
CHECK(ok, "TC-SYSLOG-118 duplicate asks merge into exactly one unlink (the ask is a flag, not a counter)");
/* 119 只删自己:清 RUN 不许碰 SELFTEST,也不许碰取证文件 wdev.log ——
* 三个名字都是严格 8.3 短名,逐个对应,不存在"通配删除"。 */
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "r");
(void)s_append(SYSLOG_SELFTEST, "s");
(void)ff_fake_mkfile_size(P_WDEV, 128u);
ok = (syslog_clear_apply(SYSLOG_RUN) == 0)
&& (ff_fake_exists(P_RUN) == 0)
&& (ff_fake_exists(P_SELFTEST) == 1)
&& (ff_fake_exists(P_WDEV) == 1)
&& (strcmp(g_ff_obs.last_unlink_path, "/LOG/RUN.LOG") == 0);
CHECK(ok, "TC-SYSLOG-119 clearing one id never touches the other log or the watchdog evidence file");
/* 120 入参守卫:三处公开入口都必须在取锁之前挡住,并且不污染合法 id 的状态 */
reset_all();
ready_only();
ok = (syslog_request_clear((syslog_id_t)SYSLOG_COUNT) == -1)
&& (syslog_clear_apply((syslog_id_t)SYSLOG_COUNT) == -1)
&& (syslog_clear_state((syslog_id_t)99u) == SYSLOG_CLEAR_NONE)
&& (s_lock_waits == 0)
&& (s_lock_releases == 0)
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0);
CHECK(ok, "TC-SYSLOG-120 out-of-range id is refused before the lock and the disk (a guard behind the lock occupies the volume for nothing)");
reset_all();
ready_only();
ok = (syslog_request_clear(SYSLOG_RUN) == 0)
&& (syslog_request_clear((syslog_id_t)SYSLOG_COUNT) == -1)
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_WAIT)
&& (syslog_clear_state((syslog_id_t)SYSLOG_COUNT) == SYSLOG_CLEAR_NONE);
CHECK(ok, "TC-SYSLOG-120b a rejected request leaves a valid id's pending ask and state untouched");
/* 121 读侧:删除成功必须让读者的 8 条窗口作废(否则屏上显示"已清除"而记录还在);
* 失败路径不许自增 —— 盘上什么都没动,窗口里仍是真数据。 */
reset_all();
ready_only();
for (i = 0u; i < 3u; i++) {
(void)s_append(SYSLOG_RUN, "w");
}
{
uint32_t gen = 0u;
ok = (syslog_get_record(SYSLOG_RUN, 2u, ts, sizeof(ts), desc, sizeof(desc)) == 0);
gen = s_rd_gen[SYSLOG_RUN];
ok = ok && (syslog_clear_apply(SYSLOG_RUN) == 0)
&& (s_rd_gen[SYSLOG_RUN] == gen + 1u)
&& (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == -1);
}
CHECK(ok, "TC-SYSLOG-121 a successful clear invalidates the reader window (no records left to show)");
reset_all();
ready_only();
(void)s_append(SYSLOG_RUN, "keep");
ok = (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0);
{
uint32_t gen = s_rd_gen[SYSLOG_RUN];
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_DISK_ERR, 1);
ok = ok && (syslog_clear_apply(SYSLOG_RUN) == -1)
&& (s_rd_gen[SYSLOG_RUN] == gen)
&& (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
&& (strcmp(desc, "keep") == 0);
}
CHECK(ok, "TC-SYSLOG-121b a failed clear neither bumps the generation nor loses the cached valid record");
/* 122 /log 目录本身不在:假件与真实 ff.c 一样落 FR_NO_PATH。这条刻意**不算**幂等
* 成功(记录读不到是真的,但成因是目录没了而不是"被清空",把卷异常洗成
* "清除成功"是另一种撒谎),而且本函数一个卷级动作都不做:不补目录、
* 不重开、不碰别的对象——补目录归 syslog_mcu_mounted() 每 2s 的重试。 */
reset_all();
ready_only();
ok = (f_unlink("/log") == FR_OK) && (ff_fake_exists("/log") == 0);
{
int mkdir_base = g_ff_obs.calls[FF_FAKE_OP_MKDIR];
ok = ok && (syslog_clear_apply(SYSLOG_RUN) == -1)
&& (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == mkdir_base)
&& (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == 0)
&& (ff_fake_exists("/log") == 0)
&& (g_sys_err_count == 1);
}
CHECK(ok, "TC-SYSLOG-122 FR_NO_PATH is not counted as an idempotent success: -1, one trace, and zero f_mkdir (no volume-level action on a name/param code)");
(void)ff_fake_mkdir_direct("/log"); /* 目录回来了(模拟挂载门补建成功) */
ok = (syslog_request_clear(SYSLOG_RUN) == 0)
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_WAIT);
syslog_drain();
ok = ok && (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK)
&& (syslog_count(SYSLOG_RUN) == 0u);
CHECK(ok, "TC-SYSLOG-122b FAIL is not sticky: once the directory is back a new request clears normally");
}
/* ===================== L 五类记录与 quiet 投递通道(task-10) ===================== */
/* 七类记录一枚描述、一枚路径:描述自带类别号,用来证明"id -> 文件"的对应关系
* 与两端表序一致(表序错 = 事故安静地写进告警文件,屏上看不出任何毛病)。 */
static const char *const s_kind_desc[SYSLOG_COUNT] = {
"k0run", "k1selftest", "k2accid", "k3alarm", "k4start", "k5oper", "k6yk"
};
static const char *const s_kind_path[SYSLOG_COUNT] = {
P_RUN, P_SELFTEST, P_ACCID, P_ALARM, P_START, P_OPER, P_YK
};
static void tc_kinds(void)
{
uint8_t raw[SYSLOG_REC_SIZE];
int ok;
uint32_t i;
int base_entries;
printf("-- TC-SYSLOG-130..136 five record kinds + quiet posting channel --\n");
/* 130 一类一个文件:每类只进自己那份,且没有第八个文件被顺手建出来 */
reset_all();
ready_only();
base_entries = ff_fake_count_entries();
for (i = 0u; i < (uint32_t)SYSLOG_COUNT; i++) {
(void)s_append((syslog_id_t)i, s_kind_desc[i]);
}
ok = 1;
for (i = 0u; i < (uint32_t)SYSLOG_COUNT; i++) {
if (ff_fake_size(s_kind_path[i]) != SYSLOG_REC_SIZE) { ok = 0; break; }
if (!raw_at(s_kind_path[i], 0u, raw)) { ok = 0; break; }
if (memcmp(raw + SYSLOG_DESC_OFFS, s_kind_desc[i], strlen(s_kind_desc[i])) != 0) { ok = 0; break; }
if (raw[SYSLOG_DESC_OFFS + strlen(s_kind_desc[i])] != 0u) { ok = 0; break; }
}
CHECK(ok, "TC-SYSLOG-130 each kind lands in its own file and carries its own description (id-to-path table order is semantics)");
CHECK(ff_fake_count_entries() == (base_entries + (int)SYSLOG_COUNT),
"TC-SYSLOG-130b exactly seven record files exist, none created under a mistyped 8.3 name");
/* 130c 五类新路径都过严格 8.3(081 只覆盖老三类 + wdev;扩 id 必须一起扩这条) */
ok = 1;
for (i = 0u; i < (uint32_t)SYSLOG_COUNT; i++) {
/* 上面已经真打开写成功过;这里再核一次名字本身:主名 <=8、扩展 = log、
* 不以点开头、不含非法字符 */
const char *p = s_kind_path[i];
const char *slash = strrchr(p, '/');
size_t body = 0u, ext = 0u;
const char *q;
if ((slash == NULL) || (slash[1] == '.')) { ok = 0; break; }
for (q = slash + 1; (*q != '\0') && (*q != '.'); q++) { body++; }
if (*q == '.') { for (q++; *q != '\0'; q++) { ext++; } }
if ((body == 0u) || (body > 8u) || (ext != 3u)) { ok = 0; break; }
}
CHECK(ok, "TC-SYSLOG-130c all seven production paths are strict 8.3 short names (FF_USE_LFN=0)");
/* 131 各用各的容量上限:accid 到自己那格(512)轮转,且不动邻居 */
reset_all();
ready_only();
(void)ff_fake_mkfile_size(P_ACCID, (SYSLOG_ACCIDENT_MAX - 1u) * SYSLOG_REC_SIZE);
(void)ff_fake_mkfile_size(P_ALARM, (SYSLOG_ALARM_MAX - 1u) * SYSLOG_REC_SIZE);
(void)put_rec(SYSLOG_ACCIDENT, "cap-a1");
ok = (syslog_count(SYSLOG_ACCIDENT) == SYSLOG_ACCIDENT_MAX)
&& (ff_fake_size(P_ACCID) == (SYSLOG_ACCIDENT_MAX * SYSLOG_REC_SIZE));
(void)put_rec(SYSLOG_ACCIDENT, "cap-a2"); /* 到顶 -> 删除重建,只剩本次一条 */
ok = ok && (syslog_count(SYSLOG_ACCIDENT) == 1u)
&& (ff_fake_size(P_ACCID) == SYSLOG_REC_SIZE);
CHECK(ok && (ff_fake_size(P_ALARM) == ((SYSLOG_ALARM_MAX - 1u) * SYSLOG_REC_SIZE)),
"TC-SYSLOG-131 the accident log rotates on its own cap and leaves the alarm log alone");
/* 131b yk 用的是自己那格(256),不是抄来的 512/1024 —— 三档上限互不相同,
* 所以这一条能区分出"s_caps[] 串格"这种安静错误 */
reset_all();
ready_only();
(void)ff_fake_mkfile_size(P_YK, (SYSLOG_YK_MAX - 1u) * SYSLOG_REC_SIZE);
(void)put_rec(SYSLOG_YK, "cap-y1");
ok = (syslog_count(SYSLOG_YK) == SYSLOG_YK_MAX);
(void)put_rec(SYSLOG_YK, "cap-y2");
ok = ok && (syslog_count(SYSLOG_YK) == 1u) && (ff_fake_size(P_YK) == SYSLOG_REC_SIZE);
CHECK(ok, "TC-SYSLOG-131b the telecontrol log rotates on its own 256-record cap (per-id caps are not shared)");
/* 132 quiet 通道(队列侧):丢弃要计数,但投递侧一条日志都不许打
* ——这正是它存在的唯一理由,退化成 syslog_post 的别名必须被照出来 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain(); /* 建队列 */
g_sys_err_count = 0;
for (i = 0u; i < (SYSLOG_QUEUE_MAX + 4u); i++) {
syslog_post_quiet(SYSLOG_ACCIDENT, "q");
}
ok = (s_q_cnt == SYSLOG_QUEUE_MAX) && (syslog_post_dropped() == 4u)
&& (g_sys_err_count == 0);
syslog_drain(); /* 落盘 + drain 侧按差值补一条 */
ok = ok && (syslog_count(SYSLOG_ACCIDENT) == SYSLOG_QUEUE_MAX)
&& (g_sys_err_count == 1) && (syslog_post_dropped() == 4u);
syslog_drain(); /* 计数没前进 -> 不得重复报 */
CHECK(ok && (g_sys_err_count == 1),
"TC-SYSLOG-132 quiet drops are counted and reported once from the writer side, never at the posting site");
/* 133 清除记录 = 一次真实运维动作 -> 落操作记录;且不影响被清那份的归零判据 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain();
syslog_post(SYSLOG_RUN, "before clear");
syslog_drain();
ok = (syslog_count(SYSLOG_RUN) == 1u);
ok = ok && (syslog_request_clear(SYSLOG_RUN) == 0);
syslog_drain(); /* 删除落地 + 投递那条操作记录 */
ok = ok && (syslog_count(SYSLOG_RUN) == 0u)
&& (syslog_clear_state(SYSLOG_RUN) == (int)SYSLOG_CLEAR_OK);
syslog_drain(); /* 操作记录这才落盘 */
ok = ok && (syslog_count(SYSLOG_OPER) == 1u) && raw_at(P_OPER, 0u, raw)
&& (memcmp(raw + SYSLOG_DESC_OFFS, "clear run.log", 13u) == 0);
CHECK(ok, "TC-SYSLOG-133 clearing a log writes one operation record naming the file, while the cleared log stays at zero");
/* 133b 反过来:清 oper.log 自己时不得投递操作记录,否则"清除成功"这一条
* 会把刚删空的文件又写成 1 条,直接砸掉 task-4 的「count 归零」判据 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain();
syslog_post(SYSLOG_OPER, "an operation");
syslog_drain();
ok = (syslog_count(SYSLOG_OPER) == 1u);
ok = ok && (syslog_request_clear(SYSLOG_OPER) == 0);
syslog_drain();
ok = ok && (syslog_clear_state(SYSLOG_OPER) == (int)SYSLOG_CLEAR_OK)
&& (syslog_count(SYSLOG_OPER) == 0u);
syslog_drain(); /* 再排一轮:真投了自我记录,这轮就写回去 */
CHECK(ok && (syslog_count(SYSLOG_OPER) == 0u) && (syslog_post_dropped() == 0u),
"TC-SYSLOG-133b clearing the operation log itself posts nothing, so the zeroed-count criterion survives");
/* 134 守卫:越界 id / NULL 描述在 quiet 通道上既不投递也不计丢弃
* ("被守卫挡下"与"满了被丢掉"是两件事,混起来就等于把参数错报成容量故障) */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain();
g_sys_err_count = 0;
syslog_post_quiet((syslog_id_t)SYSLOG_COUNT, "bad-id");
syslog_post_quiet(SYSLOG_RUN, NULL);
ok = (syslog_post_dropped() == 0u) && (s_q_cnt == 0u) && (s_preboot_cnt == 0u)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (g_sys_err_count == 0);
CHECK(ok, "TC-SYSLOG-134 quiet channel guards bad id and NULL desc without counting them as drops and without touching the volume");
/* 135 启动前数组侧同样计数、同样静默(两个通道各自独立,与 F 项那两个抑制位
* 的分区方式一致;漏掉这一侧就会"运行期有账、启动期没账") */
reset_all();
ready_only(); /* 内核未跑 -> 走 s_preboot */
g_sys_err_count = 0;
for (i = 0u; i < (SYSLOG_QUEUE_MAX + 3u); i++) {
syslog_post_quiet(SYSLOG_START, "p");
}
ok = (s_preboot_cnt == SYSLOG_QUEUE_MAX) && (syslog_post_dropped() == 3u)
&& (g_sys_err_count == 0);
s_kernel_state = osKernelRunning;
syslog_drain();
CHECK(ok && (syslog_count(SYSLOG_START) == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1),
"TC-SYSLOG-135 pre-boot array overflow is counted and traced from the writer side too (8 slots kept, 3 lost)");
/* 136 五类记录与老两类**逐字节同构**:同一枚 s_format_rec,记录体里没有类型字段
* ——这就是"文件即类型"这条决策的机器表达(task-11 页面因此只需一套解析) */
reset_all();
{
uint8_t a[SYSLOG_REC_SIZE];
uint8_t b[SYSLOG_REC_SIZE];
s_now = 1760234567u;
s_clock_ok = 1;
s_format_rec(SYSLOG_RUN, "same text", a);
s_format_rec(SYSLOG_YK, "same text", b);
ok = (memcmp(a, b, SYSLOG_REC_SIZE) == 0);
s_format_rec(SYSLOG_ACCIDENT, "same text", b);
ok = ok && (memcmp(a, b, SYSLOG_REC_SIZE) == 0);
s_format_rec(SYSLOG_OPER, "same text", b);
ok = ok && (memcmp(a, b, SYSLOG_REC_SIZE) == 0);
}
CHECK(ok, "TC-SYSLOG-136 a record body does not encode its kind: same content is byte-identical across all seven kinds");
}
/* ============ TC-SYSLOG-137 卷锁 fail-open 边沿留痕(道G task-40) ============ */
static void tc_vollock_note(void)
{
uint8_t rec[SYSLOG_REC_SIZE];
uint32_t c0;
uint32_t c1;
int ok;
printf("-- TC-SYSLOG-137 volume-lock fail-open: one self-test record per boot, never [ERR] --\n");
/* 137 边沿第一次前进:补投一条,字段带累计值,且全程不打错级/[ERR]。
* 注意投递与落盘分在两轮 drain(投递发生在 drain 尾部,本轮批次已取走),
* 所以这里显式排两轮,别把"本轮没落盘"误读成"没投"。 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain(); /* 建队列;此刻计数为 0,不该有任何留痕 */
c0 = syslog_count(SYSLOG_SELFTEST);
g_sys_err_count = 0;
s_vl_failopen_fake = 1u;
syslog_drain(); /* 读到 1 != 上次 0 -> 投递 */
ok = (syslog_count(SYSLOG_SELFTEST) == c0);
syslog_drain(); /* 落盘 */
c1 = syslog_count(SYSLOG_SELFTEST);
(void)memset(rec, 0, sizeof(rec));
(void)raw_at(P_SELFTEST, c0, rec);
ok = ok && (c1 == c0 + 1u)
&& (memcmp(rec + SYSLOG_DESC_OFFS, "vollock failopen=1", 18u) == 0)
&& (rec[SYSLOG_DESC_OFFS + 18u] == 0u)
&& (g_sys_err_count == 0);
CHECK(ok, "TC-SYSLOG-137 the first fail-open edge posts exactly one self-test record carrying the cumulative count, with zero error-level traces");
/* 137b 同一 episode 内继续前进:不得再投(每轮一条 = 每轮一次 NAND 写) */
s_vl_failopen_fake = 5u;
syslog_drain();
syslog_drain();
CHECK((syslog_count(SYSLOG_SELFTEST) == c1) && (g_sys_err_count == 0),
"TC-SYSLOG-137b a growing fail-open count inside the same boot posts no second record (the diagnostic must not become load)");
/* 137c 电平不变:多轮 drain 一条都不投——边沿判据而非电平判据 */
syslog_drain();
syslog_drain();
CHECK((syslog_count(SYSLOG_SELFTEST) == c1) && (g_sys_err_count == 0),
"TC-SYSLOG-137c an unchanged count posts nothing across further drains (edge-triggered, not level-triggered)");
/* 137d 重新上电:latch 复位,故障复发必须还能出声(否则"每 boot 一条"会退化成
* "永远一条",第二次开机就永久静默) */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
syslog_drain();
c0 = syslog_count(SYSLOG_SELFTEST);
s_vl_failopen_fake = 2u;
syslog_drain();
syslog_drain();
ok = (syslog_count(SYSLOG_SELFTEST) == c0 + 1u);
(void)memset(rec, 0, sizeof(rec));
(void)raw_at(P_SELFTEST, c0, rec);
ok = ok && (memcmp(rec + SYSLOG_DESC_OFFS, "vollock failopen=2", 18u) == 0);
CHECK(ok, "TC-SYSLOG-137d after a reboot the latch re-arms, so a repeat fail-open is audible again (one record per boot, not one per lifetime)");
}
/* ===================== 入口 ===================== */
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();
tc_clear();
tc_kinds();
tc_vollock_note();
printf("\n=== Sys Log Mcu Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}