/* * 模块名称: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 时标抑制、读失败退化与不绑空态 * ── 以下 6 条 = 定值写路径(道F task-1,docs/任务规划/UI补齐.md §2 第 1 行)── * TC-UIDATA-015 队列深度 4:四格都进得去、第五格满返 -1 且状态转 ERR_BUSY; * FIFO 顺序(最后投的一条最后落定)、空队列 drain 返 0、 * drain(NULL) 返 -1 且不动队列、投递成功即转 WAIT * TC-UIDATA-016 请求表示法:NULL/带非法位 mask 返 -2 且状态与队列都不动; * mask=0 空请求返 0 且直接落 OK 不占槽;请求尺寸与队列 .bss * 预算钉死(168 B/格 × 4 格)——加深或加宽必须先过这条 * TC-UIDATA-017 正常往返:请求 → WAIT → drain → OK → 新值可读;**只提交 mask * 内的 id**(未提交的槽一位都不动,这是"从显示值回提全表会 * 顺手改掉返回系数"那条坑的机器防线) * TC-UIDATA-018 越界不改值:整批预检任一项越界 → ERR_RANGE 且**一字不写** * (含同批里合法的那几项)+ err_mask 指认越界项 + 下一次成功 * 落定后 err_mask 自动清零 * TC-UIDATA-019 闭锁拒写:locked 置位 → ERR_LOCKED、丢弃本条(不排队等解锁)、 * 值区一字不动;解闭锁后重新提交可成功 * TC-UIDATA-020 apply 中途失败(预检与执行不一致的防御分支):如实 ERR_RANGE, * 已生效的前几项不回滚(prot_set 无回滚接口,谎称整批未写更坏) * TC-UIDATA-021 v2 帧扩展:UI_DATA_VER == 2、既有字段偏移逐字节见证(只允许尾 * 追加)、cal_block 两种态(未标定/已标定)整帧往返 * * 通过:全部 TC PASS(main 返回 0)。 * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 * * 创建日期:2026-10-25 */ #include #include /* offsetof:v2 帧布局见证(只许尾追加) */ #include /* 生产源文件原样纳入(纯内存叶子,零平台依赖,直接编) */ #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; const uint64_t dp_expect = 0x0FULL; /* V0.1 双点段 = bit0~3(YX1~4 开关位置类) */ ui_data_init(); fill_rt(&src, 5u); src.yx_bits = 0u; /* 全部点位"分/未动作" */ src.yx_valid = valid_expect; src.yx_dp = dp_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"); /* 双点位图必须原样透传:页面按它选词(合位/分位 vs 动作/复归),快照层若做"归一化" * 或按 valid 掩一次,页面就会把双点当单点形容——同一条事实两处解释必然漂 */ CHECK(got.yx_dp == dp_expect, "TC-UIDATA-009c dp mask passed through unmasked"); /* 点表与名表共用点数(1 基 IOA 1~33)。这里是**有意**再抄一遍字面量:测试的职责是 * 独立见证,协议层那个 #define 在 .c 内不可见,改点数时必须有人被绊一下——绊到后 * 要同时改:App/protocol/iec104/iec104_pts.c 的 YX_N、ui_data.h 的 UI_DATA_YX_N、 * 页面名表 ui_page_yx.c(有编译期数组守卫兜着)、本条 */ CHECK((uint32_t)UI_DATA_YX_N == 33u, "TC-UIDATA-009d point count is 33 (YX_N witness)"); } /* ===================== 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"); /* 宽度按**实际画到哪一列**判:1/2 两类(动作/返回)画在值列(4 汉字 = 12 字节), * 3..8 这些非元件事件的类型名画在名列(7 汉字 = 21 字节)。一刀切按 12 判会把 * "未标定闭锁"这类正常名字判成违规,一刀切按 21 判又放过了值列超宽——分列才咬手 */ CHECK((strlen(ui_w_soe_type_name(UI_W_SOE_T_ELM_ASSERT)) <= 12u) && (strlen(ui_w_soe_type_name(UI_W_SOE_T_ELM_RETURN)) <= 12u), "TC-UIDATA-012m element event names fit value column (<=4 CJK)"); CHECK(longest <= 21u, "TC-UIDATA-012m2 type names fit name column (<=7 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"); /* 非元件事件:类型名进名列,值列留空。15 字节 = "未标定闭锁" 5 汉字 × 3 B; * 主机测试的字面量必须纯 ASCII(无 BOM 文件里中文串会吞引号),所以这里只按 * 字节数 + "不是占位串"判,串内容由 simulator 侧的 TEXTS 断言在像素上核 */ 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) == 15u) && (name[0] != '-') && (value[0] == 0), "TC-UIDATA-014f non-element row: 5-CJK type name 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); } /* ====== TC-UIDATA-015~020:定值写路径(队列 + 状态机 + 注入执行侧) ====== */ /* * 假执行侧(替掉 App/algo/prot_set):一张 40 项值区 + 一张上下限表。 * ⚠ 本单测不链 App 侧任何 .c(prot_set 依赖 Flash/校验和编排,且写路径的**契约** * 本来就只是"三个函数的返回值"),所以测的是 ui_data.c 里那份真实的队列 + * 状态机代码:越界不改值、闭锁拒写、只提交改动 id 三条判据都落在生产代码上, * 不是测试里的副本。真实 App 实现的等价性由 ui_data_feed.c 的三个包函数 * (feed_param_locked/check/apply)在代码审查与台架侧保证。 */ #define FAKE_LO_ALL (-100000) /* 默认宽松下限:单条 TC 里再逐行收紧 */ #define FAKE_HI_ALL (100000) static int32_t s_fake_val[UI_DATA_SET_N]; static int32_t s_fake_lo[UI_DATA_SET_N]; static int32_t s_fake_hi[UI_DATA_SET_N]; static uint8_t s_fake_locked; static uint8_t s_fake_poison; /* 0xFF = 不投毒;否则该 id 的 apply 必失败 */ static uint32_t s_fake_apply_calls; static uint8_t fake_locked(void) { return s_fake_locked; } static int fake_check(uint8_t a_id, int32_t a_q10) { if (a_id >= (uint8_t)UI_DATA_SET_N) { return -1; } if ((a_q10 < s_fake_lo[a_id]) || (a_q10 > s_fake_hi[a_id])) { return -1; } return 0; } static int fake_apply(uint8_t a_id, int32_t a_q10) { if (fake_check(a_id, a_q10) != 0) { return -1; } if ((s_fake_poison != 0xFFu) && (a_id == s_fake_poison)) { return -1; /* 投毒:模拟"预检过了但执行侧仍拒"(理论不可达的防御分支) */ } s_fake_val[a_id] = a_q10; s_fake_apply_calls++; return 0; } static const ui_data_param_ops_t s_fake_ops = { fake_locked, fake_check, fake_apply }; /* 每个 TC 开头调一次:值区填可辨识的旧值 + 快照与队列复位 */ static void fake_reset(void) { uint8_t i; for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) { s_fake_val[i] = (int32_t)i * 1000; s_fake_lo[i] = FAKE_LO_ALL; s_fake_hi[i] = FAKE_HI_ALL; } s_fake_locked = 0u; s_fake_poison = 0xFFu; s_fake_apply_calls = 0u; ui_data_init(); } /* 投一条只改若干项的请求;未列出的 id 不进 mask(= 本次不提交) */ static int fake_submit(const uint8_t *a_ids, const int32_t *a_vals, uint8_t a_n) { ui_data_param_req_t req; uint8_t i; (void)memset(&req, 0, sizeof(req)); for (i = 0u; i < a_n; i++) { req.mask |= ((uint64_t)1u << a_ids[i]); req.q10[a_ids[i]] = a_vals[i]; } return ui_data_param_request(&req); } static int32_t fake_val(uint8_t a_id) { return s_fake_val[a_id]; } static void tc_param_queue(void) { ui_data_param_req_t req; uint8_t k; int accepted = 1; int was_wait = 1; fake_reset(); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_IDLE, "TC-UIDATA-015a state IDLE after init (never submitted)"); CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-015b drain on empty queue == 0"); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_IDLE, "TC-UIDATA-015c empty drain leaves state alone (not WAIT)"); CHECK(ui_data_param_err_mask() == 0u, "TC-UIDATA-015d err_mask clean at idle"); /* 四格全进得去;每投一条状态就该是 WAIT */ (void)memset(&req, 0, sizeof(req)); req.mask = 1u; /* 只改 id0 */ for (k = 0u; k < UI_DATA_PARAM_QD; k++) { req.q10[0] = (int32_t)(1000 + (int32_t)k); if (ui_data_param_request(&req) != 0) { accepted = 0; } if (ui_data_param_state() != UI_DATA_PARAM_ST_WAIT) { was_wait = 0; } } CHECK(UI_DATA_PARAM_QD == 4u, "TC-UIDATA-015e queue depth is 4 (contract witness)"); CHECK(accepted == 1, "TC-UIDATA-015f four requests all accepted"); CHECK(was_wait == 1, "TC-UIDATA-015g state WAIT while queued"); /* 第五格:满 → 返 -1 + 状态 ERR_BUSY(在队四条一条不掉) */ req.q10[0] = 9999; CHECK(ui_data_param_request(&req) == -1, "TC-UIDATA-015h fifth request rejected (-1)"); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_BUSY, "TC-UIDATA-015i rejected request surfaces ERR_BUSY"); /* FIFO:前三条落定后仍是 WAIT,第四条才落终态;值 = 最后投的那条(1003) */ CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-015j drain processes one"); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_WAIT, "TC-UIDATA-015k still WAIT with 3 requests left in queue"); (void)ui_data_param_drain(&s_fake_ops); (void)ui_data_param_drain(&s_fake_ops); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_WAIT, "TC-UIDATA-015l WAIT until the last queued request settles"); CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-015m fourth request drained"); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-015n terminal OK when drained"); CHECK((fake_val(0) == 1003) && (s_fake_apply_calls == 4u), "TC-UIDATA-015o FIFO: last queued value lands, all four applied"); CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-015p queue empty again"); /* ops 无效:不动队列也不动状态 */ fake_reset(); CHECK(fake_submit(NULL, NULL, 0u) == 0, "TC-UIDATA-015q empty request accepted (see 016)"); CHECK(ui_data_param_drain(NULL) == -1, "TC-UIDATA-015r drain(NULL) == -1"); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-015s failed drain does not rewrite state"); CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-015t failed drain does not consume"); } static void tc_param_request_shape(void) { ui_data_param_req_t req; fake_reset(); CHECK(ui_data_param_request(NULL) == -2, "TC-UIDATA-016a request(NULL) == -2"); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_IDLE, "TC-UIDATA-016b invalid request leaves state IDLE (page must not show ERR for a bug)"); (void)memset(&req, 0, sizeof(req)); req.mask = ((uint64_t)1u << 63u); /* 第 63 项不存在 */ CHECK(ui_data_param_request(&req) == -2, "TC-UIDATA-016c mask with bit63 == -2"); req.mask = ((uint64_t)1u << (UI_DATA_SET_N - 1u)); /* 第 39 项 = 最后一项,合法 */ CHECK(ui_data_param_request(&req) == 0, "TC-UIDATA-016d last legal bit accepted"); CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-016e drained last-bit request"); CHECK(s_fake_apply_calls == 1u, "TC-UIDATA-016f item 39 really reached the executor"); /* 空请求:不占槽、直接 OK("没改东西却按了保存"是正常操作) */ fake_reset(); req.mask = 0u; CHECK(ui_data_param_request(&req) == 0, "TC-UIDATA-016g empty request returns 0"); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-016h empty request settles OK"); CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-016i empty request consumed no slot"); /* 内存预算护栏:请求是 .bss 里的定长槽,168 B/格 × 4 格 = 672 B * (台账 docs/内存管理/RAM使用分析与优化方案.md;扩深度/加宽必须先过这条) */ CHECK(sizeof(ui_data_param_req_t) == 168u, "TC-UIDATA-016j request is 168 bytes"); CHECK(sizeof(s_pq) == (672u), "TC-UIDATA-016k queue .bss is 4 x 168 = 672 bytes"); CHECK(sizeof(s_pq) <= 768u, "TC-UIDATA-016l queue .bss within budget (<=768B)"); CHECK((uint32_t)UI_DATA_SET_N <= 64u, "TC-UIDATA-016m item count fits the 64-bit mask"); } static void tc_param_roundtrip(void) { const uint8_t ids[3] = { 2u, 3u, 7u }; const int32_t vals[3] = { 12345, 60000, 7 }; fake_reset(); CHECK(fake_submit(ids, vals, 3u) == 0, "TC-UIDATA-017a batch of three submitted"); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_WAIT, "TC-UIDATA-017b WAIT before drain"); CHECK(fake_val(2) == 2000, "TC-UIDATA-017c old value still in effect before drain"); CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-017d drained one request"); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-017e OK after drain"); CHECK((fake_val(2) == 12345) && (fake_val(3) == 60000) && (fake_val(7) == 7), "TC-UIDATA-017f new values readable after roundtrip"); /* 只提交改动项:mask 外的槽一位都不动(Q10 往返精度坑的机器防线) */ CHECK((fake_val(0) == 0) && (fake_val(1) == 1000) && (fake_val(4) == 4000) && (fake_val(6) == 6000) && (fake_val(8) == 8000) && (fake_val(UI_DATA_SET_N - 1u) == (int32_t)(UI_DATA_SET_N - 1u) * 1000), "TC-UIDATA-017g untouched items are byte-identical (mask-only commit)"); CHECK(s_fake_apply_calls == 3u, "TC-UIDATA-017h exactly the masked items were applied"); /* 回环的后半段:App 落值后重发参数帧 → 页面按新 seq 读到新值 */ { ui_data_par_t frame; ui_data_par_t got; uint8_t i; (void)memset(&frame, 0, sizeof(frame)); frame.valid = 1u; frame.checksum = 0x1234u; frame.cal_block = 0u; for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) { frame.q10[i] = s_fake_val[i]; } CHECK(ui_data_publish_par(&frame) == 0, "TC-UIDATA-017i par frame republished"); (void)memset(&got, 0, sizeof(got)); CHECK((ui_data_par(&got) == 0) && (got.q10[2] == 12345) && (got.q10[7] == 7), "TC-UIDATA-017j page reads back the committed values"); } } static void tc_param_range_atomic(void) { /* 非 const:本 TC 复用同一组数组跑三种越界组合(改了再投) */ uint8_t ids[2] = { 2u, 5u }; int32_t vals[2] = { 12345, 99999 }; fake_reset(); s_fake_hi[5] = 100; /* 第 5 项上限 100 → 99999 越界;第 2 项本身合法 */ CHECK(fake_submit(ids, vals, 2u) == 0, "TC-UIDATA-018a out-of-range batch accepted for queue"); CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-018b drained"); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_RANGE, "TC-UIDATA-018c state ERR_RANGE"); CHECK(ui_data_param_err_mask() == ((uint64_t)1u << 5u), "TC-UIDATA-018d err_mask points at the offending item"); CHECK((fake_val(2) == 2000) && (fake_val(5) == 5000), "TC-UIDATA-018e all-or-nothing: the legal item in the same batch NOT written"); CHECK(s_fake_apply_calls == 0u, "TC-UIDATA-018f precheck blocks every apply call"); /* 多项同时越界:err_mask 全给(页面逐行反显,不是只报第一行) */ fake_reset(); s_fake_lo[6] = 500; s_fake_hi[5] = 100; ids[0] = 5u; ids[1] = 6u; vals[0] = 99999; /* 超上限 */ vals[1] = 60; /* 低于下限 */ CHECK(fake_submit(ids, vals, 2u) == 0, "TC-UIDATA-018g two-item bad batch queued"); (void)ui_data_param_drain(&s_fake_ops); CHECK(ui_data_param_err_mask() == (((uint64_t)1u << 5u) | ((uint64_t)1u << 6u)), "TC-UIDATA-018h err_mask lists every offending item (both bounds)"); /* 下一次成功落定 → err_mask 自动清零(否则页面把上次越界显示成这次) */ ids[0] = 2u; vals[0] = 12345; CHECK(fake_submit(ids, vals, 1u) == 0, "TC-UIDATA-018i good request queued"); (void)ui_data_param_drain(&s_fake_ops); CHECK((ui_data_param_state() == UI_DATA_PARAM_ST_OK) && (ui_data_param_err_mask() == 0u), "TC-UIDATA-018j err_mask cleared on next successful commit"); } static void tc_param_locked(void) { const uint8_t ids[1] = { 2u }; const int32_t vals[1] = { 12345 }; fake_reset(); s_fake_locked = 1u; CHECK(fake_submit(ids, vals, 1u) == 0, "TC-UIDATA-019a request queued while locked"); CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-019b drained under lock"); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_LOCKED, "TC-UIDATA-019c state ERR_LOCKED"); CHECK((fake_val(2) == 2000) && (s_fake_apply_calls == 0u), "TC-UIDATA-019d locked commit writes nothing (no apply call)"); CHECK(ui_data_param_err_mask() == 0u, "TC-UIDATA-019e lock failure is not a range failure"); CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-019f locked request is dropped, not queued waiting for unlock"); /* 解闭锁后重新提交 → OK(闭锁丢弃的是那一条,不是永久拉黑写路径) */ s_fake_locked = 0u; CHECK(fake_submit(ids, vals, 1u) == 0, "TC-UIDATA-019g resubmit after unlock accepted"); (void)ui_data_param_drain(&s_fake_ops); CHECK((ui_data_param_state() == UI_DATA_PARAM_ST_OK) && (fake_val(2) == 12345), "TC-UIDATA-019h write path healthy again after unlock"); } static void tc_param_apply_fail(void) { const uint8_t ids[2] = { 3u, 7u }; const int32_t vals[2] = { 33333, 77777 }; fake_reset(); s_fake_poison = 7u; /* 预检全过、执行侧仍拒第 7 项(两真源不一致的防御分支) */ CHECK(fake_submit(ids, vals, 2u) == 0, "TC-UIDATA-020a request with poisoned item queued"); CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-020b drained"); CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_RANGE, "TC-UIDATA-020c apply failure reported as ERR_RANGE (not silent OK)"); CHECK(ui_data_param_err_mask() == ((uint64_t)1u << 7u), "TC-UIDATA-020d failing item named"); CHECK(fake_val(3) == 33333, "TC-UIDATA-020e earlier item already applied is NOT rolled back"); CHECK(fake_val(7) == 7000, "TC-UIDATA-020f failing item itself unchanged"); } static void tc_param_frame_v2(void) { ui_data_par_t src; ui_data_par_t got; /* 版本号是页面与写侧唯一对齐凭据;v2 = 尾追加 cal_block */ CHECK(UI_DATA_VER == 2u, "TC-UIDATA-021a contract version is 2"); /* v1 布局逐字节见证:字段只允许尾追加,任何插入/重排都会让下面四条红 * (页面按名字取字段,错位是静默的——只有把偏移钉死才照得出来) */ CHECK(offsetof(ui_data_par_t, seq) == 0u, "TC-UIDATA-021b seq offset unchanged"); CHECK(offsetof(ui_data_par_t, bad_mask) == 16u, "TC-UIDATA-021c bad_mask offset unchanged"); CHECK(offsetof(ui_data_par_t, q10) == 24u, "TC-UIDATA-021d q10 offset unchanged"); CHECK(offsetof(ui_data_par_t, cal_block) == 184u, "TC-UIDATA-021e cal_block is the appended tail field (offset 184)"); CHECK(sizeof(ui_data_par_t) == 192u, "TC-UIDATA-021f par frame is 192 B (+8 for v2)"); /* 写侧组装缓冲是 rt/par/info 三帧共用的 union(App/app/ui_data_feed.c 的 s_scr): * rt 恒大于 par,所以给 par 追加字段**不会**连带抬升那块 .bss——加字段之前 * 先确认这条,否则每次尾追加都悄悄多啃一份 RAM(台账只记总量,看不出是谁长的) */ CHECK(sizeof(ui_data_rt_t) == 208u, "TC-UIDATA-021g rt frame is 208 B (union stays put)"); CHECK(sizeof(ui_data_par_t) < sizeof(ui_data_rt_t), "TC-UIDATA-021h par still smaller than rt (shared scratch unchanged)"); /* 闭锁态:1 = 未标定(写侧按 UA/UN/IA 三路 VALID 现算,本层只存真值) */ ui_data_init(); (void)memset(&src, 0, sizeof(src)); src.valid = 1u; src.locked = 0u; src.cal_block = 1u; CHECK(ui_data_publish_par(&src) == 0, "TC-UIDATA-021g publish blocked frame"); (void)memset(&got, 0, sizeof(got)); CHECK((ui_data_par(&got) == 0) && (got.cal_block == 1u) && (got.locked == 0u), "TC-UIDATA-021h cal_block=1 roundtrip, distinct from locked"); /* 解卡态:写侧填 0 后覆盖发布(同一份存储不会粘住旧值) */ src.cal_block = 0u; (void)ui_data_publish_par(&src); (void)memset(&got, 0xAA, sizeof(got)); CHECK((ui_data_par(&got) == 0) && (got.cal_block == 0u), "TC-UIDATA-021i cal_block=0 roundtrip (no stale block flag)"); /* 写侧默认值纪律:调用方清零后不填本字段 = 0(未闭锁),首帧不会假报徽标 */ (void)memset(&src, 0, sizeof(src)); src.valid = 1u; (void)ui_data_publish_par(&src); (void)ui_data_par(&got); CHECK(got.cal_block == 0u, "TC-UIDATA-021j zero-initialized frame means not blocked"); } /* ===================== 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("-- param write path: queue --\n"); tc_param_queue(); printf("-- param write path: request shape --\n"); tc_param_request_shape(); printf("-- param write path: roundtrip --\n"); tc_param_roundtrip(); printf("-- param write path: range atomic --\n"); tc_param_range_atomic(); printf("-- param write path: locked --\n"); tc_param_locked(); printf("-- param write path: apply failure --\n"); tc_param_apply_fail(); printf("-- param frame v2 (cal_block) --\n"); tc_param_frame_v2(); printf("\n=== UI Data Snapshot Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; }