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
+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);