5a806d8a26
用户裁定「全部修」。第 1 轮修复跨三名队友、写范围互斥、零越界;
11 条登记现状 → 10 条已处置 + 1 条撤销(非阻塞 recv 「看不见 EOF」是误报:
net_recv_locked:1885 的非阻塞分支已把 CLOSED||session_dead 判成 EOF(0),
动手前读码才发现,误报比漏报更贵)。
存储与日志(App/log/sys_log_mcu.c、soe_store.c、cfg_file_store_mcu.c、sys_log.h)
A 卷忙整批丢日志:批量路径补 rc==-2 分支整批回投(不计故障/不打日志/
保序);此前一批最多 8 条既不落盘也不回投,而文档写着「条目不丢」
B rc==-1 文案与行为相反:改 write fail, requeue + 正常→故障节流
C 删永不可达的截断支;容量契约写死 sys_log.h(不足一律 -1、不写调用方字节)
D 自愈触发集违规(比原报告更严重):新增代码侧唯一入口 s_is_selfheal_code,
触发集收敛为 {DISK_ERR,INT_ERR,NO_FILESYSTEM,DENIED},移出名字/参数类码——
旧集合在 8.3 同名同因下每轮删一次自己的历史日志且永不自愈,正是存储架构
§10.1 第 10 条严禁的归集;soe_store 侧同源取更保守子集(打开失败绝不 unlink)
E 注释里的 FR_NO_SYSTEM 不存在,改 FR_NOT_READY / FR_NO_FILESYSTEM
F syslog_post 满态留痕节流(A 修好后「队列常驻满」成必然场景,不修等于用
新刷屏换掉静默丢);丢弃语义一字未改
NET(net_socket.c/.h、net_types.h、net_socket_sim.c、lftpd.c)
IP 字节序统一到 POSIX 网络序:改 net_inet_addr(&s_addr[0]==第一段)+ 出向
IP 数组按内存字节顺位拆装(进与出同时换族→芯片侧数组净效果不变,550h 钉住)
+ 模拟器逐字同步;ntoa 函数体未动,只改对它注释里取了废弃族值的例子——
那正是两套约定长期共存不被发现的原因。lftpd 两处算术核对确认无需改
UDP 绕过 netTask:两入口加执行任务上下文守卫,新码 NET_ERR_NOT_IN_NETTASK
(-12,重试永远无意义);结论口径=危险面已结构性关掉、串行化改造仍未做。
附带关掉第二条更隐蔽的路:非 netTask 自旋 recvfrom 期间控制块被 close 并
把同一 sockfd 分给新会话→旧自旋者消费新会话数据报
控制台(console_log.c/.h):冷却改独立 s_con_cooling 标志(旧写法在 tick==0 那
拍冷却等于没记);con_append 的判空位+推进 head+整行拷贝收进同一临界区(旧
为区外 check-then-act,环 >95% 满时可静默覆盖且既不打 [ERR] 也不计 dropped);
Instance==NULL 与僵尸通道分诊(未初始化绝不 reinit);三本账分离 + 新读口
dbg_console_tx_timeout();[ERR] 行改为发成功才销账
测试:net_socket 452、console_log 127、sys_log_mcu 104、soe_store 27、
cfg_file_store_mcu 34;回归 net_listener 34 / net_events 34 / net_pending 43 /
net_select 22 / lftpd 35 / lftpd_inet 28 / lftpd_io 33 / iec104_serv 28 /
nand_port 149 / vol_lock 33 / soe 32 / cfg_file 151 全绿。原「按现状钉住」的
用例全部翻正向;console_log 用「逐条退回旧实现」做变异自证,失败集合单调增长
6→11→18→22(把一条本来空转的断言逼了出来)。
游标抬升:TC-NETSOCK-545→560、TC-SYSLOG-100→107、TC-CONLOG-135→167。
文档:控制台设计说明 §4.1 口径与 §13-O2 销项 + v1.5;运行与自检记录 §4.4/§4.5
四条;存储架构 §10.1 第 10 条推论(指向 s_is_selfheal_code);TRAP-NET-21 后续
条目;BSD 指南新增 §9.0 s_addr 唯一约定;覆盖率口径 §9「第 1 轮修复处置」表 +
后续项 5 条 + 台架复验 7 条 + 新坑 8 条 + 变更记录。
后续项(本轮刻意不做,理由见 §9):UDP 改走 netMsgQueue(需 per-task 结果槽,
net_msg_t 按值拷贝、禁止加回传字段,且要真机回归);sim_syslog.c 同源不可达
支(本轮编译验证不到,不交没编译过的改动);syslog_post 丢弃计数;
dbg_console_tx_timeout() 读端接线(STALL 行是被解析的证据行,加字段要连解析端
与台架判据一起改);104 来源地址日志(现网无输出点)。
gate: @build 0 errors 0 warnings(Code=213720 RO=18860 RW=1204 ZI=131500)。
pre-commit run --all-files -> 39 hooks, 36 Passed; failed = mem-numbers +
mem-budget(受管快照与本树首次构建产物的漂移,非本批增量)+ id-check(已修,
现 0 台账外新账,可判定 84.63% > 基线 82.6%)。实测水位 RW_IRAM1 57.4%、
RW_CCM 87.9%,均低于 92% 红线;RAM/Flash 本批增量为数个 static 变量。
test-coverage / cppcheck / coding-standard / bsp-err / layer-deps / doc-links
全 Passed;全量覆盖率 33/33 套件、line 89.3%、func 92.3%、逐文件下降 0。
按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
1824 lines
85 KiB
C
1824 lines
85 KiB
C
/*
|
||
* 模块名称:运行/自检记录落盘后端(sys_log_mcu)主机侧单元测试(L0.5)
|
||
* 模块功能:在 PC 上离线验证 App/log/sys_log_mcu.c 的四族契约——
|
||
* A 64B 定长记录格式(时间戳 / epoch ASCII / 描述三段 + 截断 + 占位)
|
||
* B 写侧(单条 s_append_rec、批量 s_append_many、N-24 的"一批一次
|
||
* open/write/close"、容量轮转、L1 自愈、L2 留痕去重、卷锁 try)
|
||
* C 排空(syslog_drain 的 preboot + 队列两条来源、换文件分批、
|
||
* 失败回投不丢条目、-3 退回慢路径)
|
||
* D 读侧(N-24 的 8 条滑动窗口 + 写者代数失效 + 越界/忙/短读处置)
|
||
* 外加 syslog_mcu_mounted 的 挂载幂等与上电/重启分类、sys_log_evidence
|
||
* 的 wdev.log 取证通道(8KB 护栏 + 第二条写必须在释放卷锁之后)。
|
||
*
|
||
* 被测对象:真实源文件 App/log/sys_log_mcu.c(#include 原样纳入,不改生产代码)。
|
||
* 假件与桩:
|
||
* test/host/common/fatfs_fake.{h,c} —— 假 FatFs 内存卷(真实 ff.h 声明)
|
||
* soe_rec.c 不纳入;soe_store_drain() 用**计数桩**(B9 挂接点本身由
|
||
* test/host/soe_store/ 那套测,本套只断言"每次 drain 各调一次")
|
||
* stub/{cmsis_os,dbg_log,main}.h 本目录桩;sys_clock_* / vol_lock_* /
|
||
* osMessageQueue* 的可控强符号实现写在本文件里
|
||
*
|
||
* 为什么要主机侧单测(而不是只用台架)——本文件的四条坑全是"日志看不出来"型的:
|
||
* 1. **FA_OPEN_APPEND 不含 FA_WRITE**(§10.1 第 11 条):漏写位时 f_open 会成功
|
||
* (文件甚至被创建出来),只有 f_write 恒返 FR_DENIED(7)。真机表现是
|
||
* "/log 写失败 x5/开机",2026-09-20 才定位到。→ 用假卷把这条做成必然失败的断言。
|
||
* 2. **f_write 簇分配失败 = FR_OK + 短写**:只判 fr != FR_OK 就等于把丢记录当成功。
|
||
* 3. **挂载幂等**:syslog_mcu_mounted 曾在建 /log 失败后永久 no-op(运行期日志与自检
|
||
* 全部滞留),且 2s 重试会刷屏 → 幂等 + 留痕去重都要注入 mkdir 失败才测得到。
|
||
* 4. **读窗口 try 语义 / N-24 读侧**:读窗口"命中即零 I/O"与"写者一落地就必须失效重填"是同一
|
||
* 枚硬币的两面——只做前者会把陈旧读当正确,只做后者会退化成逐条 open/seek/close
|
||
* 的旧实现(UI 翻 20 条 = 20 次 FAT 往返)。两者都靠假卷的调用计数才断言得了。
|
||
*
|
||
* 覆盖点(TC 号族 SYSLOG,形如 TC-SYSLOG-001,族内按功能分档;缺口在小节末尾如实登记):
|
||
* —— A 记录格式 ——
|
||
* TC-SYSLOG-001 布局:[0..18] 时间戳、[19]=' '、[20..29]=epoch "%010u"、[30]=' '、
|
||
* [31..] 描述、尾部 NUL 填充到 64B
|
||
* TC-SYSLOG-002 描述超长截断到 SYSLOG_DESC_MAX,且不越 64B 边界
|
||
* TC-SYSLOG-003 墙钟未建立:get_str 返 NULL -> 占位 "1970-01-01 08:00:00";
|
||
* get() 返 UINT32_MAX -> epoch 字段落 "0000000000"
|
||
* TC-SYSLOG-004 单条路径与批量路径产出的同内容记录**逐字节相同**(N-24 抽出
|
||
* s_format_rec 的全部理由:UI 按定长字段偏移解析)
|
||
* —— B 写侧 ——
|
||
* TC-SYSLOG-010 单条落盘成功 + 打开模式必须 FA_WRITE|FA_OPEN_APPEND;close 配对
|
||
* TC-SYSLOG-011 只给 FA_OPEN_APPEND 的对照保真:f_write 恒 FR_DENIED(陷阱一)
|
||
* TC-SYSLOG-012 卷忙(try 失败)-> 返回 -2、零 FatFs、不计故障、不 release
|
||
* TC-SYSLOG-013 FR_OK + 短写 -> 判失败 -> unlink 重建重试 -> 恰好 64 字节
|
||
* TC-SYSLOG-014 容量满(1024 条)-> 先 unlink 再写;新文件只剩本次一条
|
||
* TC-SYSLOG-015 满且 unlink 失败(真错误码)-> 返回 -1 并释放卷锁
|
||
* TC-SYSLOG-016 满且 unlink 返 FR_NO_FILE -> 视为无害,继续写
|
||
* TC-SYSLOG-017 无效 id / 空指针守卫 -> -1
|
||
* TC-SYSLOG-018 写成功 -> 读侧代数 s_rd_gen 自增(窗口失效钩子)
|
||
* TC-SYSLOG-019 连续两轮写失败只留痕一次;成功一次复位后再失败才再留痕
|
||
* TC-SYSLOG-020 批量:8 条 = 一次 open + 一次 512B write + 一次 close
|
||
* TC-SYSLOG-021 批量入参守卫(id 越界 / n=0 / n>8 / 空指针)-> -1
|
||
* TC-SYSLOG-022 剩余容量装不下一批 -> 返回 -3 且**不 unlink**(轮转只在慢路径)
|
||
* TC-SYSLOG-023 批量卷忙 -> -2;批量短写 -> unlink 重试成功
|
||
* TC-SYSLOG-024 批量持续失败 -> -1 + 留痕去重(与单条同一判据)
|
||
* —— C 排空 ——
|
||
* TC-SYSLOG-030 卷未就绪 -> drain 直接返回,preboot 条目滞留不丢
|
||
* TC-SYSLOG-031 首次 drain 惰性建队列(内核对象在 osKernelStart 之后)
|
||
* TC-SYSLOG-032 preboot 8 条同文件 -> 一批 512B 落盘 + preboot 计数清零
|
||
* TC-SYSLOG-033 两个文件混排 -> 按文件分批,顺序不乱、条目不丢
|
||
* TC-SYSLOG-034 换文件那条不消耗(preboot 下标回退 / 队列回投)
|
||
* TC-SYSLOG-035 卷忙 -> 整批回投队列,一条不丢(A 项修复后的正向断言)
|
||
* TC-SYSLOG-035a 回投后再排空:顺序与投递顺序一致,全程静默
|
||
* TC-SYSLOG-035b 慢路径遇卷忙 -> 尾部整批回投(参照物,A 项就是向它对齐)
|
||
* TC-SYSLOG-035c 满批 8 条在卷忙时全部保住
|
||
* TC-SYSLOG-035d preboot 来源的条目卷忙时转投队列(不丢也不重复落盘)
|
||
* TC-SYSLOG-036 队列满回投失败 -> "hold-over drop" 留痕,不崩
|
||
* TC-SYSLOG-037 -3 -> 逐条慢路径,容量轮转只发生一次
|
||
* TC-SYSLOG-038 慢路径中途写失败 -> 该条丢弃留痕 + 尾部回投
|
||
* TC-SYSLOG-039 批量整体 -1 -> 整批丢弃留痕一次并结束本轮
|
||
* TC-SYSLOG-040 drain 尾部各调一次 soe_store_drain()(B9 挂接点)
|
||
* TC-SYSLOG-041 post:内核未运行走 preboot、满则丢弃留痕;内核运行走队列、满则留痕;
|
||
* id 越界/NULL 描述静默丢弃
|
||
* —— D 读侧 ——
|
||
* TC-SYSLOG-050 count = filesize/64(O(1));文件缺失/未挂载 -> 0
|
||
* TC-SYSLOG-051 窗口命中 -> 第二次读**零 FatFs 调用**
|
||
* TC-SYSLOG-052 写者落地 -> 代数变化 -> 窗口重填并读到新内容
|
||
* TC-SYSLOG-053 窗口按 8 条对齐:跨窗读 idx=8 -> 一次 open + lseek(512) + read(64)
|
||
* TC-SYSLOG-054 越界 idx -> -1;空文件 -> -1
|
||
* TC-SYSLOG-055 读时卷忙 -> -1(读窗口 try 语义:UI 每 100ms 喂心跳,绝不阻塞)
|
||
* TC-SYSLOG-056 短读 -> 作废窗口(s_rd_win_cnt=0),不把半窗当有效命中
|
||
* TC-SYSLOG-057 get_record 容量守卫:ts_len<=19 或 desc_len<=28 一律 -1
|
||
* TC-SYSLOG-057b 被拒的调用一个字节都不写调用方缓冲(C 项"宁可拒绝不截断")
|
||
* TC-SYSLOG-058 last_epoch:无记录 -1;有记录解析 10 位 ASCII
|
||
* —— E mounted / 上电重启分类 ——
|
||
* TC-SYSLOG-060 /log 不存在 -> f_mkdir 成功 + 幂等(第二次不再建目录)
|
||
* TC-SYSLOG-061 f_mkdir 失败 -> 不就绪、留痕去重、后续重试成功并复位
|
||
* TC-SYSLOG-062 卷忙 -> 本轮顺延、不置就绪
|
||
* TC-SYSLOG-063 末条 epoch 距 now < 600s -> "reboot";>=600s 或墙钟无效 -> "power on"
|
||
* TC-SYSLOG-064 f_stat /log 已存在 -> 不再调 f_mkdir(幂等靠 stat,不是靠状态位)
|
||
* —— F 取证通道 sys_log_evidence ——
|
||
* TC-SYSLOG-070 写 /log/wdev.log 变长行("ts 描述\\r\\n")+ 随后补一条 run.log
|
||
* TC-SYSLOG-071 卷锁顺序硬约束:第二条写在 release **之后**(非递归锁,反了自死锁)
|
||
* TC-SYSLOG-072 >8192B -> 先 unlink 再写(防长跑无界增长)
|
||
* TC-SYSLOG-073 卷忙 -> 返回 -1 且 wdev.log 零字节(不阻塞喂狗点)
|
||
* TC-SYSLOG-074 wdev 写失败 -> 返回 -1,但第二条 run.log 仍写(独立证据语义)
|
||
* —— H 补齐分支(自愈 / 锁后二次校验 / 回投 / 占位时标 / 空指针)——
|
||
* TC-SYSLOG-090 批量 open 失败一次 -> unlink 重开 -> 整批落全、不留痕
|
||
* TC-SYSLOG-091 批量 open 持续失败 -> -1 + 留痕一次 + 已放锁
|
||
* TC-SYSLOG-092 取锁后二次 count 失败 -> 放锁返回 -1(漏放 = FTP 永久等不到卷)
|
||
* TC-SYSLOG-093 读时 f_open 失败 -> 放锁 -1;下轮恢复后窗口正常重填
|
||
* TC-SYSLOG-094 无墙钟 -> wdev.log 落占位时标(取证行也必须可解析)
|
||
* TC-SYSLOG-095 wdev 短写 -> 返回 -1,第二条独立证据仍落 run.log
|
||
* TC-SYSLOG-096 队列创建失败 -> 一条都不排(不静默丢),堆恢复后排空并接上 SOE
|
||
* TC-SYSLOG-097 队列侧换文件那条原样回投 -> 三条全落、各文件内顺序不乱
|
||
* TC-SYSLOG-098 读侧不再截断(C 项已删死分支):拿到整 28 字符 + 唯一 NUL
|
||
* TC-SYSLOG-099 p_rec / p_desc 空指针守卫(守卫在取锁之前)
|
||
* TC-SYSLOG-100 last_epoch 读失败 -> -1 且 *p_epoch 清零
|
||
* —— I D 项:自愈触发集逐码(与 soe_store 侧同一张表)——
|
||
* TC-SYSLOG-101/101b 介质/结构类码(FR_DISK_ERR/INT_ERR/NO_FILESYSTEM/DENIED)
|
||
* -> 单条与批量路径各 unlink 恰好一次 + 重开一次并落全(自愈成功
|
||
* 不留痕;文件被删是设计代价,所以 SOE 侧不允许这么做)
|
||
* TC-SYSLOG-102 名字/参数类码(INVALID_NAME/NO_PATH/NO_FILE/INVALID_OBJECT/
|
||
* NOT_READY/NOT_ENABLED/TIMEOUT)连续 20 轮 -> 零 unlink、零重开、
|
||
* 邻居文件与 /log 目录不动、读侧代数照常自增
|
||
* TC-SYSLOG-103/103b s_is_selfheal_code() 分类表本身逐码钉死
|
||
* TC-SYSLOG-104 卷忙不计故障:恢复后第一次真失败仍立刻留痕(反证 -2 没置故障位)
|
||
* —— J F 项:投递侧丢弃留痕节流(队列通道)——
|
||
* TC-SYSLOG-105 队列满后连投 12 条 -> 只报第一次(A 项回投让队列常驻满,
|
||
* 不节流就按投递频率刷串口)
|
||
* TC-SYSLOG-106 同一故障窗口内再投 5 条 -> 完全静默(丢弃语义不变,照丢)
|
||
* TC-SYSLOG-107 复位点必须在 syslog_post 的成功分支:drain 只落盘、不复位
|
||
* TC-SYSLOG-107b 恢复后再灌满 -> 第二次故障重新留痕
|
||
* TC-SYSLOG-107c 队列通道与启动前数组通道的抑制位互不干扰
|
||
* —— G 陷阱二:名字/参数类错误码严禁升级成卷操作 ——
|
||
* TC-SYSLOG-080 短名违规分档保真:体>8 / 扩展>3 / 非法字符 / 以点开头(.fschk)
|
||
* 各自落 FR_INVALID_NAME;含 ':' 落 FR_INVALID_DRIVE
|
||
* TC-SYSLOG-081 生产路径全过严格 8.3:/log /run.log /selftest.log /wdev.log /soe.log
|
||
* TC-SYSLOG-082 FR_INVALID_NAME 反复失败 20 轮:unlink 次数有界、/log 目录与
|
||
* 邻居文件一根毛没动(自愈只针对自己的文件)
|
||
*
|
||
* 覆盖率台账(--module sys_log_mcu 自量):App/log/sys_log_mcu.c 310/313 行 =
|
||
* 99.0%、函数 12/12。未覆盖 3 行:L628(下面第三条死分支)、L727~728
|
||
* (sys_log_evidence 里 snprintf 返回 <=0 的分支——宿主 snprintf 不可能失败,
|
||
* 假件也不伪造返回值;为刷数字去伪造等于把断言写在假件规则上,宁可留空)。
|
||
*
|
||
* 本套照出的四条现状 —— 已在 task-4(2026-10-24)全部处置,相应断言已翻成正向:
|
||
*
|
||
* A【已修】卷忙整批静默丢日志:syslog_drain 批量路径 done = n 后缺 rc==-2 分支
|
||
* → 回投循环空转,最多丢 8 条。修法 = 补 else if (rc == -2) { done = 0u; },
|
||
* 与 -3 慢路径遇 -2 的处置对齐(慢路径是参照物,没动它)。三条语义纪律
|
||
* ①不计故障 ②不打日志 ③回投保序 分别由 TC-SYSLOG-035 / 035a / 035d 钉住。
|
||
* B【已修】rc==-1 的留痕文案 "drop %lu entries" 与行为相反(done=0 → 其实整批
|
||
* 回投、一条没丢)。改成 "write fail, requeue N entries",并按"正常→故障"
|
||
* 节流(新增文件级 s_requeue_reported[],成功一轮复位)——旧版持续故障时每
|
||
* 2s 刷一条。TC-SYSLOG-039 / 039b 钉住"节流一次"与"恢复后再失败要再报"。
|
||
* C【已修】syslog_get_record 的截断支永不可达:守卫已挡掉 desc_len <=
|
||
* SYSLOG_DESC_MAX,而 dl 恒 <= 28。删掉死分支,容量要求写进函数头与
|
||
* sys_log.h(**宁可整条拒绝也不静默截断**:UI 拿到截断串会显示成一条被删短
|
||
* 过的日志,比读不到更难查)。TC-SYSLOG-057 / 057b / 098 钉住。
|
||
* D【已修】自愈触发集违反 §10.1 第 10 条:FR_INVALID_NAME / FR_NO_PATH /
|
||
* FR_NO_FILE / FR_INVALID_OBJECT 都曾在 unlink 重建集合里,后果是名字/参数类
|
||
* 错误下**每轮删一次自己的历史日志且永远修不好**。现在按语义拆类,新增
|
||
* s_is_selfheal_code():触发集只留 FR_DISK_ERR / FR_INT_ERR / FR_NO_FILESYSTEM /
|
||
* FR_DENIED,逐码依据(含 ff.c 行号)写在该函数头。TC-SYSLOG-101~103 逐码钉,
|
||
* 与 test/host/soe_store/ 的 TC-SOESTORE-022b 是同一张表的两侧。
|
||
*
|
||
* F【已修】投递侧丢弃留痕无节流:syslog_post 的两个丢弃分支(队列满 / 启动前数组
|
||
* 满)原来每条被丢的投递各打一行 DBG_ERROR。A 项修复让卷忙时整批回投 -> 队列
|
||
* 可长时间常驻满态 -> 此后每条业务投递都打一行,日志自己变成串口负载(比原
|
||
* 缺陷更常触发)。修法 = 与 B 同一套"正常→故障"节流纪律,但**另立两个抑制位**
|
||
* (s_post_qfull_reported / s_post_pboot_full_reported,volatile 单字节,任意
|
||
* 上下文读写)——不复用 s_requeue_reported:那是 defaultTask 的 drain 侧"整批
|
||
* 回投",本处是任意任务的"这一条被丢了",恢复时机也不同(下一次投递成功)。
|
||
* **丢弃语义一个字没改**(满则丢仍照旧),变的只有留痕次数。
|
||
* TC-SYSLOG-041b/041d/041e/041f(数组通道)+ 105/106/107/107b/107c(队列通道)。
|
||
*
|
||
* 顺手记一条容易误读的事实(不是缺陷):自愈成功 = **删掉整个日志文件重建**,
|
||
* 所以一次打开失败会连带丢掉既有历史(TC-SYSLOG-101 明确断言文件只剩本次一条)。
|
||
* run.log 是滚动运行日志,这个代价可接受;soe.log 是事件序列,同样的动作被禁止
|
||
* (TC-SOESTORE-022 / 022b),两侧的差异是刻意的,注释也互指。
|
||
*
|
||
* 故意没测(写在这里,不留"看起来全覆盖"的错觉):
|
||
* - 真跨任务并发:读窗口用"对齐 32 位读写在 ARM 上原子"免锁,主机 x64 + 单线程
|
||
* 编不出真竞态;本套只断言单写者语义与代数失效逻辑本身。
|
||
* - f_close 内部 sync_fs -> disk_ioctl(CTRL_SYNC) -> ftl_flush_cache() 的掉电落盘
|
||
* 链:属 nand_port/GD5F 层,假卷里没有 FTL,"关文件即落盘"要靠真机 + N-22 台账。
|
||
* - snprintf 失败(n<=0)分支:宿主 snprintf 不会失败,且假件不提供注入——
|
||
* 为了刷覆盖率去伪造返回值等于把断言写在假件规则上,宁可留空。
|
||
* - DBG 文本内容:桩只计数(真实宏展开成 snprintf + 192B 行缓冲)。
|
||
*
|
||
* 通过:全部 TC PASS(main 返回 0)。
|
||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||
*
|
||
* 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2)
|
||
*/
|
||
|
||
#include <stdint.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <time.h>
|
||
|
||
#include "ff.h"
|
||
#include "fatfs_fake.h"
|
||
#include "cmsis_os.h"
|
||
#include "dbg_log.h"
|
||
#include "sys_clock.h"
|
||
#include "vol_lock.h"
|
||
#include "sys_log.h"
|
||
#include "soe_store.h"
|
||
|
||
/* ===================== 断言框架 ===================== */
|
||
|
||
static int s_total;
|
||
static int s_passed;
|
||
static int s_failed;
|
||
|
||
#define CHECK(cond, name) do { \
|
||
s_total++; \
|
||
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
|
||
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
|
||
} while (0)
|
||
|
||
/* 路径常量:与生产 s_paths[] / sys_log_evidence() 里的字面量一致(写死在测试里
|
||
* 是为了"改生产路径名必须同时改测试",改名静默失效正是 §10.1 第 10 条的老账) */
|
||
#define P_RUN "/log/run.log"
|
||
#define P_SELFTEST "/log/selftest.log"
|
||
#define P_WDEV "/log/wdev.log"
|
||
|
||
/* ===================== 假件与生产源码纳入 ===================== */
|
||
|
||
#include "fatfs_fake.c"
|
||
#include "sys_log_mcu.c"
|
||
|
||
/* ===================== dbg_log 桩计数 ===================== */
|
||
|
||
int g_sys_err_count;
|
||
int g_sys_info_count;
|
||
int g_sys_debug_count;
|
||
|
||
/* ===================== soe_store 挂接点计数桩 ===================== */
|
||
|
||
static int s_soe_drain_calls;
|
||
|
||
void soe_store_drain(void)
|
||
{
|
||
if (s_soe_drain_calls++ > 20000) {
|
||
(void)fprintf(stderr, "[FATAL] soe_store_drain guard tripped\n");
|
||
exit(2);
|
||
}
|
||
}
|
||
|
||
void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value)
|
||
{
|
||
(void)type; (void)elm_id; (void)value;
|
||
}
|
||
|
||
uint32_t soe_store_dropped(void)
|
||
{
|
||
return 0u;
|
||
}
|
||
|
||
/* ===================== 假墙钟 ===================== */
|
||
|
||
static uint32_t s_now = 1760234567u;
|
||
static int s_clock_ok = 1;
|
||
|
||
uint32_t sys_clock_get(void)
|
||
{
|
||
return (s_clock_ok != 0) ? s_now : UINT32_MAX;
|
||
}
|
||
|
||
char *sys_clock_get_str(char *buf, size_t buf_size)
|
||
{
|
||
struct tm gmt;
|
||
time_t t;
|
||
size_t need = SYSLOG_TS_LEN + 1u;
|
||
|
||
if ((s_clock_ok == 0) || (buf == NULL) || (buf_size < need)) {
|
||
return NULL;
|
||
}
|
||
t = (time_t)s_now;
|
||
#if defined(_WIN32)
|
||
if (gmtime_s(&gmt, &t) != 0) {
|
||
return NULL;
|
||
}
|
||
#else
|
||
gmt = *gmtime(&t);
|
||
#endif
|
||
(void)strftime(buf, buf_size, "%Y-%m-%d %H:%M:%S", &gmt);
|
||
return buf;
|
||
}
|
||
|
||
int sys_clock_init(void) { return 0; }
|
||
int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *p_time)
|
||
{
|
||
(void)unix_secs; (void)p_time; return 0;
|
||
}
|
||
int sys_clock_set_unix(uint32_t unix_secs) { (void)unix_secs; return 0; }
|
||
int sys_clock_get_time(sd2506_time_t *p_time) { (void)p_time; return -1; }
|
||
uint32_t HAL_GetTick(void) { return 0u; }
|
||
|
||
/* ===================== 假卷锁(try 语义 + 配对检查) ===================== */
|
||
|
||
static int s_lock_busy_left;
|
||
static int s_lock_busy_skip; /* 先放行 N 次再开始忙(同一次 drain 里多把锁的场景) */
|
||
static int s_lock_waits;
|
||
static int s_lock_releases;
|
||
static int s_lock_held;
|
||
static int s_lock_seq_err;
|
||
|
||
static void lock_busy_next(int n)
|
||
{
|
||
s_lock_busy_skip = 0;
|
||
s_lock_busy_left = n;
|
||
}
|
||
|
||
/* 前 skip 次 vol_lock_wait 正常放行,接下来 busy 次返回"忙"。
|
||
* 需要它是因为同一次 syslog_drain() 里会连拿两把锁:批量路径那把必须先成功,
|
||
* 才谈得上"退回逐条慢路径后第二把忙"。 */
|
||
static void lock_busy_after(int skip, int busy)
|
||
{
|
||
s_lock_busy_skip = skip;
|
||
s_lock_busy_left = busy;
|
||
}
|
||
|
||
static void locks_clear(void)
|
||
{
|
||
s_lock_busy_left = 0;
|
||
s_lock_busy_skip = 0;
|
||
s_lock_waits = 0;
|
||
s_lock_releases = 0;
|
||
s_lock_held = 0;
|
||
s_lock_seq_err = 0;
|
||
}
|
||
|
||
int vol_lock_init(void) { return 0; }
|
||
|
||
int vol_lock_wait(uint32_t timeout_ms)
|
||
{
|
||
int r;
|
||
|
||
(void)timeout_ms;
|
||
if (s_lock_waits > 20000) {
|
||
(void)fprintf(stderr, "[FATAL] vol_lock_wait guard tripped\n");
|
||
exit(2);
|
||
}
|
||
if (s_lock_busy_skip > 0) {
|
||
s_lock_busy_skip--;
|
||
r = 1;
|
||
} else {
|
||
r = (s_lock_busy_left > 0) ? 0 : 1;
|
||
if (s_lock_busy_left > 0) {
|
||
s_lock_busy_left--;
|
||
}
|
||
}
|
||
s_lock_waits++;
|
||
if (r != 0) {
|
||
s_lock_held = 1;
|
||
}
|
||
return r;
|
||
}
|
||
|
||
void vol_lock_release(void)
|
||
{
|
||
s_lock_releases++;
|
||
if (s_lock_held == 0) {
|
||
s_lock_seq_err++;
|
||
}
|
||
s_lock_held = 0;
|
||
}
|
||
|
||
/* ===================== 假消息队列(真 FIFO + 满/空语义) ===================== */
|
||
|
||
#define MQ_SLOTS 16u
|
||
#define MQ_MAXMSG 24u /* sizeof(syslog_entry_t) 在 x64 = 16,留余量 */
|
||
|
||
typedef struct {
|
||
int used;
|
||
uint8_t data[MQ_MAXMSG];
|
||
} mq_slot_t;
|
||
|
||
static mq_slot_t s_q_pool[MQ_SLOTS];
|
||
static uint32_t s_q_head;
|
||
static uint32_t s_q_cnt;
|
||
static uint32_t s_q_cap;
|
||
static uint32_t s_q_msgsz;
|
||
static int s_q_new_calls;
|
||
static int s_q_new_fail;
|
||
static int s_q_get_calls;
|
||
static int s_q_put_calls;
|
||
|
||
static void queue_clear(void)
|
||
{
|
||
(void)memset(s_q_pool, 0, sizeof(s_q_pool));
|
||
s_q_head = 0u;
|
||
s_q_cnt = 0u;
|
||
s_q_cap = SYSLOG_QUEUE_MAX;
|
||
s_q_msgsz = (uint32_t)sizeof(syslog_entry_t);
|
||
s_q_new_calls = 0;
|
||
s_q_new_fail = 0;
|
||
s_q_get_calls = 0;
|
||
s_q_put_calls = 0;
|
||
}
|
||
|
||
osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size,
|
||
const osMessageQueueAttr_t *attr)
|
||
{
|
||
(void)attr;
|
||
s_q_new_calls++;
|
||
if (s_q_new_fail != 0) {
|
||
return NULL;
|
||
}
|
||
s_q_cap = msg_count;
|
||
s_q_msgsz = msg_size;
|
||
s_q_head = 0u;
|
||
s_q_cnt = 0u;
|
||
return (osMessageQueueId_t)&s_q_pool[0];
|
||
}
|
||
|
||
osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr,
|
||
uint8_t msg_prio, uint32_t timeout)
|
||
{
|
||
uint32_t tail;
|
||
|
||
(void)mq_id; (void)msg_prio; (void)timeout;
|
||
if (s_q_put_calls++ > 20000) {
|
||
(void)fprintf(stderr, "[FATAL] osMessageQueuePut guard tripped\n");
|
||
exit(2);
|
||
}
|
||
if ((msg_ptr == NULL) || (s_q_cnt >= s_q_cap) || (s_q_msgsz > MQ_MAXMSG)) {
|
||
return osErrorResource;
|
||
}
|
||
tail = (s_q_head + s_q_cnt) % MQ_SLOTS;
|
||
(void)memcpy(s_q_pool[tail].data, msg_ptr, s_q_msgsz);
|
||
s_q_pool[tail].used = 1;
|
||
s_q_cnt++;
|
||
return osOK;
|
||
}
|
||
|
||
osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr,
|
||
uint8_t *msg_prio, uint32_t timeout)
|
||
{
|
||
(void)mq_id; (void)msg_prio; (void)timeout;
|
||
if (s_q_get_calls++ > 20000) {
|
||
(void)fprintf(stderr, "[FATAL] osMessageQueueGet guard tripped\n");
|
||
exit(2);
|
||
}
|
||
if ((msg_ptr == NULL) || (s_q_cnt == 0u)) {
|
||
return osErrorResource;
|
||
}
|
||
(void)memcpy(msg_ptr, s_q_pool[s_q_head].data, s_q_msgsz);
|
||
s_q_pool[s_q_head].used = 0;
|
||
s_q_head = (s_q_head + 1u) % MQ_SLOTS;
|
||
s_q_cnt--;
|
||
return osOK;
|
||
}
|
||
|
||
uint32_t osMessageQueueGetCount(osMessageQueueId_t mq_id)
|
||
{
|
||
(void)mq_id;
|
||
return s_q_cnt;
|
||
}
|
||
|
||
osStatus_t osMessageQueueReset(osMessageQueueId_t mq_id)
|
||
{
|
||
(void)mq_id;
|
||
s_q_head = 0u;
|
||
s_q_cnt = 0u;
|
||
return osOK;
|
||
}
|
||
|
||
/* ===================== 内核状态开关 ===================== */
|
||
|
||
static osKernelState_t s_kernel_state = osKernelInactive;
|
||
|
||
osKernelState_t osKernelGetState(void)
|
||
{
|
||
return s_kernel_state;
|
||
}
|
||
|
||
/* ===================== 复位与铺状态 ===================== */
|
||
|
||
static void reset_all(void)
|
||
{
|
||
ff_fake_reset();
|
||
locks_clear();
|
||
queue_clear();
|
||
s_now = 1760234567u;
|
||
s_clock_ok = 1;
|
||
s_kernel_state = osKernelInactive;
|
||
g_sys_err_count = 0;
|
||
g_sys_info_count = 0;
|
||
g_sys_debug_count = 0;
|
||
s_soe_drain_calls = 0;
|
||
/* 生产模块的 static 残留:逐位显式清(漏一个就会"上一场景的尾巴"咬下一场景) */
|
||
(void)memset(s_preboot, 0, sizeof(s_preboot));
|
||
s_preboot_cnt = 0u;
|
||
(void)memset(s_batch_src, 0, sizeof(s_batch_src));
|
||
s_queue = NULL;
|
||
s_fs_ready = 0u;
|
||
s_mount_fail_reported = 0u;
|
||
/* B / F 项整改新增的留痕节流位:跨场景必须显式清(否则第二个故障场景看不到留痕) */
|
||
s_requeue_reported[SYSLOG_RUN] = 0u;
|
||
s_requeue_reported[SYSLOG_SELFTEST] = 0u;
|
||
s_post_qfull_reported = 0u;
|
||
s_post_pboot_full_reported = 0u;
|
||
(void)memset(s_rd_win, 0, sizeof(s_rd_win));
|
||
s_rd_win_id = -1;
|
||
s_rd_win_base = 0u;
|
||
s_rd_win_cnt = 0u;
|
||
s_rd_win_gen = 0u;
|
||
s_rd_gen[SYSLOG_RUN] = 0u;
|
||
s_rd_gen[SYSLOG_SELFTEST] = 0u;
|
||
/* 卷状态:/log 由 defaultTask 挂载时建好;本套多数场景直接假定已建,
|
||
* "没建 -> 补建" 那条路径单独用 TC-SYSLOG-060/061 测 */
|
||
(void)ff_fake_mkdir_direct("/log");
|
||
}
|
||
|
||
/* 就绪但**不**走 mounted()(避免上电那条记录混进断言) */
|
||
static void ready_only(void)
|
||
{
|
||
s_fs_ready = 1u;
|
||
}
|
||
|
||
/* 用生产 API 落一条记录(时间戳/epoch 由假墙钟决定) */
|
||
static void put_rec(syslog_id_t id, const char *desc)
|
||
{
|
||
ready_only();
|
||
(void)s_append(id, desc);
|
||
}
|
||
|
||
/* 从假卷直读第 i 条原始记录 */
|
||
static int raw_at(const char *path, uint32_t i, uint8_t *p_out)
|
||
{
|
||
return (ff_fake_fetch(path, p_out, i * SYSLOG_REC_SIZE, SYSLOG_REC_SIZE)
|
||
== SYSLOG_REC_SIZE) ? 1 : 0;
|
||
}
|
||
|
||
/* ===================== A 记录格式 ===================== */
|
||
|
||
static void tc_format(void)
|
||
{
|
||
uint8_t rec[SYSLOG_REC_SIZE];
|
||
uint8_t b[SYSLOG_REC_SIZE];
|
||
int ok;
|
||
uint32_t i;
|
||
|
||
printf("-- TC-SYSLOG-001..004 记录格式(64B 定长)--\n");
|
||
|
||
/* 001 布局 */
|
||
reset_all();
|
||
s_now = 1760234567u;
|
||
s_clock_ok = 1;
|
||
s_format_rec(SYSLOG_RUN, "hello", rec);
|
||
ok = (memcmp(rec, "2025-10-12 02:02:47", SYSLOG_TS_LEN) == 0)
|
||
&& (rec[SYSLOG_TS_LEN] == ' ')
|
||
&& (memcmp(rec + SYSLOG_EPOCH_OFFS, "1760234567", SYSLOG_EPOCH_LEN) == 0)
|
||
&& (rec[SYSLOG_EPOCH_OFFS - 1u] == ' ')
|
||
&& (memcmp(rec + SYSLOG_DESC_OFFS, "hello", 5u) == 0)
|
||
&& (rec[SYSLOG_DESC_OFFS + 5u] == '\0');
|
||
CHECK(ok, "TC-SYSLOG-001 [0..18]ts / [19]空格 / [20..29]epoch / [30]空格 / [31..]desc");
|
||
ok = 1;
|
||
for (i = SYSLOG_DESC_OFFS + 6u; i < SYSLOG_REC_SIZE; i++) {
|
||
if (rec[i] != 0u) {
|
||
ok = 0;
|
||
}
|
||
}
|
||
CHECK(ok, "TC-SYSLOG-001b 尾部 NUL 填充到 64B(定长解析的地基)");
|
||
|
||
/* 002 描述截断 */
|
||
reset_all();
|
||
{
|
||
char big[SYSLOG_DESC_MAX + 16u];
|
||
(void)memset(big, 'A', sizeof(big) - 1u);
|
||
big[sizeof(big) - 1u] = '\0';
|
||
s_format_rec(SYSLOG_RUN, big, rec);
|
||
ok = 1;
|
||
for (i = 0u; i < SYSLOG_DESC_MAX; i++) {
|
||
if (rec[SYSLOG_DESC_OFFS + i] != (uint8_t)'A') {
|
||
ok = 0;
|
||
}
|
||
}
|
||
CHECK(ok && (rec[SYSLOG_DESC_OFFS + SYSLOG_DESC_MAX] == 0u)
|
||
&& (rec[SYSLOG_REC_SIZE - 1u] == 0u),
|
||
"TC-SYSLOG-002 超长描述截到 28B,不越 64B 边界");
|
||
}
|
||
|
||
/* 003 墙钟未建立 */
|
||
reset_all();
|
||
s_clock_ok = 0;
|
||
s_format_rec(SYSLOG_RUN, "x", rec);
|
||
CHECK((memcmp(rec, "1970-01-01 08:00:00", SYSLOG_TS_LEN) == 0)
|
||
&& (memcmp(rec + SYSLOG_EPOCH_OFFS, "0000000000", SYSLOG_EPOCH_LEN) == 0),
|
||
"TC-SYSLOG-003 无墙钟 -> 占位时间戳 + epoch 全 0(不造假时标)");
|
||
|
||
/* 004 单条 vs 批量逐字节相同 */
|
||
reset_all();
|
||
s_now = 1700000000u;
|
||
s_format_rec(SYSLOG_RUN, "same-content", rec);
|
||
ready_only();
|
||
(void)memset(b, 0, sizeof(b));
|
||
s_format_rec(SYSLOG_RUN, "same-content", b);
|
||
CHECK(memcmp(rec, b, SYSLOG_REC_SIZE) == 0,
|
||
"TC-SYSLOG-004 同一输入两次格式化逐字节一致(UI 定长偏移解析的前提)");
|
||
reset_all();
|
||
s_now = 1700000000u;
|
||
ready_only();
|
||
s_format_rec(SYSLOG_RUN, "ab", rec);
|
||
{
|
||
uint8_t batch[SYSLOG_REC_SIZE * 2u];
|
||
(void)memset(batch, 0, sizeof(batch));
|
||
s_format_rec(SYSLOG_RUN, "ab", batch);
|
||
s_format_rec(SYSLOG_RUN, "ab", batch + SYSLOG_REC_SIZE);
|
||
CHECK((memcmp(rec, batch, SYSLOG_REC_SIZE) == 0)
|
||
&& (memcmp(rec, batch + SYSLOG_REC_SIZE, SYSLOG_REC_SIZE) == 0),
|
||
"TC-SYSLOG-004b 批量路径复用同一 s_format_rec(两条都等于单条)");
|
||
}
|
||
}
|
||
|
||
/* ===================== B 写侧 ===================== */
|
||
|
||
static void tc_write_single(void)
|
||
{
|
||
uint8_t rec[SYSLOG_REC_SIZE];
|
||
int ok;
|
||
|
||
printf("-- TC-SYSLOG-010..019 单条写路径 --\n");
|
||
|
||
/* 010 成功 + 打开模式 */
|
||
reset_all();
|
||
ready_only();
|
||
ok = (s_append(SYSLOG_RUN, "boot") == 0)
|
||
&& (g_ff_obs.last_open_mode == (BYTE)(FA_WRITE | FA_OPEN_APPEND))
|
||
&& (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 1)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_CLOSE] == 1)
|
||
&& (s_lock_releases == 1) && (s_lock_seq_err == 0)
|
||
&& (g_sys_err_count == 0);
|
||
CHECK(ok, "TC-SYSLOG-010 单条落盘成功:模式带写位 + open/write/close 各一次 + 放锁");
|
||
|
||
/* 011 陷阱一保真对照 */
|
||
reset_all();
|
||
{
|
||
static FIL probe;
|
||
UINT bw = 99u;
|
||
FRESULT fr1;
|
||
FRESULT fr2;
|
||
|
||
fr1 = f_open(&probe, P_RUN, FA_OPEN_APPEND);
|
||
fr2 = f_write(&probe, rec, SYSLOG_REC_SIZE, &bw);
|
||
(void)f_close(&probe);
|
||
CHECK((fr1 == FR_OK) && (fr2 == FR_DENIED) && (bw == 0u),
|
||
"TC-SYSLOG-011 只给 FA_OPEN_APPEND -> f_write 恒 FR_DENIED(2026-09-20 真根因)");
|
||
}
|
||
|
||
/* 012 卷忙 */
|
||
reset_all();
|
||
ready_only();
|
||
lock_busy_next(1);
|
||
ok = (s_append(SYSLOG_RUN, "x") == -2);
|
||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0)
|
||
&& (s_lock_releases == 0) && (s_lock_seq_err == 0) && (g_sys_err_count == 0),
|
||
"TC-SYSLOG-012 卷忙 = 顺延:不碰盘、不 release、不计故障(喂狗点绝不阻塞)");
|
||
|
||
/* 013 短写 -> 自愈 */
|
||
reset_all();
|
||
ready_only();
|
||
ff_fake_inject_write_short(P_RUN, SYSLOG_REC_SIZE - 1u, 1);
|
||
ok = (s_append(SYSLOG_RUN, "short") == 0);
|
||
CHECK(ok && (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||
&& (g_ff_obs.last_write_bw == SYSLOG_REC_SIZE),
|
||
"TC-SYSLOG-013 FR_OK+短写 63B 被判失败 -> unlink 重建 -> 恰好 64 字节");
|
||
|
||
/* 014 容量满 -> 删除重建 */
|
||
reset_all();
|
||
ready_only();
|
||
(void)ff_fake_mkfile_size(P_RUN, (uint32_t)SYSLOG_RUN_MAX * SYSLOG_REC_SIZE);
|
||
ok = (s_append(SYSLOG_RUN, "wrap") == 0);
|
||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||
&& (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE)
|
||
&& (syslog_count(SYSLOG_RUN) == 1u),
|
||
"TC-SYSLOG-014 满 1024 条 -> 先 unlink 再写,新文件只剩本次一条");
|
||
|
||
/* 015 满 + unlink 真失败 */
|
||
reset_all();
|
||
ready_only();
|
||
(void)ff_fake_mkfile_size(P_RUN, (uint32_t)SYSLOG_RUN_MAX * SYSLOG_REC_SIZE);
|
||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_DISK_ERR, 1);
|
||
ok = (s_append(SYSLOG_RUN, "x") == -1);
|
||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
|
||
&& (s_lock_releases == 1) && (s_lock_seq_err == 0),
|
||
"TC-SYSLOG-015 容量轮转时 unlink 失败 -> 返回 -1 且**已释放卷锁**(漏放 = FTP 永久拿不到卷)");
|
||
|
||
/* 016 满 + unlink 返 FR_NO_FILE 无害 */
|
||
reset_all();
|
||
ready_only();
|
||
(void)ff_fake_mkfile_size(P_RUN, (uint32_t)SYSLOG_RUN_MAX * SYSLOG_REC_SIZE);
|
||
/* 注入只替换返回码、不真的删条目(真机上 FR_NO_FILE 的含义恰恰是"条目已不在"),
|
||
* 所以这里钉的是**处置**:该码不进错误分支、流程继续把这条写下去。
|
||
* 文件大小断言留给 TC-SYSLOG-014(那里 unlink 是真删)。 */
|
||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_NO_FILE, 1);
|
||
ok = (s_append(SYSLOG_RUN, "gone") == 0);
|
||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1) && (g_sys_err_count == 0),
|
||
"TC-SYSLOG-016 unlink 返 FR_NO_FILE 被当无害:不进错误分支、不留痕、继续写");
|
||
|
||
/* 017 入参守卫 */
|
||
reset_all();
|
||
ready_only();
|
||
ok = (s_append((syslog_id_t)SYSLOG_COUNT, "x") == -1)
|
||
&& (s_append_rec(SYSLOG_RUN, NULL) == -1)
|
||
&& (s_append(SYSLOG_RUN, NULL) == -1)
|
||
&& (s_append_rec((syslog_id_t)9u, rec) == -1);
|
||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
|
||
"TC-SYSLOG-017 id 越界 / 空 rec / 空 desc -> -1 且不碰卷锁与盘");
|
||
|
||
/* 018 写成功 -> 读侧代数自增 */
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "a");
|
||
ok = (s_rd_gen[SYSLOG_RUN] == 1u) && (s_rd_gen[SYSLOG_SELFTEST] == 0u);
|
||
(void)s_append(SYSLOG_SELFTEST, "b");
|
||
CHECK(ok && (s_rd_gen[SYSLOG_SELFTEST] == 1u),
|
||
"TC-SYSLOG-018 每次落盘只自增本 id 的代数(另一文件的窗口不被误失效)");
|
||
|
||
/* 019 留痕去重 */
|
||
reset_all();
|
||
ready_only();
|
||
ff_fake_inject_write_short(P_RUN, 0u, 4);
|
||
ok = (s_append(SYSLOG_RUN, "e1") == -1);
|
||
(void)s_append(SYSLOG_RUN, "e2"); /* 第二轮仍失败 */
|
||
ok = ok && (g_sys_err_count == 1);
|
||
(void)s_append(SYSLOG_RUN, "ok"); /* 注入耗尽 -> 成功 -> 复位 */
|
||
ff_fake_inject_write_short(P_RUN, 0u, 2);
|
||
(void)s_append(SYSLOG_RUN, "e3"); /* 再失败 -> 再留痕 */
|
||
CHECK(ok && (g_sys_err_count == 2),
|
||
"TC-SYSLOG-019 持续故障只报第一次;恢复后再失败必须重新留痕");
|
||
}
|
||
|
||
static void tc_write_many(void)
|
||
{
|
||
uint8_t batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE];
|
||
uint32_t i;
|
||
int ok;
|
||
|
||
printf("-- TC-SYSLOG-020..024 批量写路径(N-24)--\n");
|
||
|
||
/* 020 8 条一次 I/O */
|
||
reset_all();
|
||
ready_only();
|
||
(void)memset(batch, 0x5A, sizeof(batch));
|
||
ok = (s_append_many(SYSLOG_RUN, batch, SYSLOG_BATCH_MAX) == 0);
|
||
CHECK(ok && (ff_fake_size(P_RUN) == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 1)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1)
|
||
&& (g_ff_obs.last_write_btw == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE)
|
||
&& (g_ff_obs.last_write_bw == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_CLOSE] == 1),
|
||
"TC-SYSLOG-020 一批 8 条 = 一次 open + 一次 512B write + 一次 close(旧实现 8 轮)");
|
||
|
||
/* 021 入参守卫 */
|
||
reset_all();
|
||
ready_only();
|
||
ok = (s_append_many(SYSLOG_RUN, batch, 0u) == -1)
|
||
&& (s_append_many(SYSLOG_RUN, batch, SYSLOG_BATCH_MAX + 1u) == -1)
|
||
&& (s_append_many(SYSLOG_RUN, NULL, 1u) == -1)
|
||
&& (s_append_many((syslog_id_t)SYSLOG_COUNT, batch, 1u) == -1);
|
||
CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
|
||
"TC-SYSLOG-021 批量入参守卫在**取卷锁之前**(越界 n 不占用卷)");
|
||
|
||
/* 022 容量不足 -> -3 且不 unlink */
|
||
reset_all();
|
||
ready_only();
|
||
(void)ff_fake_mkfile_size(P_RUN, ((uint32_t)SYSLOG_RUN_MAX - 3u) * SYSLOG_REC_SIZE);
|
||
ok = (s_append_many(SYSLOG_RUN, batch, SYSLOG_BATCH_MAX) == -3);
|
||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
|
||
&& (s_lock_releases == 1) && (s_lock_seq_err == 0)
|
||
&& (ff_fake_size(P_RUN) == ((uint32_t)SYSLOG_RUN_MAX - 3u) * SYSLOG_REC_SIZE),
|
||
"TC-SYSLOG-022 装不下一批 -> -3 交回慢路径;批量路径**绝不**自己 unlink 重建");
|
||
|
||
/* 023 批量卷忙 / 短写自愈 */
|
||
reset_all();
|
||
ready_only();
|
||
lock_busy_next(1);
|
||
ok = (s_append_many(SYSLOG_RUN, batch, 4u) == -2);
|
||
ff_fake_inject_write_short(P_RUN, 100u, 1); /* 本批 4 条 = 256 B,100 才算真短 */
|
||
ok = ok && (s_append_many(SYSLOG_RUN, batch, 4u) == 0);
|
||
CHECK(ok && (ff_fake_size(P_RUN) == 4u * SYSLOG_REC_SIZE)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||
&& (g_sys_err_count == 0),
|
||
"TC-SYSLOG-023 批量卷忙 -> -2;短写(256 只回 100)-> 自愈重建后整批落全");
|
||
|
||
/* 024 批量持续失败 + 去重 */
|
||
reset_all();
|
||
ready_only();
|
||
ff_fake_inject_write_short(P_RUN, 0u, 4);
|
||
ok = (s_append_many(SYSLOG_RUN, batch, 2u) == -1);
|
||
(void)s_append_many(SYSLOG_RUN, batch, 2u);
|
||
CHECK(ok && (g_sys_err_count == 1) && (s_rd_gen[SYSLOG_RUN] == 2u),
|
||
"TC-SYSLOG-024 批量持续失败留痕去重;代数照样自增(宁多失效一轮也不读脏窗)");
|
||
(void)i;
|
||
}
|
||
|
||
/* ===================== C 排空 ===================== */
|
||
|
||
static void tc_drain(void)
|
||
{
|
||
uint8_t rec[SYSLOG_REC_SIZE];
|
||
uint32_t i;
|
||
int ok;
|
||
|
||
printf("-- TC-SYSLOG-030..041 syslog_drain 排空 --\n");
|
||
|
||
/* 030 卷未就绪:条目滞留 preboot,drain 直接返回。
|
||
* 注意两条容易被读漏的事实:
|
||
* a) 队列还没建时(s_queue == NULL)即使内核已运行,post 也走 preboot 数组
|
||
* (syslog_post 的条件是 s_queue != NULL && 内核运行,两个条件都要满足);
|
||
* b) drain 里"惰性建队列"在 s_fs_ready 判定**之前**,所以卷没就绪也会先把
|
||
* 队列建出来 —— 于是就绪前的条目一律滞留 preboot、就绪后一次排空。 */
|
||
reset_all();
|
||
s_kernel_state = osKernelRunning;
|
||
syslog_post(SYSLOG_RUN, "early");
|
||
ok = (s_preboot_cnt == 1u) && (s_q_cnt == 0u) && (s_queue == NULL);
|
||
syslog_drain();
|
||
CHECK(ok && (s_q_new_calls == 1) && (s_preboot_cnt == 1u)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_soe_drain_calls == 0),
|
||
"TC-SYSLOG-030 卷未就绪 -> 队列先建成、条目滞留 preboot、不落盘也不调 SOE");
|
||
|
||
/* 031 惰性建队列 */
|
||
reset_all();
|
||
ready_only();
|
||
ok = (s_queue == NULL);
|
||
syslog_drain();
|
||
CHECK(ok && (s_queue != NULL) && (s_q_new_calls == 1) && (s_q_msgsz == sizeof(syslog_entry_t)),
|
||
"TC-SYSLOG-031 队列在**首次 drain**(defaultTask 上下文)才创建(内核前建对象 = 启动失败)");
|
||
|
||
/* 032 preboot 8 条一批落 */
|
||
reset_all();
|
||
ready_only();
|
||
for (i = 0u; i < SYSLOG_QUEUE_MAX; i++) {
|
||
syslog_post(SYSLOG_RUN, "pre");
|
||
}
|
||
CHECK(s_preboot_cnt == SYSLOG_QUEUE_MAX, "TC-SYSLOG-032a 内核未运行:8 条全进 preboot 数组");
|
||
syslog_drain();
|
||
ok = (s_preboot_cnt == 0u) && (syslog_count(SYSLOG_RUN) == SYSLOG_BATCH_MAX)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1)
|
||
&& (g_ff_obs.last_write_btw == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE);
|
||
CHECK(ok, "TC-SYSLOG-032b preboot 8 条同文件 -> 一次 512B 合并写(数组下标看而不删)");
|
||
|
||
/* 033 + 034 两文件混排:换文件那条不消耗 */
|
||
reset_all();
|
||
ready_only();
|
||
syslog_post(SYSLOG_RUN, "r1");
|
||
syslog_post(SYSLOG_RUN, "r2");
|
||
syslog_post(SYSLOG_SELFTEST, "s1");
|
||
syslog_post(SYSLOG_RUN, "r3");
|
||
syslog_drain();
|
||
ok = (syslog_count(SYSLOG_RUN) == 3u) && (syslog_count(SYSLOG_SELFTEST) == 1u);
|
||
if (ok) {
|
||
ok = raw_at(P_RUN, 0u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "r1", 2u) == 0)
|
||
&& raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "r2", 2u) == 0)
|
||
&& raw_at(P_RUN, 2u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "r3", 2u) == 0)
|
||
&& raw_at(P_SELFTEST, 0u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "s1", 2u) == 0);
|
||
}
|
||
/* selftest 插在 run 中间,于是 run.log 被切成"r1r2"与"r3"两批:
|
||
* 合计 3 次 open/write/close,而不是 2 次 —— 换文件处即分批处。 */
|
||
CHECK(ok && (s_preboot_cnt == 0u)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 3),
|
||
"TC-SYSLOG-033/034 换文件即分批(run 被切成两批 = 共 3 轮 I/O)+ 顺序不乱");
|
||
|
||
/* 035【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");
|
||
|
||
/* 035b 对照组:慢路径(-3 分支内)遇到 -2 时**会**把尾部回投 —— 同一卷忙
|
||
* 在两条路径上行为不一致,这就是"批量路径漏了一个分支"最直接的证据 */
|
||
reset_all();
|
||
ready_only();
|
||
s_kernel_state = osKernelRunning;
|
||
syslog_drain();
|
||
for (i = 0u; i < 3u; i++) {
|
||
syslog_post(SYSLOG_RUN, "slow-hold");
|
||
}
|
||
/* 让批量路径先返回 -3(容量只剩 1 条),从而进入逐条慢路径,再让卷忙 */
|
||
(void)ff_fake_mkfile_size(P_RUN, ((uint32_t)SYSLOG_RUN_MAX - 1u) * SYSLOG_REC_SIZE);
|
||
lock_busy_after(1, 1); /* 批量那把必须拿到,慢路径那把才忙 */
|
||
/* 批量路径先拿锁 -> 发现装不下一批 -> 返回 -3(此时一条都没写);逐条慢路径
|
||
* 的第二把锁遇到卷忙 -> break -> done=0 -> 整批 3 条全部回投队列。 */
|
||
g_ff_obs.calls[FF_FAKE_OP_OPEN] = 0;
|
||
syslog_drain();
|
||
CHECK((s_q_cnt == 3u) && (syslog_count(SYSLOG_RUN) == SYSLOG_RUN_MAX - 1u)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
|
||
"TC-SYSLOG-035b 慢路径遇卷忙 -> 3 条**整批回投**(同一码在两条路径上行为相反)");
|
||
|
||
/* 035c 一整批(8 条)在卷忙时全部回投,不是只保住一条 */
|
||
reset_all();
|
||
ready_only();
|
||
s_kernel_state = osKernelRunning;
|
||
syslog_drain();
|
||
for (i = 0u; i < SYSLOG_QUEUE_MAX; i++) {
|
||
syslog_post(SYSLOG_RUN, "eight");
|
||
}
|
||
lock_busy_next(1);
|
||
syslog_drain();
|
||
CHECK((s_q_cnt == 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 回投装不下 -> 5 次 hold-over drop + drop/wfail 各 1 条留痕");
|
||
|
||
/* 037 -3 退回慢路径:容量轮转只发生一次 */
|
||
reset_all();
|
||
ready_only();
|
||
s_kernel_state = osKernelRunning;
|
||
syslog_drain();
|
||
for (i = 0u; i < 4u; i++) {
|
||
syslog_post(SYSLOG_RUN, "slow");
|
||
}
|
||
(void)ff_fake_mkfile_size(P_RUN, ((uint32_t)SYSLOG_RUN_MAX - 2u) * SYSLOG_REC_SIZE);
|
||
syslog_drain();
|
||
ok = (syslog_count(SYSLOG_RUN) == 2u)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] >= 4);
|
||
CHECK(ok && (s_q_cnt == 0u),
|
||
"TC-SYSLOG-037 -3 -> 逐条慢路径:装满剩位 -> 满 -> unlink 重建 -> 继续,全程 unlink 恰一次");
|
||
|
||
/* 038 慢路径中途写失败 -> 该条丢 + 尾部回投 */
|
||
reset_all();
|
||
ready_only();
|
||
s_kernel_state = osKernelRunning;
|
||
syslog_drain();
|
||
for (i = 0u; i < 3u; i++) {
|
||
syslog_post(SYSLOG_RUN, "mid");
|
||
}
|
||
(void)ff_fake_mkfile_size(P_RUN, ((uint32_t)SYSLOG_RUN_MAX - 1u) * SYSLOG_REC_SIZE);
|
||
ff_fake_inject_write_short(P_RUN, 0u, 2); /* 第一条就失败 */
|
||
g_sys_err_count = 0;
|
||
syslog_drain();
|
||
ok = (s_q_cnt == 2u) && (ff_fake_size(P_RUN) == 0u) && (g_sys_err_count >= 2);
|
||
syslog_drain(); /* 之后成功 */
|
||
CHECK(ok && (s_q_cnt == 0u) && (syslog_count(SYSLOG_RUN) == 2u),
|
||
"TC-SYSLOG-038 慢路径写失败:只丢**当前那一条**,尾部 2 条下轮补齐");
|
||
|
||
/* 039【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 尾部必调 soe_store_drain(同写者、同卷就绪门)");
|
||
|
||
/* 041 post 分流与守卫 */
|
||
reset_all();
|
||
ready_only();
|
||
syslog_post((syslog_id_t)SYSLOG_COUNT, "bad-id");
|
||
syslog_post(SYSLOG_RUN, NULL);
|
||
CHECK((s_preboot_cnt == 0u) && (s_q_cnt == 0u),
|
||
"TC-SYSLOG-041a id 越界 / NULL 描述 -> 静默丢弃(不占待发位)");
|
||
reset_all();
|
||
ready_only();
|
||
for (i = 0u; i < SYSLOG_QUEUE_MAX + 2u; i++) {
|
||
syslog_post(SYSLOG_RUN, "pf");
|
||
}
|
||
/* 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 内核运行 -> 走队列;队列满丢弃 + 留痕");
|
||
}
|
||
|
||
/* ===================== D 读侧 ===================== */
|
||
|
||
static void tc_read(void)
|
||
{
|
||
char ts[SYSLOG_TS_LEN + 1u];
|
||
char desc[SYSLOG_DESC_MAX + 1u];
|
||
uint32_t epoch;
|
||
uint32_t i;
|
||
int ok;
|
||
|
||
printf("-- TC-SYSLOG-050..058 读侧(N-24 滑动窗口)--\n");
|
||
|
||
/* 050 count */
|
||
reset_all();
|
||
ready_only();
|
||
CHECK(syslog_count(SYSLOG_RUN) == 0u, "TC-SYSLOG-050a 文件缺失 -> count 0");
|
||
for (i = 0u; i < 3u; i++) {
|
||
(void)s_append(SYSLOG_RUN, "c");
|
||
}
|
||
ok = (syslog_count(SYSLOG_RUN) == 3u) && (syslog_count((syslog_id_t)7u) == 0u);
|
||
CHECK(ok, "TC-SYSLOG-050b count = filesize/64(O(1),不读盘内容);id 越界 -> 0");
|
||
|
||
/* 051 窗口命中零 I/O */
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "w0");
|
||
(void)s_append(SYSLOG_RUN, "w1");
|
||
{
|
||
int opens_base = g_ff_obs.calls[FF_FAKE_OP_OPEN];
|
||
int reads_base = g_ff_obs.calls[FF_FAKE_OP_READ];
|
||
int opens_after;
|
||
ok = (syslog_get_record(SYSLOG_RUN, 1u, ts, sizeof(ts), desc, sizeof(desc)) == 0);
|
||
opens_after = g_ff_obs.calls[FF_FAKE_OP_OPEN];
|
||
ok = ok && (strcmp(desc, "w1") == 0)
|
||
&& (opens_after == opens_base + 1) /* 填窗这一次 I/O */
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_READ] == reads_base + 1);
|
||
ok = ok && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
|
||
&& (strcmp(desc, "w0") == 0)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_after) /* 命中 -> 零 I/O */
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_READ] == reads_base + 1);
|
||
}
|
||
CHECK(ok, "TC-SYSLOG-051 窗口命中 -> 第二次读**零 FatFs 调用**(UI 翻页不再逐条开合)");
|
||
|
||
/* 052 写者代数失效 */
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "old");
|
||
ok = (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
|
||
&& (strcmp(desc, "old") == 0);
|
||
(void)s_append(SYSLOG_RUN, "new"); /* 代数 +1 -> 窗口必须重填 */
|
||
ok = ok && (syslog_get_record(SYSLOG_RUN, 1u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
|
||
&& (strcmp(desc, "new") == 0);
|
||
CHECK(ok, "TC-SYSLOG-052 写者一落地代数就变 -> 窗口重填,绝不返回陈旧内容");
|
||
|
||
/* 053 跨窗口 */
|
||
reset_all();
|
||
ready_only();
|
||
for (i = 0u; i < 9u; i++) {
|
||
(void)s_append(SYSLOG_RUN, "x");
|
||
}
|
||
(void)syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc));
|
||
{
|
||
int opens_before = g_ff_obs.calls[FF_FAKE_OP_OPEN];
|
||
ok = (syslog_get_record(SYSLOG_RUN, 8u, ts, sizeof(ts), desc, sizeof(desc)) == 0);
|
||
ok = ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_before + 1)
|
||
&& (g_ff_obs.last_lseek_ofs == (FSIZE_t)(SYSLOG_RD_WINDOW * SYSLOG_REC_SIZE))
|
||
&& (g_ff_obs.last_read_btr == SYSLOG_REC_SIZE);
|
||
}
|
||
CHECK(ok, "TC-SYSLOG-053 idx=8 跨窗 -> 一次 open + lseek(8*64) + read(1 条)(窗口按 8 对齐)");
|
||
|
||
/* 054 越界 */
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "only");
|
||
ok = (syslog_get_record(SYSLOG_RUN, 1u, ts, sizeof(ts), desc, sizeof(desc)) == -1)
|
||
&& (s_read_raw(SYSLOG_RUN, 5u, (uint8_t *)desc) == -1);
|
||
reset_all();
|
||
ready_only();
|
||
ok = ok && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == -1);
|
||
CHECK(ok, "TC-SYSLOG-054 越界 idx / 空文件 -> -1(等锁前后各校验一次 count)");
|
||
|
||
/* 055 读时卷忙 */
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "busy");
|
||
(void)syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)); /* 填窗 */
|
||
/* 换到另一 id 迫使真读盘,再让卷忙 */
|
||
(void)s_append(SYSLOG_SELFTEST, "s");
|
||
lock_busy_next(1);
|
||
ok = (syslog_get_record(SYSLOG_SELFTEST, 0u, ts, sizeof(ts), desc, sizeof(desc)) == -1)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] >= 1) && (s_lock_seq_err == 0);
|
||
CHECK(ok, "TC-SYSLOG-055 读时卷忙 -> -1(读窗口 try 语义:uiTask 也绝不阻塞等锁,下轮刷新重读)");
|
||
|
||
/* 056 短读作废窗口 */
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "half");
|
||
(void)s_append(SYSLOG_RUN, "half2");
|
||
s_rd_win_id = -1;
|
||
s_rd_win_cnt = 0u;
|
||
ff_fake_inject_read_short(P_RUN, 1u, 1);
|
||
ok = (s_read_raw(SYSLOG_RUN, 0u, (uint8_t *)desc) == -1)
|
||
&& (s_rd_win_cnt == 0u) && (s_rd_win_id == -1);
|
||
CHECK(ok, "TC-SYSLOG-056 短读 -> 窗口作废(半窗当有效命中 = UI 显示半条脏记录)");
|
||
|
||
/* 057 get_record 守卫与截断 */
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "01234567890123456789012345678901234567");
|
||
/* 拒绝路径必须一个字节都不写调用方缓冲:先填满 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 守卫:ts_len<=19 或 desc_len<=28 一律 -1;29 才读得到整 28 字符");
|
||
/* 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:无记录 -1;有记录解析 10 位 ASCII");
|
||
}
|
||
|
||
/* ===================== E mounted / 上电重启分类 ===================== */
|
||
|
||
static void tc_mounted(void)
|
||
{
|
||
uint8_t rec[SYSLOG_REC_SIZE];
|
||
int ok;
|
||
|
||
printf("-- TC-SYSLOG-060..064 挂载后处理(挂载幂等 + 上电/重启分类)--\n");
|
||
|
||
/* 060 /log 不在 -> 建目录 + 写上电记录;第二次调用完全静默 */
|
||
reset_all();
|
||
s_fs_ready = 0u;
|
||
s_kernel_state = osKernelInactive;
|
||
ok = (ff_fake_exists("/log") == 2);
|
||
syslog_post(SYSLOG_RUN, "pre-mount"); /* 就绪前排队的条目也应当被带出来 */
|
||
syslog_mcu_mounted();
|
||
ok = ok && (s_fs_ready == 1u) && (syslog_count(SYSLOG_RUN) == 1u);
|
||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == 0) && (g_sys_err_count == 0),
|
||
"TC-SYSLOG-060a /log 已存在 -> 只写上电/重启记录一条");
|
||
/* 060b 幂等:再来一次既不建目录也不写第二条 */
|
||
reset_all();
|
||
s_fs_ready = 0u;
|
||
syslog_mcu_mounted(); /* 第一次:写一条上电/重启记录 */
|
||
{
|
||
uint32_t cnt1 = syslog_count(SYSLOG_RUN);
|
||
int mkdirs1 = g_ff_obs.calls[FF_FAKE_OP_MKDIR];
|
||
syslog_mcu_mounted(); /* 第二次:应完全静默 */
|
||
CHECK((s_fs_ready == 1u) && (syslog_count(SYSLOG_RUN) == cnt1)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == mkdirs1),
|
||
"TC-SYSLOG-060b 幂等再调用:不再建目录、不再写上电记录(2s 复查不刷屏)");
|
||
}
|
||
(void)rec;
|
||
|
||
/* 061 建目录失败:不就绪 + 留痕去重 + 后续重试成功 */
|
||
reset_all();
|
||
s_fs_ready = 0u;
|
||
ff_fake_inject_rc("/log", FF_FAKE_OP_STAT, FR_DISK_ERR, 3); /* 逼它每次都走 mkdir 分支 */
|
||
ff_fake_inject_rc("/log", FF_FAKE_OP_MKDIR, FR_DENIED, 2);
|
||
g_sys_err_count = 0;
|
||
syslog_mcu_mounted(); /* 失败 1 -> 留痕 */
|
||
ok = (s_fs_ready == 0u) && (g_sys_err_count == 1);
|
||
syslog_mcu_mounted(); /* 失败 2 -> 不再留痕 */
|
||
ok = ok && (g_sys_err_count == 1) && (s_lock_releases == 2) && (s_lock_seq_err == 0);
|
||
syslog_mcu_mounted(); /* 注入耗尽 -> 成功 */
|
||
CHECK(ok && (s_fs_ready == 1u) && (s_mount_fail_reported == 0u),
|
||
"TC-SYSLOG-061 mkdir 失败:不就绪、留痕一次(防 2s 刷屏)、第三轮自愈并复位标志");
|
||
|
||
/* 062 卷忙:本轮顺延,不置就绪、不碰盘 */
|
||
reset_all();
|
||
s_fs_ready = 0u;
|
||
lock_busy_next(1);
|
||
syslog_mcu_mounted();
|
||
CHECK((s_fs_ready == 0u) && (g_ff_obs.calls[FF_FAKE_OP_STAT] == 0)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_lock_releases == 0),
|
||
"TC-SYSLOG-062 卷忙 -> 直接返回(try 失败不 release,下轮 2s 后再试)");
|
||
|
||
/* 063 上电 / 重启分类:阈值 SYSLOG_REBOOT_WINDOW_S 两侧 + 墙钟无效 */
|
||
reset_all();
|
||
s_fs_ready = 0u;
|
||
s_now = 1760234567u;
|
||
(void)ff_fake_mkfile_size(P_RUN, SYSLOG_REC_SIZE); /* 有一条末条记录可解析 */
|
||
/* 末条 epoch 造在 5 分钟前 -> reboot */
|
||
{
|
||
uint8_t one[SYSLOG_REC_SIZE];
|
||
(void)memset(one, 0, sizeof(one));
|
||
(void)memcpy(one, "2025-10-12 01:57:47", SYSLOG_TS_LEN);
|
||
one[SYSLOG_TS_LEN] = ' ';
|
||
one[SYSLOG_EPOCH_OFFS - 1u] = ' ';
|
||
(void)memcpy(one + SYSLOG_EPOCH_OFFS, "1760234267", SYSLOG_EPOCH_LEN); /* -300s */
|
||
(void)ff_fake_mkfile(P_RUN, one, SYSLOG_REC_SIZE);
|
||
}
|
||
syslog_mcu_mounted();
|
||
ok = raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "reboot", 6u) == 0)
|
||
&& (rec[SYSLOG_DESC_OFFS + 6u] == '\0');
|
||
CHECK(ok, "TC-SYSLOG-063a 末条 epoch 距 now 300s(<600) -> 描述落 \"reboot\"");
|
||
|
||
reset_all();
|
||
s_fs_ready = 0u;
|
||
{
|
||
uint8_t one[SYSLOG_REC_SIZE];
|
||
(void)memset(one, 0, sizeof(one));
|
||
(void)memcpy(one, "2025-10-12 08:00:00", SYSLOG_TS_LEN);
|
||
one[SYSLOG_TS_LEN] = ' ';
|
||
one[SYSLOG_EPOCH_OFFS - 1u] = ' ';
|
||
(void)memcpy(one + SYSLOG_EPOCH_OFFS, "1760200000", SYSLOG_EPOCH_LEN); /* -9.5h */
|
||
(void)ff_fake_mkfile(P_RUN, one, SYSLOG_REC_SIZE);
|
||
}
|
||
syslog_mcu_mounted();
|
||
ok = raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "power on", 8u) == 0);
|
||
CHECK(ok, "TC-SYSLOG-063b 间隔 >=600s -> \"power on\"(掉电超过 10 分钟算重新上电)");
|
||
|
||
reset_all();
|
||
s_fs_ready = 0u;
|
||
s_clock_ok = 0;
|
||
{
|
||
uint8_t one[SYSLOG_REC_SIZE];
|
||
(void)memset(one, 0, sizeof(one));
|
||
(void)memcpy(one + SYSLOG_EPOCH_OFFS, "1760234560", SYSLOG_EPOCH_LEN); /* 距 now 7s */
|
||
(void)ff_fake_mkfile(P_RUN, one, SYSLOG_REC_SIZE);
|
||
}
|
||
syslog_mcu_mounted();
|
||
ok = raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "power on", 8u) == 0);
|
||
CHECK(ok, "TC-SYSLOG-063c 墙钟无效(UINT32_MAX)-> 保守判 \"power on\",不拿假差值判重启");
|
||
|
||
/* 064 幂等靠 stat,不靠状态位:/log 已在就不再 mkdir */
|
||
reset_all();
|
||
s_fs_ready = 0u;
|
||
syslog_mcu_mounted();
|
||
ok = (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == 0) && (g_ff_obs.calls[FF_FAKE_OP_STAT] >= 1);
|
||
CHECK(ok, "TC-SYSLOG-064 /log 已存在时靠 f_stat 短路,f_mkdir 一次都不调");
|
||
}
|
||
|
||
/* ===================== F 取证通道 sys_log_evidence ===================== */
|
||
|
||
static void tc_evidence(void)
|
||
{
|
||
uint8_t raw[8u * SYSLOG_REC_SIZE];
|
||
int ok;
|
||
|
||
printf("-- TC-SYSLOG-070..074 sys_log_evidence(wdev.log 取证通道)--\n");
|
||
|
||
/* 070 变长行 + 第二条 run.log */
|
||
reset_all();
|
||
s_now = 1760234567u;
|
||
ok = (sys_log_evidence("task stall declared") == 0);
|
||
{
|
||
uint32_t n = ff_fake_fetch(P_WDEV, raw, 0u, sizeof(raw));
|
||
ok = ok && (n > 0u)
|
||
&& (memcmp(raw, "2025-10-12 02:02:47 task stall declared\r\n", 41u) == 0)
|
||
&& (syslog_count(SYSLOG_RUN) == 1u)
|
||
&& raw_at(P_RUN, 0u, raw)
|
||
&& (memcmp(raw + SYSLOG_DESC_OFFS, "wd stall declared", 17u) == 0);
|
||
}
|
||
CHECK(ok, "TC-SYSLOG-070 wdev.log 写变长行(带 CRLF),并补一条 run.log 独立证据");
|
||
|
||
/* 071 卷锁顺序:第二条写必须在释放之后(非递归锁,反了 = 自死锁) */
|
||
reset_all();
|
||
(void)sys_log_evidence("order probe");
|
||
CHECK((s_lock_waits == 2) && (s_lock_releases == 2) && (s_lock_seq_err == 0)
|
||
&& (s_lock_held == 0),
|
||
"TC-SYSLOG-071 取锁两次各放一次(在持锁状态下调 s_append 会永远拿不到锁)");
|
||
|
||
/* 072 >8192B 护栏 */
|
||
reset_all();
|
||
(void)ff_fake_mkfile_size(P_WDEV, 8193u);
|
||
ok = (sys_log_evidence("after rotate") == 0);
|
||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||
&& (ff_fake_size(P_WDEV) > 0u) && (ff_fake_size(P_WDEV) < 200u),
|
||
"TC-SYSLOG-072 超过 8KB -> 先 unlink 再写(长跑不被无界撑爆)");
|
||
|
||
/* 073 卷忙 */
|
||
reset_all();
|
||
lock_busy_next(1);
|
||
ok = (sys_log_evidence("busy probe") == -1);
|
||
CHECK(ok && (ff_fake_exists(P_WDEV) == 0) && (syslog_count(SYSLOG_RUN) == 0u)
|
||
&& (s_lock_releases == 0) && (s_lock_seq_err == 0),
|
||
"TC-SYSLOG-073 卷忙 -> -1 且**两条写都没发生**(喂狗点绝不阻塞等锁)");
|
||
|
||
/* 074 wdev 写失败:返回 -1,但第二条仍写("独立证据"语义) */
|
||
reset_all();
|
||
ff_fake_inject_rc(P_WDEV, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
|
||
ok = (sys_log_evidence("second write still") == -1);
|
||
CHECK(ok && (syslog_count(SYSLOG_RUN) == 1u) && (g_sys_err_count == 0),
|
||
"TC-SYSLOG-074 wdev 写失败不阻断第二条 run.log(多一条独立证据的原始意图)");
|
||
}
|
||
|
||
/* ===================== G 陷阱二:名字/参数类错误码严禁升级成卷操作 ===================== */
|
||
|
||
static void tc_name_rules(void)
|
||
{
|
||
static FIL probe;
|
||
FRESULT fr_body;
|
||
FRESULT fr_ext;
|
||
FRESULT fr_bad;
|
||
FRESULT fr_dot;
|
||
FRESULT fr_colon;
|
||
int ok;
|
||
uint32_t i;
|
||
|
||
printf("-- TC-SYSLOG-080..082 陷阱二分档与「绝不升级」 --\n");
|
||
|
||
/* 080 短名违规**分档**:每条都落到 FR_INVALID_NAME;':' 落到 FR_INVALID_DRIVE */
|
||
reset_all();
|
||
fr_body = f_open(&probe, "/log/selftestt.log", FA_WRITE | FA_CREATE_ALWAYS);
|
||
if (fr_body == FR_OK) { (void)f_close(&probe); }
|
||
fr_ext = f_open(&probe, "/log/run.logbak", FA_WRITE | FA_CREATE_ALWAYS);
|
||
if (fr_ext == FR_OK) { (void)f_close(&probe); }
|
||
fr_bad = f_open(&probe, "/log/ru*n.log", FA_WRITE | FA_CREATE_ALWAYS);
|
||
if (fr_bad == FR_OK) { (void)f_close(&probe); }
|
||
fr_dot = f_open(&probe, "/log/.fschk", FA_WRITE | FA_CREATE_ALWAYS);
|
||
if (fr_dot == FR_OK) { (void)f_close(&probe); }
|
||
fr_colon = f_open(&probe, "1:/log/run.log", FA_WRITE | FA_OPEN_APPEND);
|
||
if (fr_colon == FR_OK) { (void)f_close(&probe); }
|
||
CHECK((fr_body == FR_INVALID_NAME) && (fr_ext == FR_INVALID_NAME)
|
||
&& (fr_bad == FR_INVALID_NAME) && (fr_dot == FR_INVALID_NAME)
|
||
&& (fr_colon == FR_INVALID_DRIVE),
|
||
"TC-SYSLOG-080 分档:体>8 / 扩展>3 / 非法字符 / 点开头 -> FR_INVALID_NAME;':' -> FR_INVALID_DRIVE");
|
||
|
||
/* 081 生产实际用的每个名字都必须过严格 8.3(改名 = 静默写失败的第一道防线) */
|
||
reset_all();
|
||
ok = (f_open(&probe, P_RUN, FA_WRITE | FA_OPEN_APPEND) == FR_OK) && (f_close(&probe) == FR_OK);
|
||
ok = ok && (f_open(&probe, P_SELFTEST, FA_WRITE | FA_OPEN_APPEND) == FR_OK)
|
||
&& (f_close(&probe) == FR_OK);
|
||
ok = ok && (f_open(&probe, P_WDEV, FA_WRITE | FA_OPEN_APPEND) == FR_OK)
|
||
&& (f_close(&probe) == FR_OK);
|
||
ok = ok && (f_stat("/log", NULL) == FR_OK);
|
||
CHECK(ok, "TC-SYSLOG-081 生产路径 /log、run.log、selftest.log、wdev.log 全部 8.3 合规");
|
||
|
||
/* 082 名字类错误码不得升级成卷操作:反复失败也只动自己的文件 */
|
||
reset_all();
|
||
ready_only();
|
||
(void)ff_fake_mkfile(P_SELFTEST, NULL, SYSLOG_REC_SIZE); /* 邻居文件 */
|
||
(void)ff_fake_mkfile("/config.ini", NULL, 12u); /* 同卷别的东西 */
|
||
/* D 项整改后:名字/参数类码**一次都不许 unlink**(旧实现每轮删一次自己的
|
||
* 历史日志、且永远修不好)。留痕仍受 L2 去重,只 1 条。 */
|
||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_INVALID_NAME, 200);
|
||
for (i = 0u; i < 20u; i++) {
|
||
(void)s_append(SYSLOG_RUN, "bad-name");
|
||
}
|
||
ok = (ff_fake_exists("/log") == 2)
|
||
&& (ff_fake_size(P_SELFTEST) == SYSLOG_REC_SIZE)
|
||
&& (ff_fake_size("/config.ini") == 12u)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 20) /* 也没有"重开一次" */
|
||
&& (g_sys_err_count == 1);
|
||
CHECK(ok, "TC-SYSLOG-082 20 rounds of FR_INVALID_NAME: zero unlink, zero reopen, one log");
|
||
}
|
||
|
||
/* ===================== H 补齐分支(自愈 / 二次校验 / 回投 / 占位时标) ===================== */
|
||
|
||
static void tc_extra_branches(void)
|
||
{
|
||
char ts[SYSLOG_TS_LEN + 1u];
|
||
char desc[SYSLOG_DESC_MAX + 1u];
|
||
uint8_t raw[SYSLOG_REC_SIZE];
|
||
uint32_t epoch;
|
||
uint32_t i;
|
||
int ok;
|
||
|
||
printf("-- TC-SYSLOG-090..100 补齐分支 --\n");
|
||
|
||
/* 090 批量路径的 L1 自愈:open 失败一次 -> unlink + 重开 -> 整批照常落盘
|
||
* (这条钉的是 s_append_many 里那段"和单条同一份自愈表";漏了它,
|
||
* FTP 侧删过文件之后的第一批日志就会整批丢) */
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "seed"); /* 先让文件存在 */
|
||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
|
||
{
|
||
static uint8_t batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE];
|
||
(void)memset(batch, 0x33, sizeof(batch));
|
||
ok = (s_append_many(SYSLOG_RUN, batch, 2u) == 0);
|
||
}
|
||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3) /* 单条 1 + 批量首开 1 + 自愈重开 1 */
|
||
&& (ff_fake_size(P_RUN) == 2u * SYSLOG_REC_SIZE) && (g_sys_err_count == 0),
|
||
"TC-SYSLOG-090 批量 open 失败一次 -> unlink 重开 -> 整批 2 条落全、不留痕");
|
||
|
||
/* 091 批量 open 永久失败 -> -1(走的是"打开失败"那条 else,不是写失败) */
|
||
reset_all();
|
||
ready_only();
|
||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 99);
|
||
{
|
||
static uint8_t batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE];
|
||
ok = (s_append_many(SYSLOG_RUN, batch, 3u) == -1);
|
||
}
|
||
CHECK(ok && (ff_fake_exists(P_RUN) == 0) && (g_sys_err_count == 1)
|
||
&& (s_lock_releases == 1) && (s_lock_seq_err == 0),
|
||
"TC-SYSLOG-091 批量打开持续失败 -> -1 + 留痕一次 + 已释放卷锁");
|
||
|
||
/* 092 取锁后二次校验:等锁期间写者可能已把文件删掉(容量轮转/取证重建)
|
||
* -> 释放锁再返回 -1,绝不能拿锁前的旧 total 去 lseek */
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "one");
|
||
ff_fake_inject_rc_skip(P_RUN, FF_FAKE_OP_STAT, FR_DISK_ERR, 1, 1);
|
||
/* 前面那次 s_append 已经取放锁各一次,所以这里的基线是 1,不是 0 */
|
||
ok = (s_lock_releases == 1);
|
||
ok = ok && (s_read_raw(SYSLOG_RUN, 0u, raw) == -1)
|
||
&& (s_lock_releases == 2) && (s_lock_seq_err == 0);
|
||
CHECK(ok && (s_rd_win_cnt == 0u),
|
||
"TC-SYSLOG-092 锁后二次 count 失败 -> 放锁返回 -1(漏放 = FTP 永久等不到卷)");
|
||
|
||
/* 093 读时 f_open 失败 -> 放锁返回 -1(另一条提前退出路径) */
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "one");
|
||
s_rd_win_id = -1;
|
||
s_rd_win_cnt = 0u;
|
||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
|
||
ok = (s_read_raw(SYSLOG_RUN, 0u, raw) == -1)
|
||
&& (s_lock_releases == 2) && (s_lock_seq_err == 0);
|
||
/* 第二次读(注入耗尽)应成功,证明窗口没被留在半初始化状态 */
|
||
ok = ok && (s_read_raw(SYSLOG_RUN, 0u, raw) == 0) && (s_rd_win_cnt == 1u);
|
||
CHECK(ok, "TC-SYSLOG-093 读打开失败 -> 放锁 -1;下轮恢复后窗口正常重填");
|
||
|
||
/* 094 墙钟未建立时 evidence 用占位时标(不得写半截或未初始化缓冲) */
|
||
reset_all();
|
||
s_clock_ok = 0;
|
||
ok = (sys_log_evidence("no clock") == 0);
|
||
if (ok) {
|
||
(void)ff_fake_fetch(P_WDEV, raw, 0u, SYSLOG_TS_LEN);
|
||
ok = (memcmp(raw, "1970-01-01 08:00:00", SYSLOG_TS_LEN) == 0);
|
||
}
|
||
CHECK(ok, "TC-SYSLOG-094 无墙钟 -> wdev.log 落占位时标(取证行也必须可解析)");
|
||
|
||
/* 095 wdev 短写:FR_OK + 字节数不足 -> -1,但第二条 run.log 照写 */
|
||
reset_all();
|
||
ff_fake_inject_write_short(P_WDEV, 3u, 1);
|
||
ok = (sys_log_evidence("short line") == -1);
|
||
CHECK(ok && (ff_fake_size(P_WDEV) == 3u) && (syslog_count(SYSLOG_RUN) == 1u)
|
||
&& (g_sys_err_count == 0),
|
||
"TC-SYSLOG-095 wdev 短写判失败返回 -1,第二条独立证据仍落 run.log");
|
||
|
||
/* 096 队列创建失败:条目滞留 preboot、留痕一次、下轮建成后排空 */
|
||
reset_all();
|
||
ready_only();
|
||
s_kernel_state = osKernelRunning;
|
||
s_q_new_fail = 1;
|
||
syslog_post(SYSLOG_RUN, "nq");
|
||
syslog_drain();
|
||
ok = (s_queue == NULL) && (g_sys_err_count == 1) && (s_preboot_cnt == 1u)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_soe_drain_calls == 0);
|
||
s_q_new_fail = 0;
|
||
syslog_drain();
|
||
CHECK(ok && (s_queue != NULL) && (syslog_count(SYSLOG_RUN) == 1u)
|
||
&& (s_preboot_cnt == 0u) && (s_soe_drain_calls == 1),
|
||
"TC-SYSLOG-096 队列没建成 -> 一条都不排(不静默丢),堆恢复后排空并接上 SOE");
|
||
|
||
/* 097 换文件那条来自队列 -> 原样回投队列(不是 preboot 下标回退)
|
||
* 队列里 RUN/SELFTEST/RUN 交替:三条必须全落且各自文件内顺序不乱 */
|
||
reset_all();
|
||
ready_only();
|
||
s_kernel_state = osKernelRunning;
|
||
syslog_drain(); /* 建队列 */
|
||
syslog_post(SYSLOG_RUN, "qa");
|
||
syslog_post(SYSLOG_SELFTEST, "qb");
|
||
syslog_post(SYSLOG_RUN, "qc");
|
||
ok = (s_q_cnt == 3u);
|
||
syslog_drain();
|
||
if (ok) {
|
||
ok = (syslog_count(SYSLOG_RUN) == 2u) && (syslog_count(SYSLOG_SELFTEST) == 1u)
|
||
&& raw_at(P_RUN, 0u, raw) && (memcmp(raw + SYSLOG_DESC_OFFS, "qa", 2u) == 0)
|
||
&& raw_at(P_RUN, 1u, raw) && (memcmp(raw + SYSLOG_DESC_OFFS, "qc", 2u) == 0)
|
||
&& raw_at(P_SELFTEST, 0u, raw) && (memcmp(raw + SYSLOG_DESC_OFFS, "qb", 2u) == 0)
|
||
&& (s_q_cnt == 0u);
|
||
}
|
||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3),
|
||
"TC-SYSLOG-097 队列侧换文件回投:三条全落、各文件内顺序不乱(回投不是丢弃)");
|
||
/* 098【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 空指针守卫(守卫顺序在取锁之前,不放锁)");
|
||
|
||
/* 100 last_epoch:有记录但读失败 -> -1 且把 *p_epoch 清 0
|
||
* (调用方 syslog_mcu_mounted 直接拿这个值判上电/重启,留旧值就会误判) */
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "e");
|
||
s_rd_win_id = -1;
|
||
s_rd_win_cnt = 0u;
|
||
epoch = 0xA5A5A5A5u;
|
||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
|
||
ok = (syslog_last_epoch(SYSLOG_RUN, &epoch) == -1) && (epoch == 0u);
|
||
CHECK(ok, "TC-SYSLOG-100 last_epoch 读失败 -> -1 且 *p_epoch 清零(不留调用方旧值)");
|
||
(void)i;
|
||
}
|
||
|
||
/* ===================== I D 项整改:自愈触发集逐码 ===================== */
|
||
|
||
/* 打开失败时"该不该 unlink 重建"逐码判定。断言的是**语义归属**,不是假件规则:
|
||
* 介质/结构类(能靠删文件修好)→ 必须恰好 unlink 一次并再开一次;
|
||
* 名字/参数类(删了也修不好)→ 必须零 unlink、零重开。 */
|
||
static void tc_selfheal_codes(void)
|
||
{
|
||
static const FRESULT heal_codes[] = { FR_DISK_ERR, FR_INT_ERR,
|
||
FR_NO_FILESYSTEM, FR_DENIED };
|
||
/* FR_NO_FILE 也在"不进"这一列:本模块用 FA_OPEN_APPEND(含 FA_OPEN_ALWAYS),
|
||
* 文件不存在会被直接建出来;真返回 FR_NO_FILE 只可能是同名对象是目录
|
||
*(ff.c:3854),unlink 一个目录修不好它,反而违反"自愈只针对自己的文件"。 */
|
||
static const FRESULT noheal_codes[] = { FR_INVALID_NAME, FR_NO_PATH, FR_NO_FILE,
|
||
FR_INVALID_OBJECT, FR_NOT_READY,
|
||
FR_NOT_ENABLED, FR_TIMEOUT };
|
||
static uint8_t one[SYSLOG_REC_SIZE];
|
||
uint32_t c;
|
||
uint32_t round;
|
||
uint32_t gen_seed;
|
||
int ok;
|
||
|
||
(void)memset(one, 0x11, sizeof(one));
|
||
printf("-- TC-SYSLOG-101..104 self-heal trigger set, per code (fix D) --\n");
|
||
|
||
/* 101 介质/结构类:单条与批量路径都必须"unlink 恰好一次 + 重开一次",
|
||
* 并最终把这条/这批落下去,且不留痕(自愈成功不该报故障) */
|
||
for (c = 0u; c < (uint32_t)(sizeof(heal_codes) / sizeof(heal_codes[0])); c++) {
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "seed"); /* 先让文件存在 */
|
||
{
|
||
int opens_seed = g_ff_obs.calls[FF_FAKE_OP_OPEN];
|
||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, heal_codes[c], 1);
|
||
ok = (s_append(SYSLOG_RUN, "single") == 0)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_seed + 2)
|
||
/* 自愈的前提就是删文件:seed 那条随之消失,文件只剩本次这一条。
|
||
* (SOE 侧绝不这么做,理由见 TC-SOESTORE-022:事件序列不能销毁) */
|
||
&& (ff_fake_size(P_RUN) == 1u * SYSLOG_REC_SIZE)
|
||
&& (g_sys_err_count == 0) && (s_lock_seq_err == 0);
|
||
}
|
||
CHECK(ok, "TC-SYSLOG-101 single path heals one media/structure code");
|
||
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "seed");
|
||
{
|
||
int opens_seed = g_ff_obs.calls[FF_FAKE_OP_OPEN];
|
||
static uint8_t batch[2u * SYSLOG_REC_SIZE];
|
||
(void)memset(batch, 0x22, sizeof(batch));
|
||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, heal_codes[c], 1);
|
||
ok = (s_append_many(SYSLOG_RUN, batch, 2u) == 0)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_seed + 2)
|
||
&& (ff_fake_size(P_RUN) == 2u * SYSLOG_REC_SIZE) /* 旧的 seed 被删掉 */
|
||
&& (g_sys_err_count == 0) && (s_lock_seq_err == 0);
|
||
}
|
||
CHECK(ok, "TC-SYSLOG-101b batch path heals the same code once");
|
||
}
|
||
|
||
/* 102 名字/参数类:连续 20 轮都必须零 unlink、零重开、只留痕一次 */
|
||
for (c = 0u; c < (uint32_t)(sizeof(noheal_codes) / sizeof(noheal_codes[0])); c++) {
|
||
reset_all();
|
||
ready_only();
|
||
(void)s_append(SYSLOG_RUN, "seed");
|
||
(void)ff_fake_mkfile(P_SELFTEST, NULL, SYSLOG_REC_SIZE); /* 邻居 */
|
||
{
|
||
int opens_seed = g_ff_obs.calls[FF_FAKE_OP_OPEN];
|
||
gen_seed = s_rd_gen[SYSLOG_RUN];
|
||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, noheal_codes[c], 200);
|
||
for (round = 0u; round < 20u; round++) {
|
||
ok = 1;
|
||
if (s_append(SYSLOG_RUN, "no-heal") != -1) { ok = 0; }
|
||
/* 批量路径也必须走同一条判定:给它一整条 64B 记录的缓冲 */
|
||
if (s_append_many(SYSLOG_RUN, one, 1u) != -1) { ok = 0; }
|
||
}
|
||
/* 代数每轮 +2(单条与批量各自都失效一次读窗口):宁多失效也不读脏窗 */
|
||
CHECK(ok
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0)
|
||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_seed + 40)
|
||
&& (ff_fake_exists(P_SELFTEST) == 1)
|
||
&& (ff_fake_exists("/log") == 2)
|
||
&& (s_rd_gen[SYSLOG_RUN] == gen_seed + 40u)
|
||
&& (s_lock_seq_err == 0),
|
||
"TC-SYSLOG-102 name/param codes never unlink or reopen (20 rounds)");
|
||
}
|
||
}
|
||
|
||
/* 103 s_is_selfheal_code() 直测:把分类表本身钉死(写死的期望表,不读生产逻辑) */
|
||
ok = (s_is_selfheal_code(FR_DISK_ERR) == 1) && (s_is_selfheal_code(FR_INT_ERR) == 1)
|
||
&& (s_is_selfheal_code(FR_NO_FILESYSTEM) == 1) && (s_is_selfheal_code(FR_DENIED) == 1);
|
||
CHECK(ok, "TC-SYSLOG-103 trigger set = {DISK_ERR, INT_ERR, NO_FILESYSTEM, DENIED}");
|
||
ok = (s_is_selfheal_code(FR_INVALID_NAME) == 0) && (s_is_selfheal_code(FR_NO_PATH) == 0)
|
||
&& (s_is_selfheal_code(FR_NO_FILE) == 0) && (s_is_selfheal_code(FR_INVALID_OBJECT) == 0)
|
||
&& (s_is_selfheal_code(FR_NOT_READY) == 0) && (s_is_selfheal_code(FR_NOT_ENABLED) == 0)
|
||
&& (s_is_selfheal_code(FR_TIMEOUT) == 0) && (s_is_selfheal_code(FR_OK) == 0)
|
||
&& (s_is_selfheal_code(FR_EXIST) == 0) && (s_is_selfheal_code(FR_WRITE_PROTECTED) == 0);
|
||
CHECK(ok, "TC-SYSLOG-103b name/param/volume-gate codes all excluded");
|
||
|
||
/* 104 卷忙不计故障这条纪律(A 项要求①)在写路径同样成立:-2 不产生任何留痕,
|
||
* 也不自增故障位(s_fault_reported 是 s_append_rec 的函数级 static,
|
||
* 测不到,只能从"恢复后第一次真失败仍立刻留痕"反证它没被置位) */
|
||
reset_all();
|
||
ready_only();
|
||
/* s_append_rec 的 L2 去重位是函数级 static,外部清不了:先用一次成功写复位 */
|
||
ok = (s_append(SYSLOG_RUN, "reset-dedup") == 0);
|
||
g_sys_err_count = 0;
|
||
lock_busy_next(1);
|
||
ok = ok && (s_append(SYSLOG_RUN, "busy") == -2) && (g_sys_err_count == 0);
|
||
ff_fake_inject_write_short(P_RUN, 0u, 2);
|
||
ok = ok && (s_append(SYSLOG_RUN, "real-fault") == -1);
|
||
CHECK(ok && (g_sys_err_count == 1),
|
||
"TC-SYSLOG-104 busy is not a fault: first real failure still logs immediately");
|
||
}
|
||
|
||
/* ===================== J F 项:投递侧丢弃留痕节流(队列通道) ===================== */
|
||
|
||
static void tc_post_throttle(void)
|
||
{
|
||
uint32_t i;
|
||
int ok;
|
||
|
||
printf("-- TC-SYSLOG-105..107 post-side drop-log throttle (queue channel) --\n");
|
||
|
||
/* 105 队列灌满后持续投递 -> 只报第一次。这正是 A 项修复引出的新场景:
|
||
* 卷被 ftpTask 长期占用 -> drain 把整批回投 -> 队列常驻满 -> 每条业务
|
||
* 投递都会"队列满 + 打一行",串口按投递频率刷屏。 */
|
||
reset_all();
|
||
ready_only();
|
||
s_kernel_state = osKernelRunning;
|
||
syslog_drain(); /* 建队列(8 槽) */
|
||
g_sys_err_count = 0;
|
||
for (i = 0u; i < SYSLOG_QUEUE_MAX + 4u; i++) {
|
||
syslog_post(SYSLOG_RUN, "flood");
|
||
}
|
||
CHECK((s_q_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1)
|
||
&& (s_post_qfull_reported == 1u),
|
||
"TC-SYSLOG-105 queue full: 4 drops but only the first one logged");
|
||
|
||
/* 106 同一故障窗口内继续投递 -> 完全静默(丢弃语义不变,仍照丢) */
|
||
g_sys_err_count = 0;
|
||
for (i = 0u; i < 5u; i++) {
|
||
syslog_post(SYSLOG_RUN, "flood-more");
|
||
}
|
||
CHECK((g_sys_err_count == 0) && (s_q_cnt == SYSLOG_QUEUE_MAX),
|
||
"TC-SYSLOG-106 later drops in the same window log nothing");
|
||
|
||
/* 107 恢复(排空腾出槽位 + 下一条被收下)-> 抑制位复位;再灌满必须重新报一次。
|
||
* 复位点必须在 syslog_post 的成功分支,不是 drain 里——drain 只负责落盘。 */
|
||
ok = (s_post_qfull_reported == 1u);
|
||
syslog_drain(); /* 队列清空、条目落盘 */
|
||
ok = ok && (s_q_cnt == 0u) && (s_post_qfull_reported == 1u); /* drain 不该复位 */
|
||
syslog_post(SYSLOG_RUN, "recovered");
|
||
ok = ok && (s_q_cnt == 1u) && (s_post_qfull_reported == 0u);
|
||
CHECK(ok, "TC-SYSLOG-107 recovery via a successful post clears the bit (drain does not)");
|
||
g_sys_err_count = 0;
|
||
for (i = 0u; i < SYSLOG_QUEUE_MAX + 1u; i++) {
|
||
syslog_post(SYSLOG_RUN, "again");
|
||
}
|
||
CHECK((s_q_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1),
|
||
"TC-SYSLOG-107b a second fault logs again (throttle re-armed)");
|
||
/* 顺带确认两个通道的抑制位互不干扰:数组通道此前未触发过 */
|
||
CHECK(s_post_pboot_full_reported == 0u,
|
||
"TC-SYSLOG-107c the two channels keep independent throttle bits");
|
||
}
|
||
|
||
/* ===================== 入口 ===================== */
|
||
|
||
int main(void)
|
||
{
|
||
printf("=== Sys Log Mcu Tests ===\n");
|
||
|
||
tc_format();
|
||
tc_write_single();
|
||
tc_write_many();
|
||
tc_drain();
|
||
tc_read();
|
||
tc_mounted();
|
||
tc_evidence();
|
||
tc_name_rules();
|
||
tc_extra_branches();
|
||
tc_selfheal_codes();
|
||
tc_post_throttle();
|
||
|
||
printf("\n=== Sys Log Mcu Tests: %d/%d PASSED (failed=%d) ===\n",
|
||
s_passed, s_total, s_failed);
|
||
return (s_failed == 0) ? 0 : 1;
|
||
}
|