Files
STM32F4-Base/test/host/vol_idle_sync/vol_idle_sync_test.c
T
wandering1 484d068250 feat(存储): 写回缓存空闲 flush 兜底——卷空闲周期 CTRL_SYNC(登记簿 N-22 收口)
道F 批 2 / task-16。FTL 单页写回缓存(nand_ftl.c s_cache_dirty)与 dhara
journal 只在换页/整页写/f_close 时刷出,漏网脏页的掉电驻留窗无上限。本
提交以最小面收口:不碰 GD5F/dhara 驱动,全部走既有 FatFs CTRL_SYNC 路径。

实现 App/fs/vol_idle_sync.[ch](新增,仅 MCU 端,uvprojx App 组注册):
- 四级门:STORAGE_READY 就绪门 → 5 s 节流(无符号差回绕安全判据)→
  vol_lock_wait(0) try(锁被持有=文件序列在做,跳过,不改 STOR 持有期
  语义)→ disk_ioctl(0, CTRL_SYNC);
- clean 态脏标志+journal 双短路、零 NAND 触点;脏态最坏 = 一次 f_close
  的同步段同型成本,不引入新磨损来源;
- 失败按边沿 latch 一条 DBG_ERROR、成功重新武装;失败路径也成对放锁;
- TEST_SUITE_STORAGE 构建整体 no-op(卷由 testTask 独占)。
接线:defaultTask 主循环 syslog_drain 之后(default_task.c v1.3)。

文档:docs/架构设计/STM32F4-Base存储架构说明.md §5.5 补空闲兜底与边界;
本板无掉电检测硬件、不做掉电前强制 flush、勿当掉电保护——口径同步落
注释与文档(sys_log_mcu.c 两处旧「暂不做」注释随实现改口)。

测试:新主机单测 test/host/vol_idle_sync/(TC-VOLSYNC-001..012,19 断言
全过);scripts/id_registry.json 建 TC-HOST-VOLSYNC 族(仿 ADCCAL 先例);
.pre-commit-config.yaml 加 vol-idle-sync-unit-test hook(只加自己条目)。

越界申报(①②③ 均 lead 批准,④ 为门禁连带必要):
① App/task/default_task.c 单点接线(include + 1 调用 + 头修改记录);
② docs/架构设计/STM32F4-Base存储架构说明.md §5.5;
③ .pre-commit-config.yaml 单 hook + scripts/id_registry.json 单族;
④ scripts/check_registration.py SIM_EXEMPT 加一行(vol_idle_sync.c 模拟器
   侧豁免,registration-check GAP 所需;与批准③同类别但未列名,特此申报)。

体积增量(vs scripts/ram_baseline.tsv 2026-10-25 行):
Flash(Code+RO) 237140→237328(+188 B)、RW 1224→1232(+8 B)、ZI 0;
增量 = 3 个文件级 static + 一处 DBG_ERROR 调用点;栈/堆无变化。

gate: only mem-budget/mem-numbers pending lead ledger backfill
全量 pre-commit --all-files:40 项 Passed,仅 mem-numbers(D-7 体积档)与
mem-budget 两条红 = 台账数字待回填(增量如上,属 lead 单写者禁区);
其余含 doc-links/id-check/registration/test-coverage/全部主机单测皆绿。
@build 0 错误 0 警告(Code=215932 RO=21396 RW=1232 ZI=131984)。

台架:空闲 sync 实机节奏(clean 零触点、脏态时长、无心跳扰动)属实际
测量采样类——本批跳过,留「待台架实测」。
2026-10-25 09:23:20 +08:00

