/* * 模块名称: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 #include #include #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; }