Merge branch 'v2-on-ui' into task/meas-snap
# Conflicts: # scripts/id_registry.json
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""UI 数据快照层(ui_data)主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/ui_data/ui_data_test.c(它以 #include 纳入
|
||||
**生产源文件** Drivers/BSP/UI/ui_data.c,并同时 include 契约另一侧的
|
||||
App/algo 头),在 PC 上离线跑"快照存储 + 发布盖章 + 跨层下标一致性",
|
||||
失败即非 0 退出。零硬件、零烧录、毫秒级。
|
||||
工具链探测 / 编译运行全部委托 test/host/common/unit_test_harness.py。
|
||||
|
||||
为什么要主机侧单测(而不是只用模拟器像素回归):
|
||||
快照层是页面看到的整个世界,但 ui-golden 只覆盖"已接线的页面"——遥信 27 点、
|
||||
定值 40 项、遥测 9 路这类"还没接线的通道"错了不会让任何一张参考图变红。
|
||||
跨层不透明下标(yc 序 = adc_semantic_t、q10 序 = prot_set_id_t)正是解耦换来
|
||||
的风险面,契约把它钉在编译/测试期(docs/UI设计/UI_数据供给设计说明.md §9.4)。
|
||||
|
||||
用法(仓库根目录):
|
||||
python test/host/ui_data/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误。
|
||||
设计依据:docs/UI设计/UI_数据供给设计说明.md §9.1 / §12 判据 8(道E task-14);
|
||||
创建日期:2026-10-25
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
|
||||
|
||||
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
|
||||
|
||||
# 被测侧(BSP/UI)与契约另一侧(App/algo)都要在包含路径里:测试单元在同一 TU
|
||||
# 里同时看到两侧头,才有资格断言"页面表的下标指向预期枚举"。
|
||||
BUILD = TestBuild(
|
||||
name="UI Data Snapshot Tests",
|
||||
src=os.path.join(HERE, "ui_data_test.c"),
|
||||
include_dirs=[
|
||||
HERE,
|
||||
os.path.join(ROOT, "Drivers", "BSP", "UI"),
|
||||
os.path.join(ROOT, "App", "algo"),
|
||||
os.path.join(ROOT, "App", "log"), # soe_rec.h:SOE 事件类型编号真源
|
||||
],
|
||||
summary_marker="=== UI Data Snapshot Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
@@ -0,0 +1,538 @@
|
||||
/*
|
||||
* 模块名称:UI 数据快照层主机侧单元测试
|
||||
* 模块功能:在 PC 上离线验证 Drivers/BSP/UI/ui_data.c(快照存储 / 发布盖章 /
|
||||
* 只读访问)与跨层契约下标空间的一致性。
|
||||
*
|
||||
* 被测对象:真实源文件 ../../../../Drivers/BSP/UI/ui_data.c(以 #include 原样纳入,
|
||||
* 保证测的是生产代码而非副本;本模块零平台依赖,无需任何桩)
|
||||
*
|
||||
* 为什么要主机侧单测(而不是只靠模拟器像素回归与台架):
|
||||
* 1) 快照层是"页面看到的整个世界",它错一处全部页面一起错,而像素回归只看得到
|
||||
* "当前接了线的页面"——未接线的通道(遥信 27 点、定值 40 项)错了不会让任何
|
||||
* 一张参考图变红,只有把契约下标逐项钉死才照得出来;
|
||||
* 2) 跨层"魔法下标"(yc 序 = adc_semantic_t、q10 序 = prot_set_id_t)是解耦的
|
||||
* 代价,契约把它从运行期风险降级为编译/测试期错误的第 3 条断言就落在这里
|
||||
* (同一 TU 里同时 include 两侧头,见
|
||||
* docs/UI设计/UI_数据供给设计说明.md §9.4);
|
||||
* 3) "首帧之前显示 -- 而不是 0.000"是诚实性判据,板子上电只有一次、台架碰不到,
|
||||
* 主机侧可以随时复现。
|
||||
*
|
||||
* 字符串字面量一律纯 ASCII(编码铁律:断言名是打印用的,不参与渲染);
|
||||
* 用例名里的通道/点号语义见各 TC 上方注释。
|
||||
*
|
||||
* 覆盖点(TC-UIDATA-xxx;属主登记处 = 本目录,族 TC-HOST-UIDATA):
|
||||
* TC-UIDATA-001 跨层数量一致:UI_DATA_YC_N == ADC_SEM_COUNT、SET_N == PS_ID_COUNT
|
||||
* TC-UIDATA-002 跨层语义序逐项一致(UA/UB/UC/UN/IA/IB/IC/IN/TEMP = 0..8,
|
||||
* TEMP 恒为末槽——页面据此判"哪一路是码值域")
|
||||
* TC-UIDATA-003 首帧之前:三通道读 -1、seq = 0(页面渲染 "--" 的依据)
|
||||
* TC-UIDATA-004 发布后逐字段一致 + 序号盖章(入帧 seq 被忽略、由本层 +1)
|
||||
* TC-UIDATA-005 连续发布 seq 单调递增;三通道各自独立计数、内容互不串台
|
||||
* TC-UIDATA-006 NULL 防御:publish 返 -1 且存量不动;read 返 -2 且输出不动
|
||||
* TC-UIDATA-007 init 幂等复位(发布后再 init -> 回到无首帧态)
|
||||
* TC-UIDATA-008 量纲/品质常量:eng 三档互不重合且 PIN=0;六个品质位两两不相交;
|
||||
* 三帧宽度在内存预算内(快照不是 LVGL 对象,只占 .bss)
|
||||
* TC-UIDATA-009 遥信位集合 = YX15~18/23/31(六点);bits 与 valid 相互独立
|
||||
* TC-UIDATA-010 相量槽 ph_n 原样透传(写侧已钳位,本层不再加工)
|
||||
* TC-UIDATA-011 时钟字段容量足装 "YYYY-MM-DD HH:MM:SS"(19 + NUL)且不越界
|
||||
* TC-UIDATA-012 SOE 事件类型不透明编号与 App/log/soe_rec.h 枚举逐项相等(含
|
||||
* 未标定闭锁=7 / 标定复归=8);八类事件全部有显示名、名长不超
|
||||
* 值列宽、采样与标定两组四名互不相同、越界编号回占位
|
||||
* TC-UIDATA-013 SOE 元件编号与 App/algo/prot_elm.h 枚举逐项相等(0..8 +
|
||||
* PE_COUNT == 表长);元件名不超名列宽、非元件/越界回占位
|
||||
* TC-UIDATA-014 注入 reader 后的两行渲染:时标行/内容行分工、单条读盘缓存
|
||||
* (同记录两行只读一次)、IV 时标抑制、读失败退化与不绑空态
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
*
|
||||
* 创建日期:2026-10-25
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* 生产源文件原样纳入(纯内存叶子,零平台依赖,直接编) */
|
||||
#include "ui_data.c"
|
||||
|
||||
/* 契约另一侧的头:跨层下标空间一致性断言(本目录同时 include BSP 与 App 两侧头,
|
||||
* 这就是契约 §9.4 第 3 条断言的落点) */
|
||||
#include "adc_input.h" /* adc_semantic_t:遥测下标真源 */
|
||||
#include "prot_set.h" /* prot_set_id_t:定值下标真源 */
|
||||
|
||||
/* 第二个被测件:SOE 显示名映射组件(道E task-14 落的表现层映射表)。
|
||||
* 本组件已解除 LVGL 依赖(容量宏本地化,见其 .c 内注释),可直接编进主机单测 */
|
||||
#include "ui_w_soe.c"
|
||||
#include "soe_rec.h" /* soe_type_t:事件类型编号真源 */
|
||||
#include "prot_elm.h" /* prot_elm_id_t:元件编号真源 */
|
||||
|
||||
/* ===================== 断言框架 ===================== */
|
||||
|
||||
static int s_total;
|
||||
static int s_passed;
|
||||
static int s_failed;
|
||||
|
||||
#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)
|
||||
|
||||
/* 造一帧可辨识内容的实时帧(每个字段都填不同值,串台/漏拷立刻暴露) */
|
||||
static void fill_rt(ui_data_rt_t *p, uint8_t seed) {
|
||||
uint8_t i;
|
||||
|
||||
(void)memset(p, 0, sizeof(*p));
|
||||
p->seq = 777u; /* 故意填非 0:本层应忽略并自己盖章 */
|
||||
p->tick_ms = 12345u + seed;
|
||||
p->valid = 1u;
|
||||
p->eng = UI_DATA_ENG_PIN;
|
||||
p->ph_n = (uint8_t)(seed % (UI_DATA_PH_MAX + 1u));
|
||||
for (i = 0u; i < UI_DATA_YC_N; i++) {
|
||||
p->yc[i].val = (float)i + 0.5f;
|
||||
p->yc[i].qds = (uint8_t)(i + 1u);
|
||||
}
|
||||
for (i = 0u; i < UI_DATA_PH_MAX; i++) {
|
||||
p->ph[i].val = (float)i * 2.0f;
|
||||
p->ph[i].qds = (uint8_t)(0xA0u + i);
|
||||
}
|
||||
p->yx_bits = 0x000000010000ULL;
|
||||
p->yx_valid = 0x4043C000ULL;
|
||||
p->freq_valid = 1u;
|
||||
p->freq_hz = 50.02f;
|
||||
p->clock_ok = 1u;
|
||||
(void)snprintf(p->clock, sizeof(p->clock), "%s", "2026-10-25 08:00:00");
|
||||
}
|
||||
|
||||
/* 逐字段比对(seq 除外:本层盖章后必然与入帧不同,比对前由调用方对齐) */
|
||||
static int rt_equals(const ui_data_rt_t *a, const ui_data_rt_t *b) {
|
||||
if (a->tick_ms != b->tick_ms) return 0;
|
||||
if (a->valid != b->valid) return 0;
|
||||
if (a->eng != b->eng) return 0;
|
||||
if (a->ph_n != b->ph_n) return 0;
|
||||
if (a->yx_bits != b->yx_bits) return 0;
|
||||
if (a->yx_valid != b->yx_valid) return 0;
|
||||
if (a->freq_valid != b->freq_valid) return 0;
|
||||
if (a->freq_hz != b->freq_hz) return 0;
|
||||
if (a->clock_ok != b->clock_ok) return 0;
|
||||
if (strcmp(a->clock, b->clock) != 0) return 0;
|
||||
if (memcmp(a->yc, b->yc, sizeof(a->yc)) != 0) return 0;
|
||||
if (memcmp(a->ph, b->ph, sizeof(a->ph)) != 0) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ===================== TC-UIDATA-001 / 002:跨层下标空间 ===================== */
|
||||
|
||||
static void tc_contract_indices(void)
|
||||
{
|
||||
CHECK(UI_DATA_YC_N == (uint32_t)ADC_SEM_COUNT, "TC-UIDATA-001a yc slots == ADC_SEM_COUNT");
|
||||
CHECK(UI_DATA_SET_N == (uint32_t)PS_ID_COUNT, "TC-UIDATA-001b q10 slots == PS_ID_COUNT");
|
||||
CHECK((uint32_t)ADC_SEM_UA == 0u, "TC-UIDATA-002a UA == 0");
|
||||
CHECK((uint32_t)ADC_SEM_UB == 1u, "TC-UIDATA-002b UB == 1");
|
||||
CHECK((uint32_t)ADC_SEM_UC == 2u, "TC-UIDATA-002c UC == 2");
|
||||
CHECK((uint32_t)ADC_SEM_UN == 3u, "TC-UIDATA-002d UN == 3");
|
||||
CHECK((uint32_t)ADC_SEM_IA == 4u, "TC-UIDATA-002e IA == 4");
|
||||
CHECK((uint32_t)ADC_SEM_IB == 5u, "TC-UIDATA-002f IB == 5");
|
||||
CHECK((uint32_t)ADC_SEM_IC == 6u, "TC-UIDATA-002g IC == 6");
|
||||
CHECK((uint32_t)ADC_SEM_IN == 7u, "TC-UIDATA-002h IN == 7");
|
||||
/* TEMP 必须是末槽:写侧"sem < ADC_SEM_TEMP 才是电气量"的 eng 算法与页面的
|
||||
* RAW 位判定都靠这个位置,插中间等于两条判据一起错位 */
|
||||
CHECK((uint32_t)ADC_SEM_TEMP == (UI_DATA_YC_N - 1u), "TC-UIDATA-002i TEMP is last slot");
|
||||
}
|
||||
|
||||
/* ===================== TC-UIDATA-003:首帧之前 ===================== */
|
||||
|
||||
static void tc_before_first_frame(void)
|
||||
{
|
||||
ui_data_rt_t rt;
|
||||
ui_data_par_t par;
|
||||
ui_data_info_t info;
|
||||
|
||||
ui_data_init();
|
||||
(void)memset(&rt, 0x5A, sizeof(rt));
|
||||
(void)memset(&par, 0x5A, sizeof(par));
|
||||
(void)memset(&info, 0x5A, sizeof(info));
|
||||
|
||||
CHECK(ui_data_rt(&rt) == -1, "TC-UIDATA-003a rt read before first frame == -1");
|
||||
CHECK(ui_data_par(&par) == -1, "TC-UIDATA-003b par read before first frame == -1");
|
||||
CHECK(ui_data_info(&info) == -1, "TC-UIDATA-003c info read before first frame == -1");
|
||||
CHECK((ui_data_rt_seq() == 0u) && (ui_data_par_seq() == 0u) && (ui_data_info_seq() == 0u),
|
||||
"TC-UIDATA-003d all three seq == 0");
|
||||
/* 输出缓冲必须一字不动——页面缓存被写脏会把"无数据"渲染成"有数据" */
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)&rt;
|
||||
int dirty = 0;
|
||||
size_t i;
|
||||
|
||||
for (i = 0u; i < sizeof(rt); i++) {
|
||||
if (p[i] != 0x5Au) {
|
||||
dirty = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(dirty == 0, "TC-UIDATA-003e output buffer untouched on failure");
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== TC-UIDATA-004 / 005:发布盖章与通道独立 ===================== */
|
||||
|
||||
static void tc_publish_and_seq(void)
|
||||
{
|
||||
ui_data_rt_t src;
|
||||
ui_data_rt_t got;
|
||||
ui_data_par_t par;
|
||||
ui_data_par_t pg;
|
||||
uint32_t seq1;
|
||||
|
||||
ui_data_init();
|
||||
fill_rt(&src, 3u);
|
||||
CHECK(ui_data_publish_rt(&src) == 0, "TC-UIDATA-004a publish returns 0");
|
||||
CHECK(ui_data_rt_seq() == 1u, "TC-UIDATA-004b seq stamped 1 (incoming 777 ignored)");
|
||||
(void)memset(&got, 0, sizeof(got));
|
||||
CHECK(ui_data_rt(&got) == 0, "TC-UIDATA-004c readable after first publish");
|
||||
got.seq = src.seq; /* 序号由本层盖章,其余字段须逐字节一致 */
|
||||
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-004d all fields preserved verbatim");
|
||||
|
||||
/* 连续发布:seq 单调 +1,内容取最新一帧 */
|
||||
fill_rt(&src, 9u);
|
||||
(void)ui_data_publish_rt(&src);
|
||||
seq1 = ui_data_rt_seq();
|
||||
CHECK(seq1 == 2u, "TC-UIDATA-005a second publish bumps seq to 2");
|
||||
(void)ui_data_rt(&got);
|
||||
got.seq = src.seq;
|
||||
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-005b latest frame is the one served");
|
||||
|
||||
/* 三通道独立:发 par 不动 rt 的 seq,也不动 rt 内容 */
|
||||
(void)memset(&par, 0, sizeof(par));
|
||||
par.valid = 1u;
|
||||
par.checksum = 0xABCDu;
|
||||
par.q10[0] = PROT_SET_Q10(10.0);
|
||||
par.q10[UI_DATA_SET_N - 1u] = PROT_SET_Q10(55.5);
|
||||
(void)ui_data_publish_par(&par);
|
||||
CHECK((ui_data_par_seq() == 1u) && (ui_data_rt_seq() == seq1),
|
||||
"TC-UIDATA-005c par publish does not touch rt seq");
|
||||
(void)memset(&pg, 0, sizeof(pg));
|
||||
CHECK((ui_data_par(&pg) == 0) && (pg.checksum == 0xABCDu)
|
||||
&& (pg.q10[0] == PROT_SET_Q10(10.0))
|
||||
&& (pg.q10[UI_DATA_SET_N - 1u] == PROT_SET_Q10(55.5)),
|
||||
"TC-UIDATA-005d par stored verbatim (Q10 not rescaled here)");
|
||||
(void)ui_data_rt(&got);
|
||||
got.seq = src.seq;
|
||||
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-005e par publish does not pollute rt");
|
||||
}
|
||||
|
||||
/* ===================== TC-UIDATA-006:NULL 防御 ===================== */
|
||||
|
||||
static void tc_null_guard(void)
|
||||
{
|
||||
ui_data_rt_t src;
|
||||
ui_data_rt_t got;
|
||||
|
||||
ui_data_init();
|
||||
fill_rt(&src, 1u);
|
||||
(void)ui_data_publish_rt(&src);
|
||||
CHECK(ui_data_publish_rt(NULL) == -1, "TC-UIDATA-006a publish(NULL) == -1");
|
||||
CHECK(ui_data_rt_seq() == 1u, "TC-UIDATA-006b failed publish keeps seq");
|
||||
CHECK(ui_data_rt(NULL) == -2, "TC-UIDATA-006c read(NULL) == -2");
|
||||
(void)memset(&got, 0, sizeof(got));
|
||||
(void)ui_data_rt(&got);
|
||||
got.seq = src.seq;
|
||||
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-006d stored frame still intact");
|
||||
}
|
||||
|
||||
/* ===================== TC-UIDATA-007:init 幂等复位 ===================== */
|
||||
|
||||
static void tc_init_idempotent(void)
|
||||
{
|
||||
ui_data_rt_t src;
|
||||
ui_data_rt_t got;
|
||||
|
||||
ui_data_init();
|
||||
fill_rt(&src, 2u);
|
||||
(void)ui_data_publish_rt(&src);
|
||||
(void)ui_data_publish_rt(NULL);
|
||||
ui_data_init();
|
||||
ui_data_init(); /* 重复调用仍幂等 */
|
||||
CHECK(ui_data_rt_seq() == 0u, "TC-UIDATA-007a seq reset to 0 by init");
|
||||
CHECK(ui_data_rt(&got) == -1, "TC-UIDATA-007b back to no-frame state after init");
|
||||
}
|
||||
|
||||
/* ===================== TC-UIDATA-008:量纲与品质常量 ===================== */
|
||||
|
||||
static void tc_constants(void)
|
||||
{
|
||||
CHECK((UI_DATA_ENG_PIN == 0u) && (UI_DATA_ENG_SEC == 1u) && (UI_DATA_ENG_PRI == 2u),
|
||||
"TC-UIDATA-008a eng levels are 0/1/2 (pin-V / secondary / primary)");
|
||||
CHECK((UI_DATA_Q_IV | UI_DATA_Q_NT | UI_DATA_Q_OV | UI_DATA_Q_SB | UI_DATA_Q_BL
|
||||
| UI_DATA_Q_RAW) == 0x3Fu, "TC-UIDATA-008b six quality bits fill low 6 bits");
|
||||
CHECK((UI_DATA_Q_IV & UI_DATA_Q_NT) == 0u, "TC-UIDATA-008c IV/NT disjoint");
|
||||
CHECK((UI_DATA_Q_OV & UI_DATA_Q_SB) == 0u, "TC-UIDATA-008d OV/SB disjoint");
|
||||
CHECK((UI_DATA_Q_BL & UI_DATA_Q_RAW) == 0u, "TC-UIDATA-008e BL/RAW disjoint");
|
||||
/* 内存预算护栏(docs/UI设计/UI_数据供给设计说明.md §10):快照是 .bss 里的
|
||||
* 定长结构,不是 LVGL 对象;一旦有人往里塞大数组或指针,这里先响 */
|
||||
CHECK(sizeof(ui_data_rt_t) <= 256u, "TC-UIDATA-008f rt frame within budget (<=256B)");
|
||||
CHECK(sizeof(ui_data_par_t) <= 200u, "TC-UIDATA-008g par frame within budget (<=200B)");
|
||||
CHECK(sizeof(ui_data_info_t) <= 128u, "TC-UIDATA-008h info frame within budget (<=128B)");
|
||||
}
|
||||
|
||||
/* ===================== TC-UIDATA-009:遥信位集合与独立性 ===================== */
|
||||
|
||||
static void tc_yx_points(void)
|
||||
{
|
||||
/* V0.1 有硬件来源的遥信点集合(YX15~18/23/31,1 基序号):与 104 点表同源,
|
||||
* 写侧 App/app/ui_data_feed.c 与 simulator/main.c 固定帧都用同一组位号。
|
||||
* 任何一处改动都会让本条红,逼着改动方同时更新页面名表与位图镜像注释 */
|
||||
const uint64_t valid_expect = ((uint64_t)1u << 14u) | ((uint64_t)1u << 15u) |
|
||||
((uint64_t)1u << 16u) | ((uint64_t)1u << 17u) |
|
||||
((uint64_t)1u << 22u) | ((uint64_t)1u << 30u);
|
||||
ui_data_rt_t src;
|
||||
ui_data_rt_t got;
|
||||
|
||||
ui_data_init();
|
||||
fill_rt(&src, 5u);
|
||||
src.yx_bits = 0u; /* 全部点位"分/未动作" */
|
||||
src.yx_valid = valid_expect;
|
||||
(void)ui_data_publish_rt(&src);
|
||||
(void)memset(&got, 0, sizeof(got));
|
||||
(void)ui_data_rt(&got);
|
||||
CHECK(got.yx_valid == valid_expect, "TC-UIDATA-009a valid mask == YX15/16/17/18/23/31");
|
||||
CHECK(got.yx_bits == 0u, "TC-UIDATA-009b bits and valid are independent fields");
|
||||
}
|
||||
|
||||
/* ===================== TC-UIDATA-010:相量槽透传 ===================== */
|
||||
|
||||
static void tc_phasor_slots(void)
|
||||
{
|
||||
ui_data_rt_t src;
|
||||
ui_data_rt_t got;
|
||||
uint8_t i;
|
||||
|
||||
ui_data_init();
|
||||
fill_rt(&src, UI_DATA_PH_MAX);
|
||||
src.ph_n = UI_DATA_PH_MAX;
|
||||
(void)ui_data_publish_rt(&src);
|
||||
(void)memset(&got, 0, sizeof(got));
|
||||
(void)ui_data_rt(&got);
|
||||
CHECK(got.ph_n == UI_DATA_PH_MAX, "TC-UIDATA-010a ph_n passed through unchanged");
|
||||
i = 0u;
|
||||
while ((i < UI_DATA_PH_MAX) && (got.ph[i].val == src.ph[i].val)) {
|
||||
i++;
|
||||
}
|
||||
CHECK(i == UI_DATA_PH_MAX, "TC-UIDATA-010b every phasor slot kept its value");
|
||||
}
|
||||
|
||||
/* ===================== TC-UIDATA-011:时钟字段容量 ===================== */
|
||||
|
||||
static void tc_clock_field(void)
|
||||
{
|
||||
ui_data_rt_t src;
|
||||
ui_data_rt_t got;
|
||||
char probe[UI_DATA_CLOCK_LEN];
|
||||
|
||||
(void)snprintf(probe, sizeof(probe), "%s", "2026-10-25 08:00:00");
|
||||
CHECK(strlen(probe) == 19u, "TC-UIDATA-011a clock string is 19 chars");
|
||||
CHECK(UI_DATA_CLOCK_LEN == 20u, "TC-UIDATA-011b capacity is 19 + NUL");
|
||||
ui_data_init();
|
||||
fill_rt(&src, 7u);
|
||||
(void)ui_data_publish_rt(&src);
|
||||
(void)memset(&got, 0, sizeof(got));
|
||||
(void)ui_data_rt(&got);
|
||||
CHECK((got.clock_ok == 1u) && (strcmp(got.clock, "2026-10-25 08:00:00") == 0)
|
||||
&& (got.clock[UI_DATA_CLOCK_LEN - 1u] == '\0'),
|
||||
"TC-UIDATA-011c full string stored, last byte still NUL");
|
||||
}
|
||||
|
||||
/* ====== TC-UIDATA-012 / 013:SOE 不透明编号与显示名映射(现场核对枚举头) ====== */
|
||||
|
||||
/* 名字表版式护栏:名列 7 汉字 = 21 字节、值列 4 汉字 = 12 字节(UTF-8 3 字节/字);
|
||||
* 超宽只会被屏幕裁掉,不会被任何像素门禁报出来,所以在这里钉死
|
||||
* (宽度口径 docs/UI设计/页面扩充详细方案.md §2.3 第 4 条) */
|
||||
static int all_names_fit(const char *const *tbl, unsigned n, size_t max_bytes) {
|
||||
unsigned i;
|
||||
|
||||
for (i = 0u; i < n; i++) {
|
||||
if (tbl[i] == NULL) return 0;
|
||||
if (strlen(tbl[i]) == 0u) return 0;
|
||||
if (strlen(tbl[i]) > max_bytes) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void tc_soe_codes(void)
|
||||
{
|
||||
/* 编号与 App/log/soe_rec.h 枚举逐项相等(不是照抄文档转述,是 include 真头对表) */
|
||||
CHECK((uint8_t)SOE_T_ELM_ASSERT == UI_W_SOE_T_ELM_ASSERT, "TC-UIDATA-012a type ELM_ASSERT");
|
||||
CHECK((uint8_t)SOE_T_ELM_RETURN == UI_W_SOE_T_ELM_RETURN, "TC-UIDATA-012b type ELM_RETURN");
|
||||
CHECK((uint8_t)SOE_T_PLATE_OUT == UI_W_SOE_T_PLATE_OUT, "TC-UIDATA-012c type PLATE_OUT");
|
||||
CHECK((uint8_t)SOE_T_PLATE_IN == UI_W_SOE_T_PLATE_IN, "TC-UIDATA-012d type PLATE_IN");
|
||||
CHECK((uint8_t)SOE_T_SAMPLE_BLOCK == UI_W_SOE_T_SAMPLE_BLOCK, "TC-UIDATA-012e type SAMPLE_BLOCK");
|
||||
CHECK((uint8_t)SOE_T_SAMPLE_RESUME == UI_W_SOE_T_SAMPLE_RESUME, "TC-UIDATA-012f type SAMPLE_RESUME");
|
||||
CHECK((uint8_t)SOE_T_CAL_BLOCK == UI_W_SOE_T_CAL_BLOCK, "TC-UIDATA-012g type CAL_BLOCK == 7");
|
||||
CHECK((uint8_t)SOE_T_CAL_RESUME == UI_W_SOE_T_CAL_RESUME, "TC-UIDATA-012h type CAL_RESUME == 8");
|
||||
CHECK((uint8_t)SOE_T_COUNT == UI_W_SOE_T_COUNT, "TC-UIDATA-012i sentinel COUNT == 9");
|
||||
CHECK(SOE_ELM_NONE == UI_W_SOE_ELM_NONE, "TC-UIDATA-012j ELM_NONE == 0xFF");
|
||||
CHECK(SOE_FLG_IV == UI_W_SOE_FLG_IV, "TC-UIDATA-012k FLG_IV == 0x80");
|
||||
/* 八类事件全部有名(新加类型忘配名 = 屏上"--",像素门禁照不出来) */
|
||||
{
|
||||
uint8_t t;
|
||||
int ok = 1;
|
||||
size_t longest = 0u;
|
||||
|
||||
for (t = 1u; t < UI_W_SOE_T_COUNT; t++) {
|
||||
const char *nm = ui_w_soe_type_name(t);
|
||||
|
||||
if (nm == NULL) {
|
||||
ok = 0; /* 契约上不可达(映射表恒返回串),留作防御分支 */
|
||||
} else {
|
||||
if ((strcmp(nm, "--") == 0) || (strlen(nm) == 0u)) {
|
||||
ok = 0;
|
||||
}
|
||||
if (strlen(nm) > longest) {
|
||||
longest = strlen(nm);
|
||||
}
|
||||
}
|
||||
}
|
||||
CHECK(ok == 1, "TC-UIDATA-012l every type has a display name");
|
||||
CHECK(longest <= 12u, "TC-UIDATA-012m type names fit value column (<=4 CJK)");
|
||||
CHECK(strcmp(ui_w_soe_type_name(0u), "--") == 0, "TC-UIDATA-012n code 0 -> placeholder");
|
||||
CHECK(strcmp(ui_w_soe_type_name(UI_W_SOE_T_COUNT), "--") == 0,
|
||||
"TC-UIDATA-012o out-of-range -> placeholder");
|
||||
/* 类型 5/6(采样)与 7/8(标定)四者必须互不相同:同名 = 抄表时串行,
|
||||
* 现场排障会把"未标定闭锁"读成"采样闭锁" */
|
||||
CHECK((strcmp(ui_w_soe_type_name(UI_W_SOE_T_SAMPLE_BLOCK),
|
||||
ui_w_soe_type_name(UI_W_SOE_T_CAL_BLOCK)) != 0)
|
||||
&& (strcmp(ui_w_soe_type_name(UI_W_SOE_T_SAMPLE_RESUME),
|
||||
ui_w_soe_type_name(UI_W_SOE_T_CAL_RESUME)) != 0)
|
||||
&& (strcmp(ui_w_soe_type_name(UI_W_SOE_T_CAL_BLOCK),
|
||||
ui_w_soe_type_name(UI_W_SOE_T_CAL_RESUME)) != 0),
|
||||
"TC-UIDATA-012p sample/cal lock-resume names are distinct");
|
||||
}
|
||||
}
|
||||
|
||||
static void tc_soe_elms(void)
|
||||
{
|
||||
CHECK(PE_OC1 == 0, "TC-UIDATA-013a PE_OC1 == 0");
|
||||
CHECK(PE_OC2 == 1, "TC-UIDATA-013b PE_OC2 == 1");
|
||||
CHECK(PE_OC3 == 2, "TC-UIDATA-013c PE_OC3 == 2");
|
||||
CHECK(PE_OL == 3, "TC-UIDATA-013d PE_OL == 3");
|
||||
CHECK(PE_OV == 4, "TC-UIDATA-013e PE_OV == 4");
|
||||
CHECK(PE_UV == 5, "TC-UIDATA-013f PE_UV == 5");
|
||||
CHECK(PE_3U0 == 6, "TC-UIDATA-013g PE_3U0 == 6");
|
||||
CHECK(PE_OF == 7, "TC-UIDATA-013h PE_OF == 7");
|
||||
CHECK(PE_UF == 8, "TC-UIDATA-013i PE_UF == 8");
|
||||
CHECK((uint32_t)PE_COUNT == UI_W_SOE_ELM_N, "TC-UIDATA-013j PE_COUNT == UI_W_SOE_ELM_N");
|
||||
CHECK(all_names_fit(s_elm_name, UI_W_SOE_ELM_N, 21u) == 1,
|
||||
"TC-UIDATA-013k element names fit name column (<=7 CJK)");
|
||||
CHECK(strcmp(ui_w_soe_elm_name(UI_W_SOE_ELM_NONE), "--") == 0,
|
||||
"TC-UIDATA-013l non-element id -> placeholder");
|
||||
CHECK(strcmp(ui_w_soe_elm_name(UI_W_SOE_ELM_N), "--") == 0,
|
||||
"TC-UIDATA-013m out-of-range element -> placeholder");
|
||||
}
|
||||
|
||||
/* ====== TC-UIDATA-014:注入 reader 后的两行渲染与单条缓存 ====== */
|
||||
|
||||
static uint32_t s_get_calls;
|
||||
|
||||
static ui_w_soe_rec_t s_fake_rec;
|
||||
|
||||
static uint16_t fake_count(void)
|
||||
{
|
||||
return 3u;
|
||||
}
|
||||
|
||||
static int fake_get(uint32_t a_idx, ui_w_soe_rec_t *a_out)
|
||||
{
|
||||
s_get_calls++;
|
||||
if (a_idx >= 3u) {
|
||||
return -1;
|
||||
}
|
||||
(void)memcpy(a_out, &s_fake_rec, sizeof(*a_out));
|
||||
a_out->seq = a_idx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const ui_soe_reader_t s_fake_reader = { fake_count, fake_get };
|
||||
|
||||
static void tc_soe_render(void)
|
||||
{
|
||||
ui_w_soe_t inst;
|
||||
char name[UI_W_SOE_TS_LEN + 16u];
|
||||
char value[UI_W_SOE_TS_LEN + 16u];
|
||||
|
||||
(void)memset(&inst, 0, sizeof(inst));
|
||||
(void)memset(&s_fake_rec, 0, sizeof(s_fake_rec));
|
||||
s_reader = NULL;
|
||||
CHECK(ui_w_soe_count(&inst) == 1u, "TC-UIDATA-014a unbound reader -> empty pseudo row");
|
||||
ui_w_soe_bind(&s_fake_reader);
|
||||
CHECK(ui_w_soe_count(&inst) == 6u, "TC-UIDATA-014b 3 records -> 6 rows");
|
||||
|
||||
/* 元件事件:名列 = 元件,值列 = 动作/返回 */
|
||||
(void)strcpy(s_fake_rec.ts, "2026-10-25 08:00:00");
|
||||
s_fake_rec.type = UI_W_SOE_T_ELM_ASSERT;
|
||||
s_fake_rec.elm = (uint8_t)PE_OC1;
|
||||
s_fake_rec.flags = 0u;
|
||||
s_get_calls = 0u;
|
||||
(void)memset(name, 0, sizeof(name));
|
||||
(void)memset(value, 0, sizeof(value));
|
||||
ui_w_soe_refresh(&inst, 0u, name, value);
|
||||
CHECK((name[0] != 0) && (strlen(name) == 19u) && (value[0] == 0),
|
||||
"TC-UIDATA-014c ts row shows full timestamp");
|
||||
ui_w_soe_refresh(&inst, 1u, name, value);
|
||||
CHECK((strlen(name) == 12u) && (strlen(value) == 6u) && (s_get_calls == 1u),
|
||||
"TC-UIDATA-014d content row uses cached record (single disk read)");
|
||||
|
||||
/* IV 时标:不显示猜测时间,值列给"时钟无效" */
|
||||
s_fake_rec.flags = UI_W_SOE_FLG_IV;
|
||||
(void)memset(&inst, 0, sizeof(inst));
|
||||
ui_w_soe_refresh(&inst, 0u, name, value);
|
||||
CHECK((strcmp(name, "--") == 0) && (strlen(value) == 12u),
|
||||
"TC-UIDATA-014e IV timestamp suppressed, marked in value column");
|
||||
|
||||
/* 非元件事件:类型名进名列,值列留空 */
|
||||
s_fake_rec.flags = 0u;
|
||||
s_fake_rec.type = UI_W_SOE_T_CAL_BLOCK;
|
||||
s_fake_rec.elm = UI_W_SOE_ELM_NONE;
|
||||
(void)memset(&inst, 0, sizeof(inst));
|
||||
ui_w_soe_refresh(&inst, 3u, name, value);
|
||||
CHECK((strlen(name) == 12u) && (value[0] == 0), "TC-UIDATA-014f non-element row: type in name column");
|
||||
|
||||
/* 越界记录号:读失败退化为占位,不留脏缓存 */
|
||||
ui_w_soe_refresh(&inst, 998u, name, value);
|
||||
CHECK(strcmp(name, "--") == 0, "TC-UIDATA-014g failed read -> placeholder, no stale cache");
|
||||
ui_w_soe_bind(NULL);
|
||||
}
|
||||
|
||||
/* ===================== main ===================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== UI Data Snapshot Tests ===\n");
|
||||
|
||||
printf("-- contract index space --\n");
|
||||
tc_contract_indices();
|
||||
printf("-- before first frame --\n");
|
||||
tc_before_first_frame();
|
||||
printf("-- publish and seq --\n");
|
||||
tc_publish_and_seq();
|
||||
printf("-- null guard --\n");
|
||||
tc_null_guard();
|
||||
printf("-- init idempotent --\n");
|
||||
tc_init_idempotent();
|
||||
printf("-- constants --\n");
|
||||
tc_constants();
|
||||
printf("-- yx points --\n");
|
||||
tc_yx_points();
|
||||
printf("-- phasor slots --\n");
|
||||
tc_phasor_slots();
|
||||
printf("-- clock field --\n");
|
||||
tc_clock_field();
|
||||
printf("-- soe codes and names --\n");
|
||||
tc_soe_codes();
|
||||
printf("-- soe elements --\n");
|
||||
tc_soe_elms();
|
||||
printf("-- soe render --\n");
|
||||
tc_soe_render();
|
||||
|
||||
printf("\n=== UI Data Snapshot Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""写回缓存空闲 flush(vol_idle_sync)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/vol_idle_sync/vol_idle_sync_test.c(它以 #include
|
||||
纳入**生产源文件** App/fs/vol_idle_sync.c),在 PC 上离线跑就绪门控 / 节流边界 /
|
||||
卷忙跳过 / 失败边沿 latch / 成对放锁 / tick 回绕,失败即非 0 退出。
|
||||
零硬件、零烧录、毫秒级。
|
||||
|
||||
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
|
||||
`test/host/common/unit_test_harness.py`(本文件只做声明)。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
本模块是「掉电窗口兜底网」——掉电与脏页驻留窗在台架上都不可按需复现;卷忙 /
|
||||
ioctl 失败 / 32 位 tick 回绕这些分支在真机上构造成本极高。门控、节流、latch
|
||||
全是纯状态逻辑,主机侧一次钉死。
|
||||
|
||||
用法(仓库根目录):
|
||||
python test/host/vol_idle_sync/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
|
||||
|
||||
设计依据:App/fs/vol_idle_sync.h 文件头(含「无掉电检测硬件」边界声明);
|
||||
创建日期:2026-10-25
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||||
# harness 位于 test/host/common/
|
||||
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
|
||||
|
||||
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
|
||||
|
||||
# 顺序敏感:stub 目录必须**先于**生产目录,才能覆盖 test_config.h / cmsis_os(_2).h /
|
||||
# FreeRTOS.h / task.h / dbg_log.h(真实头会拖入整套 FreeRTOS/CMSIS/HAL)。
|
||||
# ff.h / diskio.h / vol_lock.h / sys_events.h 用**真实头**(接口契约与就绪位数值
|
||||
# 原汁,同 lftpd_io / nand_port 先例;本模块经真实 sys_events.h 拿就绪位定义)。
|
||||
BUILD = TestBuild(
|
||||
name="Volume Idle Sync Tests",
|
||||
src=os.path.join(HERE, "vol_idle_sync_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "App", "fs"),
|
||||
os.path.join(ROOT, "App", "app"),
|
||||
os.path.join(ROOT, "Lib", "FatFs"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== Volume Idle Sync Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 主机侧单测桩:FreeRTOS.h(vol_idle_sync 模块专用)
|
||||
* 模块功能:最小 FreeRTOS 时基类型与宏(真实头会拉整棵 portmacro/projdefs)。
|
||||
* ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。
|
||||
* 保真点:TickType_t = uint32_t;configTICK_RATE_HZ = 1000
|
||||
* (Inc/FreeRTOSConfig.h)→ pdMS_TO_TICKS(ms) == ms。
|
||||
* 节流用例的 4999/5000 拍边界直接依赖该等式。
|
||||
* 创建日期:2026-10-25
|
||||
*/
|
||||
|
||||
#ifndef FREERTOS_H
|
||||
#define FREERTOS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef uint32_t TickType_t;
|
||||
|
||||
#define configTICK_RATE_HZ 1000U
|
||||
#define pdMS_TO_TICKS(ms) ((TickType_t)((uint32_t)(ms) * (1000U / configTICK_RATE_HZ)))
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* FREERTOS_H */
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* 主机侧单测桩:cmsis_os.h(vol_idle_sync 模块专用)
|
||||
* 模块功能:真实 cmsis_os.h(CMSIS-RTOS V2 包装)会拖入整套 FreeRTOS;本模块
|
||||
* 只需要事件标志接口,全部收敛在 cmsis_os2.h 桩里。
|
||||
* ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-25
|
||||
*/
|
||||
|
||||
#ifndef CMSIS_OS_H
|
||||
#define CMSIS_OS_H
|
||||
|
||||
#include "cmsis_os2.h"
|
||||
|
||||
#endif /* CMSIS_OS_H */
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 主机侧单测桩:cmsis_os2.h(vol_idle_sync 模块专用)
|
||||
* 模块功能:为 App/fs/vol_idle_sync.c(含经 sys_events.h)提供主机可编译的
|
||||
* CMSIS-RTOS v2 事件标志最小子集。
|
||||
* ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。
|
||||
* 保真点(对齐真实 cmsis_os2.h):
|
||||
* - osEventFlagsId_t = void*;
|
||||
* - uint32_t osEventFlagsGet(osEventFlagsId_t)(返回当前位图,不消费——
|
||||
* 就绪位 osFlagsNoClear 语义在生产调用点之外,本桩只回静态值);
|
||||
* - 就绪位数值一律来自**真实** App/app/sys_events.h(不在本桩复刻)。
|
||||
* osEventFlagsGet 由测试单元给强符号桩(可注入位图)。
|
||||
* 创建日期:2026-10-25
|
||||
*/
|
||||
|
||||
#ifndef CMSIS_OS2_H
|
||||
#define CMSIS_OS2_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void *osEventFlagsId_t;
|
||||
|
||||
uint32_t osEventFlagsGet(osEventFlagsId_t ef_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* CMSIS_OS2_H */
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 主机侧单测桩:dbg_log.h(vol_idle_sync 模块专用)
|
||||
* 模块功能:DBG_* 换成计数,让"失败留痕 = 边沿 latch、成功不刷屏"可断言。
|
||||
* ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-25
|
||||
*/
|
||||
|
||||
#ifndef DBG_LOG_H
|
||||
#define DBG_LOG_H
|
||||
|
||||
extern int g_vis_error_count;
|
||||
extern int g_vis_info_count;
|
||||
|
||||
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_vis_info_count++; } while (0)
|
||||
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_vis_error_count++; } while (0)
|
||||
|
||||
#endif /* DBG_LOG_H */
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 主机侧单测桩:task.h(vol_idle_sync 模块专用)
|
||||
* 模块功能:只声明 xTaskGetTickCount,由测试单元给可控时钟强符号桩
|
||||
* (驱动节流边界与 32 位回绕用例)。
|
||||
* ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-25
|
||||
*/
|
||||
|
||||
#ifndef TASK_H
|
||||
#define TASK_H
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
TickType_t xTaskGetTickCount(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* TASK_H */
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 主机侧单测桩:test_config.h(vol_idle_sync 模块专用)
|
||||
* 模块功能:给主机编译一个"产品构建"形态——四个 TEST_SUITE_* 全部未定义,
|
||||
* vol_idle_sync.c 走产品分支(STORAGE 套件的 no-op 分支由板端
|
||||
* test-suite-switch 构建侧覆盖,主机侧不重复裁剪测试)。
|
||||
* ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-25
|
||||
*/
|
||||
|
||||
#ifndef TEST_CONFIG_H
|
||||
#define TEST_CONFIG_H
|
||||
|
||||
/* 刻意不定义任何 TEST_SUITE_* / ENABLE_DESTRUCTIVE_TESTS */
|
||||
|
||||
#endif /* TEST_CONFIG_H */
|
||||
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* 模块名称: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;
|
||||
}
|
||||
Reference in New Issue
Block a user