484d068250
道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 零触点、脏态时长、无心跳扰动)属实际 测量采样类——本批跳过,留「待台架实测」。
283 lines
11 KiB
C
283 lines
11 KiB
C
/*
|
||
* 模块名称: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;
|
||
}
|