283 lines
11 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 Idle Sync(写回缓存空闲 flush)主机侧单元测试
* 模块功能:在 PC 上对 App/fs/vol_idle_sync.c 做离线验证(产品分支全函数覆盖)。
*
* 被测对象:真实源文件 ../../../App/fs/vol_idle_sync.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本)。平台依赖由本文件给出的**可注入强符号桩**:
* - xTaskGetTickCount -> 可控时钟(节流边界 + 32 位回绕)
* - osEventFlagsGet -> 可注入就绪位图 / 句柄判空
* - vol_lock_wait -> 可注入 空闲/忙(try 语义)+ 透传记录
* - vol_lock_release -> 计数(断言成对、失败不泄漏)
* - disk_ioctl -> 可注入 RES_OK/RES_ERROR + pdrv/cmd 记录
* 事件位数值取自**真实** App/app/sys_events.h(不在桩里复刻)。
*
* 为什么要主机侧单测(而不是只用台架):
* 本模块是"掉电窗口兜底网"——掉电本身不可台架复现,脏页驻留窗更不能;
* 失败路径(卷忙 / ioctl 失败 / tick 回绕)在真机上极难按需构造。
* 门控/节流/latch 全是纯状态逻辑,主机侧一次钉死,板端只验证接入存在性。
*
* 覆盖点:
* TC-VOLSYNC-001 句柄未建 -> 完全静默(不读事件/不取锁/不 ioctl)
* TC-VOLSYNC-002 STORAGE_READY 未置 -> 静默(读了事件但不取锁)
* TC-VOLSYNC-003 就绪首拍:try 锁(timeout=0) + CTRL_SYNC(pdrv=0) + 放锁各一次
* TC-VOLSYNC-004 周期内重复拍 -> 完全抑制(不取锁不 ioctl)
* TC-VOLSYNC-005 节流边界:周期-1 拍抑制 / 满周期拍放行(1ms tick 等式)
* TC-VOLSYNC-006 卷忙(try 失败)-> 不 ioctl、不 release(不得空放锁)
* TC-VOLSYNC-007 卷忙跳过不欠账:节奏恒定(下次尝试 = 满周期之后)
* TC-VOLSYNC-008 ioctl 失败一个 episode 只打一条(边沿 latch,持续失败不刷屏)
* TC-VOLSYNC-009 成功后 latch 重新武装:下一个失败 episode 再报一条
* TC-VOLSYNC-010 失败路径同样成对放锁(release 次数 == 有效 ioctl 次数)
* TC-VOLSYNC-011 tick 32 位回绕:跨 0 后节流判据仍正确(该抑制抑制/该放行放行)
* TC-VOLSYNC-012 成功路径零日志(不打 DBG_ERROR/DBG_INFO)
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-25
*/
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
/* ===================== 真实中间件头(与 lftpd_io/nand_port 同一份) ======== */
#include "ff.h" /* FatFs 基础类型(diskio.h 依赖,先纳) */
#include "diskio.h"
/* ===================== 平台桩(stub 目录优先于真实头) ==================== */
#include "test_config.h"
#include "cmsis_os.h"
#include "FreeRTOS.h"
#include "task.h"
#include "sys_events.h" /* 真实头:SYS_EVT_STORAGE_READY / sys_evtHandle extern */
#include "dbg_log.h"
/* ===================== 强符号桩 =====================
* 时钟:非零起步(1000),避免"初值 0"与"回绕"在断言里混同 */
static uint32_t s_tick_now;
TickType_t xTaskGetTickCount(void) {
return (TickType_t)s_tick_now;
}
/* 事件标志:句柄实体在本测试单元定义(真实 sys_events.h 只 extern) */
static uint32_t s_evt_flags;
static int s_efget_calls;
osEventFlagsId_t sys_evtHandle;
uint32_t osEventFlagsGet(osEventFlagsId_t ef_id) {
(void)ef_id;
s_efget_calls++;
return s_evt_flags;
}
/* 卷锁:1 = 获取成功,0 = 忙(try 语义下即"本轮跳过") */
static int s_lock_ok;
static int s_lock_calls;
static int s_release_calls;
static uint32_t s_last_wait_to;
int vol_lock_wait(uint32_t timeout_ms) {
s_lock_calls++;
s_last_wait_to = timeout_ms;
return s_lock_ok;
}
void vol_lock_release(void) {
s_release_calls++;
}
/* 磁盘控制:只接住被测代码真正会发的那一发(pdrv=0 / CTRL_SYNC) */
static DRESULT s_diskio_res;
static int s_diskio_calls;
static BYTE s_diskio_pdrv;
static BYTE s_diskio_cmd;
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) {
(void)buff;
s_diskio_calls++;
s_diskio_pdrv = pdrv;
s_diskio_cmd = cmd;
return s_diskio_res;
}
/* dbg_log 桩计数(stub/dbg_log.h 声明 extern) */
int g_vis_error_count;
int g_vis_info_count;
/* ===================== 被测代码(原样纳入生产源) ===================== */
#include "vol_idle_sync.c"
/* ===================== 断言 ===================== */
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)
#define FAKE_EVT_HANDLE ((osEventFlagsId_t)0xE7)
#define PERIOD_TICKS 5000u /* = pdMS_TO_TICKS(VOL_IDLE_SYNC_PERIOD_MS),
* 1 kHz tick 下与毫秒数值等值(桩保真点) */
/* 复位所有桩与被测模块状态量(白盒:生产源已纳入本编译单元,文件级 static
* 可直接复位,等价"刚上电") */
static void fakes_reset(void) {
s_tick_now = 1000u;
s_evt_flags = 0u;
sys_evtHandle = NULL;
s_efget_calls = 0;
s_lock_ok = 1;
s_lock_calls = 0;
s_release_calls = 0;
s_last_wait_to = 0xFFFFFFFFu;
s_diskio_res = RES_OK;
s_diskio_calls = 0;
s_diskio_pdrv = 0xFFu;
s_diskio_cmd = 0xFFu;
g_vis_error_count = 0;
g_vis_info_count = 0;
s_vol_sync_last_ms = 0u;
s_vol_sync_armed = 0u;
s_vol_sync_fail_latch = 0u;
}
/* 便捷:推进时钟并空转主循环拍(模拟 defaultTask 循环里的连续 poll) */
static void advance_and_poll(uint32_t ticks) {
s_tick_now += ticks;
vol_idle_sync_poll();
}
/* ===================== TC-VOLSYNC-001/002:就绪门控 ===================== */
static void tc_gate(void) {
fakes_reset();
vol_idle_sync_poll();
CHECK((s_efget_calls == 0) && (s_lock_calls == 0) && (s_diskio_calls == 0),
"TC-VOLSYNC-001 handle NULL: fully silent (no flags read, no lock, no ioctl)");
fakes_reset();
sys_evtHandle = FAKE_EVT_HANDLE; /* 句柄已建但未就绪 */
vol_idle_sync_poll();
CHECK((s_efget_calls == 1) && (s_lock_calls == 0) && (s_diskio_calls == 0),
"TC-VOLSYNC-002 STORAGE_READY clear: read flags then bail (no lock, no ioctl)");
}
/* ===================== TC-VOLSYNC-003..007:放行/节流/卷忙 ===================== */
static void tc_run_throttle(void) {
fakes_reset();
sys_evtHandle = FAKE_EVT_HANDLE;
s_evt_flags = SYS_EVT_STORAGE_READY;
vol_idle_sync_poll();
CHECK((s_lock_calls == 1) && (s_last_wait_to == 0u),
"TC-VOLSYNC-003 first ready tick: try-lock taken with timeout=0 (non-blocking)");
CHECK((s_diskio_calls == 1) && (s_diskio_pdrv == 0u) && (s_diskio_cmd == (BYTE)CTRL_SYNC),
"TC-VOLSYNC-003b ioctl exactly once with pdrv=0 / CTRL_SYNC");
CHECK(s_release_calls == 1,
"TC-VOLSYNC-003c lock released exactly once after ioctl");
vol_idle_sync_poll();
vol_idle_sync_poll();
CHECK((s_lock_calls == 1) && (s_diskio_calls == 1),
"TC-VOLSYNC-004 repeats within period fully suppressed (no lock, no ioctl)");
/* 边界:+4999 抑制 / +1(累计满 5000)放行 */
advance_and_poll(PERIOD_TICKS - 1u);
CHECK(s_diskio_calls == 1, "TC-VOLSYNC-005 period-1 tick: still suppressed");
advance_and_poll(1u);
CHECK(s_diskio_calls == 2, "TC-VOLSYNC-005b exact period tick: fires again");
/* 卷忙:ioctl 不加、release 不得空放(相对断言:忙尝试整段零增量) */
{
const int lk = s_lock_calls;
const int rel = s_release_calls;
s_lock_ok = 0;
advance_and_poll(PERIOD_TICKS);
CHECK((s_lock_calls == lk + 1) && (s_diskio_calls == 2) && (s_release_calls == rel),
"TC-VOLSYNC-006 lock busy: attempt logged, no ioctl and NO release (never unlock what we do not hold)");
/* 跳过不欠账:忙的那拍消费了槽位 -> 下次尝试在一个满周期之后 */
s_lock_ok = 1;
advance_and_poll(PERIOD_TICKS - 1u);
CHECK(s_lock_calls == lk + 1,
"TC-VOLSYNC-007 busy skip consumed the slot: constant cadence");
advance_and_poll(1u);
CHECK((s_lock_calls == lk + 2) && (s_diskio_calls == 3) && (s_release_calls == rel + 1),
"TC-VOLSYNC-007b next attempt exactly one full period after the skip");
}
}
/* ===================== TC-VOLSYNC-008..010/012:失败 latch / 成对放锁 ======= */
static void tc_fail_latch(void) {
fakes_reset();
sys_evtHandle = FAKE_EVT_HANDLE;
s_evt_flags = SYS_EVT_STORAGE_READY;
vol_idle_sync_poll(); /* 首拍成功 */
CHECK((g_vis_error_count == 0) && (g_vis_info_count == 0),
"TC-VOLSYNC-012 success path logs nothing");
s_diskio_res = RES_ERROR;
advance_and_poll(PERIOD_TICKS);
CHECK(g_vis_error_count == 1, "TC-VOLSYNC-008 first failure logs exactly one line");
advance_and_poll(PERIOD_TICKS);
advance_and_poll(PERIOD_TICKS);
CHECK((g_vis_error_count == 1) && (s_diskio_calls == 4),
"TC-VOLSYNC-008b continuing failures do not spam (edge latch holds)");
CHECK(s_release_calls == s_diskio_calls,
"TC-VOLSYNC-010 release paired with every attempt incl. failures (no lock leak)");
s_diskio_res = RES_OK;
advance_and_poll(PERIOD_TICKS);
CHECK((g_vis_error_count == 1) && (s_vol_sync_fail_latch == 0u),
"TC-VOLSYNC-009 success clears latch (re-armed for next episode)");
s_diskio_res = RES_ERROR;
advance_and_poll(PERIOD_TICKS);
CHECK(g_vis_error_count == 2, "TC-VOLSYNC-009b new failing episode logs again (one line)");
}
/* ===================== TC-VOLSYNC-011:tick 回绕 ===================== */
static void tc_wrap(void) {
fakes_reset();
sys_evtHandle = FAKE_EVT_HANDLE;
s_evt_flags = SYS_EVT_STORAGE_READY;
/* 白盒置态:上次尝试贴着 32 位顶端,当前拍刚跨过 0 */
s_vol_sync_armed = 1u;
s_vol_sync_last_ms = 0xFFFFF000u;
s_tick_now = 0x0300u; /* 无符号差 = 0x1300 = 4864 < 5000 */
vol_idle_sync_poll();
CHECK(s_diskio_calls == 0, "TC-VOLSYNC-011 wrap: unsigned-diff still suppresses early tick");
s_vol_sync_last_ms = 0xFFFFF000u;
s_tick_now = 0x0800u; /* 无符号差 = 0x1800 = 6144 >= 5000 */
vol_idle_sync_poll();
CHECK(s_diskio_calls == 1, "TC-VOLSYNC-011b wrap past zero fires when diff says so");
}
/* ===================== main ===================== */
int main(void) {
printf("=== Volume Idle Sync Tests ===\n");
printf("-- gate (readiness) --\n");
tc_gate();
printf("-- run / throttle / idle-skip --\n");
tc_run_throttle();
printf("-- failure latch / pairing --\n");
tc_fail_latch();
printf("-- tick wraparound --\n");
tc_wrap();
printf("\n=== Volume Idle Sync Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}