/* * 模块名称:NET 层事件分发与周期对账(net_events)主机侧单元测试 * 模块功能:在 PC 上离线验证 Drivers/BSP/NET/net_events.c 的五个事件入口与周期对账器: * 对端六元组解析、**陈旧中断防误判**(芯片说还在连接就不许拆会话)、疑似 * 误报延后交给对账器、超时把会话标成异常释放(喂给 net_pending 收尾报 * CONNRESET 的那条输入)、掉线自愈自接,以及对账器的 100ms 周期闸门、 * 1000ms 冷却、五种不一致形态的稳定计数(死亡要连观 4 次、重开监听 2 次、 * 自接 1 次)与 U11 的 CLOSED->LISTENING 重同步。 * * 被测对象:真实源文件 ../../../Drivers/BSP/NET/net_events.c(#include 原样纳入)。 * net_events.h / net_types.h / net_pending.h / net_listener.h 全用**真实头** * (net_event_cb_t 回调签名、NET_EVENT_* / NET_SOCK_STATE_* / * net_listener_settle_t / NET_LISTENER_REQ_* 一改即编译期报错)。假件: * 共享假 CH395F(本批补齐对端六元组与链路变更计数)、可控 socket 表 * (net_sock_raw 对未在用槽位也返回项,与真实语义一致)、**可观测的事件 * 回调**(记 fd / 事件 / arg)、记录型 net_listener_settle/request 与 * net_pending_cancel / net_send_pending_cancel、可控时基、dbg_log 计数。 * * 为什么要主机侧单测(而不是只靠台架): * 1. CH395F 的中断是**整体读即清**且单连接 socket 复用同一个号,"迟到的 DISCONNECT" * 与"丢了 CONNECT 的已连接"在真机上都会发生,却无法按需复现。误判的代价直接落在 * 现场设备上:把还活着的会话拆掉(104 主站看到莫名掉线),或让 socket 永远停在 * 已连接假象里不再重开。 * 2. 对账器的**稳定计数**(死亡 4 次、重开监听 2 次、自接 1 次)本质是"别拿一次 * 采样就把连接折腾死"。真机一次采样 100ms,凑够 4 次要 400ms,且必须一路稳定; * 台架上没法按毫秒喂,也看不出为什么"偶尔不重开"。主机侧把时基做成显式量后, * 第 3 次不该动、第 4 次该动、之后 1000ms 内不许再动,全部可精确断言。 * 3. handle_timeout_event 会置 released_abnormal=1,而 net_pending 收尾时正是靠 * 这个标志把挂起 recv 报成 CONNRESET 而不是 0(见 test/host/net_pending 的 * TC-NETPEND-014)。两个模块之间这条**跨文件因果**,只有两边都能离线跑时才可测。 * * 覆盖点:TC-NETEV-001..006 CONNECT;010..014 DISCONNECT(含两条陈旧中断防误判); * 020/021 RECV 与 SEND;030..032 TIMEOUT;040..042 掉线自愈;050..058 周期对账 * (周期闸门、链路变更留痕、五种不一致形态、稳定计数、冷却、in_use 兜底)。 * * 通过:全部 TC PASS(main 返回 0)。 * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 * * 创建日期:2026-10-23 */ #include #include #include "net_events.h" #include "net_listener.h" #include "net_pending.h" #include "net_socket.h" #include "ch395f.h" #include "ch395f_fake.c" #include "net_events.c" 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) /* ===================== dbg_log 计数 ===================== */ int g_nev_info_count; int g_nev_error_count; int g_nev_debug_count; /* ===================== 可观测的事件回调 ===================== */ static int g_ev_n; static int g_ev_fd[16]; static int g_ev_kind[16]; static void *g_ev_arg[16]; static int g_ev_token; static void ev_cb(int sockfd, net_event_t event, void *arg) { if (g_ev_n < (int)(sizeof(g_ev_fd) / sizeof(g_ev_fd[0]))) { g_ev_fd[g_ev_n] = sockfd; g_ev_kind[g_ev_n] = (int)event; g_ev_arg[g_ev_n] = arg; } g_ev_n++; } /* ===================== 记录型监听与挂起取消 ===================== */ static int g_settle_n; static int g_settle_sock; static int g_settle_level; static int g_req_n; static int g_req_sock; static int g_req_reason; static int g_pcan_n; static int g_scan_n; static int g_pcan_err; void net_listener_settle(int sockfd, net_listener_settle_t level) { g_settle_n++; g_settle_sock = sockfd; g_settle_level = (int)level; } void net_listener_request(int sockfd, net_listener_req_t reason) { g_req_n++; g_req_sock = sockfd; g_req_reason = (int)reason; } void net_listener_clear(int sockfd) { (void)sockfd; } void net_listener_tick(void) { } void net_pending_cancel(int sockfd, int err) { (void)sockfd; g_pcan_n++; g_pcan_err = err; } void net_send_pending_cancel(int sockfd, int err) { (void)sockfd; (void)err; g_scan_n++; } /* ===================== 可控时基、errno 与 socket 表 ===================== */ static uint32_t g_now; static int g_errno_last; uint32_t HAL_GetTick(void) { return g_now; } int net_set_errno(int err) { g_errno_last = err; return err; } uint32_t xTaskGetTickCount(void) { return g_now; } static net_sock_t g_tbl[NET_MAX_SOCKETS]; net_sock_t *net_sock_raw(int sockfd) { if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { return NULL; } /* 与真实语义一致:对账循环必须能读到 in_use == 0 的槽位,所以这里不给 NULL */ return &g_tbl[sockfd]; } net_sock_t *net_get_sock(int sockfd) { net_sock_t *p = net_sock_raw(sockfd); if ((p == NULL) || (p->in_use == 0u)) { return NULL; } return p; } static void reset_all(void) { int i; (void)memset(g_tbl, 0, sizeof(g_tbl)); g_now = 1000u; g_errno_last = 0; g_ev_n = 0; g_settle_n = 0; g_settle_sock = -1; g_settle_level = -1; g_req_n = 0; g_req_sock = -1; g_req_reason = -1; g_pcan_n = 0; g_scan_n = 0; g_pcan_err = 0; g_nev_info_count = 0; g_nev_error_count = 0; g_nev_debug_count = 0; ch395f_fake_reset(); (void)i; } /* 一个"带应用回调的在用槽位",ch395_sock 与数组下标同号 */ static net_sock_t *mk(int fd, net_sock_state_t st, uint16_t local_port) { net_sock_t *p = &g_tbl[fd]; p->in_use = 1u; p->ch395_sock = (uint8_t)fd; p->type = NET_SOCK_STREAM; p->state = st; p->auto_relisten = 1u; p->local_port = local_port; p->event_cb = ev_cb; p->event_arg = (void *)&g_ev_token; return p; } /* ===================== CONNECT ===================== */ static void tc_connect(void) { net_sock_t *p; printf("-- TC-NETEV-001..006 CONNECT --\n"); reset_all(); p = mk(0, NET_SOCK_STATE_LISTENING, 2404u); ch395f_fake_set_peer(0, 192u, 168u, 1u, 50u, 51234u); handle_connect_event(p); /* remote_ip 是**低字节在前**拼的(remote[0] 落在最低 8 位),与网络序相反: * 192.168.1.50 -> 0x3201A8C0。这是既有实现的字节序,钉住它是防止有人"顺手 * 改成大端"导致所有按 IP 比对/白名单的判断静默错位。 */ CHECK((p->remote_ip_arr[0] == 192u) && (p->remote_ip_arr[3] == 50u) && (p->remote_ip == ((192u) | (168u << 8) | (1u << 16) | (50u << 24))) && (p->remote_port == 51234u), "TC-NETEV-001 peer tuple parsing: ip assembled little-endian into 32 bits, port from bytes 5/6"); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (p->session_dead == 0u) && (p->lost_conn_seen == 0u) && (p->send_ready == 1u), "TC-NETEV-002 a listened socket becomes established after self-accept and is writable at once"); CHECK((g_chip.keepalive_calls == 1) && (g_chip.last_keepalive == 1u) && (g_chip.int_status_calls == 1), "TC-NETEV-003 on connect, enable keepalive and clear the pending bit once (read clears)"); CHECK((g_ev_n == 1) && (g_ev_kind[0] == (int)NET_EVENT_CONNECTED) && (g_ev_fd[0] == 0) && (g_ev_arg[0] == (void *)&g_ev_token), "TC-NETEV-004 the event calls back into the app with fd and the caller arg"); reset_all(); p = mk(0, NET_SOCK_STATE_CONNECTING, 0u); ch395f_fake_set_peer(0, 10u, 0u, 0u, 1u, 2404u); handle_connect_event(p); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (p->send_ready == 0u) && (g_chip.keepalive_calls == 1) && (g_ev_n == 1), "TC-NETEV-005 the active-connect branch also becomes established but does NOT touch send_ready (differs from self-accept)"); reset_all(); p = mk(0, NET_SOCK_STATE_CLOSED, 2404u); handle_connect_event(p); CHECK((g_ev_n == 0) && (p->state == NET_SOCK_STATE_CLOSED), "TC-NETEV-006 a CONNECT interrupt in any other state is ignored (never invent a session)"); } /* ===================== DISCONNECT ===================== */ static void tc_disconnect(void) { net_sock_t *p; printf("-- TC-NETEV-010..014 DISCONNECT --\n"); reset_all(); p = mk(0, NET_SOCK_STATE_ESTABLISHED, 2404u); g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; g_fake.status_byte1[0] = CH395F_TCP_ESTABLISHED; handle_disconnect_event(p); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (g_ev_n == 0) && (g_nev_error_count == 1), "TC-NETEV-010 the chip still says connecting: ignore this stale DISCONNECT (never tear down a live session)"); reset_all(); p = mk(0, NET_SOCK_STATE_ESTABLISHED, 2404u); g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; g_fake.status_byte1[0] = CH395F_TCP_CLOSED; ch395f_fake_push_recv_len(0, 9u); /* 还有 9 字节没读走 */ handle_disconnect_event(p); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (p->lost_conn_seen == 1u) && (g_ev_n == 0) && (g_settle_n == 0), "TC-NETEV-011 suspected false alarm (dropped before any data): defer to the reconciler instead of tearing down"); reset_all(); p = mk(0, NET_SOCK_STATE_ESTABLISHED, 2404u); p->remote_port = 51234u; g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; g_fake.status_byte1[0] = CH395F_TCP_CLOSED; handle_disconnect_event(p); CHECK((g_settle_n == 1) && (g_settle_level == (int)NET_LISTENER_SETTLE_GRACEFUL) && (p->session_dead == 1u) && (p->released_abnormal == 0u) && (p->last_peer_port == 51234u) && (p->state == NET_SOCK_STATE_CLOSED) && (g_ev_kind[0] == (int)NET_EVENT_DISCONNECTED), "TC-NETEV-012 normal disconnect: graceful settle + mark session dead + remember old port + post event"); reset_all(); p = mk(0, NET_SOCK_STATE_LISTENING, 2404u); g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; g_fake.status_byte1[0] = CH395F_TCP_LISTEN; handle_disconnect_event(p); CHECK((p->state == NET_SOCK_STATE_LISTENING) && (g_ev_n == 0) && (g_nev_info_count == 1) && (g_nev_error_count == 0), "TC-NETEV-013 listen still alive: a stale listen DISCONNECT is only logged, not reported as ERROR"); reset_all(); p = mk(0, NET_SOCK_STATE_LISTENING, 2404u); g_fake.status_byte1[0] = CH395F_TCP_CLOSED; /* 芯片说不在 LISTEN */ handle_disconnect_event(p); CHECK((p->state == NET_SOCK_STATE_LISTENING) && (g_ev_n == 0) && (g_req_n == 0), "TC-NETEV-013b even a lost listener is only logged: reopening belongs to the reconciler, the event layer must not overreach"); reset_all(); p = mk(0, NET_SOCK_STATE_CONNECTING, 0u); handle_disconnect_event(p); CHECK((g_nev_error_count == 1) && (g_ev_kind[0] == (int)NET_EVENT_DISCONNECTED), "TC-NETEV-014 DISCONNECT in an unusual state: report ERROR but still notify the application"); } /* ===================== RECV / SEND / TIMEOUT ===================== */ static void tc_recv_send_timeout(void) { net_sock_t *p; printf("-- TC-NETEV-020/021/030..032 --\n"); reset_all(); p = mk(0, NET_SOCK_STATE_ESTABLISHED, 2404u); handle_recv_event(p); CHECK((p->readable == 1u) && (g_ev_kind[0] == (int)NET_EVENT_DATA_RECEIVED), "TC-NETEV-020 a RECV event sets readable and notifies the application"); reset_all(); p = mk(0, NET_SOCK_STATE_ESTABLISHED, 2404u); p->send_ready = 1u; handle_send_event(p); CHECK((g_ev_kind[0] == (int)NET_EVENT_SEND_COMPLETE) && (p->send_ready == 1u), "TC-NETEV-021 SEND completion only posts the event; the fields stay owned by the write path"); reset_all(); p = mk(0, NET_SOCK_STATE_CONNECTING, 0u); handle_timeout_event(p); CHECK((p->state == NET_SOCK_STATE_CREATED) && (g_nev_error_count == 1) && (g_ev_kind[0] == (int)NET_EVENT_TIMEOUT), "TC-NETEV-030 connect timeout: fall back to CREATED and notify the application"); reset_all(); p = mk(0, NET_SOCK_STATE_ESTABLISHED, 2404u); p->remote_port = 777u; handle_timeout_event(p); CHECK((g_settle_level == (int)NET_LISTENER_SETTLE_QUARANTINE) && (p->released_abnormal == 1u) && (p->session_dead == 1u) && (p->last_peer_port == 777u) && (p->state == NET_SOCK_STATE_CLOSED), "TC-NETEV-031 session timeout: quarantine-level settle + mark released abnormally (net_pending reports CONNRESET from it)"); reset_all(); p = mk(0, NET_SOCK_STATE_LISTENING, 2404u); handle_timeout_event(p); CHECK((p->state == NET_SOCK_STATE_LISTENING) && (g_nev_error_count == 1) && (g_ev_kind[0] == (int)NET_EVENT_TIMEOUT), "TC-NETEV-032 listen timeout is only logged: state unchanged but waiters are still notified"); } /* ===================== 掉线自愈 ===================== */ static void tc_lost_probe(void) { net_sock_t *p; printf("-- TC-NETEV-040..042 lost-CONNECT probe --\n"); reset_all(); p = mk(0, NET_SOCK_STATE_CLOSED, 2404u); p->lost_conn_seen = 1u; g_fake.cached_tcp[0] = CH395F_TCP_CLOSED; net_lost_connect_probe(p); CHECK((p->lost_conn_seen == 0u) && (g_ev_n == 0), "TC-NETEV-040 the chip is not established: only clear the flag, do not create a session on our own"); reset_all(); p = mk(0, NET_SOCK_STATE_CLOSED, 2404u); p->lost_conn_seen = 1u; p->last_peer_port = 51234u; g_fake.cached_tcp[0] = CH395F_TCP_ESTABLISHED; ch395f_fake_set_peer(0, 10u, 0u, 0u, 9u, 51234u); net_lost_connect_probe(p); CHECK((p->lost_conn_seen == 0u) && (g_ev_n == 0), "TC-NETEV-041 peer port identical to last time: assume we missed CONNECT, do not accept a second time"); /* 自愈的适用面是软件还写着 LISTENING、芯片其实已经连上——accept 分支只认 * LISTENING/CONNECTING。若软件态已被别处改成 CLOSED,本函数打完 INFO 后什么都 * 不会发生(现状留痕,见第二断言)。 */ reset_all(); p = mk(0, NET_SOCK_STATE_LISTENING, 2404u); p->lost_conn_seen = 1u; p->last_peer_port = 1u; g_fake.cached_tcp[0] = CH395F_TCP_ESTABLISHED; ch395f_fake_set_peer(0, 10u, 0u, 0u, 9u, 55555u); net_lost_connect_probe(p); CHECK((g_ev_n == 1) && (g_ev_kind[0] == (int)NET_EVENT_CONNECTED) && (p->state == NET_SOCK_STATE_ESTABLISHED) && (p->remote_port == 55555u) && (p->lost_conn_seen == 0u), "TC-NETEV-042 self-heal after a lost CONNECT interrupt: make up one accept so the session is not stranded forever"); reset_all(); p = mk(0, NET_SOCK_STATE_CLOSED, 2404u); g_fake.cached_tcp[0] = CH395F_TCP_ESTABLISHED; ch395f_fake_set_peer(0, 10u, 0u, 0u, 9u, 55555u); net_lost_connect_probe(p); CHECK((g_ev_n == 0) && (p->state == NET_SOCK_STATE_CLOSED), "TC-NETEV-042b current state: self-heal cannot accept once the software side is CLOSED (logged only, no invented session)"); } /* ===================== 周期对账 ===================== */ static void recon_step(uint32_t now) { g_now = now; net_reconcile_tick(); } static void tc_reconcile(void) { net_sock_t *p; int infos; int c; printf("-- TC-NETEV-050..058 reconcile --\n"); /* 对账器里的 s_last / s_bad[] / s_cool[] 是函数内 static(外部够不着), * 所以每个形态**各占一个 fd**、时间统一单调前进,互不串扰。 */ reset_all(); infos = g_nev_info_count; recon_step(1000u); /* priming:链路计数记基线 */ CHECK(g_nev_info_count == infos, "TC-NETEV-051a the first pass after power-up logs nothing for the link counter (the 0xFFFF sentinel only records the baseline)"); c = g_chip.cached_calls; recon_step(1000u); /* 同一时刻再调一次 */ CHECK(g_chip.cached_calls == c, "TC-NETEV-050 100ms period gate: do not query the chip twice at the same instant"); reset_all(); p = mk(1, NET_SOCK_STATE_ESTABLISHED, 2404u); g_fake.cached_open[1] = CH395F_SOCKET_CLOSED; g_fake.cached_tcp[1] = CH395F_TCP_CLOSED; infos = g_nev_info_count; recon_step(1100u); recon_step(1200u); recon_step(1300u); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (g_ev_n == 0) && (g_nev_info_count == infos), "TC-NETEV-052 a dead session needs 4 consecutive observations before acting: the first three change nothing"); recon_step(1400u); CHECK((p->state == NET_SOCK_STATE_CLOSED) && (p->session_dead == 1u) && (g_ev_kind[0] == (int)NET_EVENT_DISCONNECTED), "TC-NETEV-052b once the 4th observation is stable, run the disconnect teardown"); p->state = NET_SOCK_STATE_ESTABLISHED; /* 假装又活了,验证冷却 */ recon_step(1500u); recon_step(1600u); recon_step(1700u); recon_step(1800u); CHECK(g_ev_n == 1, "TC-NETEV-052c no repeat action inside the 1000ms cooldown after acting (even when the stable count is met)"); reset_all(); p = mk(2, NET_SOCK_STATE_ESTABLISHED, 2404u); g_fake.cached_open[2] = CH395F_SOCKET_OPEN; g_fake.cached_tcp[2] = CH395F_TCP_LISTEN; /* 芯片自己重开监听了,却没发 DISCONNECT */ recon_step(2900u); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (g_pcan_n == 0), "TC-NETEV-053 the chip reopened the listener behind our back: needs 2 stable passes, the first one only accumulates"); recon_step(3000u); CHECK((p->state == NET_SOCK_STATE_LISTENING) && (g_pcan_n == 1) && (g_scan_n == 1) && (g_pcan_err == NET_ERR_CONNRESET), "TC-NETEV-053b second stable pass: resync to listen and cancel both pending waits (otherwise a task would wait forever)"); reset_all(); p = mk(3, NET_SOCK_STATE_LISTENING, 2404u); p->last_peer_port = 1u; g_fake.cached_tcp[3] = CH395F_TCP_ESTABLISHED; /* 连上了却没 CONNECT 中断 */ ch395f_fake_set_peer(3, 172u, 16u, 0u, 5u, 40000u); recon_step(4100u); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (p->remote_port == 40000u) && (g_ev_kind[0] == (int)NET_EVENT_CONNECTED), "TC-NETEV-054 connect completed but the interrupt was lost: a single pass is enough to make up the accept"); reset_all(); p = mk(4, NET_SOCK_STATE_LISTENING, 2404u); g_fake.cached_open[4] = CH395F_SOCKET_CLOSED; /* 监听没了 */ g_fake.cached_tcp[4] = CH395F_TCP_CLOSED; recon_step(5200u); recon_step(5300u); CHECK((g_req_n == 1) && (g_req_sock == 4) && (g_req_reason == (int)NET_LISTENER_REQ_RECON), "TC-NETEV-055 listener lost: request a reopen after 2 stable passes (the event layer does not open it itself)"); reset_all(); p = mk(5, NET_SOCK_STATE_CLOSED, 2404u); g_fake.cached_open[5] = CH395F_SOCKET_OPEN; /* 芯片还在 LISTEN */ g_fake.cached_tcp[5] = CH395F_TCP_LISTEN; p->remote_port = 999u; p->lost_conn_seen = 1u; recon_step(6400u); CHECK((p->state == NET_SOCK_STATE_LISTENING) && (p->last_peer_port == 999u) && (p->lost_conn_seen == 0u) && (g_req_n == 0), "TC-NETEV-056 U11 resync: software CLOSED while the chip is LISTEN -> follow the chip, no redundant reopen"); reset_all(); p = mk(6, NET_SOCK_STATE_CLOSED, 2404u); g_fake.cached_open[6] = CH395F_SOCKET_CLOSED; recon_step(7500u); recon_step(7600u); CHECK((g_req_n == 1) && (g_req_sock == 6), "TC-NETEV-057 software CLOSED and the chip is not listening either: request a reopen (the listener layer rebuilds it)"); reset_all(); (void)memset(&g_tbl[7], 0, sizeof(g_tbl[7])); /* 槽位不在用 */ infos = g_nev_info_count; recon_step(8700u); CHECK(g_nev_info_count == infos, "TC-NETEV-058 unused slots are skipped outright (no action at all)"); /* 链路日志:s_link_last 初值 0xFFFF 是哨兵——第一次看到计数只记基线、不打日志 * (否则每次上电都会冒一条"链路 up"的假事件);此后计数一变就必须留痕。 * 本文件开头那次 priming 就是"基线"那一趟(TC-NETEV-051a),这里是"变化"那一趟。 */ reset_all(); g_fake.link_change = 7u; infos = g_nev_info_count; recon_step(9800u); CHECK(g_nev_info_count == (infos + 1), "TC-NETEV-051b any change of the link-change counter is logged (the baseline pass is already covered by 051a)"); } int main(void) { printf("=== NET Events Tests ===\n"); tc_connect(); tc_disconnect(); tc_recv_send_timeout(); tc_lost_probe(); tc_reconcile(); printf("\n=== NET Events Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; }