Files
STM32F4-Base/test/host/ui_data/ui_data_test.c
T
wandering1 52e28885cb feat(UI): 装置维护 3 页落地(时钟 / 强制复归 / 清除记录),内容页黄金 27→40
三个薄页面模块 ui_page_clock / ui_page_reset / ui_page_clear,页面注册由生成器
自动写双端(裁定 A-1:「三叶各绑一页」与「零新增 .c」不可兼得,选薄页面加生成器,
不写手写 node switch——那种分发在增删叶子时会静默换功能,且没有任何门禁照得到)。

数据侧是注入式函数表:UI 不 include App 层的头。两条硬理由都实测过——分层方向
(layer-deps)之外,模拟器根本不链 sys_clock,ALLOW 豁免挡得住 layer-deps,
挡不住模拟器链接期。面板对时的唯一真判据是回读 RTC:sys_clock_set() 恒返 0
(RTC 写失败只打日志),所以真机侧写 1 后回读 1,最坏约 200 ms 落在一次「保存」
按键上,IWDG 800 ms 窗有余;上界与依据都写进注释。

「无时钟」徽标位置有两个像素级实测坑:放标题带右侧会被 4 字标题连成「时钟设置
无时钟」一句话;放页脚带左侧 x=7 会压住页脚——页脚从 x=6 起排,不是居中。
最终落在清单末行与页脚之间的空带 y=124。clear_done 那张顺带给出像素证据:
清 run.log 之后「操作记录」那一行从 2 变 3,页面上没缓存、没撒谎。

ui_w_records 的 UI_W_REC_LOG_* 顺序必须等于 syslog_id_t 顺序(7 类,只准尾追加),
已加编译期顺序守卫;执行侧 7 条映射表同样带编译期守卫。

测试与真值:内容页黄金 27 → 40 页(+13),文本断言 +68、反向断言 ABSENT +19;
27 张旧参考 git diff 为空 = 逐字节未变。往返池 5448 B 与菜单常驻堆 5368 B 均未变;
字库未扩(源码在用 171 并 KEEP_EXTRA 39 = 清单 183),font-charset 与 cn-chars、
cn-font 字节一致。@build 0 错误 0 警告;uiTask 静态最坏链 1,336 B / 16,384
(余 91%)与基线相同,页面钩子没进最坏链。

一条测试学结论单独留痕:反向断言不能当「位置不重叠」的证据——本脚本的字形匹配
把 y 带判得比字面宽(徽标在 125..136、页脚在 144..155,仍被 (140,160) 判命中)。
该防线改由正向断言「页脚未被徽标压住」承担(压字那一版实测过不了它)。

两条自我更正一并记档(作者自报,按真值而非原口径):
① 先前报「文案零新增汉字」是错的——观测行拟用「动作元件」,「元」不在子集,
cn-chars 与 font-charset 如实红;最终改「保护动作 N」才让「零新增」成为事实。
顺序是:先按机器红,再按真值写结论。
② 先前报「6 个文件 BOM 逐个核过」被 bom-guard 证伪:丢 BOM 发生在上一次编译之后,
正是 AGENTS 那句「历史 0 警告不代表安全」的活例;此后一律以退出码为准。

未覆盖项不假装:台架「N>0 → N=0」在产品构建的未标定闭锁期拿不到(闭锁期 N 恒 0),
判据显式退化为「页面受理 + 落一条操作记录」,写在 rs_watch 注释里。

生成物入库例外照用:uvprojx 只加 3 个 ui_page_ 的 FilePath(真增删源文件),
CMakeLists 同批;两处都由 gen_menu.py 自己写,没手写。

本笔先走了一次真钩子:20 条钩子实跑通过(ui-golden 菜单 8 状态加内容页 40 页、
generated-fresh、registration-check、font-charset、ui-data 单测、cppcheck、
coding-standard、bom、mojibake、id-check、doc-links 等),只有 mem-numbers 与
mem-budget 两条红——磁盘上的 .map 出自 20:39 那次构建、含三条在飞道的代码,
与 HEAD 台账必然不同码。按仓库既有约定(这两条等台账回填时允许 --no-verify 并
正文留证)落这一笔;回填由我统一在收尾一笔做。

