Files
STM32F4-Base/test/host/ui_sink/ui_sink_test.c
T
super_agent_bot 493fa6ba0e feat(ui/remo): 7003 总线新增 STATUS 身份查询与 GOTO 直达页(#69)
STATUS(命令 4)一帧回答"现在在哪个画面":kind(MENU/PAGE/STANDBY)
+ 菜单路径/选中 + 页 id,布局契约与字节序见 ui_net_sink.h。由 uiTask
轮询驱动,同上下文读静态导航状态:零画面副作用、免锁、不发日志。
新增只读接口 ui_menu_get_nav_state / ui_content_page_current_desc。

GOTO(命令 5)按页 id 查生成表直开内容页,以 VIEW 权限进入(与
"已过查看密码"同档,不代输任何密码)。编译期门控
DTU_BENCH || UI_REMO_ALLOW_GOTO:产品构建恒拒(status=4),模拟器
构建默认开启便于无板验证;执行类页(clear/reset)固件侧同样拒绝
(status=2,与主机 EXEC_PAGE_IDS 同一事实源);待机中拒绝(status=5),
由主机按键唤醒后再来。成功应答带回目标叶节点下标便于对账。

ui_sink 主机单测补导航侧链接假件 + 最小 lvgl 桩(仅本测试目录)。
产品构建 Keil 0 Error 0 Warning;ui_sink 单测 17/17。
2026-11-02 22:19:58 +08:00

283 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:ui_net_sink 心跳边界主机侧单元测试(F3,2026-10-25 大窗判读)
* 模块功能:以桩 net_* 假件驱动生产 ui_net_sink.c,钉死两条判据:
* ① 未注册心跳钩 = 行为逐字不变(模拟器/黄金回归形态);
* ② 注册后每个可等待 net 往返的返回处恰好喂一次心跳
* (风暴期单轮多往返不再串成 UI 心跳缺口)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
* 创建日期:2026-10-25
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "net_socket.h"
#include "ui_net_sink.h"
#include "ui_net_sink_backend.h"
int g_uisink_err_count = 0;
/* ===================== 假件:net 层 ===================== */
static int f_rpc; /* 本轮已发生的 net RPC 次数 */
static int f_closes;
static int f_accepts;
static int f_armed;
static int f_can_read;
static int f_accept_next; /* >=0 返回该 fd;<0 返回 -1 并置 WOULDBLOCK */
static int f_recv_mode; /* 0=取队 1=WOULDBLOCK(-1) 2=EOF(0) */
static uint32_t f_gen;
static int f_errno_v;
static int f_next_fd;
static int f_sock_alive;
static uint8_t s_last_tx[4200];
static int s_last_tx_len;
static uint8_t s_last_tx_cmd;
static uint8_t rxq[1024];
static int rxq_len, rxq_pos;
uint32_t net_get_generation(void) { return f_gen; }
uint16_t net_htons(uint16_t v) { return (uint16_t)((v << 8) | (v >> 8)); }
int net_socket(int d, int t) { (void)d; (void)t; f_rpc++; return f_next_fd; }
int net_bind(int fd, const struct net_sockaddr *a, int l) { (void)fd; (void)a; (void)l; f_rpc++; return 0; }
int net_listen(int fd) { (void)fd; f_rpc++; return 0; }
int net_accept(int fd, struct net_sockaddr *a, int *l) {
(void)fd; (void)a; (void)l;
f_rpc++; f_accepts++;
if (f_accept_next >= 0) {
int c = f_accept_next;
f_accept_next = -1;
return c;
}
f_errno_v = NET_ERR_WOULDBLOCK;
return -1;
}
int net_listener_armed(int fd) { (void)fd; return f_armed; }
int net_can_read(int fd) { (void)fd; return f_can_read; }
int net_send(int fd, const void *b, int n, int fl) {
(void)fd; (void)fl;
f_rpc++;
memcpy(s_last_tx, b, (size_t)n);
s_last_tx_len = n;
s_last_tx_cmd = ((const uint8_t *)b)[1];
return n;
}
int net_recv_nb(int fd, void *b, int n) {
(void)fd;
f_rpc++;
if (f_recv_mode == 1) { f_errno_v = NET_ERR_WOULDBLOCK; return -1; }
if (f_recv_mode == 2) { return 0; }
if (rxq_pos >= rxq_len) { f_errno_v = NET_ERR_WOULDBLOCK; return -1; }
int take = rxq_len - rxq_pos;
if (take > n) { take = n; }
memcpy(b, rxq + rxq_pos, (size_t)take);
rxq_pos += take;
return take;
}
int net_close(int fd) { (void)fd; f_rpc++; f_closes++; return 0; }
int net_get_errno(void) { return f_errno_v; }
void *net_get_sock(int fd) { (void)fd; return f_sock_alive ? (void *)&f_gen : (void *)0; }
/* ===================== 假件:平台后端 ===================== */
static int s_lcdmem_calls;
static int s_keys[16];
static int s_key_n;
int ui_net_sink_get_lcdmem(uint8_t *out, uint32_t size) {
s_lcdmem_calls++;
memset(out, 0xA5, size);
return 0;
}
void ui_net_sink_inject_key(uint8_t k) {
if (s_key_n < 16) { s_keys[s_key_n++] = k; }
}
/* ============ 假件:导航侧(#69 STATUS/GOTO,2026-11-02 新增) ============
* 本单测不发 STATUS/GOTO 帧,以下仅为链接占位:STATUS 恒为根菜单
* depth=0、无页、非待机。若后续要断言 STATUS/GOTO 语义,把这些假件
* 改成可控状态并新增用例,不要动生产分支。 */
#include "ui_menu.h"
#include "ui_content_page.h"
#include "ui_page_registry.h"
#include "ui_standby.h"
static const uint8_t s_nav_zero[4] = { 0u, 0u, 0u, 0u }; /* 根菜单 path/sel */
void ui_menu_get_nav_state(uint8_t *p_depth, const uint8_t **p_path,
const uint8_t **p_sel)
{
if (p_depth != NULL) { *p_depth = 0u; }
if (p_path != NULL) { *p_path = s_nav_zero; }
if (p_sel != NULL) { *p_sel = s_nav_zero; }
}
const content_page_def_t *ui_content_page_current_desc(void) { return NULL; }
bool ui_content_page_open(const content_page_def_t *desc, uint8_t perm)
{
(void)desc; (void)perm; return false;
}
void ui_content_page_close(void) { }
bool ui_standby_is_active(void) { return false; }
/* 生成表占位:尺寸必须按 ui_page_registry.h 的固定条数(本头文件 extern
* 了 g_ui_page_map[UI_PAGE_MAP_COUNT])。静态零初始化 = 全表 {node=0,
* desc=NULL}:本单测从不发 GOTO 帧,生产的 desc 扫描不会被触达 */
const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = { { 0u,
(const content_page_def_t *)0 } };
/* ===================== 被测代码 ===================== */
#include "ui_net_sink.c"
/* ===================== 测试框架 ===================== */
static int s_total, s_passed, 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 int s_beats;
static void beat_cb(void) { s_beats++; }
static void fresh(int hook) {
memset(rxq, 0, sizeof(rxq));
rxq_len = rxq_pos = 0;
f_rpc = 0; f_closes = 0; f_accepts = 0;
f_armed = 0; f_can_read = 0; f_accept_next = -1;
f_recv_mode = 1; f_gen = 1; f_errno_v = 0;
f_next_fd = 3; f_sock_alive = 1;
s_key_n = 0; s_lcdmem_calls = 0;
g_uisink_err_count = 0; s_last_tx_len = 0;
ui_net_sink_set_beat_cb(hook != 0 ? beat_cb : (void (*)(void))0);
ui_net_sink_start(UI_NET_SINK_PORT);
f_rpc = 0; /* start 的三件套不计入本用例观察窗 */
s_beats = 0; /* 重建三件套返回点的合法心跳同样不属于观察窗 */
}
static void qframe(uint8_t cmd, const uint8_t *d, int len) {
int i;
f_recv_mode = 0; /* 有排队帧 = recv 走取队分支(fresh 默认 WOULDBLOCK) */
rxq[rxq_len++] = UI_REMO_TAG_CLIENT;
rxq[rxq_len++] = cmd;
rxq[rxq_len++] = (uint8_t)(len >> 8);
rxq[rxq_len++] = (uint8_t)(len & 0xFF);
for (i = 0; i < len; i++) { rxq[rxq_len++] = d[i]; }
uint8_t c = 0;
for (i = 0; i < len; i++) { c ^= d[i]; }
rxq[rxq_len++] = c;
}
/* ---------- 用例 ---------- */
static void case_null_hook_full_protocol(void) {
printf("case: unregistered hook runs the whole protocol path unchanged\n");
fresh(0);
f_armed = 1; f_accept_next = 3; /* MCU 单连接模型:client fd = listen fd */
ui_net_sink_poll(); /* accept -> g_client=3 */
CHECK(s_beats == 0, "no hook: zero beat side effects");
f_armed = 0; /* 已建连后不再走 accept */
uint8_t k = 13;
qframe(UI_REMO_CMD_KEY, &k, 1);
f_can_read = 1;
ui_net_sink_poll();
CHECK(s_key_n == 1 && s_keys[0] == 13, "KEY frame injected to backend");
CHECK(s_last_tx_len == 5 && s_last_tx_cmd == UI_REMO_CMD_KEY, "KEY ack frame shape");
uint8_t off0[4] = { 0, 0, 0, 0 };
qframe(UI_REMO_CMD_LCDMEM, off0, 4);
ui_net_sink_poll();
CHECK(s_lcdmem_calls == 1, "LCDMEM request reached backend");
CHECK(s_last_tx_cmd == UI_REMO_CMD_LCDMEM && s_last_tx_len == 4 + 4 + UI_FB_SIZE + 1,
"LCDMEM answer frame shape (3209 B)");
/* CRC = payload(4B 偏移全 0 ^ 3200 个 0xA5,偶数个 10100101) 异或 = 0x00 */
CHECK(s_last_tx[4 + 4 + UI_FB_SIZE] == 0u, "LCDMEM frame CRC byte at tail (even 0xA5 run -> 0x00)");
}
static void case_gated_zero_rpc(void) {
printf("case: gated-off rounds issue zero RPC and zero beats\n");
fresh(1);
for (int i = 0; i < 5; i++) { ui_net_sink_poll(); } /* armed=0,无监听活 */
CHECK(f_rpc == 0 && s_beats == 0, "listener unarmed: 5 rounds no RPC no beat");
f_armed = 1; f_accept_next = 3;
ui_net_sink_poll(); /* 建连(1 RPC + 1 心跳) */
f_armed = 0;
int b0 = s_beats;
int r0 = f_rpc;
for (int i = 1; i < 32; i++) { /* 无数据 31 轮:门控全挡 */
ui_net_sink_poll();
}
CHECK(f_rpc == r0 && s_beats == b0, "31 no-data rounds: gate holds RPC and beats flat");
ui_net_sink_poll(); /* 第 32 轮兜底 recv */
CHECK(f_rpc == r0 + 1 && s_beats == b0 + 1,
"liveness round: exactly one recv RPC and exactly one beat on its return");
}
static void case_accept_beat(void) {
printf("case: accept roundtrip return feeds beat exactly once\n");
fresh(1);
f_armed = 1; f_accept_next = -1; /* WOULDBLOCK 空手而归 */
ui_net_sink_poll();
CHECK(f_rpc == 1 && s_beats == 1, "one accept RPC -> one beat (even on failure return)");
}
static void case_storm_chain_beats(void) {
printf("case: storm of accept roundtrips beats every roundtrip (gap <= single RPC)\n");
fresh(1);
f_armed = 1;
for (int i = 0; i < 10; i++) { ui_net_sink_poll(); }
CHECK(f_accepts == 10 && f_rpc == 10 && s_beats == 10,
"10 storm rounds: beats == RPCs (old code: no beat inside poll, chain summed) ");
}
static void case_rebuild_beats(void) {
printf("case: generation rebuild feeds after close pair and after re-listen\n");
fresh(1);
f_armed = 1; f_accept_next = 3;
ui_net_sink_poll(); /* client=3=listen */
int r0 = f_rpc; int b0 = s_beats;
f_gen = 2; f_next_fd = 9;
ui_net_sink_poll(); /* 代次变化:close+start */
CHECK(f_closes == 1 && f_rpc == r0 + 4,
"rebuild does close x1 (client==listen guard) + socket/bind/listen");
CHECK(s_beats == b0 + 2, "two beat points: after close pair and after re-listen");
f_armed = 0; f_accept_next = -1;
ui_net_sink_poll();
CHECK(s_beats == b0 + 2, "gated-off round after rebuild adds nothing");
}
static void case_eof_keepalive(void) {
printf("case: EOF (MCU model, client==listen) never closes the listen fd\n");
fresh(1);
f_armed = 1; f_accept_next = 3;
ui_net_sink_poll();
f_armed = 0; /* EOF 后无 accept 通路的静默形态 */
f_recv_mode = 2; f_can_read = 1;
int r0 = f_rpc; int b0 = s_beats;
for (int i = 0; i < 32; i++) { ui_net_sink_poll(); }
CHECK(f_closes == 0, "listen fd survives EOF in single-connection model");
CHECK(s_beats == b0 + 1 && f_rpc == r0 + 1,
"EOF recv fed one beat; later rounds fully gated (client=-1, armed=0)");
/* 风暴形态:armed 持续放行 -> EOF 后每轮 accept 空手而归也每轮喂位 */
f_armed = 1;
int r1 = f_rpc; int b1 = s_beats;
for (int i = 0; i < 10; i++) { ui_net_sink_poll(); }
CHECK(f_rpc == r1 + 10 && s_beats == b1 + 10 && f_closes == 0,
"10 storm accept rounds: beats == RPCs, listen fd still never closed");
}
int main(void) {
printf("=== ui_net_sink beat-boundary tests ===\n\n");
case_null_hook_full_protocol();
fresh(0);
case_gated_zero_rpc();
fresh(0);
case_accept_beat();
fresh(0);
case_storm_chain_beats();
fresh(0);
case_rebuild_beats();
fresh(0);
case_eof_keepalive();
printf("\n=== UI Sink Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}