e4d70f9fcb
证据源 = 主树 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)。
2526 lines
127 KiB
C
2526 lines
127 KiB
C
/*
|
||
* 模块名称:运行/自检记录落盘后端(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;
|
||
}
|