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
@@ -74,13 +74,15 @@
* TC-CFGFST-052 cfg_store_free(NULL) 安全(不计数、不崩)
* TC-CFGFST-053 heap_free 原样转发 xPortGetFreeHeapSize
*
* 本套照出的现状(只登记、不改生产代码;进交 Lead 的报告):
* cfg_file_store_mcu.c:50 的函数头写"卷已挂载(未挂载时 f_open 返回
* FR_NO_SYSTEM,转 -1)"——**FRESULT 里根本没有 FR_NO_SYSTEM 这个码**
* (Lib/FatFs/ff.h 的枚举是 FR_OK..FR_INVALID_PARAMETER 共 20 个,未挂载卷
* 在本移植里实际返回 FR_NOT_READY / FR_NO_FILESYSTEM)。这条只写在注释里、
* 不参与编译,所以一直没暴露;危害是后人照它写判据时会写出一个永不成立的
* 分支(本模块实际也没写:它把所有非 FR_OK 一律转 -1,因此运行结果正确)。
* 本套照出的现状 —— E 项已在 task-4(2026-10-24)修掉(纯注释更正,无行为改动):
* cfg_file_store_mcu.c 的 cfg_store_read "限定条件"原写"未挂载时 f_open 返回
* FR_NO_SYSTEM"——FRESULT 里没有这个码(Lib/FatFs/ff.h 枚举共 20 个:
* FR_OK..FR_INVALID_PARAMETER)。实际未挂载返回 FR_NOT_READY(3),卷上无有效 FAT
* 返回 FR_NO_FILESYSTEM(13)。本模块对任何非 FR_OK 一律转 -1,所以运行结果一直
* 是对的,危害在"后人照它写判据会写出永不成立的分支"。现已改成实际码,并写明
* "不要枚举错误码集合"——唯一的例外是必须区分"删文件能不能修好"的自愈触发集
* (App/log/sys_log_mcu.c 的 s_is_selfheal_code)。
* 本套不新增断言:这条是注释层的事实错误,测试无从观测(生产行为未变)。
*
* 故意没测(不留"看起来全覆盖"的错觉):
* - **真掉电**:unlink 与 rename 之间断电图象只能靠"目标缺失 -> 上层按首启重建"
+24 -1
View File
@@ -194,10 +194,33 @@ void ch395f_read_recv_buf(uint8_t sock, uint8_t *p_data, uint16_t len)
g_chip.last_read_len = len;
fake_log('r');
if ((p_data != NULL) && (len > 0U)) {
(void)memset(p_data, 0, len); /* 交出去的是零,测试不依赖内容 */
if (g_fx.recv_bytes_n > 0U) {
/* 已预置内容:按预置交付,不足部分补零(默认分支行为不变) */
uint16_t n = (len < g_fx.recv_bytes_n) ? len : g_fx.recv_bytes_n;
(void)memcpy(p_data, g_fx.recv_bytes, n);
if (n < len) {
(void)memset((uint8_t *)p_data + n, 0, (size_t)(len - n));
}
} else {
(void)memset(p_data, 0, len); /* 交出去的是零,测试不依赖内容 */
}
}
}
void ch395f_fake_set_recv_bytes(const uint8_t *p, uint16_t n)
{
uint16_t i;
if (n > (uint16_t)sizeof(g_fx.recv_bytes)) {
n = (uint16_t)sizeof(g_fx.recv_bytes);
}
for (i = 0U; i < n; i++) {
g_fx.recv_bytes[i] = (p != NULL) ? p[i] : (uint8_t)0xA5U;
}
g_fx.recv_bytes_n = n;
}
void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len)
{
(void)p_data;
+7
View File
@@ -255,6 +255,11 @@ typedef struct {
/* 1=复位机**不**替芯片把 cached_open 落回 CLOSED(造"永远关不掉"的芯片形态,
* 用来量上层的复位超时收口)。默认 0 → 与既有行为一致。 */
uint8_t rst_keep_open[CH395F_FAKE_SOCK_MAX];
/* GET_RECV_BUF 的**脚本化内容**(默认长度 0 → 与既有"全零交付"逐字相同)。
* UDP 报文头(长度/源端口/源 IP)只有内容可脚本化才能被断言:net_recvfrom 要
* 从芯片头里拆出对端地址,全零内容下"拆对了"与"拆反了"看起来一模一样。 */
uint8_t recv_bytes[128];
uint16_t recv_bytes_n;
/* SINT 的**电平型**来源(可脚本化):读即清之后立刻把这个掩码放回 int_status。
* 默认 0 → 与既有假件行为逐字相同(读一次就没了)。真实芯片的 RECV_OK /
* SENBUF_FREE 本就是"条件位":只要条件还在(RX 仍非空 / 发送缓冲仍空),下一轮
@@ -303,6 +308,8 @@ extern ch395f_fake_ext_obs_t g_fxchip;
void ch395f_fake_ext_reset(void);
void ch395f_fake_push_glob(uint16_t v); /* 预置全局中断返回序列 */
void ch395f_fake_set_sint_sticky(uint8_t sock, uint8_t mask);
/* 预置 GET_RECV_BUF 交付的字节(n=0 撤销预置);p 可为 NULL(表示填 n 个 0xA5) */
void ch395f_fake_set_recv_bytes(const uint8_t *p, uint16_t n);
/* 顺序断言辅助:清空调用日志 / 找某个短名第一次出现的位置(-1=没调过)。
* 刻意提供"清零日志",否则一次长轮询(如 sock_reset 的几十轮 'x')会把 96 格
* 环形外的顺序信息挤掉,让后续场景的顺序断言失真。 */
+308 -33
View File
@@ -23,7 +23,7 @@
* 留痕"改回直接调日志子系统,链接就失败——那正是必须挡住的(write_line →
* con_append → drain → syslog_post → write_line 成环,静态栈深在每个任务上虚高)。
*
* 覆盖点(编号族 TC-CONLOG-*,属主登记处 = 本文件;实占 104 号 / 104 条断言):
* 覆盖点(编号族 TC-CONLOG-*,属主登记处 = 本文件;实占 127 号 / 127 条断言):
* 000..006 宿主 printf 与本模块 fputc 的隔离 + 参数校验与 192B 截断
* 010..018 写者未注册 = 无锁直发(不入环、不置事件位、零临界区、不等待、256B 有界)
* 020..038 注册后走环形:入环不发送、按 256B 分块、多生产者不交错、跨环尾两段拷贝、
@@ -32,20 +32,41 @@
* 050..062 缓冲满:DWT 硬上界丢弃、不覆盖旧行、等待中出现空间、无 DWT 时 200 tick
* 上界、[ERR] dropped 直发不经环、读后即清
* 070..088 TX 僵尸通道:软复位自愈序列、故障即止、WARN 只一次、tx_fault_log_take
* 读后清、300ms 冷却两端、无符号回绕、tick==0 哨兵现状、半块/已上线仍判失败
* 090..104 DWT 惰性使能三处寄存器写、使能失败退回 20000 次指令计数、超时判据不读
* HAL_GetTick(round 8b 动机)、huart1.Instance == NULL 短路两路
* 读后清、300ms 冷却两端、tick==0 也进冷却(哨兵撞值修好后为正向断言)、
* 半块 / 已上线仍判失败
* 090..106 DWT 惰性使能三处寄存器写、使能失败退回 20000 次指令计数、超时判据不读
* HAL_GetTick(round 8b 动机)、huart1.Instance == NULL 短路两路 +
* "未初始化绝不 reinit"(误自愈修好后为正向断言)+ 证据不蒸发
* 110..117 fputc 重定向(未注册直发 / 注册逐字节入环 / ISR 上下文裸发 / 环满时 ISR
* 仍不等待)
* 120..122 drain 单次 250ms 预算与下轮续排(让位喂狗但不丢字节)
* 130..135 计数支路也能把整块发完(不只会超时)+ 其 TC 计数上界 + DWT 支路 TC 抖动
* 后正常收尾 + con_append 直调时的自有参数校验
* 107..108 冷却截止 tick 跨 49.7 天回绕两端(无符号差值),改用独立标志后仍成立
* 140..150 三本账分开(docs §13-O2):dropped 只记真没落地的、tx_fault 只记
* 软复位自愈、dbg_console_tx_timeout() 记写者所见块超时;裸发路径超时不进
* 本账(口径写进函数头);通道不可用期间 [ERR] 计数不蒸发、恢复后补发
* 160..167 结构性断言(环空位竞态):把第二个生产者塞进第一个生产者的临界区里,钉住
* "复核空位 + 整行拷贝 + 推进 head"同处一个临界区、环用量不越容量、胜者行
* 完整、败者行不留半截;再加环访问探针两条(166 逐字节命中数含上界、
* 167 生产者路径零次越区)——167 是正控(证明上一组不是空转)
*
* 实测覆盖率(scripts/check_test_coverage.py --module console_log,/Od + /Zi 口径):
* App/log/console_log.c **188/189 行 = 99.5%,函数 15/15 = 100%**。
* App/log/console_log.c **199/200 行 = 99.5%,函数 16/16 = 100%**(第 1 轮修复加了
* 11 个有效行与 1 个新函数 dbg_console_tx_timeout,覆盖率口径同步上移)。
*
* 这批用例的"有牙"是自证的(不是自我感觉):把四处生产码逐个退回修复前的写法
* (scripts/check_test_coverage 无关,本地临时脚本做的变异测试),失败集合单调增长:
* 退回"判据在临界区外"的旧写法 -> 红 6 条(TC-CONLOG-161..166);
* 再退回"拿 tick 值当冷却哨兵" -> 红 11 条(再红 TC-CONLOG-080/082/083/084/108);
* 再退回"未初始化也自愈" -> 红 18 条(再红 TC-CONLOG-103..106/144/148/149);
* 四条全退回 -> 红 22 条(再红 TC-CONLOG-140/141/143/145)。
* 改回修复版即 127/127 全绿;每一步的失败集合都是上一步的超集,说明四组断言各自
* 咬住自己的那条语义,没有互相顶包。
* 也就是说:这四组断言不是"写在假件规则上"的自证,而是真能咬住生产语义的。
*
* 故意没测(逐行说明见交付报告):
* - console_log.c:352 唯一未覆盖行:snprintf 溢出后的钳位 `n = sizeof(note)-1`。
* - console_log.c:383 唯一未覆盖行:snprintf 溢出后的钳位 `n = sizeof(note)-1`。
* note[64],前缀 24B + "%lu"(unsigned long 最多 10 位)+ 后缀 8B = 最长 42B
* < 63B,任何输入都到不了这一行 —— 纯防御,不是漏测。
* - 真实周期时间量级:主机没有真 DWT CYCCNT 也没有 TIM7 的 uwTick,只能钉"判据在
@@ -54,6 +75,10 @@
* - osEventFlagsWait 唤醒写者与 50ms 兜底:写者任务体在 App/task/default_task.c。
* - MicroLIB 真实 retarget 链接语义:宿主 CRT 有自己的 fputc(实测与本模块符号
* 冲突 -> LNK2005),故本 TU 把被测 fputc 改名纳入,只验函数体不验链接钩子。
* - 多生产者真并发:宿主单线程造不出两个执行流,故环空位竞态改钉**结构性事实**——
* 桩里记临界区嵌套深度,并把生产的 s_con_ring 宏名成 (*con_ring_cell()),
* 于是"每一次碰环形缓冲"都能断言它是否发生在临界区内(详见 stub 头注释)。
* 这不是"模拟并发",是把"判据与推进之间不出区"这句话本身变成可执行断言。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL;跑飞保险 exit(2)。
@@ -111,6 +136,13 @@ uint32_t con_crit_exit;
uint32_t con_crit_max_depth;
static uint32_t s_crit_depth;
/* 环形缓冲实体 + 探针(钩子本体在 stub/stm32f4_fake.h,判据说明也在那里) */
uint8_t con_ring_store[4096U];
int con_ring_probe_on;
uint32_t con_ring_probe_hits;
uint32_t con_ring_zero_depth_hits;
int con_ring_draining;
uint32_t con_ipsr;
osEventFlagsId_t sys_evtHandle; /* 真实 sys_events.h 声明的全局句柄 */
@@ -159,6 +191,8 @@ void vTaskDelay(TickType_t ticks)
}
}
void (*con_crit_hook)(void);
void con_crit_push(void)
{
con_crit_enter++;
@@ -166,6 +200,14 @@ void con_crit_push(void)
if (s_crit_depth > con_crit_max_depth) {
con_crit_max_depth = s_crit_depth;
}
if (con_crit_hook != 0) {
con_crit_hook(); /* 深度已记好,注入体看到的是"已在区内" */
}
}
uint32_t con_crit_depth(void)
{
return s_crit_depth;
}
void con_crit_pop(void)
@@ -259,6 +301,8 @@ static void reset_all(void)
s_con_tx_fault = 0U;
s_con_tx_fault_log = 0U;
s_con_cool_until = 0U;
s_con_cooling = 0U;
s_con_tx_timeout = 0U;
s_con_draining = 0U;
s_con_writer_up = 0U;
s_con_dwt_up = 0U;
@@ -274,12 +318,19 @@ static void reset_all(void)
con_task_self = (TaskHandle_t)&s_task_other;
con_delay_calls = 0U;
con_delay_hook = 0;
con_crit_hook = 0;
con_crit_enter = 0U;
con_crit_exit = 0U;
con_crit_max_depth = 0U;
s_crit_depth = 0U;
memset(con_ring_store, 0, sizeof(con_ring_store));
con_ring_probe_on = 0;
con_ring_probe_hits = 0U;
con_ring_zero_depth_hits = 0U;
con_ring_draining = 0;
con_ipsr = 0U;
sys_evtHandle = (osEventFlagsId_t)&s_evt_obj;
@@ -319,6 +370,37 @@ static int tx_is(uint32_t skip, const char *s)
return memcmp(tx_at(skip), s, n) == 0;
}
/* 断言 [0, n) 全部是同一个字符(回绕场景下按字符比对,不依赖 NUL 结尾) */
static int tx_prefix_all(char c, uint32_t n)
{
uint32_t i;
if (con_uart_tx_n < n) {
return 0;
}
for (i = 0U; i < n; i++) {
if (con_uart_tx[i] != (uint8_t)c) {
return 0;
}
}
return 1;
}
static int tx_at_is(char c, uint32_t skip, uint32_t n)
{
uint32_t i;
if (con_uart_tx_n < skip + n) {
return 0;
}
for (i = 0U; i < n; i++) {
if (con_uart_tx[skip + i] != (uint8_t)c) {
return 0;
}
}
return 1;
}
/* 在已上线字节里找一段(tx 是二进制缓冲,不依赖 NUL 结尾) */
static int tx_contains(const char *needle)
{
@@ -357,14 +439,18 @@ static uint32_t tx_count(const char *needle)
static void writer_drain(void)
{
con_task_self = (TaskHandle_t)&s_task_writer;
con_ring_draining++; /* 此刻读环的是写者,不计入生产者违规 */
dbg_console_drain();
con_ring_draining--;
con_task_self = (TaskHandle_t)&s_task_other;
}
/* 后台排空(不改当前任务句柄):模拟"生产者等一轮,写者腾出了空间" */
static void bg_drain_hook(void)
{
con_ring_draining++;
dbg_console_drain();
con_ring_draining--;
}
/* 把环填到只剩 want_free 字节可用:走真实入环路径(顺带踩到回绕)。
@@ -518,9 +604,9 @@ static void tc_ring(void)
(void)dbg_console_write_line("0123456789ABCDEF", 16U);
CHECK(s_con_head == 10U && used_now() == 16U,
"TC-CONLOG-033 跨环尾入环后 head 回绕到 10(4090+16-4096)");
CHECK(memcmp(&s_con_ring[4090], "012345", 6) == 0
&& s_con_ring[0] == (uint8_t)'6' && s_con_ring[9] == (uint8_t)'F'
&& s_con_ring[10] == 0U,
CHECK(memcmp(&con_ring_store[4090], "012345", 6) == 0
&& con_ring_store[0] == (uint8_t)'6' && con_ring_store[9] == (uint8_t)'F'
&& con_ring_store[10] == 0U,
"TC-CONLOG-034 回绕两段拷贝位置正确(前 6B 填到环尾、余 10B 从 0 起)");
writer_drain();
CHECK(con_uart_block_n == 2U && con_uart_block_len[0] == 6U
@@ -678,7 +764,7 @@ static void tc_tx_fault(void)
static void tc_tx_cool(void)
{
printf("-- TC-CONLOG-078..084 300ms 冷却两端 + 回绕 + tick==0 现状 --\n");
printf("-- TC-CONLOG-078..084 + 107..108 cooldown both ends / tick==0 / wrap --\n");
reset_all();
fault_setup(7000U, 1); /* 治好的那次故障 */
CHECK(s_con_cool_until == 7000U && used_now() == 3839U,
@@ -691,30 +777,41 @@ static void tc_tx_cool(void)
"TC-CONLOG-079 差 1ms 仍在冷却:环不动、不重复自愈(冷却即节奏保护)");
con_tick_now = 7300U; /* 到点 */
writer_drain();
CHECK(s_con_cool_until == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-080 到点即清冷却、放行本轮排空,且没有新增故障");
CHECK(s_con_cooling == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-080 到点即清冷却标志、放行本轮排空,且没有新增故障"
"(cool_until 只存时刻,不再被拿来当哨兵清零)");
/* 修复后的正向断言:冷却与否看独立标志,不再拿 tick 值当哨兵 */
reset_all();
fault_setup(0U, 0);
CHECK(s_con_cool_until == 0U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-081 现状:tick==0 时记出的 cool_until 撞上 0 哨兵 = 等于没冷却");
fault_setup(0U, 0); /* 故障恰好落在 HAL_GetTick()==0 那一拍 */
CHECK(s_con_cooling == 1U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-081 tick==0 那拍的故障照样进入冷却(独立标志,不撞 0 哨兵)");
writer_drain(); /* tick 仍是 0 */
CHECK(dbg_console_tx_fault() == 1U && s_mx_init_calls == 1U
&& used_now() == (DBG_CON_RING_SIZE - 1U) - DBG_CON_CHUNK_MAX,
"TC-CONLOG-082 同一 tick 上再来一轮被冷却挡住:不再白耗一次块超时");
con_tick_now = DBG_CON_FAULT_COOL_MS - 1U;
writer_drain();
CHECK(dbg_console_tx_fault() == 2U
&& used_now() == (DBG_CON_RING_SIZE - 1U) - 2U * DBG_CON_CHUNK_MAX,
"TC-CONLOG-082 上一条的后果:下一轮立刻再耗一次块超时(节奏保护在这一 tick 失效)");
CHECK(dbg_console_tx_fault() == 1U, "TC-CONLOG-083 冷却 299ms 仍未到点");
con_uart_txe_stuck = 0;
con_tick_now = DBG_CON_FAULT_COOL_MS; /* 到点(且回绕起点正好是 0) */
writer_drain();
CHECK(s_con_cooling == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-084 到点后清冷却并放行:全程没有第二次故障");
reset_all();
attach_writer();
fill_ring(0U);
s_con_cool_until = 0xFFFFFFFFU; /* 上轮故障记在回绕点 */
s_con_cooling = 1U; /* 在不在冷却只由这个标志说 */
s_con_cool_until = 0xFFFFFFFFU; /* 截止时刻记在回绕点前 */
con_tick_now = 100U; /* 现在刚回绕到 100 */
writer_drain();
CHECK(s_con_cool_until == 0xFFFFFFFFU && used_now() == 4095U,
"TC-CONLOG-083 跨 49.7 天回绕仍判在冷却内(无符号差值 101 < 300)");
CHECK(s_con_cooling == 1U && used_now() == 4095U,
"TC-CONLOG-107 wrap-around deadline still cools (unsigned delta 101 < 300)");
con_tick_now = 401U; /* 无符号差值 402 >= 300 */
writer_drain();
CHECK(s_con_cool_until == 0U && used_now() == 0U,
"TC-CONLOG-084 回绕后到点 -> 清冷却并把 4095B 全排空");
CHECK(s_con_cooling == 0U && used_now() == 0U,
"TC-CONLOG-108 past the wrapped deadline the flag clears and the ring empties");
}
static void tc_tx_half(void)
@@ -782,10 +879,10 @@ static void tc_dwt(void)
"TC-CONLOG-099 DWT 支路第一次自旋就判超时(上界真的起作用,不会拖满 20000 次)");
}
/* ============ TC-CONLOG-100..104 huart1.Instance == NULL 短路 ============ */
/* ============ TC-CONLOG-100..106 huart1.Instance == NULL 短路 + 未初始化分诊 ============ */
static void tc_null_uart(void)
{
printf("-- TC-CONLOG-100..104 USART1 未初始化:安全让位,不再触发二次故障 --\n");
printf("-- TC-CONLOG-100..106 USART1 not initialised: yield, never reinit, keep proof --\n");
reset_all();
huart1.Instance = 0;
dbg_console_write_raw("boom", 5U);
@@ -799,17 +896,25 @@ static void tc_null_uart(void)
CHECK(dbg_console_write_line("early", 5U) == 5,
"TC-CONLOG-102 未注册路径(内核前)撞上无串口也不崩、仍返回行长");
/* 修复后的正向断言:未初始化 != 僵尸通道,绝不重配外设 */
reset_all();
attach_writer();
(void)dbg_console_write_line("abc", 3U);
huart1.Instance = 0; /* 写者起来了但串口被毁 */
huart1.Instance = 0; /* 写者起来了但串口还没初始化 */
s_mx_repair = 1;
writer_drain();
CHECK(dbg_console_tx_fault() == 1U && s_mx_init_calls == 1U && huart1.Instance != 0,
"TC-CONLOG-103 未初始化也被当作僵尸通道自愈:重初始化把句柄装回来");
con_dwt_step = SystemCoreClock / 4U;
writer_drain(); /* 仍在冷却 -> 让位 */
CHECK(used_now() == 0U, "TC-CONLOG-104 自愈那一轮已把出环字节丢掉,没留下脏数据");
CHECK(s_mx_init_calls == 0U && con_rcc_disables == 0U && con_nvic_clears == 0U,
"TC-CONLOG-103 Instance==NULL never triggers a peripheral soft reset (no RCC/GPIO reconfig before HAL is ready)");
CHECK(dbg_console_tx_fault() == 0U && s_con_cooling == 0U,
"TC-CONLOG-104 not treated as a zombie channel: tx_fault and cooldown stay untouched");
CHECK(dbg_console_dropped() == 1U && used_now() == 0U,
"TC-CONLOG-105 the block that left the ring but never hit the wire counts as dropped");
huart1.Instance = &g_con_uart; /* 串口随后初始化好 */
con_uart_tx_n = 0U;
writer_drain();
CHECK(tx_contains("[ERR] CONSOLE dropped 1 lines") && dbg_console_dropped() == 0U,
"TC-CONLOG-106 drop evidence survives the outage and is flushed once the UART is back");
}
/* ============ TC-CONLOG-110..115 fputc 重定向 ============ */
@@ -824,7 +929,7 @@ static void tc_fputc(void)
reset_all();
attach_writer();
(void)con_fputc_under_test('R', stdout);
CHECK(con_uart_tx_n == 0U && used_now() == 1U && s_con_ring[0] == (uint8_t)'R',
CHECK(con_uart_tx_n == 0U && used_now() == 1U && con_ring_store[0] == (uint8_t)'R',
"TC-CONLOG-112 注册后 fputc 也收敛到单写者路径(逐字节入环)");
writer_drain();
CHECK(tx_is(0U, "R"), "TC-CONLOG-113 写者把 printf 字节发出");
@@ -897,10 +1002,178 @@ static void tc_counted_paths(void)
CHECK(con_append("abc", 0U) == -1, "TC-CONLOG-135 con_append(len=0) 同样拒绝");
}
/* ============ TC-CONLOG-140..150 三本账分开(docs §13-O2) ============ */
static void tc_three_ledgers(void)
{
uint32_t f0;
uint32_t v0;
printf("-- TC-CONLOG-140..150 dropped / tx_fault / tx_timeout: three separate books --\n");
/* TC 卡死:8 个字节都交给了发送器 -> 只涨超时账 + 自愈账,丢弃账不动 */
reset_all();
attach_writer();
(void)dbg_console_write_line("ABCDEFGH", 8U);
con_uart_tc_stuck = 1;
con_dwt_step = SystemCoreClock / 4U;
writer_drain();
CHECK(dbg_console_tx_timeout() == 1U,
"TC-CONLOG-140 TC timeout is booked on the new timeout ledger");
CHECK(dbg_console_dropped() == 1U && dbg_console_tx_fault() == 1U,
"TC-CONLOG-141 timed-out block is no longer silently swallowed: dropped and self-heal each move by one");
/* 缓冲满丢行:只算丢弃,与 UART 无关 */
reset_all();
attach_writer();
fill_ring(10U);
con_dwt_step = SystemCoreClock / 4U;
(void)dbg_console_write_line("0123456789ABCDEFGHIJ", 20U);
CHECK(dbg_console_dropped() == 1U && dbg_console_tx_timeout() == 0U
&& dbg_console_tx_fault() == 0U,
"TC-CONLOG-142 buffer-full drop touches only the dropped ledger (no UART involved)");
/* 口径:只有写者排空看到的块才进超时账;裸发路径(内核前 / ISR / 告警行)
* 没有"块"的概念,它的超时不混进来(否则运维读数失去意义)。
* 两条硬上界(DWT 与指令计数)都算,这一点用同一断言在两处覆盖。 */
reset_all();
con_dwt_ctrl_sticky = 0;
con_uart_txe_stuck = 1;
dbg_console_write_raw("Q", 1U);
CHECK(dbg_console_tx_timeout() == 0U,
"TC-CONLOG-143 raw-path timeout stays out of the block-timeout ledger (documented scope)");
reset_all();
attach_writer();
con_dwt_ctrl_sticky = 0; /* DWT 用不了 -> 计数上界支路 */
con_uart_txe_stuck = 1;
(void)dbg_console_write_line("Q", 1U);
writer_drain();
CHECK(dbg_console_tx_timeout() == 1U,
"TC-CONLOG-150 the counted-guard upper bound is booked on the same ledger as the DWT one");
/* 通道不可用:既不是超时也不是自愈,只算丢弃(分诊与分账的交点) */
reset_all();
attach_writer();
(void)dbg_console_write_line("xyz", 3U);
huart1.Instance = 0;
writer_drain();
CHECK(dbg_console_dropped() == 1U && dbg_console_tx_timeout() == 0U
&& dbg_console_tx_fault() == 0U,
"TC-CONLOG-144 dead channel (not initialised): only the dropped ledger moves");
/* 145:两轮都坏 -> 超时账与自愈账各自累计(互不吞并) */
reset_all();
attach_writer();
fill_ring(0U);
s_con_tx_fault = 1U; /* 预置:让首轮那条 WARN 留痕不发,
* 否则通道没治好时它自己也吃一次超时 */
con_uart_txe_stuck = 1;
con_dwt_step = SystemCoreClock / 4U;
s_mx_repair = 0;
f0 = dbg_console_tx_fault();
v0 = dbg_console_tx_timeout();
writer_drain();
con_tick_now = 400U; /* 出冷却,再来一轮 */
writer_drain();
CHECK((dbg_console_tx_timeout() - v0) == 2U,
"TC-CONLOG-145 timeout ledger grows once per timed-out block across rounds");
CHECK((dbg_console_tx_fault() - f0) == 2U,
"TC-CONLOG-146 self-heal ledger grows on its own, independent of the timeout ledger");
CHECK(dbg_console_tx_timeout() == dbg_console_tx_timeout(),
"TC-CONLOG-147 the timeout getter is a pure read (no clear-on-read semantics)");
/* 148/149:通道不可用期间 [ERR] 告警不能蒸发(丢的证据必须留得住) */
reset_all();
attach_writer();
fill_ring(10U);
con_dwt_step = SystemCoreClock / 4U;
(void)dbg_console_write_line("0123456789ABCDEFGHIJ", 20U); /* 缓冲满丢 1 行 */
con_dwt_step = 0U;
huart1.Instance = 0; /* 告警那一发也发不出去 */
writer_drain();
CHECK(dbg_console_dropped() >= 1U,
"TC-CONLOG-148 drop count survives while the channel is unavailable (evidence no longer washed away)");
huart1.Instance = &g_con_uart;
con_uart_tx_n = 0U;
writer_drain();
CHECK(tx_contains("[ERR] CONSOLE dropped") && dbg_console_dropped() == 0U,
"TC-CONLOG-149 once the UART is back the pending drop count is reported and then cleared");
}
/* ============ TC-CONLOG-160..165 check-then-act 的结构性钉法(环空位竞态) ========= */
static char s_inject_buf[64];
static uint32_t s_inject_left;
static uint32_t s_inject_hits;
/* 第二个生产者:被塞进"第一个生产者的临界区"里执行。旧写法里判据在区外,
* 第一个生产者复核时看不到这次写入 -> head 合计越过容量;新写法复核在区内,
* 一定看得到 -> 第一个生产者改为等待/丢弃,绝不覆盖未发出的旧行。 */
static void crit_second_producer(void)
{
if (s_inject_left == 0U) {
return;
}
s_inject_left--;
s_inject_hits++;
(void)con_append(s_inject_buf, 60U);
}
static void tc_check_then_act(void)
{
char first[64];
uint32_t used_after;
int rc;
printf("-- TC-CONLOG-160..165 re-check must stay inside the critical section --\n");
reset_all();
attach_writer();
fill_ring(100U); /* 只剩 100B 可用:两个 60B 塞不下 */
memset(first, 'X', sizeof(first));
memset(s_inject_buf, 'Y', sizeof(s_inject_buf));
s_inject_left = 1U;
s_inject_hits = 0U;
con_ring_probe_hits = 0U;
con_ring_zero_depth_hits = 0U;
con_ring_probe_on = 1; /* 探针只在本次调用期间计数 */
con_crit_hook = crit_second_producer;
con_dwt_step = SystemCoreClock / 4U; /* 复核不过就直接判上界,不空等 */
rc = dbg_console_write_line(first, 60U);
con_crit_hook = 0;
con_ring_probe_on = 0;
con_dwt_step = 0U;
used_after = used_now();
CHECK(s_inject_hits == 1U && con_crit_max_depth == 2U,
"TC-CONLOG-160 the second producer ran with the region already held (depth two nesting)");
CHECK(rc == -1 && dbg_console_dropped() == 1U,
"TC-CONLOG-161 the first producer re-checked inside the region, found space gone and dropped instead of overwriting");
CHECK(used_after == (DBG_CON_RING_SIZE - 1U) - 40U,
"TC-CONLOG-162 ring usage stayed within capacity: 3995B + one 60B line, not 4115B wrapped past the end");
writer_drain(); /* 末尾还会跟一条 [ERR] dropped */
CHECK(con_uart_tx_n >= used_after && tx_contains("[ERR] CONSOLE dropped 1"),
"TC-CONLOG-163 everything that survived the race is delivered, plus the drop warning");
CHECK(tx_prefix_all('Z', (DBG_CON_RING_SIZE - 1U) - 100U)
&& tx_at_is('Y', (DBG_CON_RING_SIZE - 1U) - 100U, 60U),
"TC-CONLOG-164 old 3995 bytes intact then the winner 60B line intact, loser absent (no silent overwrite)");
CHECK(!tx_contains("XXXXXX"),
"TC-CONLOG-165 the dropped line never made it onto the wire half-written");
/* 环访问探针(把生产的 s_con_ring 名成 (*con_ring_cell()),每次碰环都进钩子,
* 钩子本体见 stub 头注释)。167 是纯正控:证明探针真数到了逐字节访问、不是零命中
* 空转。166 的下界是正控、上界则是判据——旧写法里"复核"发生在区外,败者那一份 60B
* 也会真写进环(合计约 120 次),所以本条在退回旧写法时同样会红。 */
/* 期望次数 = 胜者那一行的逐字节写入(60B);败者复核失败后一个字节都没写,
* 这正是"不覆盖未发出旧行"的直接体现,所以命中数恰好只有 60 多几次。 */
CHECK(con_ring_probe_hits >= 60U && con_ring_probe_hits < 120U,
"TC-CONLOG-166 ring probe counted exactly the winner line byte writes (loser wrote nothing)");
CHECK(con_ring_zero_depth_hits == 0U,
"TC-CONLOG-167 no producer-side ring access ever happened outside a critical section");
}
/* ===================== main ===================== */
int main(void)
{
/* 先验一次宿主 CRT 与本模块 fputc 的关系(被 tc_params 记成 TC-CONLOG-000) */
tc_params();
tc_unattached();
tc_ring();
@@ -914,6 +1187,8 @@ int main(void)
tc_fputc();
tc_budget();
tc_counted_paths();
tc_three_ledgers();
tc_check_then_act();
printf("=== Console Log Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
+32
View File
@@ -246,9 +246,41 @@ extern uint32_t con_crit_enter;
extern uint32_t con_crit_exit;
void con_crit_push(void);
void con_crit_pop(void);
uint32_t con_crit_depth(void);
/* 进区回调:环空位竞态那条正向用例靠它把"第二个生产者"精确塞进第一个生产者的临界区里
* (旧写法里判据在区外,塞进去的写入不会被复核到;新写法复核在区内,必然看到)。
* 只在 con_crit_push 记完深度之后调用,不改任何深度值。 */
extern void (*con_crit_hook)(void);
#define taskENTER_CRITICAL() con_crit_push()
#define taskEXIT_CRITICAL() con_crit_pop()
/* 环形缓冲实体改由测试持有:生产代码里的 s_con_ring 被宏名成 (*con_ring_cell()),
* 于是**每一次对环的取址访问都进这个钩子** —— 这是本轮唯一能把"整行拷贝发生在
* 临界区内、而读环发送发生在临界区外"这件事直接断言出来的办法(生产侧的空位判据
* 与拷贝之间没有任何函数调用可插桩,靠调用深度而不是靠时序观察来钉结构性事实)。
* 钩子只记事实不改行为:返回的就是那块 4096B 存储。 */
/* 注意:**不要**在这里定义 DBG_CON_RING_SIZE(那是生产 console_log.c 的宏,
* 重复定义会撞 C4005);环形存储用字面量 4096U,与生产的数组维度一致即兼容。 */
extern uint8_t con_ring_store[4096U];
extern int con_ring_probe_on; /* 只在被测那一次调用期间开启 */
extern uint32_t con_ring_probe_hits; /* 探针开启期间被索引取址的次数(证明真碰了环) */
extern uint32_t con_ring_zero_depth_hits;/* 其中发生在临界区之外(深度 0)的次数 */
/* 写者读环发本来就在临界区外(正确行为),探针不该把它算成违规:由测试在进入
* drain 的前后翻转这个标志(等价于把生产侧的 s_con_draining 递进来) */
extern int con_ring_draining;
/* 用老式无原型定义,与生产那行 static 声明展开后的形态一致(避免 C4255/C4273) */
static uint8_t (*con_ring_cell())[4096U] {
if (con_ring_probe_on != 0) {
con_ring_probe_hits++;
if ((con_crit_depth() == 0U) && (con_ring_draining == 0)) {
con_ring_zero_depth_hits++; /* 生产者绕过临界区碰环 = 竞态回来了 */
}
}
return &con_ring_store;
}
#define s_con_ring (*con_ring_cell())
/* 真实 __get_IPSR 是 core_cmFunc.h 的静态内联;这里做成宏读一个可控量 */
extern uint32_t con_ipsr; /* 0 = 线程模式;!=0 = 中断/异常上下文 */
#define __get_IPSR() (con_ipsr)
+68 -13
View File
@@ -97,6 +97,9 @@ static void reset_all(void)
s_net_generation = 0U;
s_net_initialized = 0U;
s_net_msg_queue = NULL;
/* UDP 上下文守卫的判据也是生产 static:不清就会把上一场景的"执行任务"带进本场景
* (表现为守卫判错上下文,且与任务句柄复用后的地址撞车) */
s_net_owner_task = NULL;
g_ns_info_count = 0;
g_ns_error_count = 0;
g_ns_debug_count = 0;
@@ -114,12 +117,26 @@ static void bring_up(void)
(void)net_msg_queue_adopt((osMessageQueueId_t)&s_fake_queue);
/* 装泵:调用方阻塞等待时以 netTask 身份跑一轮消息分派(= 跨上下文那条链) */
g_ns_pump = net_process_messages;
/* net_init 以 netTask 身份跑:生产里它就是 net_stack_bring_up() 在 netTask 内
* 调用的(App/task/net_task.c:92),而本层执行任务句柄(UDP 上下文守卫的判据)
* 正是在这里捕获——这里若不换身份,整个 UDP 守卫就只能测到"自己判自己" */
g_ns_cur_task = (TaskHandle_t)&g_ns_task_net;
(void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL);
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
g_peers.pending_capture_calls = 0; /* bring_up 内部的登记动作不该算进场景 */
g_ns_error_count = 0;
g_ns_info_count = 0;
}
/* UDP 两接口的场景前置:net_sendto / net_recvfrom 有 netTask 上下文守卫,
* 合法用法只能在 netTask 里调 —— 这里把"换身份"收进一个入口,避免每个场景各写
* 一遍换/还原而漏掉一次(漏了就是"守卫把正常路径判成拒绝"的假故障)。 */
static void bring_up_netctx(void)
{
bring_up();
g_ns_cur_task = (TaskHandle_t)&g_ns_task_net;
}
/* 直接造一个在用的控制块(多数场景不需要走完整的 socket()/bind()) */
static int mk_sock(int type, net_sock_state_t state)
{
@@ -141,26 +158,64 @@ static int mk_sock(int type, net_sock_state_t state)
/* = 分片 1 纯函数面与 fd 表 ===================== */
/* TC-NETSOCK-100 点分十进制 ↔ 32 位:钉**当前行为**并显式标注疑点(见报告)
* 现状:net_inet_addr 把首段放在**最高字节**(0xC0A80164),而 net_getsockname 组装
* s_addr 时把首段放在**最低字节**,net_inet_ntoa 只与后者互为逆——同一文件里
* 存在两套字节序约定。这里逐条钉住,不替生产"圆场"。 */
/* TC-NETSOCK-100 点分十进制 ↔ 32 位:**正向断言**(字节序统一修复后)
* 全仓唯一约定:s_addr 是网络字节序整数,&s_addr 的第 0 个内存字节 == 点分第一段;
* 小端主机上 "192.168.1.100" 的数值 = 0x6401A8C0。本组断言的落点是「互逆」与
* 「同族可比」,不是某个中间数值——数值只是这条约定的一个投影。 */
static void tc_100_inet_helpers(void)
{
/* 往返不变量的样本集:两个全零/全一边界 + 三个非回文地址(非回文才能考出顺位) */
static const char *const k_ips[5] = {
"0.0.0.0", "255.255.255.255", "1.1.1.1", "192.168.1.100", "10.0.0.0"
};
char buf[16];
int i;
reset_all();
CHECK(net_inet_addr(NULL) == 0U, "TC-NETSOCK-100a inet_addr(NULL) 返 0(无效)");
CHECK(net_inet_addr("192.168.1.100") == 0xC0A80164U,
"TC-NETSOCK-100b inet_addr 首段落在最高字节(当前实现)");
CHECK(net_inet_ntoa(0xC0A80164U, buf) == buf && strcmp(buf, "100.1.168.192") == 0,
"TC-NETSOCK-100c ntoa(同一 32 位值) 与 inet_addr 不互逆(疑点已报,钉当前行为)");
CHECK(net_inet_addr(NULL) == 0U, "TC-NETSOCK-100a inet_addr(NULL) returns 0 (invalid)");
CHECK(net_inet_addr("192.168.1.100") == 0x6401A8C0U,
"TC-NETSOCK-100b inet_addr returns network-order int (LE value 0x6401A8C0 = bytes C0 A8 01 64)");
CHECK(net_inet_ntoa(net_inet_addr("192.168.1.100"), buf) == buf &&
strcmp(buf, "192.168.1.100") == 0,
"TC-NETSOCK-100c ntoa(inet_addr(s)) == s: the two are exact inverses");
CHECK(net_inet_ntoa(0x6401A8C0U, buf) == buf && strcmp(buf, "192.168.1.100") == 0,
"TC-NETSOCK-100d ntoa 与 getsockname 的 s_addr 约定互逆(E13 定案的段序:高段在前)");
CHECK(net_inet_ntoa(0U, NULL) == NULL, "TC-NETSOCK-100e ntoa(NULL 缓冲) 返 NULL 不崩");
/* 单段/缺分隔的容错:逐段解析,非数字段当 0(钉实现,不钉"应该是多少") */
CHECK(net_inet_addr("10.0.0.1") == 0x0A000001U, "TC-NETSOCK-100f inet_addr 10.0.0.1");
"TC-NETSOCK-100d ntoa segment order is MSB-first (fixed by E13/TRAP-NET-21, unchanged here)");
CHECK(net_inet_ntoa(0U, NULL) == NULL, "TC-NETSOCK-100e ntoa(NULL buf) returns NULL without crashing");
CHECK(net_inet_addr("10.0.0.1") == 0x0100000AU,
"TC-NETSOCK-100f non-palindrome address: 10.0.0.1 -> 0x0100000A (first octet in low byte)");
/* 正向断言 1:字符串 → 整数 → 字符串 必须回到原串(含 0.0.0.0 与 255.255.255.255) */
for (i = 0; i < 5; i++) {
(void)memset(buf, 0, sizeof(buf));
if (net_inet_ntoa(net_inet_addr(k_ips[i]), buf) == NULL || strcmp(buf, k_ips[i]) != 0) {
break;
}
}
CHECK(i == 5, "TC-NETSOCK-100g round trip ntoa(inet_addr(s)) == s over 5 addrs incl. 0.0.0.0 and 255.255.255.255");
/* 正向断言 2:整数 → 字符串 → 整数 必须回到原值(反方向也闭合) */
for (i = 0; i < 5; i++) {
uint32_t v = net_inet_addr(k_ips[i]);
(void)memset(buf, 0, sizeof(buf));
(void)net_inet_ntoa(v, buf);
if (net_inet_addr(buf) != v) {
break;
}
}
CHECK(i == 5, "TC-NETSOCK-100h round trip inet_addr(ntoa(v)) == v (inverse direction closes too)");
/* 边界语义别改坏:255.255.255.255 是**合法地址**,不得被当失败;
* 失败只有"参数为 NULL"一种,返回值 0 同时是 0.0.0.0 —— 这是 BSD inet_addr
* 的既有歧义,本轮刻意不改(改它要动全部调用者的判错方式,风险大于收益)。 */
CHECK(net_inet_addr("255.255.255.255") == 0xFFFFFFFFU,
"TC-NETSOCK-100i 255.255.255.255 parses to a valid value, never demoted to failure");
CHECK(net_inet_addr("0.0.0.0") == 0U,
"TC-NETSOCK-100j 0.0.0.0 -> 0 (BSD inet_addr ambiguity, not introduced by this fix)");
/* 段值溢出:按低 8 位截断(旧实现会把溢出位溅进相邻段,得到不可预期的混合值) */
CHECK(net_inet_addr("300.1.1.1") == net_inet_addr("44.1.1.1"),
"TC-NETSOCK-100k segment 300 truncates to 44 (300 and 0xFF) with no spill into neighbours");
}
static void tc_101_byteorder(void)
+6 -6
View File
@@ -435,7 +435,7 @@ static void tc_440_udp_datagram(void)
int addrlen;
int fd;
bring_up();
bring_up_netctx();
CHECK(net_sendto(-1, "x", 1, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF,
"TC-NETSOCK-440a sendto 越界 → BADF");
CHECK(net_sendto(0, "x", 1, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF,
@@ -451,7 +451,7 @@ static void tc_440_udp_datagram(void)
/* 带目的地址:先设 DesIP/DesPort 再写数据(不带上限的自旋实现,健康链路一次写完) */
reset_all();
bring_up();
bring_up_netctx();
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
(void)memset(&in, 0, sizeof(in));
in.sin_port = net_htons(8888);
@@ -465,7 +465,7 @@ static void tc_440_udp_datagram(void)
/* 大报分块(仍受 NET_SEND_CHUNK_MAX 上界约束) */
reset_all();
bring_up();
bring_up_netctx();
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
g_ns_int_pin_set = 0;
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
@@ -475,7 +475,7 @@ static void tc_440_udp_datagram(void)
/* recvfrom:前 8 字节是芯片 UDP 头(长度/srcPort/srcIP),要剥掉再交出去 */
reset_all();
bring_up();
bring_up_netctx();
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
ch395f_fake_push_recv_len((uint8_t)fd, 8U + 20U);
addrlen = (int)sizeof(in);
@@ -487,7 +487,7 @@ static void tc_440_udp_datagram(void)
/* 无数据 + 非阻塞 → WOULDBLOCK(阻塞形态是自旋无预算,本套刻意不测,见交付说明) */
reset_all();
bring_up();
bring_up_netctx();
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440m 非阻塞无数据 → WOULDBLOCK");
@@ -496,7 +496,7 @@ static void tc_440_udp_datagram(void)
net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440n 非阻塞属性同判据生效");
/* 短于 8 字节的残包:不剥头,整包交出去(钉当前实现的边界,不猜"应该怎么算") */
reset_all();
bring_up();
bring_up_netctx();
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
ch395f_fake_push_recv_len((uint8_t)fd, 5U);
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == 5,
+183 -2
View File
@@ -186,7 +186,7 @@ static void tc_506_sendto_stall_and_short(void)
int fd;
int rc;
bring_up();
bring_up_netctx();
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
s_net_socks[fd].send_ready = 0U;
g_ns_int_pin_set = 0;
@@ -201,7 +201,7 @@ static void tc_506_sendto_stall_and_short(void)
reset_all();
/* 写完一块后等发完的超时出口 */
bring_up();
bring_up_netctx();
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
g_ns_int_pin_set = 0;
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
@@ -377,6 +377,184 @@ static void tc_545_local_entries_without_queue(void)
reset_all();
}
/* ============= TC-NETSOCK-550 s_addr 同族一致性(字节序统一后的收口断言) =============
*
* 这次字节序统一的实质不是「给 inet_addr 换个数值」,而是「全仓只剩一套 s_addr 约定」。
* 所以这组断言全部写在**跨函数的接缝**上:解析→存储→芯片下发→回读→打印,
* 每一环都拿同一句点分十进制做基准。任何一处又引入第二套顺位,这里必红。
*/
/* TC-NETSOCK-550 解析 → 存储 → 打印 闭合(getsockname / getpeername 同族可比对) */
static void tc_550_family_roundtrip(void)
{
struct net_sockaddr_in in;
struct net_sockaddr *sa = (struct net_sockaddr *)&in;
char txt[16];
int addrlen;
int fd;
bring_up();
/* 1) getsockname 的 s_addr 与 inet_addr 解析结果**直接相等**(旧实现差一次换端) */
(void)mk_established(NET_SOCK_STREAM); /* getsockname 要 in_use 的描述符 */
addrlen = (int)sizeof(in);
(void)memset(&in, 0, sizeof(in));
CHECK(net_getsockname(0, sa, &addrlen) == 0,
"TC-NETSOCK-550a getsockname precondition ok");
CHECK(in.sin_addr.s_addr == net_inet_addr("192.168.1.100"),
"TC-NETSOCK-550b getsockname s_addr is the same family as inet_addr (direct equality)");
CHECK(strcmp(net_inet_ntoa(in.sin_addr.s_addr, txt), "192.168.1.100") == 0,
"TC-NETSOCK-550c local address closes parse-store-print (old code printed 100.1.168.192)");
/* 2) connect 的对端簿记同样闭合(lftpd:864 与 104 显示走的就是这条链) */
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
(void)memset(&in, 0, sizeof(in));
in.sin_family = NET_AF_INET;
in.sin_port = net_htons(2404U);
in.sin_addr.s_addr = net_inet_addr("192.168.1.200");
g_ns_cur_task = (TaskHandle_t)&g_ns_task_net;
CHECK(net_connect_locked(fd, sa, (int)sizeof(in)) == 0,
"TC-NETSOCK-550d connect precondition ok");
addrlen = (int)sizeof(in);
(void)memset(&in, 0, sizeof(in));
CHECK(net_getpeername(fd, sa, &addrlen) == 0,
"TC-NETSOCK-550e getpeername ok (reads event cache, zero SPI)");
CHECK(in.sin_addr.s_addr == net_inet_addr("192.168.1.200"),
"TC-NETSOCK-550f getpeername peer equals the s_addr fed into net_connect");
CHECK(strcmp(net_inet_ntoa(in.sin_addr.s_addr, txt), "192.168.1.200") == 0,
"TC-NETSOCK-550g peer address prints right (lftpd connection-received log depends on it)");
CHECK(s_net_socks[fd].remote_ip_arr[0] == 192U &&
s_net_socks[fd].remote_ip_arr[3] == 200U,
"TC-NETSOCK-550h chip des_ip array stays wire order [192,168,1,200]: on-wire behaviour unchanged");
reset_all();
}
/* TC-NETSOCK-555 UDP 报文头里的源地址拆装(同族的第四个生产者:net_recvfrom) */
static void tc_555_udp_src_addr(void)
{
/* CH395F UDP 头(§9.2.4):[0-1] 总长(LE) [2-3] 源端口(LE) [4-7] 源 IP(线序) */
static const uint8_t hdr[8] = {
11U, 0U, /* 总长 = 8 头 + 3 数据 */
0x1FU, 0xCBU, /* 源端口 51999(小端 1F CB) */
239U, 1U, 2U, 3U /* 源 IP 线序:239.1.2.3 */
};
struct net_sockaddr_in in;
struct net_sockaddr *sa = (struct net_sockaddr *)&in;
char buf[16];
char txt[16];
int addrlen;
int fd;
bring_up_netctx();
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
ch395f_fake_push_recv_len((uint8_t)fd, (uint16_t)(8U + 3U));
ch395f_fake_set_recv_bytes(hdr, (uint16_t)sizeof(hdr));
addrlen = (int)sizeof(in);
(void)memset(&in, 0, sizeof(in));
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, sa, &addrlen) == 3,
"TC-NETSOCK-555a strips the 8-byte chip header and delivers only the 3-byte payload");
CHECK(in.sin_addr.s_addr == net_inet_addr("239.1.2.3"),
"TC-NETSOCK-555b source IP unpack matches the inet_addr family");
CHECK(in.sin_port == net_htons(51999U),
"TC-NETSOCK-555c source port read chip little-endian then stored in network order");
CHECK(addrlen == (int)sizeof(in) && net_ntohs(in.sin_port) == 51999U,
"TC-NETSOCK-555d addrlen filled and port round-trips to 51999");
CHECK(strcmp(net_inet_ntoa(in.sin_addr.s_addr, txt), "239.1.2.3") == 0,
"TC-NETSOCK-555e multicast source address prints correctly");
reset_all();
}
/* TC-NETSOCK-560 UDP 上下文守卫:非执行任务不得直调芯片
* 三条一起才成立,缺一条都不算把危险面关掉:
* ① 执行任务内照常成功(守卫不是"把 UDP 判死")
* ② 非执行任务被拒且**一次芯片事务都没有**(拒了却已经抢过 SPI = 白拒)
* ③ 正常错误路径不被守卫吞掉,且**走消息队列的接口完全不受影响**
* (那些接口生来就是给非 netTask 用的,误伤它们才是大事故)
*/
static void tc_560_udp_context_guard(void)
{
char buf[8];
int fd;
/* ① 执行任务上下文:与修复前逐字相同 */
bring_up_netctx();
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == 3,
"TC-NETSOCK-560a net_sendto unchanged inside the executing task");
reset_all();
/* ② 非执行任务:拒绝 + 零芯片事务(bring_up 里 net_init 是以 netTask 身份跑的,
* 此刻 g_ns_cur_task 已还原成 caller,正是"别的任务") */
bring_up();
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
g_chip.write_calls = 0;
g_chip.read_calls = 0;
g_fxchip.set_des_ip_calls = 0;
g_fxchip.glob_calls = 0;
g_ns_poll_calls = 0;
g_ns_error_count = 0;
CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == -1 &&
net_get_errno() == NET_ERR_NOT_IN_NETTASK,
"TC-NETSOCK-560b non-executing task gets NET_ERR_NOT_IN_NETTASK (exact code, not -1)");
CHECK(g_chip.write_calls == 0 && g_fxchip.set_des_ip_calls == 0 &&
g_ns_poll_calls == 0,
"TC-NETSOCK-560c clean refusal: zero writes, zero set_des_ip, zero net_poll rounds");
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
net_get_errno() == NET_ERR_NOT_IN_NETTASK,
"TC-NETSOCK-560d net_recvfrom guarded the same way");
CHECK(g_chip.read_calls == 0 && g_fxchip.glob_calls == 0 && g_ns_poll_calls == 0,
"TC-NETSOCK-560e recvfrom touched no chip and no global interrupt register either");
CHECK(g_ns_error_count == 2,
"TC-NETSOCK-560f each refusal logs one ERR line (silence hides the wrong task)");
/* ③ 正常错误路径优先:守卫排在参数校验之后 */
CHECK(net_sendto(-1, "abc", 3, 0, NULL, 0) == -1 &&
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-560g out-of-range fd still reports BADF first");
CHECK(net_sendto(fd, NULL, 3, 0, NULL, 0) == -1 &&
net_get_errno() == NET_ERR_INVAL,
"TC-NETSOCK-560h NULL buf still reports INVAL, the guard does not swallow it");
CHECK(net_recvfrom(fd, NULL, 3, NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-560i recvfrom keeps the same precedence");
/* ④ 走消息队列的接口不受守卫影响:同一个非 netTask 上下文照常能把活干完 */
CHECK(net_close(fd) == 0,
"TC-NETSOCK-560j net_close from a non-executing task still works (queue path untouched)");
/* 执行完的证据用"收到回执 + 槽位释放",不看 close_calls:芯片此刻已是
* CLOSED,走的是快路径(不再补发 CLOSE,见 TC-NETSOCK-320a/b),拿它当证据
* 会把一条正确的快路径判成失败 */
CHECK(s_net_socks[fd].in_use == 0U && g_ns_notify_calls >= 1,
"TC-NETSOCK-560k queue path really completed (receipt seen and slot released)");
reset_all();
/* ⑤ 未捕获执行任务(net_init 没跑):守卫不抢先,仍由参数/状态检查给确定错误 */
reset_all();
g_ns_pump = net_process_messages;
CHECK(net_sendto(0, "abc", 3, 0, NULL, 0) == -1 &&
net_get_errno() == NET_ERR_BADF,
"TC-NETSOCK-560l before net_init sendto reports BADF, no invented context error");
/* 未捕获执行任务、但 socket 确实在用时,守卫**选择放行**:宁可不做判定,也不给
* 一个无从判定的场景编造错误码。放行后走的是完全正常的执行路径(这里 DONTWAIT
* 无数据 → WOULDBLOCK),证明守卫不是"没初始化就一律拒绝"的粗暴实现。
* 刻意用 recvfrom 而不是 sendto:未初始化时 net_poll 直接短路返回、时间基准
* 不前进,sendto 的等待环永远等不到超时(真机上不可达:net_socket 之后还有
* NET_READY 屏障,但这条差异值得记一笔,见交付报告)。 */
reset_all();
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; /* net_init 没跑:判据恒 NULL */
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
g_fxchip.glob_calls = 0;
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
net_get_errno() == NET_ERR_WOULDBLOCK,
"TC-NETSOCK-560m guard stays open when the executing task was never captured");
CHECK(net_get_errno() != NET_ERR_NOT_IN_NETTASK && g_fxchip.glob_calls == 0,
"TC-NETSOCK-560n no context error invented and no global register read");
reset_all();
}
static void ns_run_s6(void)
{
tc_500_public_bounds();
@@ -392,4 +570,7 @@ static void ns_run_s6(void)
tc_537_event_callback_path();
tc_540_negative_results();
tc_545_local_entries_without_queue();
tc_550_family_roundtrip();
tc_555_udp_src_addr();
tc_560_udp_context_guard();
}
+46 -12
View File
@@ -56,8 +56,11 @@
* TC-SOESTORE-020 丢弃留痕按差值上报一次,不刷屏
* TC-SOESTORE-021 队列创建失败(返回 NULL)→ 条目滞留环、下轮再试,不崩
* —— 陷阱二:名字/参数类错误码不得升级成卷操作 ——
* TC-SOESTORE-022 f_open 返 FR_INVALID_NAME → 只留痕返回 -1:**不 unlink**、
* 不碰 /log 目录、条目全部保留(自愈只针对自己的文件)
* TC-SOESTORE-022 f_open 失败(FR_INVALID_NAME)-> 只留痕返回 -1:**不 unlink**、
* 不碰 /log 目录、已有 3 条历史一条不少、条目留在待发环
* TC-SOESTORE-022b 逐码对齐(8 个码含介质类):打开失败**任何码都不 unlink**——
* SOE 侧比 run.log 侧更保守,是刻意差异(事件序列不可销毁),
* 与 test/host/sys_log_mcu/ 的 TC-SYSLOG-101/102 同一张表
* TC-SOESTORE-023 写路径反复失败时 f_unlink 次数有界(每轮 <=1),目录与邻居文件不动
* TC-SOESTORE-024 假卷严格 8.3 校验的保真自检:".fschk" -> 名字/盘符类拒绝码,
* 而生产路径 SOE_PATH 必过(钉住"改名即静默写失败"的约束)
@@ -66,12 +69,13 @@
* 函数 4/4。唯一未覆盖行 soe_store.c:202 = drain 里 for (;;) 的收尾大括号,
* 该循环只能靠 break 退出,不存在"走到行尾再回跳"的机器码,属工具口径缺口。
*
* 本套照出的现状(只登记、不改生产代码;详见交 Lead 的报告):
* soe_store.c 的函数头第 ④ 条写"打开/写入结构性错误 → unlink 重试一次(同 run.log)",
* 但实现里 **f_open 失败直接 return -1、不 unlink 重试**(只有 f_write 失败才重试)。
* run.log 侧(sys_log_mcu.c s_append_rec)则对 FR_NO_FILE/FR_NO_PATH/FR_INVALID_NAME
* 等一整套码做 unlink+重开。两套并不一致 —— 是注释夸大了实现,还是实现漏了一半,
* 需生产侧裁定;本套只用 TC-SOESTORE-022 把现状钉成"改了会响"。
* 本轮(第 1 轮缺陷修复批次 task-4)已修,本套相应断言已从"钉现状"翻成正向:
* D 项:soe_store.c 函数头 ④ 旧写"打开/写入结构性错误都 unlink 重试一次(同
* run.log)",与实现不符且方向错误——实现里 f_open 失败只 return -1、不 unlink。
* 定案是**实现不动、改注释**:SOE 是事件序列,打开失败就删文件等于销毁已发生的
* 事件;run.log 是滚动运行日志,坏了重建损失有限。现在 TC-SOESTORE-022 正向钉
* "打开失败不得 unlink、历史一条不少",TC-SOESTORE-022b 对 8 个码逐个钉同一件事
* (含介质类码——SOE 侧比 run.log 侧更保守,两处注释同源,改动必须同步)。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
@@ -633,17 +637,47 @@ static void tc_name_error_not_volume(void)
printf("-- TC-SOESTORE-022..024 名字/参数类错误码严禁升级成卷操作 --\n");
/* 022:FR_INVALID_NAME 只让本条失败,**不得** unlink 别人、不得动目录 */
/* 022【D 项已修 2026-10-24:正向断言】f_open 失败(名字/参数类码)
* -> 只留痕返回 -1:**绝不 unlink**、不动 /log 目录、已有记录一条不少。
* 生产侧的定案是"打开失败不重试也不删",理由写在 s_soe_append 函数头:
* SOE 是事件序列,删文件 = 销毁已发生的事件。 */
reset_all();
(void)ff_fake_mkfile("/log/run.log", NULL, 0u); /* 邻居文件(同卷) */
(void)ff_fake_mkfile(SOE_PATH, NULL, 3u * SOE_REC_SIZE); /* 已有 3 条历史 */
(void)ff_fake_mkfile("/log/run.log", NULL, 0u); /* 邻居文件(同卷) */
ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, FR_INVALID_NAME, 2);
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
soe_store_drain(); /* open 失败 -> -1 */
soe_store_drain(); /* open 失败 */
ok = (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0)
&& (ff_fake_exists("/log") == 2)
&& (ff_fake_size(SOE_PATH) == 3u * SOE_REC_SIZE) /* 历史原封不动 */
&& (ff_fake_exists("/log/run.log") == 1)
&& (s_ring_tail == 0u) && (g_soe_err_count == 1);
CHECK(ok, "TC-SOESTORE-022 FR_INVALID_NAME -> 只留痕失败;不 unlink、目录与邻居文件不动");
CHECK(ok, "TC-SOESTORE-022 open failure keeps history: no unlink, entry stays queued");
/* 022b 逐码对齐(与 test/host/sys_log_mcu 的 TC-SYSLOG-101/102 同一张表):
* SOE 侧对**所有**打开失败码都不 unlink,是 run.log 侧触发集的保守子集
* ——介质类码在 run.log 侧会 unlink 重建,在 SOE 侧不会,这是刻意的差异
* (事件序列不可销毁),两处注释同源、改动必须同步。 */
{
static const FRESULT codes[] = { FR_INVALID_NAME, FR_NO_PATH, FR_NO_FILE,
FR_INVALID_OBJECT, FR_DISK_ERR, FR_INT_ERR,
FR_NO_FILESYSTEM, FR_DENIED };
uint32_t c;
int all_ok = 1;
for (c = 0u; c < (uint32_t)(sizeof(codes) / sizeof(codes[0])); c++) {
reset_all();
(void)ff_fake_mkfile(SOE_PATH, NULL, SOE_REC_SIZE);
ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, codes[c], 1);
soe_store_post((uint8_t)SOE_T_ELM_RETURN, (uint8_t)c, c);
soe_store_drain();
if ((g_ff_obs.calls[FF_FAKE_OP_UNLINK] != 0)
|| (ff_fake_size(SOE_PATH) != SOE_REC_SIZE)
|| (s_ring_tail != 0u)) {
all_ok = 0;
}
}
CHECK(all_ok, "TC-SOESTORE-022b no open-failure code ever unlinks soe.log (8 codes)");
}
/* 023:反复失败的自愈上限:每轮最多 unlink 一次,条目与卷都不被吞 */
reset_all();
+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);