Files
STM32F4-Base/test/host/ui_sink/ui_sink_test.c
T
wandering1 f6e9106f0d fix(UI): 推流往返返回点喂位——多个慢 RPC 之和不再串成 UI 停摆(大窗判读 F3)
道F task-24。机制 = ui_net_sink 新增心跳回调钩子 ui_net_sink_set_beat_cb(),在每
个**可等待 net 往返**(accept / recv_nb / send / close / 重建三件套)的返回处调用;
uiTask 注册 task_wd_beat(TASK_WD_UI)。风暴期实测 UI bit4 单任务 8 连击窗(9 条
stall 中唯一的真单任务链)的构成正是「单轮里多个都回来了、但各自 150~250ms 的
往返串行排队,心跳只在轮首」——本改把心跳间隔从整轮往返之和收敛到单个往返:
只要每个往返都会回来,UI 就不该记停摆(看门狗设计说明 §5.3 按位判据抓真挂死)。

【边界(lead 裁定口径)】往返语义零改动:不加超时、不动回执协议(§11.6 否决的
「net_* 裸加有界等待」不重演,请求序号方案与事件驱动 accept 均**不做**,上界
收口归登记簿 E-27/E-28 批 3)。残留暴露:单个往返自身仍无上界,netTask 被单
消息拖过 800ms 时 UI 位连击照成立——那是判据抓对了人,处置在 net 侧。

【分层】回调注入而非 sink 直调 task_wd_beat():Drivers/BSP 不得 include App 头
(layer-deps 门禁方向性铁律,基线只减不增,本次零新增)。

【零行为变化面】钩子未注册(模拟器/黄金回归形态、ui_task 注册之前)= 与改前
逐字一致;协议/帧格式/门控/兜底节拍全不动。ui-golden(菜单 8 状态 + 内容页
11 页)通过。

【测试】新增主机套件 test/host/ui_sink/(stub 假件驱动生产 ui_net_sink.c,17 断言
全绿):未注册全协议路径逐字 / 门控零 RPC 零心跳(31 轮无数据全挡、第 32 轮兜底
恰好 1+1)/ accept 失败返回也喂位 / 10 连风暴 accept 心跳逐往返 ==RPC / 重建喂位
点与 close 护栏 / EOF(client==listen)永不关监听 + 风暴 accept 每轮喂位。
suite 由 test-coverage rglob 自动发现(无需改台账)。

【文档】UI_设计架构.md 新增 §5.8(推流心跳边界:现象/处置/边界/分层/残留)+
变更记录行;ui_net_sink.h 钩子契约注释;ui_task.c v1.4 记录与 :271 注释更新。

【门禁】@build 0/0(Code=224860,vs F2 后:Code +92B / ER_IROM1 →249500 /
RW+4 / ZI+4 / RAM 79008)。--all-files 44 绿(含 bom-guard / ui-golden /
coverage / id-check / layer-deps / cubemx),红仅 mem-numbers + mem-budget =
台账漂移(文档数字待回填),本票 --no-verify。
gate: only mem-budget/mem-numbers pending lead ledger backfill

【台架(待台架实测,等你排)】两轮各自独立 ≥30min:① 单独 relisten_stress :7003
② 探针轮。v1.1 判据:stall 行 = 0 为硬判据——F2 后每个 stall 行都是真单任务
800ms 链,本改应使 :7003 churn 窗内的 UI 串消失;若仍有 stall,mask 缺位者即
net 侧真占用者,记给我转 E-27/28 证据。MISS start 为诊断行不计违规。
2026-10-25 18:42:14 +08:00

251 lines
9.8 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; }
}
/* ===================== 被测代码 ===================== */
#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;
}