fix(log,net): 处置主机单测照出的 10 条现状,撤销 1 条误报

用户裁定「全部修」。第 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 回填留待主工作树合并后。
This commit is contained in:
2026-10-24 19:43:56 +08:00
parent 794906cdc8
commit 5a806d8a26
29 changed files with 1690 additions and 278 deletions
+348 -59
View File
@@ -60,7 +60,11 @@
* TC-SYSLOG-032 preboot 8 条同文件 -> 一批 512B 落盘 + preboot 计数清零
* TC-SYSLOG-033 两个文件混排 -> 按文件分批,顺序不乱、条目不丢
* TC-SYSLOG-034 换文件那条不消耗(preboot 下标回退 / 队列回投)
* TC-SYSLOG-035 卷忙 -> 整批回投队列,下一轮全部落盘(旧实现 -2 直接丢一条)
* 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 慢路径中途写失败 -> 该条丢弃留痕 + 尾部回投
@@ -76,7 +80,8 @@
* TC-SYSLOG-054 越界 idx -> -1;空文件 -> -1
* TC-SYSLOG-055 读时卷忙 -> -1(读窗口 try 语义:UI 每 100ms 喂心跳,绝不阻塞)
* TC-SYSLOG-056 短读 -> 作废窗口(s_rd_win_cnt=0),不把半窗当有效命中
* TC-SYSLOG-057 get_record:缓冲容量守卫 + 描述按 desc_len 截断
* TC-SYSLOG-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 成功 + 幂等(第二次不再建目录)
@@ -99,9 +104,25 @@
* TC-SYSLOG-095 wdev 短写 -> 返回 -1,第二条独立证据仍落 run.log
* TC-SYSLOG-096 队列创建失败 -> 一条都不排(不静默丢),堆恢复后排空并接上 SOE
* TC-SYSLOG-097 队列侧换文件那条原样回投 -> 三条全落、各文件内顺序不乱
* TC-SYSLOG-098 desc_len=29 时截断分支不可达(守卫取值导致,登记死分支)
* 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
@@ -114,29 +135,41 @@
* (sys_log_evidence 里 snprintf 返回 <=0 的分支——宿主 snprintf 不可能失败,
* 假件也不伪造返回值;为刷数字去伪造等于把断言写在假件规则上,宁可留空)。
*
* 本套照出的现状(**只钉现状、不改生产代码**;三条都进交 Lead 的报告):
* 本套照出的四条现状 —— 已在 task-4(2026-10-24)全部处置,相应断言已翻成正向:
*
* 【疑似缺陷 A:卷忙时整批日志被静默丢弃】sys_log_mcu.c:509~537 的批量路径里
* done 先被置成 n,只有 rc==-3 与 rc==-1 两个分支会改它;**rc==-2(卷忙)没有
* 任何分支**,于是紧随其后的 "for (i = done; i < n; i++) osMessageQueuePut(...)"
* 一次都不执行 —— 整批(最多 8 条)既没落盘也没回投,且不计故障、不打日志。
* 而同一段代码自己的注释写的是"rc == -2(卷忙):整批回投队列,静默顺延",
* 函数头 465~470 行与设计文档 §4.4 又都写明"**条目不丢**(旧实现 -2 卷忙会
* 直接丢掉那一条)"。⇒ 现状比旧实现更糟:旧实现丢 1 条,现在一次丢 8 条。
* 触发条件极常见:卷锁 v1.2 之后 ftpTask 传文件期间 defaultTask 每 2s 排空
* 一次,每次撞上卷忙就吞一批。对照:逐条慢路径(-3 分支)遇 -2 会正确回投
* 尾部 —— 同一个错误码在两条路径上行为相反(TC-SYSLOG-035 与 035b 成对钉住)。
* 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 是同一张表的两侧。
*
* 【疑似缺陷 B:rc==-1 的留痕文案与实际行为相反】sys_log_mcu.c:527~529 打
* DBG_ERROR("drop %lu entries id=%d"),但该分支把 done 置 0 -> 整批**回投队列**,
* 一条都没丢(下一轮重试)。排障时按"日志说丢了"去找会走错方向;而真正会丢的
* 是上面那个没有任何日志的 -2 分支。另外写故障持续时每轮 drain 都会重投重
* 试一遍(s_append_many 内部的 L2 去重只压得住它自己那条,压不住这条)。
* 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(队列通道)。
*
* 【死分支:syslog_get_record 的截断不可达】sys_log_mcu.c:616 的守卫是
* desc_len <= SYSLOG_DESC_MAX 直接 -1,而函数里 dl 恒 <= SYSLOG_DESC_MAX,
* 于是 627~628 的 "if (dl >= desc_len) dl = desc_len - 1" 永不成立。
* 无害(宁可拒绝也不越界),但"传小缓冲就能拿到截断内容"的直觉是错的(TC-SYSLOG-098)。
* 顺手记一条容易误读的事实(不是缺陷):自愈成功 = **删掉整个日志文件重建**,
* 所以一次打开失败会连带丢掉既有历史(TC-SYSLOG-101 明确断言文件只剩本次一条)。
* run.log 是滚动运行日志,这个代价可接受;soe.log 是事件序列,同样的动作被禁止
* (TC-SOESTORE-022 / 022b),两侧的差异是刻意的,注释也互指。
*
* 故意没测(写在这里,不留"看起来全覆盖"的错觉):
* - 真跨任务并发:读窗口用"对齐 32 位读写在 ARM 上原子"免锁,主机 x64 + 单线程
@@ -459,6 +492,11 @@ static void reset_all(void)
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;
@@ -828,16 +866,10 @@ static void tc_drain(void)
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 3),
"TC-SYSLOG-033/034 换文件即分批(run 被切成两批 = 共 3 轮 I/O)+ 顺序不乱");
/* 035【现状桩 = 疑似生产缺陷,见文件头"本套照出的现状"】
* sys_log_mcu.c:509~537 的批量路径里 done 先被置成 n,只有 -3 / -1 分支会
* 把它改回 0;**rc == -2(卷忙)没有任何分支**,于是紧随其后的
* "for (i = done; i < n; i++) osMessageQueuePut(...)" 一次都不执行 ——
* 整批(最多 8 条)**静默丢失**,而同一段代码上方的注释写的是
* "rc == -2(卷忙):整批回投队列,静默顺延",设计文档 §4.4 也写明
* "队列条目在落盘失败(-2 卷忙 / -1 写失败 / -3 容量不足)时回投队列——
* 条目不丢(旧实现 -2 卷忙会直接丢掉那一条)"。
* ⇒ 本条按**当前实现**断言(丢了就是丢了),并显式命名为 DEFECT,
* 生产侧一旦按注释补上 done=0,这条会红,提醒同步翻转成"不丢"的正向断言。 */
/* 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;
@@ -846,10 +878,22 @@ static void tc_drain(void)
syslog_post(SYSLOG_RUN, "hold");
}
lock_busy_next(1);
syslog_drain(); /* 批量 -2 */
CHECK((syslog_count(SYSLOG_RUN) == 0u) && (s_q_cnt == 0u)
&& (g_sys_err_count == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
"TC-SYSLOG-035[DEFECT 现状] 卷忙 -> 整批 3 条既没落盘也没回投(静默丢失)");
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 时**会**把尾部回投 —— 同一卷忙
* 在两条路径上行为不一致,这就是"批量路径漏了一个分支"最直接的证据 */
@@ -871,7 +915,7 @@ static void tc_drain(void)
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
"TC-SYSLOG-035b 慢路径遇卷忙 -> 3 条**整批回投**(同一码在两条路径上行为相反)");
/* 035c 卷忙丢掉的是"整批"而不是"一条":8 条一批全丢 */
/* 035c 一整批(8 条)在卷忙时全部回投,不是只保住一条 */
reset_all();
ready_only();
s_kernel_state = osKernelRunning;
@@ -881,8 +925,24 @@ static void tc_drain(void)
}
lock_busy_next(1);
syslog_drain();
CHECK((s_q_cnt == 0u) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0),
"TC-SYSLOG-035c 卷忙一次吞掉一整批(8 条),不是旧实现的 1 条");
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 与队列两个来源时,
* 回投的目标队列已经被"没被取走的那些"占着 -> 装不下的部分才真被丢掉。
@@ -941,10 +1001,10 @@ static void tc_drain(void)
CHECK(ok && (s_q_cnt == 0u) && (syslog_count(SYSLOG_RUN) == 2u),
"TC-SYSLOG-038 慢路径写失败:只丢**当前那一条**,尾部 2 条下轮补齐");
/* 039【现状桩 = 第二个不一致】rc == -1 分支把 done 置 0 并打
* DBG_ERROR("drop %lu entries id=%d"),但 done=0 意味着紧随其后的循环把
* **整批全部回投队列** —— 日志说"丢了多少条",实际一条没丢(下轮还会重试)。
* 措辞与行为相反,排障时会把人往错方向带;本条按现状钉住。 */
/* 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;
@@ -952,14 +1012,23 @@ static void tc_drain(void)
for (i = 0u; i < 3u; i++) {
syslog_post(SYSLOG_RUN, "wfail");
}
ff_fake_inject_write_short(P_RUN, 0u, 2);
ff_fake_inject_write_short(P_RUN, 0u, 4); /* 撑住两轮失败(每轮 2 次写) */
g_sys_err_count = 0;
syslog_drain();
CHECK((s_q_cnt == 3u) && (syslog_count(SYSLOG_RUN) == 0u) && (g_sys_err_count == 2),
"TC-SYSLOG-039[DEFECT 现状] 批量 -1:日志报 drop,条目却整批回投(3 条仍在队列)");
syslog_drain(); /* 注入耗尽 -> 这次真的落下去 */
CHECK((syslog_count(SYSLOG_RUN) == 3u) && (s_q_cnt == 0u),
"TC-SYSLOG-039b 回投的 3 条下一轮补齐(与 035 的卷忙整批丢失形成对照)");
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();
@@ -983,8 +1052,31 @@ static void tc_drain(void)
for (i = 0u; i < SYSLOG_QUEUE_MAX + 2u; i++) {
syslog_post(SYSLOG_RUN, "pf");
}
CHECK((s_preboot_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 2),
"TC-SYSLOG-041b preboot 满(8 槽)-> 丢弃 + 每条一次留痕(日志不得反压业务)");
/* 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;
@@ -1107,6 +1199,9 @@ static void tc_read(void)
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)
@@ -1119,6 +1214,27 @@ static void tc_read(void)
* 从来没有截断"。登记为死分支,见文件头"本套照出的现状"。 */
&& (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();
@@ -1343,6 +1459,8 @@ static void tc_name_rules(void)
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");
@@ -1350,9 +1468,10 @@ static void tc_name_rules(void)
ok = (ff_fake_exists("/log") == 2)
&& (ff_fake_size(P_SELFTEST) == SYSLOG_REC_SIZE)
&& (ff_fake_size("/config.ini") == 12u)
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] <= (int)(2u * 20u))
&& (g_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 反复拿 FR_INVALID_NAME 也只 unlink 自己的文件:目录/邻居不动、留痕一次");
CHECK(ok, "TC-SYSLOG-082 20 rounds of FR_INVALID_NAME: zero unlink, zero reopen, one log");
}
/* ===================== H 补齐分支(自愈 / 二次校验 / 回投 / 占位时标) ===================== */
@@ -1476,17 +1595,19 @@ static void tc_extra_branches(void)
}
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3),
"TC-SYSLOG-097 队列侧换文件回投:三条全落、各文件内顺序不乱(回投不是丢弃)");
/* 098 dl >= desc_len 的截断分支:守卫已挡掉 desc_len <= SYSLOG_DESC_MAX,
* 而 dl 由记录内容决定、恒 <= SYSLOG_DESC_MAX -> 该分支在**当前守卫取值下
* 永不可达**。这条断言把"不可达"这件事本身钉住:一旦有人把守卫放宽成
* desc_len == 0,本条就会因为读到截断结果而红,提醒补截断用例。 */
/* 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, "TC-SYSLOG-098 desc_len=29 时截断分支不可达(拿到整 28 字符,非 28-1)");
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();
@@ -1512,6 +1633,172 @@ static void tc_extra_branches(void)
(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)
@@ -1527,6 +1814,8 @@ int main(void)
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);