Files
STM32F4-Base/test/host/vol_lock/vol_lock_test.c
T
wandering1 b623a406ad fix(栈·存储·UI 收尾): 断掉临界区↔断言幻影环回到 1,464,标定门控单点化,卷锁五态可观测
这四笔在主树里已经互相纠缠(同一次 @build 编译、同一份 .htm 度量),拆成四笔就不
存在任何一个被真编过的中间态,所以合一笔入库;下面按模块分段陈述,作者与出证归属
逐段写明。

—— 一、崩溃证据路径的整行独占收尾(log-maint-writer,task-26)——
上午那笔 1d4b6a17 之后,stack_report 报 iec104Task 静态最坏链 1,544 / margin 1,016,
低于 1,024 的 ISR 底线。真因不是新增局部缓冲,而是本模块与内核临界区之间新长出
一条环:taskEXIT_CRITICAL ⇒ vPortExitCritical ⇒ configASSERT ⇒ vApplicationAssertFailed
⇒ dbg_console_write_raw ⇒ 回到本模块。armlink 把这一整坨计入**每一个含临界区的
任务链**(vApplicationAssertFailed 的 Max Depth 由约 40 B 涨到 128 B + In Cycle),
所以九条链同涨约 80 B——同涨本身就排除了任何单个调用方。
改法两处:① 本模块三处窗口(取凭证 / 还凭证 / 整行入环)改用 CON_CRIT_ENTER/EXIT
(PRIMASK 保存恢复到局部变量;注释写全为什么不能直接 __enable_irq()——嵌套时会
替别人打开任务级临界区——以及屏蔽面变宽是有意为之);② 异常/终点钩子里那对
自证空转的 begin/end 摘掉(异常态 begin 放行但不持有、end 是幂等空操作,调它只有
成本没有收益),任务态 STALL 行保留 begin/end,这个区分写进两处注释与文档 §4.4.1。
实测四棵树逐个 .htm 解析(身份钉 tree + Linker run 时刻 + 产物来源):A=77779d49
链 1,456;B=1d4b6a17 链 1,464 / margin 1,096 PASS;C=含未提交钩子接线的工作树
1,544 / 1,016 FAIL;D=本轮修复后(Linker run 22:52)回到 **1,464 / 1,096 PASS**,
九条链齐降 80 B。断环图证:vPortExitCritical 由 136 B + In Cycle 变 8 B 且不再
In Cycle,con_tx_release 子树 152→8;Error_Handler 回到 280 + In Cycle = A 树同值
(既有发送环无人拆动)。顺带删掉 Src/freertos.c 崩溃路径里最后一个 HAL_Delay(200)
(该文件真实调用现为 0)。
测试:console_log 152→**153** 条(只增不减);换钉不删防——015 现在钉「PRIMASK
窗口 2 次、深度 1、内核临界区 0 次」,新钉 015c 绝不等待,160 注入点搬到
con_pm_hook,176 强化到未调度路径连 PRIMASK 窗口都不开;红线水位
con_pm_at_tx_max 与 con_uart_crit_at_tx_max 双 0;判据⑦(延后的行与当场直发逐
字节相同)170b/171/178 仍全绿。作者自报并纠正一处:换钉时曾把 TC-CONLOG-015b 改
编号,而 015b 在生产注释与文档里都被引用——已恢复「编号不退役、语义搬过去」。
覆盖率 App/log/console_log.c 199/200 → 256/257 行、21/21 函数(只增)。

—— 二、未标定闭锁判据收成单点访问器(ui-param-writer,task-16)——
「UA/UN/IA 三相 VALID 全置位才允许跑判据」这条门控原先在两处各自镜像实现
(protect_task.c 的 s_cal_locked 与 ui_data_feed.c 的 feed_cal_block),改一处另一处
静默失效且无门禁可照。现新增 adc_cal_prot_blocked() 落在 App/algo/adc_cal,DTU_BENCH
特例收进一处;两个消费者同批改调、本地副本删除。作者还查实并纠正了我先前一句
错判:全仓 App/app→App/task 的 include 实为 0 条,故访问器不能落在 protect_task。
新增 TC-ADCCAL-018 八条(含冷启动即闭锁),adc_cal 套件 99/99。