三条在飞道(task-26 崩溃证据路径、task-16 与 task-17 谓词收敛和卷锁观测、
task-36 台架工具)的未暂存改动被 autostash 暂时收走,钩子看到的是 HEAD 加本笔;
过程中曾把 comm 道未跟踪的 iec104_comm.c 临时移出(它不在 stash 覆盖面内),
那次移出反而让 ui-golden 撞见 CMake 缓存里的悬空引用而红——已改用「先落 comm
那笔、再落本笔」的顺序消掉这个中间态。ui_data_maint_ready() 待删(只有测试在读
的自检口),排在本笔之后单独一笔。
2026-10-26 22:43:54 +08:00

1355 lines
65 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.
/*
* 模块名称: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 两种态(未标定/已标定)整帧往返
* ── 以下 5 组 = 装置维护通道与时钟换算(道F task-3,
* docs/任务规划/UI补齐.md §2 第 3 行「装置维护 3 页」的地基)──
* TC-UIDATA-022 时钟换算与"无效值拒收":12 条锚点正向换算 + 反函数互逆 +
* **时区反证**(不得含 28800 偏移)+ 逐月天数(闰/平)+ 非法组合
* 逐条 -2 + epoch 0/越域反算 -2(1970 绝不被反算出来)
* TC-UIDATA-023 时标串 parse/format:正常串、空串(无效时钟)、记录侧占位串
* "1970-01-01 08:00:00" 必须解析失败、分隔位/长度/尾字符/型对值非法
* TC-UIDATA-024 跨层编号顺序契约:UI_W_REC_LOG_* 逐字等于 syslog_id_t 枚举序 +
* UI 侧缓冲容量与 SYSLOG 契约同宽 + UI_DATA_CLEAR_ST_* 与
* SYSLOG_CLEAR_* 逐值相等 + 面板两条留痕串各自不超描述上限
* ("与远端串不同"不在这里判:字面量互比是恒真空断言,被
* test-cppcheck 抓过,真判据落在 TC-UIDATA-026s)
* TC-UIDATA-025 维护通道未注入防御:六口安全返回 + 残表整表拒绝且不覆盖好表
* TC-UIDATA-026 注入后转发言行:epoch 0 哨兵不下发、reset_req 自行投操作记录=0 次
* (谁触发谁投,防"远端 + 本地"双投)、clear 状态码直通不改码
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-25
*/
#include <stdio.h>
#include <stddef.h> /* offsetof:v2 帧布局见证(只许尾追加) */
#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:元件编号真源 */
#include "sys_log.h" /* syslog_id_t / SYSLOG_CLEAR_*:记录编号与清除状态真源 */
#include "ui_w_records.h" /* UI_W_REC_LOG_*:UI 侧记录源编号(同一 TU 里核对顺序契约) */
/* ===================== 断言框架 ===================== */
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");
}
/* ============ 装置维护写通道与时钟换算(道F task-3,UI 补齐 §2 第 3 行)============ */
/*
* 维护执行侧桩件:只回答"哪个口被调了几次、最后一次收到什么",不做任何业务判断。
* 计数而不是布尔:多处偷投同一条操作记录(本道最怕的"远端 + 本地各投一次")
* 只有靠次数才照得出来。
*/
static int s_stub_clock_rc;
static int s_stub_clock_calls;
static uint32_t s_stub_clock_last;
static int s_stub_reset_rc;
static int s_stub_reset_calls;
static uint32_t s_stub_active;
static int s_stub_note_calls;
static char s_stub_note_last[32];
static int s_stub_clear_calls;
static uint8_t s_stub_clear_last_id;
static int s_stub_clear_state_calls;
static uint8_t s_stub_clear_state_id;
static int s_stub_clear_state_rc;
static int stub_clock_set(uint32_t a_epoch)
{
s_stub_clock_calls++;
s_stub_clock_last = a_epoch;
return s_stub_clock_rc;
}
static int stub_reset_req(void)
{
s_stub_reset_calls++;
return s_stub_reset_rc;
}
static uint32_t stub_active_count(void)
{
return s_stub_active;
}
static void stub_oper_note(const char *a_desc)
{
s_stub_note_calls++;
if (a_desc != NULL) {
(void)strncpy(s_stub_note_last, a_desc, sizeof(s_stub_note_last) - 1u);
s_stub_note_last[sizeof(s_stub_note_last) - 1u] = 0;
}
}
static int stub_clear_req(uint8_t a_id)
{
s_stub_clear_calls++;
s_stub_clear_last_id = a_id;
return 0;
}
static int stub_clear_state(uint8_t a_id)
{
s_stub_clear_state_calls++;
s_stub_clear_state_id = a_id;
return s_stub_clear_state_rc;
}
static void stub_reset_all(void)
{
s_stub_clock_rc = 0;
s_stub_clock_calls = 0;
s_stub_clock_last = 0u;
s_stub_reset_rc = 0;
s_stub_reset_calls = 0;
s_stub_active = 0u;
s_stub_note_calls = 0;
s_stub_note_last[0] = '\0';
s_stub_clear_calls = 0;
s_stub_clear_last_id = 0u;
s_stub_clear_state_calls = 0;
s_stub_clear_state_id = 0u;
s_stub_clear_state_rc = UI_DATA_CLEAR_ST_NONE;
}
static const ui_data_maint_ops_t s_maint_stub = {
stub_clock_set, stub_reset_req, stub_active_count,
stub_oper_note, stub_clear_req, stub_clear_state
};
/* 故意缺一条(clear_state 为 NULL)的残表:注入必须整表拒绝,不能半途接住 */
static const ui_data_maint_ops_t s_maint_broken = {
stub_clock_set, stub_reset_req, stub_active_count,
stub_oper_note, stub_clear_req, NULL
};
/* 日历用例:字段 → 期望秒(期望值由独立实现(Python/JS 格里历)算出,不自我复制) */
typedef struct {
uint16_t y;
uint8_t mo;
uint8_t d;
uint8_t h;
uint8_t mi;
uint8_t s;
uint32_t epoch;
} clock_case_t;
static const clock_case_t k_clock_cases[] = {
{ 2000u, 1u, 1u, 0u, 0u, 0u, 946684800u }, /* 年域下限(RTC 年字段起点) */
{ 2000u, 2u, 29u, 0u, 0u, 0u, 951782400u }, /* 千禧闰年 2/29 */
{ 2000u, 3u, 1u, 0u, 0u, 0u, 951868800u }, /* 闰日次日:月序跨界 */
{ 2001u, 2u, 28u, 23u, 59u, 59u, 983404799u }, /* 平年 2 月末 + 当日最大秒 */
{ 2004u, 2u, 29u, 12u, 0u, 0u, 1078056000u }, /* 4 年闰规则 */
{ 2016u,12u, 31u, 0u, 0u, 0u, 1483142400u }, /* 年末 */
{ 2020u, 6u, 15u, 6u, 30u, 45u, 1592202645u }, /* 任意中段 */
{ 2024u, 2u, 29u, 12u, 34u, 56u, 1709210096u }, /* 近一个闰日 */
{ 2025u, 2u, 28u, 0u, 0u, 1u, 1740700801u }, /* 平年 2/28 */
{ 2026u,10u, 25u, 8u, 0u, 0u, 1792915200u }, /* 模拟器固定帧那一刻 */
{ 2037u,12u, 31u, 23u, 59u, 59u, 2145916799u }, /* 上界前一天 */
{ 2038u, 1u, 19u, 3u, 14u, 7u, 2147483647u } /* uint32 秒域上界 = 0x7FFFFFFF */
};
/*
* TC-UIDATA-022 时钟换算与"无效值拒收"
* a) 12 条锚点正向换算(期望值来自独立格里历实现,不是本函数的自我复述)
* b) 反函数逐条回原文(互逆性:屏上显示的时间 = 存进去的时间,不多不少一小时)
* c) **时区反证**:2026-10-25 08:00:00 必须是 1792915200,而不是它 +28800;
* 本工程墙钟 epoch 域就是本地时间,谁在这里补了 8 小时,模拟器照样自洽、
* 只有真机上"屏上输 08:00、存进去 16:00"(App/task/ui_task.c 的 SOE 时标注记
* 是同一条纪律的另一处防线)
* d) 逐月天数(含闰年 2 月 29 / 平年 2 月 28 / 出域月返 0)
* e) 非法组合逐条拒(2 月 30、非闰 2 月 29、月 0/13、日 0/32、时 24、分秒 60、
* 年 1999/2039),且 -2 与"越能力上界"的 -3 可分
* f) 边界外秒域反算(epoch 0 与 0x80000000 都拒;0 是记录侧占位起点,绝不能
* 被反算成 1970 显示到屏上)
*/
static void tc_clock_convert(void)
{
ui_data_clock_t t;
uint32_t e;
uint32_t i;
int rc;
int ok_fwd = 1;
int ok_rev = 1;
int n = (int)(sizeof(k_clock_cases) / sizeof(k_clock_cases[0]));
for (i = 0u; i < (uint32_t)n; i++) {
(void)memset(&t, 0, sizeof(t));
t.year = k_clock_cases[i].y;
t.month = k_clock_cases[i].mo;
t.day = k_clock_cases[i].d;
t.hour = k_clock_cases[i].h;
t.minute = k_clock_cases[i].mi;
t.second = k_clock_cases[i].s;
e = 0u;
rc = ui_data_clock_to_epoch(&t, &e);
if ((rc != 0) || (e != k_clock_cases[i].epoch)) {
ok_fwd = 0;
printf(" case %u: rc=%d got=%lu want=%lu\n", (unsigned)i, rc,
(unsigned long)e, (unsigned long)k_clock_cases[i].epoch);
}
(void)memset(&t, 0xFF, sizeof(t));
rc = ui_data_clock_from_epoch(k_clock_cases[i].epoch, &t);
if ((rc != 0) || (t.year != k_clock_cases[i].y) || (t.month != k_clock_cases[i].mo) ||
(t.day != k_clock_cases[i].d) || (t.hour != k_clock_cases[i].h) ||
(t.minute != k_clock_cases[i].mi) || (t.second != k_clock_cases[i].s)) {
ok_rev = 0;
printf(" rev case %u: rc=%d got=%u-%u-%u %u:%u:%u\n", (unsigned)i, rc,
(unsigned)t.year, (unsigned)t.month, (unsigned)t.day,
(unsigned)t.hour, (unsigned)t.minute, (unsigned)t.second);
}
}
CHECK(ok_fwd, "TC-UIDATA-022a fields to epoch over 12 anchors");
CHECK(ok_rev, "TC-UIDATA-022b epoch to fields is the exact inverse");
/* 时区反证:本函数不得含任何时区偏移 */
(void)memset(&t, 0, sizeof(t));
t.year = 2026u; t.month = 10u; t.day = 25u; t.hour = 8u; t.minute = 0u; t.second = 0u;
e = 0u;
(void)ui_data_clock_to_epoch(&t, &e);
CHECK(e == 1792915200u, "TC-UIDATA-022c no timezone shift baked in (local epoch domain)");
CHECK(e != (1792915200u + 28800u), "TC-UIDATA-022c2 must not be plus 8 hours");
/* 逐月天数(闰/平两年 + 出域月) */
{
static const uint8_t k_leap[12] = { 31u,29u,31u,30u,31u,30u,31u,31u,30u,31u,30u,31u };
static const uint8_t k_flat[12] = { 31u,28u,31u,30u,31u,30u,31u,31u,30u,31u,30u,31u };
int ok_dim = 1;
for (i = 1u; i <= 12u; i++) {
if (ui_data_clock_days_in_month(2000u, (uint8_t)i) != k_leap[i - 1u]) ok_dim = 0;
if (ui_data_clock_days_in_month(2025u, (uint8_t)i) != k_flat[i - 1u]) ok_dim = 0;
}
CHECK(ok_dim, "TC-UIDATA-022d days in month (leap 2000 / flat 2025)");
CHECK(ui_data_clock_days_in_month(2000u, 0u) == 0u, "TC-UIDATA-022d2 month 0 means invalid");
CHECK(ui_data_clock_days_in_month(2000u, 13u) == 0u, "TC-UIDATA-022d3 month 13 means invalid");
}
/* 非法组合逐条:一律 -2(输入错),不是 -3(能力边界) */
{
static const clock_case_t k_bad[] = {
{ 2025u, 2u, 29u, 0u, 0u, 0u, 0u }, /* 非闰年 2/29 */
{ 2026u, 2u, 30u, 0u, 0u, 0u, 0u }, /* 2 月 30 日 */
{ 2026u, 4u, 31u, 0u, 0u, 0u, 0u }, /* 4 月无 31 日 */
{ 2026u, 0u, 1u, 0u, 0u, 0u, 0u }, /* 月 0 */
{ 2026u,13u, 1u, 0u, 0u, 0u, 0u }, /* 月 13 */
{ 2026u,10u, 0u, 0u, 0u, 0u, 0u }, /* 日 0 */
{ 2026u,10u, 32u, 0u, 0u, 0u, 0u }, /* 日 32 */
{ 2026u,10u, 25u,24u, 0u, 0u, 0u }, /* 时 24 */
{ 2026u,10u, 25u, 0u,60u, 0u, 0u }, /* 分 60 */
{ 2026u,10u, 25u, 0u, 0u,60u, 0u }, /* 秒 60 */
{ 1999u, 1u, 1u, 0u, 0u, 0u, 0u }, /* 年 < RTC 年域 */
{ 2039u, 1u, 1u, 0u, 0u, 0u, 0u } /* 年 > uint32 秒域 */
};
int ok_bad = 1;
for (i = 0u; i < (uint32_t)(sizeof(k_bad) / sizeof(k_bad[0])); i++) {
/* 逐字段搬(两种结构体尺寸不同,靠强转按字节拷是给自己埋雷) */
t.year = k_bad[i].y;
t.month = k_bad[i].mo;
t.day = k_bad[i].d;
t.hour = k_bad[i].h;
t.minute = k_bad[i].mi;
t.second = k_bad[i].s;
rc = ui_data_clock_to_epoch(&t, &e);
if (rc != -2) {
ok_bad = 0;
printf(" bad case %u: rc=%d (want -2)\n", (unsigned)i, rc);
}
}
CHECK(ok_bad, "TC-UIDATA-022e invalid field combos rejected with -2");
CHECK(ui_data_clock_to_epoch(NULL, &e) == -1, "TC-UIDATA-022e2 NULL input is -1");
CHECK(ui_data_clock_to_epoch(&t, NULL) == -1, "TC-UIDATA-022e3 NULL output is -1");
}
/* 反算越界:0 是"墙钟无效"的占位起点,绝不能显示成 1970-01-01 */
CHECK(ui_data_clock_from_epoch(0u, &t) == -2, "TC-UIDATA-022f epoch 0 rejected (never show 1970)");
CHECK(ui_data_clock_from_epoch(946684799u, &t) == -2, "TC-UIDATA-022f2 epoch below 2000 rejected");
CHECK(ui_data_clock_from_epoch(0x80000000u, &t) == -2, "TC-UIDATA-022f3 epoch above uint32 domain");
CHECK(ui_data_clock_from_epoch(0u, NULL) == -1, "TC-UIDATA-022f4 NULL output is -1");
}
/*
* TC-UIDATA-023 时标串 parse / format
* 快照 rt.clock 的串型(19 字符)是页面预填的唯一来源;无效时钟时它是空串,
* 记录侧的占位串 "1970-01-01 08:00:00" 同样必须解析失败——否则页面会把
* 占位串当真实时间预填,用户照抄保存就等于把 1970 写进 RTC。
*/
static void tc_clock_string(void)
{
ui_data_clock_t t;
char buf[24];
uint32_t e;
(void)memset(&t, 0, sizeof(t)); /* 解析失败按契约不写输出,先给个确定的初值 */
e = 0u;
CHECK(ui_data_clock_parse("2026-10-25 08:00:00", &t) == 0, "TC-UIDATA-023a parse well-formed ts");
if (t.year == 2026u && t.month == 10u && t.day == 25u && t.hour == 8u) {
(void)ui_data_clock_to_epoch(&t, &e);
CHECK(e == 1792915200u, "TC-UIDATA-023b parsed ts maps to same epoch as fields");
} else {
CHECK(0, "TC-UIDATA-023b parsed fields wrong");
}
CHECK(ui_data_clock_parse("", &t) == -2, "TC-UIDATA-023c empty ts (clock invalid) is -2");
CHECK(ui_data_clock_parse("1970-01-01 08:00:00", &t) == -2,
"TC-UIDATA-023d record placeholder ts must not parse (no 1970 on screen)");
CHECK(ui_data_clock_parse("2026-10-25T08:00:00", &t) == -2, "TC-UIDATA-023e wrong separator");
CHECK(ui_data_clock_parse("2026-10-25 08:00:0", &t) == -2, "TC-UIDATA-023f short ts");
CHECK(ui_data_clock_parse("2026-10-25 08:00:001", &t) == -2, "TC-UIDATA-023g trailing junk");
CHECK(ui_data_clock_parse("2O26-10-25 08:00:00", &t) == -2, "TC-UIDATA-023h non digit year");
CHECK(ui_data_clock_parse("2026-02-30 00:00:00", &t) == -2, "TC-UIDATA-023i shaped but invalid day");
CHECK(ui_data_clock_parse("20260101 00:00:00", &t) == -2, "TC-UIDATA-023j missing dashes");
CHECK(ui_data_clock_parse(NULL, &t) == -1, "TC-UIDATA-023k NULL ts is -1");
CHECK(ui_data_clock_parse("2026-10-25 08:00:00", NULL) == -1, "TC-UIDATA-023l NULL out is -1");
(void)memset(buf, 0x7A, sizeof(buf));
CHECK(ui_data_clock_format(&t, buf, sizeof(buf)) == 0, "TC-UIDATA-023m format ok into 24B buffer");
CHECK(strcmp(buf, "2026-10-25 08:00:00") == 0, "TC-UIDATA-023n formatted string round-trips");
(void)memset(buf, 0x7A, sizeof(buf));
CHECK(ui_data_clock_format(&t, buf, 19u) == -1, "TC-UIDATA-023o capacity 19 refused (need 20)");
CHECK(buf[0] == (char)0x7A, "TC-UIDATA-023p refused format writes nothing");
CHECK(ui_data_clock_format(NULL, buf, sizeof(buf)) == -1, "TC-UIDATA-023q NULL in is -1");
}
/*
* TC-UIDATA-024 跨层编号顺序契约(Lead 裁定:UI 侧编号逐字等于 syslog_id_t 枚举序,
* 只准尾部追加)+ 清除状态码逐值相等 + 墙钟字段容量与串型一致。
* 适配层始终显式映射(从不强转),这条断言钉的是"两套编号今天确实相等"这件事
* 本身——枚举中间插值会安静地把事故读成告警,只有在这里才会响。
*/
static void tc_record_codes(void)
{
CHECK(UI_W_REC_LOG_RUN == (uint8_t)SYSLOG_RUN, "TC-UIDATA-024a RUN order");
CHECK(UI_W_REC_LOG_SELFTEST == (uint8_t)SYSLOG_SELFTEST, "TC-UIDATA-024b SELFTEST order");
CHECK(UI_W_REC_LOG_ACCIDENT == (uint8_t)SYSLOG_ACCIDENT, "TC-UIDATA-024c ACCIDENT order");
CHECK(UI_W_REC_LOG_ALARM == (uint8_t)SYSLOG_ALARM, "TC-UIDATA-024d ALARM order");
CHECK(UI_W_REC_LOG_START == (uint8_t)SYSLOG_START, "TC-UIDATA-024e START order");
CHECK(UI_W_REC_LOG_OPER == (uint8_t)SYSLOG_OPER, "TC-UIDATA-024f OPER order");
CHECK(UI_W_REC_LOG_YK == (uint8_t)SYSLOG_YK, "TC-UIDATA-024g YK order");
CHECK(UI_W_REC_LOG_N == (uint8_t)SYSLOG_COUNT, "TC-UIDATA-024h count matches sentinel (append only below)");
CHECK(UI_DATA_CLOCK_TS_LEN == SYSLOG_TS_LEN, "TC-UIDATA-024i ts field length same contract on both sides");
CHECK(UI_W_REC_TS_LEN == (SYSLOG_TS_LEN + 1u), "TC-UIDATA-024j UI ts buffer covers SYSLOG_TS_LEN + NUL");
CHECK(UI_W_REC_DESC_LEN == (SYSLOG_DESC_MAX + 1u), "TC-UIDATA-024k UI desc buffer covers max + NUL");
CHECK(UI_DATA_CLEAR_ST_NONE == SYSLOG_CLEAR_NONE, "TC-UIDATA-024l clear NONE value");
CHECK(UI_DATA_CLEAR_ST_OK == SYSLOG_CLEAR_OK, "TC-UIDATA-024m clear OK value");
CHECK(UI_DATA_CLEAR_ST_WAIT == SYSLOG_CLEAR_WAIT, "TC-UIDATA-024n clear WAIT value");
CHECK(UI_DATA_CLEAR_ST_FAIL == SYSLOG_CLEAR_FAIL, "TC-UIDATA-024o clear FAIL value");
/* 操作记录描述上限:本地/远端两条留痕串都必须装得下,超了会被投递侧静默截断 */
CHECK(strlen("force reset by panel") <= (size_t)SYSLOG_DESC_MAX,
"TC-UIDATA-024p panel reset note fits desc limit");
CHECK(strlen("clock set by panel") <= (size_t)SYSLOG_DESC_MAX,
"TC-UIDATA-024q panel clock note fits desc limit");
/* 这里曾写 CHECK(strcmp("force reset by panel", "force reset") != 0, ...):
* 两个字面量互比 = 恒真,test-cppcheck 一眼抓出(钉口径的断言永不可能失败,
* 比根本没有断言更坏——它让人以为这条已经被管住了)。同一判据挪到
* TC-UIDATA-026s:拿实际落进执行侧缓冲的串去比远端那条文案 */
}
/*
* TC-UIDATA-025 维护通道未注入时的防御:六个口一律安全返回、不崩、ready 返 0;
* 残表(某成员 NULL)整表拒绝且不覆盖已生效的好表——半途接住会在"按下才崩"的位置崩。
*/
static void tc_maint_not_bound(void)
{
(void)ui_data_maint_bind(NULL);
stub_reset_all();
CHECK(ui_data_maint_ready() == 0u, "TC-UIDATA-025a not bound before bind");
CHECK(ui_data_maint_clock_set(1792915200u) == -1, "TC-UIDATA-025b clock_set not bound is -1");
CHECK(ui_data_maint_reset_req() == -1, "TC-UIDATA-025c reset_req not bound is -1");
CHECK(ui_data_maint_active_count() == 0u, "TC-UIDATA-025d active_count not bound is 0");
CHECK(ui_data_maint_oper_note("x") == -1, "TC-UIDATA-025e oper_note not bound is -1");
CHECK(ui_data_maint_clear_req(UI_W_REC_LOG_RUN) == -1, "TC-UIDATA-025f clear_req not bound is -1");
CHECK(ui_data_maint_clear_state(UI_W_REC_LOG_RUN) == UI_DATA_CLEAR_ST_NONE,
"TC-UIDATA-025g clear_state not bound is NONE");
CHECK(ui_data_maint_bind(&s_maint_broken) == -1, "TC-UIDATA-025h incomplete table refused");
CHECK(ui_data_maint_ready() == 0u, "TC-UIDATA-025i refused table left channel unbound");
CHECK(ui_data_maint_bind(&s_maint_stub) == 0, "TC-UIDATA-025j complete table accepted");
CHECK(ui_data_maint_ready() == 1u, "TC-UIDATA-025k bound after accept");
CHECK(ui_data_maint_bind(&s_maint_broken) == -1, "TC-UIDATA-025l broken table refused again");
CHECK(ui_data_maint_ready() == 1u, "TC-UIDATA-025m refusal keeps the good table active");
}
/*
* TC-UIDATA-026 注入后的转发言行(页面侧全部判据的地基):
* - clock_set(0) 是本层保留的"未初始化"哨兵,必须挡在下发之前(桩计数为 0)
* - reset_req **不得**自己投操作记录:谁触发谁投,这条留痕属页面;
* 在转发层偷投 = 同一次操作两个真源,也正是"远端那条已投过"会被重复投的形态
* - clear_state 返回值直通不改码(WAIT/FAIL 都是正常显示态)
* - oper_note(NULL) 不投递、不崩
*/
static void tc_maint_forwarding(void)
{
(void)ui_data_maint_bind(&s_maint_stub);
stub_reset_all();
CHECK(ui_data_maint_clock_set(0u) == -2, "TC-UIDATA-026a epoch 0 sentinel refused locally");
CHECK(s_stub_clock_calls == 0, "TC-UIDATA-026b sentinel never reaches executor");
s_stub_clock_rc = 0;
CHECK(ui_data_maint_clock_set(1792915200u) == 0, "TC-UIDATA-026c clock_set forwarded");
CHECK((s_stub_clock_calls == 1) && (s_stub_clock_last == 1792915200u),
"TC-UIDATA-026d executor got the exact epoch");
s_stub_clock_rc = -1;
CHECK(ui_data_maint_clock_set(1792915200u) == -1, "TC-UIDATA-026e executor failure propagates");
CHECK(ui_data_maint_reset_req() == 0, "TC-UIDATA-026f reset accepted");
CHECK(s_stub_reset_calls == 1, "TC-UIDATA-026g executor reset called once");
CHECK(s_stub_note_calls == 0, "TC-UIDATA-026h forwarding layer must NOT post the oper record");
(void)ui_data_maint_oper_note("force reset by panel");
CHECK((s_stub_note_calls == 1) && (strcmp(s_stub_note_last, "force reset by panel") == 0),
"TC-UIDATA-026i page-side note reaches executor verbatim");
/* 面板这条与远端 YK11 那条(App/protocol/iec104/iec104_pts.c 投的
* "force reset")必须是不同串,否则审计面再也分不出"主站下发的"还是
* "现场有人按了面板"。比的是执行侧缓冲里的实际内容,不是两个常量互比 */
CHECK(strcmp(s_stub_note_last, "force reset") != 0,
"TC-UIDATA-026s posted note differs from the remote YK11 wording");
CHECK(ui_data_maint_oper_note(NULL) == -1, "TC-UIDATA-026j NULL note refused");
CHECK(s_stub_note_calls == 1, "TC-UIDATA-026k NULL note not posted");
s_stub_reset_rc = -1;
CHECK(ui_data_maint_reset_req() == -1, "TC-UIDATA-026l rejected reset propagates");
s_stub_active = 3u;
CHECK(ui_data_maint_active_count() == 3u, "TC-UIDATA-026m active count forwarded (proof of reset)");
CHECK((ui_data_maint_clear_req(UI_W_REC_LOG_YK) == 0) && (s_stub_clear_last_id == UI_W_REC_LOG_YK),
"TC-UIDATA-026n clear req id passed through unchanged");
s_stub_clear_state_rc = UI_DATA_CLEAR_ST_WAIT;
CHECK(ui_data_maint_clear_state(UI_W_REC_LOG_OPER) == UI_DATA_CLEAR_ST_WAIT,
"TC-UIDATA-026o WAIT passed through (normal state, not failure)");
s_stub_clear_state_rc = UI_DATA_CLEAR_ST_FAIL;
CHECK(ui_data_maint_clear_state(UI_W_REC_LOG_OPER) == UI_DATA_CLEAR_ST_FAIL,
"TC-UIDATA-026p FAIL passed through unchanged");
CHECK(s_stub_clear_state_id == UI_W_REC_LOG_OPER, "TC-UIDATA-026q state queried the asked id");
CHECK(s_stub_clear_state_calls == 2, "TC-UIDATA-026r state queried once per call");
}
/* ===================== 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("-- clock convert --\n");
tc_clock_convert();
printf("-- clock ts string --\n");
tc_clock_string();
printf("-- record id order contract --\n");
tc_record_codes();
printf("-- maint channel: not bound --\n");
tc_maint_not_bound();
printf("-- maint channel: forwarding --\n");
tc_maint_forwarding();
printf("\n=== UI Data Snapshot Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}