test(NET): 假 CH395F 芯片落地 + net_listener 模块(34 条,覆盖 99.2%)
按 §9 规格做假芯片的第一批消费者。新增 test/host/common/ch395f_fake.{h,c}:15 个
函数的**真实原型逐字照抄**(含 ch395f_sock_state_t{open,tcp_state} 与 ERR_SUCCESS/
PROTO_TYPE_TCP/SOCKET_CLOSED/TCP_CLOSED/TCP_LISTEN 的真实取值),脚本化模型沿用本仓
既有假件风格:读函数按调用序号取返回队列(耗尽回落最后一个值)、写函数记次数与
最近入参、外加一条**调用顺序日志**让时序契约可断言。两条芯片怪癖必须复刻:
① get_sock_int_status **整体读即清**、无按位清除手段(AGENTS 模块约定 B3 实证坑,
不复刻就照不出"顺手全量清一遍把别的在用 Socket 门焊死");② *_cached 是**缓存值**
而非现场值,"监听还活着"的判据是 (open != CLOSED && tcp_state == LISTEN)。
各模块 stub/ch395f.h **只做转发** include 共享件(规格里拍板的方案):三份假芯片各自
漂移会让"假件与被测代码同时错"再也报不出来,签名与常量只能有一份定义。
新增 test/host/net_listener/,34 条断言,net_listener.c 覆盖 125/126 = **99.2%**。
钉住的契约(真机上一闪而过、台架凑不齐的组合):
- 四道互相牵制的时间闸门:30ms 退避(20ms 不重试、30ms 重试)、隔离级 400ms 与
优雅级 100ms(399ms/99ms 不放行、400ms/100ms 放行)、**800ms 上限只留痕且不放弃
请求**(留痕只打一次,到 2000ms 预算耗尽才报 ERROR 交对账器)。排期登记里那句
"沉降窗 800ms 上限只留痕"至此第一次有了可执行的验证。
- 该不该重建:芯片仍报 LISTEN 就不重建且清空请求;有挂起 recv/send 时让路但**保留
请求**(挂起结束自动补做);五种条件静默放弃(非 LISTENING/CLOSED 态、
auto_relisten=0、非 STREAM、local_port=0、in_use=0)。
- 失败不留半成品:open 失败关回去并回 BUSY;listen 失败也必须 close_socket。
- 重建后簿记:转 LISTENING、清 send_ready/recv_bytes/remote_ip(_arr)/remote_port、旧
remote_port 记进 last_peer_port、清 lost_conn_seen/released_abnormal;源端口与协议
取自表项本身(监听端口不能漂)。
- 芯片时序:清接收 → 关断 → 读即清 → 读状态(顺序不可换);清理接收区单次读钳到
256B、最多 16 轮、读到空即停。越界 fd 全程静默且不打扰芯片;请求去重保留首个 reason。
测试自查修了两处(都不是生产问题但值得记):
① reset_all 只复位假世界、**没复位被测模块自己的 static**(s_relisten_req/_next/_dead/
_quarantine/_settle_*/_capped),一上来红了 9 条;改用模块自带的 net_listener_clear()
清,顺带验证了 clear 的完整性。
② 沉降窗边界必须在**独立场景**测:放行判定与 30ms 退避共用 s_relisten_next,同一条
链里"恰好 400ms"那轮 tick 会被上一轮设的退避槽挡掉。这本身是个该知道的事实——
**hold 到点后最坏还要再等一个退避周期(实测 ≤30ms)才真正放行**,已写进用例注释。
另留痕一处死分支:net_relisten_locked 的 NET_ERR_BADF 从 tick **不可达**(tick 在调用
前就因表项为空放弃),TC-NETLIS-026 记着,新增调用点时要重新审视。
桩的口径:net_listener.h/net_types.h/net_config.h/net_pending.h 用**真实头**(net_sock_t
字段与各枚举一改即编译期报错);net_socket.h 是桩,但 net_set_errno 与 NET_ERR_RETURN_1
照抄真实头同名宏(net_socket.h:528/532);真实 net_pending.h 原型带 TaskHandle_t,故
宿主 FreeRTOS.h 补一个 void* typedef(只出现在签名里,被测代码从不传它)。
覆盖率与台账:套件 22 → 23 全绿;files_tested 26 → 27;未触及 .c 40 → 39;分母
3,446 → 3,572 行;line=3128/3572(87.6%,本批净涨因为新进来的文件几乎全绿)、
func=243/266(91.3%)。恒等式 39 = 5(P1 剩) + 5(P2) + 29(P3) 仍咬合。编号补建
TC-HOST-NETLIS(游标 026),族数 16 → 17;注册表仍只做增量插入。
自查:test-cppcheck Passed(连假芯片一起过);check_ids --strict 通过;doc-links 35 处
(= 存量);新增文件 BOM/CR/U+FFFD/行尾逐项净。**本增量零生产代码改动**,未跑 @build。
--all-files 仍只有 mem-numbers / mem-budget(.map 台账漂移,等 Lead 合并后统一回填)
与 ui-golden(ref/*.bin 不入库,新 worktree 天生没有黄金参考)三条失败,与本分支无关。
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 模块名称:假 CH395F 芯片实现(NET 域主机侧单测共享件)
|
||||
* 模块功能:实现 ch395f_fake.h 声明的假芯片。由测试单元 #include 本 .c 使用
|
||||
* (本仓主机单测的既有做法:生产源与假件都进同一个 TU)。
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "ch395f_fake.h"
|
||||
|
||||
ch395f_fake_world_t g_fake;
|
||||
ch395f_fake_obs_t g_chip;
|
||||
|
||||
static void fake_log(char c)
|
||||
{
|
||||
if (g_chip.log_n < (CH395F_FAKE_LOG_LEN - 1)) {
|
||||
g_chip.log[g_chip.log_n++] = c;
|
||||
g_chip.log[g_chip.log_n] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_fake_reset(void)
|
||||
{
|
||||
int i;
|
||||
|
||||
(void)memset(&g_fake, 0, sizeof(g_fake));
|
||||
(void)memset(&g_chip, 0, sizeof(g_chip));
|
||||
for (i = 0; i < CH395F_FAKE_SOCK_MAX; i++) {
|
||||
g_fake.cached_open[i] = CH395F_SOCKET_CLOSED;
|
||||
g_fake.cached_tcp[i] = CH395F_TCP_CLOSED;
|
||||
g_fake.status_byte0[i] = CH395F_SOCKET_CLOSED;
|
||||
g_fake.status_byte1[i] = CH395F_TCP_CLOSED;
|
||||
g_fake.open_rc[i] = CH395F_ERR_SUCCESS;
|
||||
g_fake.listen_rc[i] = CH395F_ERR_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_fake_push_recv_len(uint8_t sock, uint16_t v)
|
||||
{
|
||||
if ((sock < CH395F_FAKE_SOCK_MAX) &&
|
||||
(g_fake.recv_len_n[sock] < CH395F_FAKE_QUEUE_LEN)) {
|
||||
g_fake.recv_len_q[sock][g_fake.recv_len_n[sock]++] = v;
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_fake_set_listening(uint8_t sock)
|
||||
{
|
||||
if (sock < CH395F_FAKE_SOCK_MAX) {
|
||||
g_fake.cached_open[sock] = CH395F_SOCKET_OPEN;
|
||||
g_fake.cached_tcp[sock] = CH395F_TCP_LISTEN;
|
||||
}
|
||||
}
|
||||
|
||||
ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn)
|
||||
{
|
||||
ch395f_sock_state_t s;
|
||||
|
||||
g_chip.cached_calls++;
|
||||
g_chip.last_sock = sn;
|
||||
s.open = (sn < CH395F_FAKE_SOCK_MAX) ? g_fake.cached_open[sn] : CH395F_SOCKET_CLOSED;
|
||||
s.tcp_state = (sn < CH395F_FAKE_SOCK_MAX) ? g_fake.cached_tcp[sn] : CH395F_TCP_CLOSED;
|
||||
return s;
|
||||
}
|
||||
|
||||
ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn)
|
||||
{
|
||||
return ch395f_sock_state_cached(sn);
|
||||
}
|
||||
|
||||
void ch395f_get_socket_status(uint8_t sock, uint8_t *status)
|
||||
{
|
||||
g_chip.get_status_calls++;
|
||||
g_chip.last_sock = sock;
|
||||
fake_log('s');
|
||||
if (status != NULL) {
|
||||
status[0] = (sock < CH395F_FAKE_SOCK_MAX) ? g_fake.status_byte0[sock] : CH395F_SOCKET_CLOSED;
|
||||
status[1] = (sock < CH395F_FAKE_SOCK_MAX) ? g_fake.status_byte1[sock] : CH395F_TCP_CLOSED;
|
||||
}
|
||||
}
|
||||
|
||||
/* 整体读即清:返回后该 socket 的挂起位图清零(真实芯片没有按位清除手段) */
|
||||
uint8_t ch395f_get_sock_int_status(uint8_t sock)
|
||||
{
|
||||
uint8_t v = 0U;
|
||||
|
||||
g_chip.int_status_calls++;
|
||||
g_chip.last_sock = sock;
|
||||
fake_log('i');
|
||||
if (sock < CH395F_FAKE_SOCK_MAX) {
|
||||
v = g_fake.int_status[sock];
|
||||
g_fake.int_status[sock] = 0U;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
uint16_t ch395f_get_recv_len(uint8_t sock)
|
||||
{
|
||||
uint16_t v;
|
||||
|
||||
g_chip.last_sock = sock;
|
||||
fake_log('R');
|
||||
if ((sock >= CH395F_FAKE_SOCK_MAX) || (g_fake.recv_len_n[sock] == 0)) {
|
||||
return 0U;
|
||||
}
|
||||
/* 按调用序号取;耗尽后回落最后一个值(便于表达"一直有数据"的持续态) */
|
||||
if (g_fake.recv_len_i[sock] < g_fake.recv_len_n[sock]) {
|
||||
v = g_fake.recv_len_q[sock][g_fake.recv_len_i[sock]++];
|
||||
} else {
|
||||
v = g_fake.recv_len_q[sock][g_fake.recv_len_n[sock] - 1];
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void ch395f_read_recv_buf(uint8_t sock, uint8_t *p_data, uint16_t len)
|
||||
{
|
||||
g_chip.read_calls++;
|
||||
g_chip.last_sock = sock;
|
||||
g_chip.last_read_len = len;
|
||||
fake_log('r');
|
||||
if ((p_data != NULL) && (len > 0U)) {
|
||||
(void)memset(p_data, 0, len); /* 交出去的是零,测试不依赖内容 */
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len)
|
||||
{
|
||||
(void)p_data;
|
||||
g_chip.write_calls++;
|
||||
g_chip.last_sock = sock;
|
||||
g_chip.last_read_len = len;
|
||||
fake_log('w');
|
||||
}
|
||||
|
||||
uint8_t ch395f_open_socket(uint8_t sock)
|
||||
{
|
||||
g_chip.open_calls++;
|
||||
g_chip.last_sock = sock;
|
||||
fake_log('o');
|
||||
return (sock < CH395F_FAKE_SOCK_MAX) ? g_fake.open_rc[sock] : 0x01U;
|
||||
}
|
||||
|
||||
uint8_t ch395f_close_socket(uint8_t sock)
|
||||
{
|
||||
g_chip.close_calls++;
|
||||
g_chip.last_sock = sock;
|
||||
fake_log('c');
|
||||
return CH395F_ERR_SUCCESS;
|
||||
}
|
||||
|
||||
uint8_t ch395f_tcp_listen(uint8_t sock)
|
||||
{
|
||||
g_chip.listen_calls++;
|
||||
g_chip.last_sock = sock;
|
||||
fake_log('l');
|
||||
return (sock < CH395F_FAKE_SOCK_MAX) ? g_fake.listen_rc[sock] : 0x01U;
|
||||
}
|
||||
|
||||
void ch395f_set_proto_type(uint8_t sock, uint8_t proto)
|
||||
{
|
||||
g_chip.set_proto_calls++;
|
||||
g_chip.last_sock = sock;
|
||||
g_chip.last_proto = proto;
|
||||
fake_log('P');
|
||||
}
|
||||
|
||||
void ch395f_set_sour_port(uint8_t sock, uint16_t port)
|
||||
{
|
||||
g_chip.set_port_calls++;
|
||||
g_chip.last_sock = sock;
|
||||
g_chip.last_port = port;
|
||||
fake_log('p');
|
||||
}
|
||||
|
||||
void ch395f_get_remot_ipp(uint8_t sock, uint8_t *p_buf)
|
||||
{
|
||||
(void)sock;
|
||||
if (p_buf != NULL) {
|
||||
(void)memset(p_buf, 0, 4U);
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_set_keepalive_enable(uint8_t sock, uint8_t enable)
|
||||
{
|
||||
g_chip.keepalive_calls++;
|
||||
g_chip.last_sock = sock;
|
||||
g_chip.last_keepalive = enable;
|
||||
}
|
||||
|
||||
uint8_t ch395f_link_up_cached(void)
|
||||
{
|
||||
return 1U;
|
||||
}
|
||||
|
||||
uint16_t ch395f_link_change_count(void)
|
||||
{
|
||||
return 0U;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 模块名称:假 CH395F 芯片(NET 域主机侧单测共享件)
|
||||
* 模块功能:给 NET 域的主机侧单测一个**可脚本化的假芯片**。真实 ch395f.h 拉 HAL 与
|
||||
* SPI 寄存器定义,PC 上编不动;而 net_listener / net_pending / net_events
|
||||
* 的行为恰恰完全由"芯片此刻报什么状态"决定,必须有假件才能测。
|
||||
*
|
||||
* ⚠ 仅供 test/host/** 使用,不参与 MCU 构建。
|
||||
*
|
||||
* 为什么值得共享一份(而不是每模块自带桩):三份假芯片各自漂移时,"假件与被测代码
|
||||
* 同时错"这类问题再也不会报出来。各模块的 stub/ch395f.h 只做一件事——include 本文件,
|
||||
* 于是签名与常量只有一份定义。
|
||||
*
|
||||
* 常量与类型必须与 Drivers/BSP/CH395F/ch395f.h 逐字一致(2026-10-23 从真实头抄录):
|
||||
* CH395F_ERR_SUCCESS 0x00 / CH395F_PROTO_TYPE_TCP 0x03 / CH395F_SOCKET_CLOSED 0x00
|
||||
* CH395F_TCP_CLOSED 0x00 / CH395F_TCP_LISTEN 0x01;ch395f_sock_state_t 为
|
||||
* { uint8_t open; uint8_t tcp_state; } 两字节。
|
||||
*
|
||||
* 必须复刻的两条芯片怪癖(不复刻就等于把被测代码的坑假件掉):
|
||||
* 1. ch395f_get_sock_int_status() 是**整体读即清**、芯片没有按位清除手段。本假件
|
||||
* 返回该 socket 的挂起位图并同步清零。AGENTS 模块约定里 B3 的实证坑("顺手
|
||||
* 全量清一遍"把别的在用 Socket 的门焊死)靠这条才照得出来。
|
||||
* 2. *_cached 系列返回的是**缓存值**而不是现场值:open 状态与 TCP 状态分开、且
|
||||
* "监听还活着"的判据是 (open != CLOSED && tcp_state == LISTEN)。测试据此能构造
|
||||
* "缓存陈旧"的形态。
|
||||
*
|
||||
* 脚本化模型(沿用本仓既有假件风格:nand_port 的真头+假 diskio、lftpd_io 的可注入假堆):
|
||||
* - 每个读函数一条按调用序号取的返回队列,队列耗尽后回落到**最后一个值**(便于
|
||||
* 写"一直可读""一直关不掉"这类持续状态);
|
||||
* - 每个写/控制函数一条调用记录(次数 + 最近一次入参),另给逐函数调用计数,
|
||||
* 让"这一轮根本不该碰芯片"成为可断言的事;
|
||||
* - g_chip.log[] 记录**调用顺序**(短名),用于断言"先清接收、再关、再读即清"这类
|
||||
* 时序契约;
|
||||
* - ch395f_fake_reset() 一次全清。
|
||||
*
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
|
||||
#ifndef CH395F_FAKE_H
|
||||
#define CH395F_FAKE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* —— 与真实 ch395f.h 逐字一致的常量(值抄自真实头,勿"顺手改")—— */
|
||||
#define CH395F_ERR_SUCCESS 0x00U
|
||||
#define CH395F_PROTO_TYPE_TCP 0x03U
|
||||
#define CH395F_SOCKET_CLOSED 0x00U
|
||||
#define CH395F_SOCKET_OPEN 0x01U
|
||||
#define CH395F_TCP_CLOSED 0x00U
|
||||
#define CH395F_TCP_LISTEN 0x01U
|
||||
#define CH395F_TCP_ESTABLISHED 0x05U
|
||||
|
||||
#define CH395F_FAKE_SOCK_MAX 8 /* 与 NET_MAX_SOCKETS 同量级,测试自用 */
|
||||
#define CH395F_FAKE_QUEUE_LEN 24
|
||||
#define CH395F_FAKE_LOG_LEN 96
|
||||
|
||||
typedef struct {
|
||||
uint8_t open;
|
||||
uint8_t tcp_state;
|
||||
} ch395f_sock_state_t;
|
||||
|
||||
/* —— 可预置的芯片侧世界 —— */
|
||||
typedef struct {
|
||||
uint8_t cached_open[CH395F_FAKE_SOCK_MAX]; /* sock_state_cached().open */
|
||||
uint8_t cached_tcp[CH395F_FAKE_SOCK_MAX]; /* sock_state_cached().tcp_state */
|
||||
uint8_t status_byte0[CH395F_FAKE_SOCK_MAX]; /* get_socket_status 的 sock 状态 */
|
||||
uint8_t status_byte1[CH395F_FAKE_SOCK_MAX]; /* get_socket_status 的 TCP 状态 */
|
||||
uint8_t int_status[CH395F_FAKE_SOCK_MAX]; /* 读即清的挂起位图 */
|
||||
uint16_t recv_len_q[CH395F_FAKE_SOCK_MAX][CH395F_FAKE_QUEUE_LEN];
|
||||
int recv_len_n[CH395F_FAKE_SOCK_MAX];
|
||||
int recv_len_i[CH395F_FAKE_SOCK_MAX];
|
||||
uint8_t open_rc[CH395F_FAKE_SOCK_MAX]; /* open_socket 返回码 */
|
||||
uint8_t listen_rc[CH395F_FAKE_SOCK_MAX]; /* tcp_listen 返回码 */
|
||||
} ch395f_fake_world_t;
|
||||
|
||||
typedef struct {
|
||||
/* 调用计数("不该碰芯片"的断言面) */
|
||||
int open_calls;
|
||||
int close_calls;
|
||||
int listen_calls;
|
||||
int int_status_calls;
|
||||
int get_status_calls;
|
||||
int read_calls;
|
||||
int write_calls;
|
||||
int set_proto_calls;
|
||||
int set_port_calls;
|
||||
int cached_calls;
|
||||
/* 最近一次入参 */
|
||||
uint8_t last_sock;
|
||||
uint16_t last_read_len;
|
||||
uint16_t last_port;
|
||||
uint8_t last_proto;
|
||||
uint8_t last_keepalive;
|
||||
int keepalive_calls;
|
||||
/* 调用顺序日志(短名首字母):
|
||||
* 'c'=close 'i'=int_status(读即清) 's'=get_socket_status 'r'=read_recv_buf
|
||||
* 'R'=get_recv_len 'o'=open_socket 'l'=tcp_listen 'p'=set_sour_port
|
||||
* 'P'=set_proto_type */
|
||||
char log[CH395F_FAKE_LOG_LEN];
|
||||
int log_n;
|
||||
} ch395f_fake_obs_t;
|
||||
|
||||
extern ch395f_fake_world_t g_fake;
|
||||
extern ch395f_fake_obs_t g_chip;
|
||||
|
||||
void ch395f_fake_reset(void); /* 全清并把所有 socket 设为"已关闭" */
|
||||
void ch395f_fake_push_recv_len(uint8_t sock, uint16_t v);
|
||||
void ch395f_fake_set_listening(uint8_t sock); /* 缓存:open!=CLOSED 且 tcp=LISTEN */
|
||||
|
||||
/* —— 假芯片实现(签名与真实 ch395f.h 一字不改)—— */
|
||||
ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn);
|
||||
ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn);
|
||||
void ch395f_get_socket_status(uint8_t sock, uint8_t *status);
|
||||
uint8_t ch395f_get_sock_int_status(uint8_t sock);
|
||||
uint16_t ch395f_get_recv_len(uint8_t sock);
|
||||
void ch395f_read_recv_buf(uint8_t sock, uint8_t *p_data, uint16_t len);
|
||||
void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len);
|
||||
uint8_t ch395f_open_socket(uint8_t sock);
|
||||
uint8_t ch395f_close_socket(uint8_t sock);
|
||||
uint8_t ch395f_tcp_listen(uint8_t sock);
|
||||
void ch395f_set_proto_type(uint8_t sock, uint8_t proto);
|
||||
void ch395f_set_sour_port(uint8_t sock, uint16_t port);
|
||||
void ch395f_get_remot_ipp(uint8_t sock, uint8_t *p_buf);
|
||||
void ch395f_set_keepalive_enable(uint8_t sock, uint8_t enable);
|
||||
uint8_t ch395f_link_up_cached(void);
|
||||
uint16_t ch395f_link_change_count(void);
|
||||
|
||||
#endif /* CH395F_FAKE_H */
|
||||
@@ -0,0 +1,545 @@
|
||||
/*
|
||||
* 模块名称:NET 层监听重建(net_listener)主机侧单元测试
|
||||
* 模块功能:在 PC 上离线验证 Drivers/BSP/NET/net_listener.c 的**自动重开监听**状态机:
|
||||
* 请求去重、五种放弃条件、挂起 recv 让路、30ms 退避与 2000ms 预算、
|
||||
* 隔离/优雅两级沉降窗(400ms / 100ms)、800ms 上限"只留痕不放弃"、
|
||||
* 接收区清理的 256 字节钳位与 16 轮上限、重建后的 peer 簿记,以及
|
||||
* "先清接收 → 关断 → 读即清 → 读状态"的芯片调用顺序。
|
||||
*
|
||||
* 被测对象:真实源文件 ../../../Drivers/BSP/NET/net_listener.c(#include 原样纳入)。
|
||||
* net_listener.h / net_types.h / net_config.h / net_pending.h 全用**真实头**
|
||||
* (net_sock_t 字段名、NET_SOCK_STATE_* / NET_SOCK_STREAM / NET_ERR_* /
|
||||
* net_listener_req_t 枚举一改就编译期报错)。假件:
|
||||
* - 假 CH395F 芯片:test/host/common/ch395f_fake.h/.c(15 个真实原型 +
|
||||
* 按调用序号取值的返回队列 + 调用计数与**调用顺序日志**);
|
||||
* - 可控 socket 表(net_get_sock 可写回项)、可控时基(HAL_GetTick 由测试
|
||||
* 显式设定,400/100/800/2000 四条时间线都能卡点)、挂起 recv 的有无、
|
||||
* dbg_log 计数、net_set_errno 记录。
|
||||
*
|
||||
* 为什么要主机侧单测(而不是只靠台架):
|
||||
* 1. CH395F 是**单连接 socket**:监听与数据通道复用同一号,断线后必须重开成 LISTEN,
|
||||
* 否则端口"连接被拒"。重开时机由"芯片此刻报什么状态"决定,而这些状态在真机上
|
||||
* 一闪而过——对端配合、时序、总线全都得凑齐才见得到一次,台架无法稳定复现。
|
||||
* 2. 这里的时间窗是**四道互相牵制的闸门**(30ms 退避 / 400 与 100 沉降 / 800 留痕 /
|
||||
* 2000 预算),排期登记里那句"沉降窗 800ms 上限只留痕"就是从这里来的:芯片永远
|
||||
* 关不掉时,代码**不能放弃请求**,也不能每 30ms 刷一条日志。真机上验证一次要等
|
||||
* 到真断线,主机侧把 tick 做成显式量后,1399ms 与 1400ms 的差别就是一行断言。
|
||||
* 3. GET_SOCK_INT_STATUS 是**整体读即清**,"顺手全量清一遍"会把别的在用 socket 的
|
||||
* 门焊死(AGENTS 模块约定 B3 实证坑)。假芯片必须复刻读即清,本文件用调用顺序
|
||||
* 日志钉住"只读自己这一路"的时序。
|
||||
*
|
||||
* 覆盖点:
|
||||
* TC-NETLIS-001 越界 fd:settle/request/clear 静默返回,tick 不产生任何芯片调用
|
||||
* TC-NETLIS-010 芯片显示仍在监听 → 不重建,且请求被 clear(下次还能受理)
|
||||
* TC-NETLIS-011 有挂起 recv / 挂起 send 时让路,但**保留请求**(不丢重建)
|
||||
* TC-NETLIS-012 五种放弃条件(非 LISTENING/CLOSED 态、auto_relisten=0、非 STREAM、
|
||||
* local_port=0、表项 in_use=0)→ 静默 clear,不报 ERROR
|
||||
* TC-NETLIS-013 重建成功后请求清空,重复 tick 不重复重建
|
||||
* TC-NETLIS-014 open 失败:关回去 + 报 BUSY + 30ms 退避(20ms 后不重试、30ms 重试),
|
||||
* 首轮建立 2000ms 预算
|
||||
* TC-NETLIS-015 tcp_listen 失败也要关断 socket
|
||||
* TC-NETLIS-016 重建成功后簿记:转 LISTENING、清 send_ready/recv_bytes/remote_*、
|
||||
* 把旧 remote_port 记进 last_peer_port、清 lost_conn_seen/released_abnormal
|
||||
* TC-NETLIS-017 重建用的源端口与协议取自表项本身(端口不能漂)
|
||||
* TC-NETLIS-018 预算耗尽:清请求 + 报一次 ERROR 交对账器接手,之后不再自己试
|
||||
* TC-NETLIS-019 请求去重:已有请求时后来的 reason 不覆盖首个
|
||||
* TC-NETLIS-020 隔离级沉降:关不掉就等,且**只在第一次**打 INFO
|
||||
* TC-NETLIS-021 隔离级 hold = 400ms(399ms 不放行、1400ms 放行)
|
||||
* TC-NETLIS-021b 优雅级 hold = 100ms(99ms 不放行、1100ms 放行)
|
||||
* TC-NETLIS-022 800ms 上限**只留痕一次且不放弃**,到 2000ms 才交给对账器
|
||||
* TC-NETLIS-023 清理接收区:单次读钳到 256 字节、最多 16 轮(读到空即停)
|
||||
* TC-NETLIS-024 芯片调用顺序:清接收 → 关断 → 读即清 → 读状态
|
||||
* TC-NETLIS-025 读即清真的清零(第二次读到 0)
|
||||
* TC-NETLIS-026 表项不存在由 tick 直接放弃(net_relisten_locked 里的 BADF 分支
|
||||
* 从 tick 不可达 —— 死分支留痕,见注释)
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
*
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "net_listener.h"
|
||||
#include "net_pending.h"
|
||||
#include "net_socket.h"
|
||||
#include "ch395f.h"
|
||||
|
||||
/* 假芯片实现与生产源进同一个 TU(本仓主机单测既有做法) */
|
||||
#include "ch395f_fake.c"
|
||||
#include "net_listener.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_nlis_info_count;
|
||||
int g_nlis_error_count;
|
||||
int g_nlis_debug_count;
|
||||
|
||||
/* ===================== 可控 socket 表 ===================== */
|
||||
|
||||
static net_sock_t g_sock[NET_MAX_SOCKETS];
|
||||
static uint8_t g_sock_valid[NET_MAX_SOCKETS];
|
||||
|
||||
net_sock_t *net_get_sock(int sockfd)
|
||||
{
|
||||
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
|
||||
return NULL;
|
||||
}
|
||||
if (g_sock_valid[sockfd] == 0u) {
|
||||
return NULL;
|
||||
}
|
||||
return &g_sock[sockfd];
|
||||
}
|
||||
|
||||
/* ===================== 挂起 recv / send 的有无 ===================== */
|
||||
|
||||
static int g_pending[NET_MAX_SOCKETS];
|
||||
static int g_send_pending[NET_MAX_SOCKETS];
|
||||
|
||||
int net_pending_active(int sockfd)
|
||||
{
|
||||
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
|
||||
return 0;
|
||||
}
|
||||
return g_pending[sockfd];
|
||||
}
|
||||
|
||||
int net_send_pending_active(int sockfd)
|
||||
{
|
||||
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
|
||||
return 0;
|
||||
}
|
||||
return g_send_pending[sockfd];
|
||||
}
|
||||
|
||||
/* ===================== 可控时基与 errno ===================== */
|
||||
|
||||
static uint32_t g_now;
|
||||
static int g_errno_last;
|
||||
static int g_errno_calls;
|
||||
|
||||
uint32_t HAL_GetTick(void)
|
||||
{
|
||||
return g_now; /* 由测试显式设定:400/100/800/2000 四条线要能卡点 */
|
||||
}
|
||||
|
||||
int net_set_errno(int err)
|
||||
{
|
||||
g_errno_last = err;
|
||||
g_errno_calls++;
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ===================== 复位与建表 ===================== */
|
||||
|
||||
static void reset_all(void)
|
||||
{
|
||||
(void)memset(g_sock, 0, sizeof(g_sock));
|
||||
(void)memset(g_sock_valid, 0, sizeof(g_sock_valid));
|
||||
(void)memset(g_pending, 0, sizeof(g_pending));
|
||||
(void)memset(g_send_pending, 0, sizeof(g_send_pending));
|
||||
g_now = 1000u;
|
||||
g_errno_last = 0;
|
||||
g_errno_calls = 0;
|
||||
g_nlis_info_count = 0;
|
||||
g_nlis_error_count = 0;
|
||||
g_nlis_debug_count = 0;
|
||||
ch395f_fake_reset();
|
||||
/* 关键:被测模块的 static 状态(s_relisten_req / _next / _dead / _quarantine /
|
||||
* _settle_start / _settle_dead / _capped)不会随假世界一起复位。上一场景残留的
|
||||
* next/dead 会把后面每一条时序断言都带偏(实测一次红了 9 条)。用模块自己的
|
||||
* net_listener_clear() 清——它清的正是这些项,也顺带验证了 clear 的完整性。 */
|
||||
for (int i = 0; i < NET_MAX_SOCKETS; i++) {
|
||||
net_listener_clear(i);
|
||||
}
|
||||
}
|
||||
|
||||
/* 一个"刚断线、等待重开监听"的槽位 */
|
||||
static void mk_closed_listen(int fd, uint16_t port)
|
||||
{
|
||||
g_sock_valid[fd] = 1u;
|
||||
g_sock[fd].in_use = 1u;
|
||||
g_sock[fd].type = NET_SOCK_STREAM;
|
||||
g_sock[fd].state = NET_SOCK_STATE_CLOSED;
|
||||
g_sock[fd].auto_relisten = 1u;
|
||||
g_sock[fd].local_port = port;
|
||||
}
|
||||
|
||||
/* ===================== 边界与去重 ===================== */
|
||||
|
||||
static void tc_bounds(void)
|
||||
{
|
||||
printf("-- TC-NETLIS-001/019 bounds --\n");
|
||||
reset_all();
|
||||
net_listener_settle(-1, NET_LISTENER_SETTLE_GRACEFUL);
|
||||
net_listener_settle(NET_MAX_SOCKETS, NET_LISTENER_SETTLE_QUARANTINE);
|
||||
net_listener_request(-1, NET_LISTENER_REQ_RECON);
|
||||
net_listener_request(NET_MAX_SOCKETS, NET_LISTENER_REQ_RECON);
|
||||
net_listener_clear(-1);
|
||||
net_listener_clear(NET_MAX_SOCKETS);
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 0) && (g_chip.close_calls == 0) &&
|
||||
(g_nlis_error_count == 0),
|
||||
"TC-NETLIS-001 越界 fd 静默返回,不打扰芯片");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
net_listener_request(0, NET_LISTENER_REQ_ACCEPT);
|
||||
net_listener_request(0, NET_LISTENER_REQ_DROP); /* 应被忽略 */
|
||||
CHECK(s_relisten_req[0] == (uint8_t)(NET_LISTENER_REQ_ACCEPT + 1U),
|
||||
"TC-NETLIS-019 请求去重:后来的 reason 不覆盖首个");
|
||||
}
|
||||
|
||||
/* ===================== 该不该重建 ===================== */
|
||||
|
||||
static void tc_should_or_not(void)
|
||||
{
|
||||
printf("-- TC-NETLIS-010..013 --\n");
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
ch395f_fake_set_listening(0); /* 芯片说:还在 LISTEN */
|
||||
net_listener_request(0, NET_LISTENER_REQ_ACCEPT);
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 0) && (s_relisten_req[0] == 0u),
|
||||
"TC-NETLIS-010 监听仍活着就不重建,且请求已 clear");
|
||||
g_fake.cached_open[0] = CH395F_SOCKET_CLOSED; /* 之后才真死了 */
|
||||
net_listener_request(0, NET_LISTENER_REQ_RECON);
|
||||
net_listener_tick();
|
||||
CHECK(g_chip.open_calls == 1,
|
||||
"TC-NETLIS-010b 上一步确实清掉了请求(否则这里会被去重吞掉)");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
g_pending[0] = 1;
|
||||
net_listener_request(0, NET_LISTENER_REQ_EOF);
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 0) && (s_relisten_req[0] != 0u),
|
||||
"TC-NETLIS-011 挂起 recv 未结束时让路但保留请求");
|
||||
g_pending[0] = 0;
|
||||
g_send_pending[0] = 1;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 0) && (s_relisten_req[0] != 0u),
|
||||
"TC-NETLIS-011b 挂起 send 同样挡路");
|
||||
g_send_pending[0] = 0;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 1) && (s_relisten_req[0] == 0u),
|
||||
"TC-NETLIS-011c 挂起结束后自动补上重建");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
net_listener_request(0, NET_LISTENER_REQ_RECON);
|
||||
net_listener_tick();
|
||||
net_listener_tick();
|
||||
CHECK(g_chip.open_calls == 1, "TC-NETLIS-013 重建成功后不重复重建");
|
||||
}
|
||||
|
||||
static void tc_giveup_conditions(void)
|
||||
{
|
||||
int i;
|
||||
int opened[5];
|
||||
int cleared[5];
|
||||
int errs[5];
|
||||
|
||||
printf("-- TC-NETLIS-012 五种放弃条件 --\n");
|
||||
reset_all();
|
||||
for (i = 0; i < 5; i++) {
|
||||
uint8_t before_open = (uint8_t)g_chip.open_calls;
|
||||
(void)before_open;
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
switch (i) {
|
||||
case 0: g_sock[0].auto_relisten = 0u; break; /* 明确不自动重开 */
|
||||
case 1: g_sock[0].type = (NET_SOCK_STREAM + 1); break; /* 不是 TCP */
|
||||
case 2: g_sock[0].local_port = 0u; break; /* 没绑端口 */
|
||||
case 3: g_sock[0].in_use = 0u; break; /* 槽位已释放 */
|
||||
default: g_sock[0].state = NET_SOCK_STATE_ESTABLISHED; break;
|
||||
}
|
||||
net_listener_request(0, NET_LISTENER_REQ_RECON);
|
||||
net_listener_tick();
|
||||
opened[i] = g_chip.open_calls;
|
||||
cleared[i] = (s_relisten_req[0] == 0u) ? 1 : 0;
|
||||
errs[i] = g_nlis_error_count;
|
||||
}
|
||||
CHECK((opened[0] == 0) && (opened[1] == 0) && (opened[2] == 0) &&
|
||||
(opened[3] == 0) && (opened[4] == 0),
|
||||
"TC-NETLIS-012 五种条件下都不碰芯片");
|
||||
CHECK((cleared[0] == 1) && (cleared[1] == 1) && (cleared[2] == 1) &&
|
||||
(cleared[3] == 1) && (cleared[4] == 1),
|
||||
"TC-NETLIS-012b 放弃即清请求(不留在表里反复白试)");
|
||||
CHECK((errs[0] == 0) && (errs[2] == 0) && (errs[4] == 0),
|
||||
"TC-NETLIS-012c 放弃是静默的(不该报 ERROR,那是对账器的活)");
|
||||
|
||||
reset_all();
|
||||
g_sock_valid[0] = 0u; /* 连表项都没有 */
|
||||
net_listener_request(0, NET_LISTENER_REQ_RECON);
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 0) && (s_relisten_req[0] == 0u),
|
||||
"TC-NETLIS-026 表项不存在由 tick 直接放弃");
|
||||
/* 注:net_relisten_locked() 里的 NET_ERR_BADF 分支因此从 tick **不可达**
|
||||
* (net_listener.c:112 那段只在 p_sock 非空时才可能被调到)——留痕:它是
|
||||
* 内部兜底,不是活路径;将来若新增调用点,要重新审视这条。 */
|
||||
}
|
||||
|
||||
/* ===================== 失败退避与预算 ===================== */
|
||||
|
||||
static void tc_backoff_and_budget(void)
|
||||
{
|
||||
printf("-- TC-NETLIS-014/015/018 --\n");
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
g_fake.open_rc[0] = 0x01U; /* open 永远失败 */
|
||||
g_now = 1000u;
|
||||
net_listener_request(0, NET_LISTENER_REQ_RECON);
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 1) && (g_chip.close_calls == 1) &&
|
||||
(g_errno_last == NET_ERR_BUSY) && (s_relisten_req[0] != 0u) &&
|
||||
(s_relisten_next[0] == 1030u) && (s_relisten_dead[0] == 3000u),
|
||||
"TC-NETLIS-014 open 失败:关回去 + BUSY + 30ms 退避 + 首轮建 2000ms 预算");
|
||||
g_now = 1020u;
|
||||
net_listener_tick();
|
||||
CHECK(g_chip.open_calls == 1, "TC-NETLIS-014b 退避未到不重试(不空转打芯片)");
|
||||
g_now = 1030u;
|
||||
net_listener_tick();
|
||||
CHECK(g_chip.open_calls == 2, "TC-NETLIS-014c 退避到点再试一次");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
g_fake.listen_rc[0] = 0x01U; /* open 成功、listen 失败 */
|
||||
net_listener_request(0, NET_LISTENER_REQ_RECON);
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.listen_calls == 1) && (g_chip.close_calls == 1) &&
|
||||
(s_relisten_req[0] != 0u),
|
||||
"TC-NETLIS-015 listen 失败也要关断(不留下开了一半的 socket)");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
g_fake.open_rc[0] = 0x01U;
|
||||
g_now = 1000u;
|
||||
net_listener_request(0, NET_LISTENER_REQ_RECON);
|
||||
net_listener_tick(); /* 建预算 dead=3000 */
|
||||
g_fake.open_rc[0] = CH395F_ERR_SUCCESS; /* 芯片后来能开了 */
|
||||
g_now = 3000u; /* 但预算已耗尽 */
|
||||
net_listener_tick();
|
||||
CHECK((g_nlis_error_count == 1) && (s_relisten_req[0] == 0u) &&
|
||||
(g_chip.open_calls == 1),
|
||||
"TC-NETLIS-018 预算耗尽:报一次 ERROR 交对账器,之后不再自己试");
|
||||
}
|
||||
|
||||
/* ===================== 成功重建的簿记 ===================== */
|
||||
|
||||
static void tc_bookkeeping(void)
|
||||
{
|
||||
net_sock_t *p;
|
||||
|
||||
printf("-- TC-NETLIS-016/017 bookkeeping --\n");
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
p = &g_sock[0];
|
||||
p->state = NET_SOCK_STATE_CLOSED;
|
||||
p->remote_port = 1234u;
|
||||
p->remote_ip = 0x0A000001u;
|
||||
p->remote_ip_arr[0] = 9u;
|
||||
p->remote_ip_arr[3] = 7u;
|
||||
p->send_ready = 1u;
|
||||
p->recv_bytes = 77u;
|
||||
p->released_abnormal = 1u;
|
||||
p->lost_conn_seen = 1u;
|
||||
net_listener_request(0, NET_LISTENER_REQ_DROP);
|
||||
net_listener_tick();
|
||||
CHECK((p->state == NET_SOCK_STATE_LISTENING) && (p->send_ready == 0u) &&
|
||||
(p->recv_bytes == 0u) && (p->remote_port == 0u) &&
|
||||
(p->remote_ip == 0u),
|
||||
"TC-NETLIS-016 重建后回到干净监听态");
|
||||
CHECK((p->last_peer_port == 1234u) && (p->lost_conn_seen == 0u) &&
|
||||
(p->released_abnormal == 0u) && (p->remote_ip_arr[0] == 0u) &&
|
||||
(p->remote_ip_arr[3] == 0u),
|
||||
"TC-NETLIS-016b 旧对端端口进 last_peer_port,其余对端痕迹清零");
|
||||
CHECK((g_chip.set_port_calls == 1) && (g_chip.last_port == 2404u) &&
|
||||
(g_chip.set_proto_calls == 1) &&
|
||||
(g_chip.last_proto == CH395F_PROTO_TYPE_TCP),
|
||||
"TC-NETLIS-017 源端口与协议取自表项本身(监听端口不能漂)");
|
||||
}
|
||||
|
||||
/* ===================== 沉降窗:隔离 400 / 优雅 100 ===================== */
|
||||
|
||||
static void tc_settle_windows(void)
|
||||
{
|
||||
printf("-- TC-NETLIS-020/021 沉降窗 --\n");
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; /* 芯片关不掉 */
|
||||
g_fake.status_byte1[0] = CH395F_TCP_ESTABLISHED;
|
||||
net_listener_settle(0, NET_LISTENER_SETTLE_QUARANTINE);
|
||||
net_listener_request(0, NET_LISTENER_REQ_DROP);
|
||||
g_now = 1000u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 0) && (g_nlis_info_count == 1) &&
|
||||
(s_relisten_settle_start[0] == 1000u) &&
|
||||
(s_relisten_settle_dead[0] == 1800u) && (g_errno_last == NET_ERR_BUSY),
|
||||
"TC-NETLIS-020 关不掉就等:首次记 start 并给 800ms 留痕线,返回 BUSY");
|
||||
g_now = 1030u;
|
||||
net_listener_tick();
|
||||
CHECK(g_nlis_info_count == 1,
|
||||
"TC-NETLIS-020b 等待期间**只在第一次**打日志(否则每 30ms 刷一条)");
|
||||
|
||||
/* 边界必须在**独立场景**里测:放行判定与 30ms 退避共用 s_relisten_next,
|
||||
* 上一轮等待会把 next 设成"当次 + 30",于是同一条链里"恰好 400ms"那一次 tick
|
||||
* 根本不会发起尝试。这本身是个该知道的事实——**hold 到点后最坏还要再等一个
|
||||
* 退避周期才真正放行**(实测最多晚 30ms),写在这里免得后人以为是漏判。 */
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
g_fake.status_byte0[0] = CH395F_SOCKET_CLOSED;
|
||||
g_fake.status_byte1[0] = CH395F_TCP_CLOSED;
|
||||
net_listener_settle(0, NET_LISTENER_SETTLE_QUARANTINE);
|
||||
net_listener_request(0, NET_LISTENER_REQ_DROP);
|
||||
g_now = 1000u;
|
||||
net_listener_tick(); /* 记 start=1000,next=1030 */
|
||||
g_now = 1399u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 0) && (s_relisten_quarantine[0] != 0u),
|
||||
"TC-NETLIS-021 隔离级 hold=400ms:第 399ms 仍不放行");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
g_fake.status_byte0[0] = CH395F_SOCKET_CLOSED;
|
||||
g_fake.status_byte1[0] = CH395F_TCP_CLOSED;
|
||||
net_listener_settle(0, NET_LISTENER_SETTLE_QUARANTINE);
|
||||
net_listener_request(0, NET_LISTENER_REQ_DROP);
|
||||
g_now = 1000u;
|
||||
net_listener_tick();
|
||||
g_now = 1400u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 1) && (s_relisten_quarantine[0] == 0u),
|
||||
"TC-NETLIS-021b 第 400ms 放行并清隔离(独立场景,不受退避槽遮挡)");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
net_listener_settle(0, NET_LISTENER_SETTLE_GRACEFUL);
|
||||
net_listener_request(0, NET_LISTENER_REQ_EOF);
|
||||
g_now = 1000u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 0) && (s_relisten_settle_start[0] == 1000u),
|
||||
"TC-NETLIS-021c 优雅级同样先等(0 < 100ms)");
|
||||
g_now = 1099u;
|
||||
net_listener_tick();
|
||||
CHECK(g_chip.open_calls == 0, "TC-NETLIS-021c 优雅级 hold=100ms:第 99ms 不放行");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
net_listener_settle(0, NET_LISTENER_SETTLE_GRACEFUL);
|
||||
net_listener_request(0, NET_LISTENER_REQ_EOF);
|
||||
g_now = 1000u;
|
||||
net_listener_tick();
|
||||
g_now = 1100u;
|
||||
net_listener_tick();
|
||||
CHECK(g_chip.open_calls == 1, "TC-NETLIS-021d 优雅级第 100ms 放行(比隔离级快 4 倍)");
|
||||
}
|
||||
|
||||
static void tc_cap_is_trace_only(void)
|
||||
{
|
||||
printf("-- TC-NETLIS-022 800ms 上限只留痕 --\n");
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; /* 永远关不掉 */
|
||||
g_fake.status_byte1[0] = CH395F_TCP_ESTABLISHED;
|
||||
net_listener_settle(0, NET_LISTENER_SETTLE_QUARANTINE);
|
||||
net_listener_request(0, NET_LISTENER_REQ_PENDING);
|
||||
g_now = 1000u;
|
||||
net_listener_tick(); /* start=1000 settle_dead=1800 */
|
||||
g_now = 1800u;
|
||||
net_listener_tick();
|
||||
CHECK((s_relisten_capped[0] == 1u) && (g_nlis_info_count == 2) &&
|
||||
(g_chip.open_calls == 0) && (s_relisten_req[0] != 0u),
|
||||
"TC-NETLIS-022 到 800ms 只**留痕一次**,既不重建也**不放弃请求**");
|
||||
g_now = 1830u;
|
||||
net_listener_tick();
|
||||
CHECK(g_nlis_info_count == 2,
|
||||
"TC-NETLIS-022b 留痕只打一次(之后继续默默等,不刷屏)");
|
||||
g_now = 3000u;
|
||||
net_listener_tick();
|
||||
CHECK((g_nlis_error_count == 1) && (s_relisten_req[0] == 0u),
|
||||
"TC-NETLIS-022c 最终由 2000ms 预算接手:报 ERROR 并交出请求");
|
||||
}
|
||||
|
||||
/* ===================== 芯片交互:清理、顺序、读即清 ===================== */
|
||||
|
||||
static void tc_chip_interaction(void)
|
||||
{
|
||||
printf("-- TC-NETLIS-023/024/025 --\n");
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
net_listener_settle(0, NET_LISTENER_SETTLE_GRACEFUL);
|
||||
ch395f_fake_push_recv_len(0, 1000u); /* 一直有 1000 字节 */
|
||||
net_listener_request(0, NET_LISTENER_REQ_DROP);
|
||||
g_now = 1000u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.read_calls == 16) && (g_chip.last_read_len == 256u),
|
||||
"TC-NETLIS-023 清接收区:单次钳到 256 字节、最多 16 轮(有守卫不死循环)");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
net_listener_settle(0, NET_LISTENER_SETTLE_GRACEFUL);
|
||||
ch395f_fake_push_recv_len(0, 5u);
|
||||
ch395f_fake_push_recv_len(0, 0u); /* 第二轮就空了 */
|
||||
net_listener_request(0, NET_LISTENER_REQ_DROP);
|
||||
g_now = 1000u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.read_calls == 1) && (g_chip.last_read_len == 5u),
|
||||
"TC-NETLIS-023b 读到空立刻停手(不硬凑 16 轮)");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; /* 停在等待分支,便于看顺序 */
|
||||
net_listener_settle(0, NET_LISTENER_SETTLE_QUARANTINE);
|
||||
net_listener_request(0, NET_LISTENER_REQ_DROP);
|
||||
g_now = 1000u;
|
||||
net_listener_tick();
|
||||
CHECK((strcmp(g_chip.log, "Rcis") == 0) &&
|
||||
(g_chip.int_status_calls == 1) && (g_chip.get_status_calls == 1),
|
||||
"TC-NETLIS-024 顺序:清接收 → 关断 → 读即清 → 读状态(不可换)");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
g_fake.int_status[0] = 0x3FU;
|
||||
g_fake.status_byte0[0] = CH395F_SOCKET_OPEN;
|
||||
net_listener_settle(0, NET_LISTENER_SETTLE_QUARANTINE);
|
||||
net_listener_request(0, NET_LISTENER_REQ_DROP);
|
||||
g_now = 1000u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.int_status_calls >= 1) && (g_fake.int_status[0] == 0u),
|
||||
"TC-NETLIS-025 读即清真的清零(复刻芯片无按位清除的怪癖)");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== NET Listener Tests ===\n");
|
||||
|
||||
tc_bounds();
|
||||
tc_should_or_not();
|
||||
tc_giveup_conditions();
|
||||
tc_backoff_and_budget();
|
||||
tc_bookkeeping();
|
||||
tc_settle_windows();
|
||||
tc_cap_is_trace_only();
|
||||
tc_chip_interaction();
|
||||
|
||||
printf("\n=== NET Listener Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""NET 层监听自动重建(net_listener)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/net_listener/net_listener_test.c(以 #include 纳入生产
|
||||
源 Drivers/BSP/NET/net_listener.c 与共享假芯片 test/host/common/ch395f_fake.c),
|
||||
在 PC 上离线验证自动重开监听的整套闸门:请求去重、五种放弃条件、挂起 recv/send
|
||||
让路、30ms 退避、400/100ms 两级沉降窗、800ms 上限只留痕、2000ms 预算交对账器、
|
||||
接收区清理的 256B 钳位与 16 轮上限、重建后的 peer 簿记与芯片调用顺序。
|
||||
工具链与判定委托 test/host/common/unit_test_harness.py。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
CH395F 是单连接 socket——监听与数据通道复用同一号,断线后必须重开成 LISTEN,否则
|
||||
端口"连接被拒"。而重开时机取决于"芯片此刻报什么状态",这些状态真机上一闪而过,
|
||||
要凑齐对端行为、时序与总线才见得到一次。四道时间闸门(30/400·100/800/2000ms)互相
|
||||
牵制,台架无法稳定复现;把 HAL_GetTick 做成显式量后,399ms 与 400ms 的差别就是一行
|
||||
断言。GET_SOCK_INT_STATUS 是整体读即清("顺手全量清一遍"会把别的在用 socket 的门
|
||||
焊死),必须由假芯片复刻才照得出来。
|
||||
|
||||
桩的边界:net_listener.h / net_types.h / net_config.h / net_pending.h 用真实头(net_sock_t
|
||||
字段名与各枚举一改即编译期报错);net_socket.h 是桩(真实那份带 netTask 消息队列,
|
||||
PC 上编不动),但 net_set_errno 与 NET_ERR_RETURN_1 的写法照抄真实头同名宏;
|
||||
ch395f.h 只做转发,真身在 test/host/common/ch395f_fake.h/.c —— NET 域三个模块共享
|
||||
一份假芯片,签名与常量只有一处定义。main.h / cmsis_os.h / FreeRTOS.h / task.h /
|
||||
test_config.h / net_latency_test.h / dbg_log.h 是本目录桩;刻意不定义
|
||||
TEST_LATENCY_PROBE(延迟探针不是被测对象,且它要写真实时间)。
|
||||
|
||||
用法(仓库根目录):
|
||||
python test/host/net_listener/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误。
|
||||
|
||||
设计依据:docs/网络/网络自愈架构设计.md §4.10、docs/测试说明/主机单测覆盖率口径.md §9(假 CH395F 实施规格)
|
||||
创建日期:2026-10-23
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
|
||||
|
||||
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
|
||||
|
||||
# 顺序敏感:stub 必须最先(覆盖 ch395f.h / net_socket.h / main.h / dbg_log.h);
|
||||
# common 提供共享假芯片;NET 目录提供真实的 net_types.h / net_pending.h 等。
|
||||
BUILD = TestBuild(
|
||||
name="NET Listener Tests",
|
||||
src=os.path.join(HERE, "net_listener_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "test", "host", "common"),
|
||||
os.path.join(ROOT, "Drivers", "BSP", "NET"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== NET Listener Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - FreeRTOS.h(net_listener 模块专用)
|
||||
* 模块功能:本模块不调任何内核 API,但真实 net_pending.h 的注册类原型里带
|
||||
* TaskHandle_t 参数(include 链绕不开),故这里给出它的宿主等价物。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* TaskHandle_t 在 FreeRTOS 里就是 void * 不透明句柄,这里保持一致——它只出现在
|
||||
* 原型签名中、net_listener.c 从不传它,所以宿主侧用什么表示都不影响被测语义;
|
||||
* 但**原型必须能编**,否则真实 net_pending.h 就进不来(进不来就丢掉接口真实性)。
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
#ifndef FREERTOS_H
|
||||
#define FREERTOS_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef void *TaskHandle_t; /* 与 FreeRTOS 一致:不透明指针句柄 */
|
||||
|
||||
#endif /* FREERTOS_H */
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - ch395f.h(net_listener 模块入口)
|
||||
* 模块功能:本文件**只做转发**——真正的假芯片在 test/host/common/ch395f_fake.h/.c。
|
||||
* ⚠ 转发而不是各写一份:NET 域三个模块共用同一份假芯片,签名与常量只有一处定义,
|
||||
* 避免"三份桩各自漂移、假件与被测代码同时错却互相圆场"。
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
#ifndef CH395F_H
|
||||
#define CH395F_H
|
||||
#include "ch395f_fake.h"
|
||||
#endif /* CH395F_H */
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - cmsis_os.h(net_listener 模块专用)
|
||||
* 模块功能:空壳。net_listener.c include 它但不用内核 API(重建在 tick 里同步做完)。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
#ifndef CMSIS_OS_H
|
||||
#define CMSIS_OS_H
|
||||
#include <stdint.h>
|
||||
#endif /* CMSIS_OS_H */
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - dbg_log.h(net_listener 模块专用)
|
||||
* 模块功能:DBG_* 换成计数,让"只在第一次打日志""上限只留痕一次""预算耗尽才报 ERROR"
|
||||
* 这三条日志契约可断言——它们正是排期登记里"沉降窗 800ms 上限只留痕"的原话。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
#ifndef DBG_LOG_H
|
||||
#define DBG_LOG_H
|
||||
extern int g_nlis_info_count;
|
||||
extern int g_nlis_error_count;
|
||||
extern int g_nlis_debug_count;
|
||||
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_nlis_info_count++; } while (0)
|
||||
#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_nlis_debug_count++; } while (0)
|
||||
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_nlis_error_count++; } while (0)
|
||||
#endif /* DBG_LOG_H */
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - main.h(net_listener 模块专用)
|
||||
* 模块功能:只给 HAL_GetTick(沉降窗/退避/预算三条时间轴的唯一致度源)。
|
||||
* ⚠ 仅供 test/host/net_listener/ 使用,不参与 MCU 构建。测试把 tick 做成"写多少
|
||||
* 就是多少"的显式量,于是 400ms / 100ms / 800ms / 2000ms 四条线都能精确卡点。
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
#ifndef MAIN_H
|
||||
#define MAIN_H
|
||||
#include <stdint.h>
|
||||
uint32_t HAL_GetTick(void);
|
||||
#endif /* MAIN_H */
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - net_latency_test.h(net_listener 模块专用)
|
||||
* 模块功能:空壳,配合 TEST_LATENCY_PROBE 不定义即被全部条件编译掉。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
#ifndef NET_LATENCY_TEST_H
|
||||
#define NET_LATENCY_TEST_H
|
||||
#endif /* NET_LATENCY_TEST_H */
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - net_socket.h(NET 域共享形态)
|
||||
* 模块功能:net_listener.c 需要 socket 表项类型与三个入口:net_get_sock()(本表项
|
||||
* 可变指针,重建时要写回簿记)、net_set_errno() 与 NET_ERR_RETURN_1()。
|
||||
* 状态类型与错误码常量一律取**真实** net_types.h(字段名/取值一改即编译期
|
||||
* 报错);net_set_errno 与 NET_ERR_RETURN_1 的写法照抄真实 net_socket.h:528/532。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。刻意不 include 真实 net_socket.h:它带着
|
||||
* 整条 netTask 消息队列与芯片依赖,主机侧编不动。
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
#ifndef NET_SOCKET_H
|
||||
#define NET_SOCKET_H
|
||||
|
||||
#include "net_types.h"
|
||||
|
||||
net_sock_t *net_get_sock(int sockfd);
|
||||
int net_set_errno(int err);
|
||||
#define NET_ERR_RETURN_1(err) (net_set_errno(err), -1)
|
||||
|
||||
#endif /* NET_SOCKET_H */
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - task.h(net_listener 模块专用)
|
||||
* 模块功能:空壳。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
#ifndef TASK_H
|
||||
#define TASK_H
|
||||
#endif /* TASK_H */
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - test_config.h(net_listener 模块专用)
|
||||
* 模块功能:空壳。net_listener.c 只在 TEST_LATENCY_PROBE 打开时才用延迟探针宏,
|
||||
* 主机单测刻意**不开**该宏(探针不是被测对象,且它要写真实时间)。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
#ifndef TEST_CONFIG_H
|
||||
#define TEST_CONFIG_H
|
||||
#endif /* TEST_CONFIG_H */
|
||||
Reference in New Issue
Block a user