—— 三、共享卷锁五态可观测性(ui-param-writer,task-17)——
原状比登记簿写的更糟:五个可区分态(未调用 / try 超时 / 分片超时 / fail-open /
未持锁却 release)生产侧零留痕,且 vol_lock.c 的返回值注释与代码相反——
fail-open(锁没建成、互斥已失效却在读写盘)被返 1 **谎报成取到锁**,而那正是
vol_lock.h 记录的台架实证故障(并发写坏 FTL → 喂狗点饥饿 → IWDG 误复位)的前置。
锁层加四个 volatile 计数(16 B 静态,零新局部、零新 callee、零日志、零 FatFs)与
不取锁的 vol_lock_get_stats();13 处 try 调用点一字未动。**刻意不在锁层记时刻**:
记时刻要读 tick,等于给 defaultTask 取锁链添帧——诊断不值那几个字节;消费方若
自盖时间戳,那只能是被观测到的时刻而非发生时刻,.h 里已写明不得写成首次/末次。
四套本地假件各加 s_last_wait_to(字段名逐字一致),把「喂狗点取锁一律 timeout=0」
与「ftpTask 分片一律 100 ms」从注释纪律升级成会 FAIL 的断言;红-绿反证:临时把
sys_log_mcu.c 的 0u 改 50u → TC-SYSLOG-010/012 双红,把 lftpd_io.c 的 100u 改 0u →
TC-LFTPIO-034 红,两处按原串改回并用 SHA-256 证明逐字节还原。TC-VOLLOCK-015~019
共 45/45(无新桩)。

—— 四、UI 死接口清理(ui-maint-writer,task-6 收尾)——
删掉 ui_data_maint_ready():全仓零消费者、只有它自己的用例在读。**更值得记的是
它为什么能活过一整轮评审**:头注释写的是「页面用它决定这页能不能操作」,而页面
从来没用过它——注释陈述的是意图,评审读的是意图,门禁读的是可达路径,两者都不
反驳「意图未实现」。作者对「连用例一起删」做了显式抗命:TC-UIDATA-025 的 13 条里
只有 4 条读那个死口,另 9 条钉的是真防线(六口未注入安全返回 / 残表整表拒绝 /
被拒不覆盖已生效的好表),故删冗余的 025a、把 025i/025k/025m 换成用 reset_req()
的可观察返回值判定,性质不丢。ui_data 套件 244 → 243(净减 1),黄金 40 页与
池 5448 B 未变(该刀零像素影响,参考图无 diff)。

—— 门禁与未跑项(本笔如实留账)——
cubemx-survival 基线已 --write(漂移 6 项全部是上述意图内区域:断言/栈溢出钩子
实现、看门狗模块行等),基线文件同笔入库,随后 --check 通过。
本笔未跑的钩子只有两条:mem-numbers 与 mem-budget——磁盘 .map 出自 22:52 那次
构建、含尚未入库的 UI 与工具改动,与 HEAD 台账必然不同码;按仓库既有约定允许
--no-verify + 正文留证,回填在紧随的台账一笔统一做(含 ram_baseline --update 与
覆盖率基线两条 [NOTE]:ui_data.c 65→316、iec104_comm.c 36/36)。
两条已挂着的账:① id-check 不可判定引用 1,162 / 上限 1,166,**余量只剩 4**,
主要吃进者是我今天推进的 UI 那笔(68 条文本断言的注释引用),账在 Lead;
② 崩溃注入腿(故意复位)台架复验未跑,需先由代码道提交触发器,且判据只判原子性
不判 [ERR]=0——IPSR 路径的主张归主机侧套件,硬件照不出来。

产物身份(按 D 表要求钉死,避免数字与树脱钩):本笔包含的固件树就是那张 D 表对应的
那一棵——MDK-ARM/.../STM32F407-Demo.htm 的 mtime = 2026-10-26 22:52:12(.htm/.map 按
约定不入库,故把时刻写进正文)。提交前在本树复跑 stack_report.py:exit 0,
iec104Task 1,464 / margin 1,096(42%)PASS、defaultTask 3,040 / 2,080(40%)PASS,
与 D 表逐项一致;本次运行不含任何未提交固件源码之外的新改动。
2026-10-26 23:31:05 +08:00

297 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:Volume Lock(卷锁)主机侧单元测试
* 模块功能:在 PC 上对 App/fs/vol_lock.c 做离线验证(3 个公开函数全覆盖)。
*
* 被测对象:真实源文件 ../../../App/fs/vol_lock.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本)。平台依赖由本文件提供的**可注入桩**实现:
* - cmsis_os.h -> osMutexNew / osMutexWait / osMutexRelease
* (可注入 创建失败 / 等待超时 / 获取成功)
*
* 为什么要主机侧单测(而不是只用板端套件):
* 1. 本模块最要紧的语义是 **fail-open**(锁创建失败时 `vol_lock_wait` 必须返回 1
* 且**绝不阻塞**)——这是"defaultTask 喂狗点绝不能因等锁而停摆"的最后一道保险。
* 真机上 `osMutexNew` 失败几乎不可能复现(需堆耗尽),板端套件无法构造。
* 2. 幂等性(重复 `vol_lock_init` 不得重复创建)、等待超时透传、未创建时 release 无害,
* 都是纯状态逻辑,主机侧一次钉死。
* 3. 不占 MCU 的 RAM,毫秒级完成。
*
* 覆盖点:
* TC-VOLLOCK-001 init 首次调用创建互斥量并返回 0
* TC-VOLLOCK-002 init 幂等(重复调用不再创建)
* TC-VOLLOCK-003 init 在 osMutexNew 失败时返回 -1
* TC-VOLLOCK-004 init 失败后可重试成功(s_lock 未被污染)
* TC-VOLLOCK-005 wait 成功后返回 1,且把 timeout 原样透传给 osMutexWait
* TC-VOLLOCK-006 wait 超时(osMutexWait != osOK)返回 0
* TC-VOLLOCK-007 wait 在锁未创建时自动 init,再正常获取
* TC-VOLLOCK-008 ⭐ fail-open:锁未创建且创建失败 -> 返回 1 且**不调用** osMutexWait
* TC-VOLLOCK-009 fail-open 之后 release 无害(s_lock 仍为 NULL,不调用 osMutexRelease)
* TC-VOLLOCK-010 已创建时 wait 不再调用 osMutexNew
* TC-VOLLOCK-011 release 在已持锁时调用 osMutexRelease 一次
* TC-VOLLOCK-012 release 在锁未创建时静默返回(不崩溃、不调用)
* TC-VOLLOCK-013 完整周期 init -> wait -> release -> wait -> release
* TC-VOLLOCK-014 wait(0) 的 timeout 透传为 0(defaultTask 的 try 语义)
* —— 观测计数(道G task-17「卷锁 5 态可观测性」)——
* TC-VOLLOCK-015 成功路径只涨 acquired,三个异常计数必须为 0
* TC-VOLLOCK-016 timeout=0 未取到 -> 只进 try_timeout(T2,没等)
* TC-VOLLOCK-017 timeout=100 未取到 -> 只进 blk_timeout(T3,等了没等到),
* 与 T2 互不冒领
* TC-VOLLOCK-018 ⭐ fail-open 仍返 1(口径不变)但 failopen 计数自增并可累加
* —— 把"谎报成功"那一态变成事后可查的证据
* TC-VOLLOCK-019 观测口:NULL 无害、重复读取不消耗证据
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-10
*/
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
/* ===================== 平台桩(先纳入,覆盖真实头) ===================== */
#include "cmsis_os.h"
/* ===================== CMSIS-RTOS 互斥量桩 =====================
* s_new_fail osMutexNew 返回 NULL(注入创建失败)
* s_wait_ok 0 -> osMutexWait 返回 osErrorTimeout(注入超时)
* s_fake_mutex 成功创建时返回的伪句柄(非 NULL 即可)
* s_last_timeout 记录 osMutexWait 收到的 millisec(断言透传)
*/
static int s_new_calls;
static int s_new_fail;
static int s_wait_calls;
static int s_wait_ok;
static int s_release_calls;
static uint32_t s_last_timeout;
static void *s_fake_mutex = (void *)0x1234;
osMutexId_t osMutexNew(const osMutexAttr_t *attr) {
(void)attr;
s_new_calls++;
return s_new_fail ? NULL : (osMutexId_t)s_fake_mutex;
}
osStatus_t osMutexWait(osMutexId_t mutex_id, uint32_t millisec) {
(void)mutex_id;
s_wait_calls++;
s_last_timeout = millisec;
return s_wait_ok ? osOK : osErrorTimeout;
}
osStatus_t osMutexRelease(osMutexId_t mutex_id) {
(void)mutex_id;
s_release_calls++;
return osOK;
}
/* ===================== 断言 ===================== */
static int s_total = 0, s_passed = 0, s_failed = 0;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== 被测代码(原样纳入生产源) ===================== */
#include "vol_lock.c"
/* ===================== 工具 ===================== */
/*
* 函数功能:把全部桩与模块静态态复位到"刚上电"初值
* 入口参数:无
* 返 回 值:无
* 限定条件:无
* 函数说明:`s_lock` 是 vol_lock.c 的文件静态量(源文件已被纳入同一编译单元),
* 直接置 NULL 等价于"未初始化过"——这是白盒单测的可接受代价。
*/
static void fakes_reset(void) {
s_new_calls = 0;
s_new_fail = 0;
s_wait_calls = 0;
s_wait_ok = 1;
s_release_calls = 0;
s_last_timeout = 0xFFFFFFFFu;
s_fake_mutex = (void *)0x1234;
s_lock = NULL;
/* 观测计数同属被测模块的文件静态量(道G task-17):复位后每条 TC 才能读到
* "本条自己造出来的那几个事件",而不是前一条的残留 */
s_st_acquired = 0u;
s_st_try_timeout = 0u;
s_st_blk_timeout = 0u;
s_st_failopen = 0u;
}
/* ===================== TC-VOLLOCK-001..004:vol_lock_init ===================== */
static void tc_init(void) {
fakes_reset();
CHECK(vol_lock_init() == 0, "TC-VOLLOCK-001 first init call returns 0");
CHECK(s_new_calls == 1, "TC-VOLLOCK-001b init creates the mutex exactly once (osMutexNew)");
CHECK(s_lock != NULL, "TC-VOLLOCK-001c module static state holds the handle");
CHECK(vol_lock_init() == 0, "TC-VOLLOCK-002 repeated init still returns 0 (idempotent)");
CHECK(s_new_calls == 1, "TC-VOLLOCK-002b idempotent: no second mutex created");
fakes_reset();
s_new_fail = 1;
CHECK(vol_lock_init() == -1, "TC-VOLLOCK-003 osMutexNew failure -> init returns -1");
CHECK(s_lock == NULL, "TC-VOLLOCK-003b failure leaves no half-initialized handle");
s_new_fail = 0;
CHECK(vol_lock_init() == 0, "TC-VOLLOCK-004 retry after failure can succeed (returns 0)");
CHECK(s_new_calls == 2, "TC-VOLLOCK-004b retry really calls osMutexNew again");
}
/* ===================== TC-VOLLOCK-005..010:vol_lock_wait ===================== */
static void tc_wait(void) {
/* --- 已创建:超时值原样透传 --- */
fakes_reset();
(void)vol_lock_init();
CHECK(vol_lock_wait(100U) == 1, "TC-VOLLOCK-005 wait returns 1 on acquire success");
CHECK(s_wait_calls == 1, "TC-VOLLOCK-005b osMutexWait called once");
CHECK(s_last_timeout == 100U, "TC-VOLLOCK-005c timeout passed through to osMutexWait unchanged");
fakes_reset();
(void)vol_lock_init();
CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-014 wait(0) returns 1 on acquire (try semantics)");
CHECK(s_last_timeout == 0U, "TC-VOLLOCK-014b wait(0) timeout passes through as 0");
/* --- 超时:osMutexWait != osOK -> 返回 0 --- */
fakes_reset();
(void)vol_lock_init();
s_wait_ok = 0;
CHECK(vol_lock_wait(50U) == 0, "TC-VOLLOCK-006 osMutexWait not osOK -> wait returns 0");
CHECK(s_wait_calls == 1, "TC-VOLLOCK-006b timeout path really attempted the wait");
/* --- 锁未创建:自动 init 后正常获取 --- */
fakes_reset();
CHECK(vol_lock_wait(10U) == 1, "TC-VOLLOCK-007 wait auto-inits and acquires when no lock exists");
CHECK(s_new_calls == 1, "TC-VOLLOCK-007b auto init created the mutex");
CHECK(s_wait_calls == 1, "TC-VOLLOCK-007c then performed one wait");
/* --- ⭐ fail-open:创建失败必须返回 1 且绝不阻塞 --- */
fakes_reset();
s_new_fail = 1;
CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-008 fail-open: returns 1 even if lock creation fails (never blocks a feed point)");
CHECK(s_wait_calls == 0, "TC-VOLLOCK-008b fail-open **does not call** osMutexWait (zero blocking)");
CHECK(s_lock == NULL, "TC-VOLLOCK-008c still no handle after fail-open");
/* --- fail-open 之后 release 无害 --- */
vol_lock_release();
CHECK(s_release_calls == 0, "TC-VOLLOCK-009 after fail-open release does not call osMutexRelease (NULL guard, silent)");
/* --- 已创建时 wait 不再创建 --- */
fakes_reset();
(void)vol_lock_init();
(void)vol_lock_wait(0U);
(void)vol_lock_wait(0U);
CHECK(s_new_calls == 1, "TC-VOLLOCK-010 repeated wait does not recreate the mutex");
CHECK(s_wait_calls == 2, "TC-VOLLOCK-010b each wait performs one wait call");
}
/* ===================== TC-VOLLOCK-011..013:vol_lock_release ===================== */
static void tc_release(void) {
/* --- 已持锁:释放一次 --- */
fakes_reset();
(void)vol_lock_init();
(void)vol_lock_wait(0U);
vol_lock_release();
CHECK(s_release_calls == 1, "TC-VOLLOCK-011 release while holding the lock calls osMutexRelease once");
/* --- 锁未创建:静默返回 --- */
fakes_reset();
vol_lock_release();
CHECK(s_release_calls == 0, "TC-VOLLOCK-012 release with no lock returns silently (no call, no crash)");
/* --- 完整周期 --- */
fakes_reset();
CHECK(vol_lock_init() == 0, "TC-VOLLOCK-013 cycle: init -> 0");
CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-013b cycle: wait -> 1");
vol_lock_release();
CHECK(vol_lock_wait(100U) == 1, "TC-VOLLOCK-013c cycle: wait again -> 1 (re-acquirable)");
vol_lock_release();
CHECK(s_new_calls == 1, "TC-VOLLOCK-013d cycle: only one mutex created overall");
CHECK(s_release_calls == 2, "TC-VOLLOCK-013e cycle: released twice");
CHECK(s_last_timeout == 100U, "TC-VOLLOCK-013f cycle: second wait timeout passes through correctly");
}
/* ===================== TC-VOLLOCK-015..019:观测计数(道G task-17) =====================
* 这一组是"5 态可观测性"的机器证据:
* T-a acquired / T2 try_timeout / T3 blk_timeout / T4 failopen 各自独立自增,
* T2 与 T3 不许互相冒领(那是本卡唯一"语义合并"风险),T4 从"谎报成功"变成可查。
*/
static void tc_stats(void) {
vol_lock_stats_t st;
/* 015 成功路径:只有 acquired 动,其余三项必须仍是 0 */
fakes_reset();
(void)vol_lock_init();
CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-015 try acquire succeeds");
vol_lock_get_stats(&st);
CHECK((st.acquired == 1u) && (st.try_timeout == 0u) && (st.blk_timeout == 0u)
&& (st.failopen == 0u),
"TC-VOLLOCK-015b acquired only: a successful lock never inflates any anomaly counter");
/* 016 T2:timeout=0 的 try 失败 -> 只进 try_timeout */
fakes_reset();
(void)vol_lock_init();
s_wait_ok = 0;
CHECK(vol_lock_wait(0U) == 0, "TC-VOLLOCK-016 try on a held volume returns 0");
vol_lock_get_stats(&st);
CHECK((st.try_timeout == 1u) && (st.blk_timeout == 0u) && (st.acquired == 0u),
"TC-VOLLOCK-016b timeout=0 miss counts as try_timeout and NOT as blk_timeout (feed point never waited)");
/* 017 T3:timeout>0 的分片等待超时 -> 只进 blk_timeout(与 T2 分道) */
fakes_reset();
(void)vol_lock_init();
s_wait_ok = 0;
CHECK(vol_lock_wait(100U) == 0, "TC-VOLLOCK-017 100ms slice on a held volume returns 0");
vol_lock_get_stats(&st);
CHECK((st.blk_timeout == 1u) && (st.try_timeout == 0u),
"TC-VOLLOCK-017b slice timeout counts as blk_timeout and NOT as try_timeout (waited-but-missed is a different fact)");
/* 018 T4 核心:锁不存在时 wait 仍返 1(TC-VOLLOCK-008 的口径),但现在必须留下
* 可查证据 —— 这一态此前被谎报成"取到锁",整机没有任何地方能发现它 */
fakes_reset();
s_new_fail = 1;
CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-018 fail-open still returns 1 (unchanged contract, TC-VOLLOCK-008)");
vol_lock_get_stats(&st);
CHECK((st.failopen == 1u) && (st.acquired == 0u),
"TC-VOLLOCK-018b fail-open is now provable: failopen counts up while acquired stays 0 (zero mutual exclusion)");
CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-018c second fail-open call also returns 1");
vol_lock_get_stats(&st);
CHECK(st.failopen == 2u, "TC-VOLLOCK-018d fail-open accumulates (consumer latches on the delta, not on the event)");
/* 019 观测口自身:NULL 无害、读取不消耗证据、逐字段稳定 */
vol_lock_get_stats(NULL);
CHECK(1, "TC-VOLLOCK-019 get_stats(NULL) is harmless (a defensive caller must never crash the feed point)");
vol_lock_get_stats(&st);
CHECK((st.failopen == 2u) && (st.try_timeout == 0u) && (st.blk_timeout == 0u)
&& (st.acquired == 0u),
"TC-VOLLOCK-019b reading does not consume the evidence: counters survive repeated reads");
}
/* ===================== main ===================== */
int main(void) {
printf("=== Volume Lock Tests ===\n");
printf("-- vol_lock_init --\n");
tc_init();
printf("-- vol_lock_wait (incl. fail-open) --\n");
tc_wait();
printf("-- vol_lock_release --\n");
tc_release();
printf("-- observability counters (task-17) --\n");
tc_stats();
printf("\n=== Volume Lock Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}