merge: task/cov2-p2-coverage 合入 v2-on-ui
任务 cov2(4 个提交)经 task-worktree 回收: - fix(log): syslog_post 投递即拷贝(台架实证驱动的 B 方案整改) - test(host): 断言字面量统一纯 ASCII,30 文件 1535 行 - fix(log,net): 处置主机单测照出的 10 条现状,撤销 1 条误报 - test(host): P2 覆盖率批次,5 套主机单测与共享假 FatFs
This commit is contained in:
@@ -18,6 +18,10 @@
|
||||
* dhara journal 竞争实证);busy 返回 -1 由调用方
|
||||
* 按既有失败语义处理(poll 保留 RAM/保存待重试),
|
||||
* 本任务 = 看门狗喂狗点,绝不阻塞等锁
|
||||
* v1.5 2026-10-24 王建锋 仅注释更正:cfg_store_read 的"限定条件"引用了
|
||||
* FRESULT 里不存在的 FR_NO_SYSTEM,改成实际会返回的
|
||||
* FR_NOT_READY / FR_NO_FILESYSTEM 并写明"非 FR_OK 一律
|
||||
* 转 -1,不要枚举错误码集合"(无行为改动)
|
||||
* v1.4 2026-10-05 王建锋 原子写临时文件由 p_path 推导(P3-2):与目标同目录的
|
||||
* 固定 8.3 名 config.TMP,不再硬编码卷根;目标恒为
|
||||
* "/config.ini" 时推导结果与旧值逐字节一致
|
||||
@@ -47,7 +51,15 @@ static const char s_path_tmp[] = "/config.TMP";
|
||||
* cap - 缓冲容量 size_t
|
||||
* p_nread - 实际读字节数输出(不可为 NULL) size_t *
|
||||
* 返回值:0 - 成功;-1 - 文件缺失/读失败 int
|
||||
* 限定条件:卷已挂载(未挂载时 f_open 返回 FR_NO_SYSTEM,转 -1)
|
||||
* 限定条件:卷已挂载。未挂载时本函数一律转 -1,但**别照着不存在的错误码去写
|
||||
* 判据**:Lib/FatFs/ff.h 的 FRESULT 共 20 个码(FR_OK..FR_INVALID_PARAMETER),
|
||||
* 没有 FR_NO_SYSTEM(旧注释此处笔误,2026-10-24 更正)。本移植里卷未挂载
|
||||
* 实际返回 FR_NOT_READY(3)(物理驱动器不可用)或 FF_VOLUMES 内无已挂载卷时
|
||||
* 由 mount_volume 直接给错;卷上无有效 FAT 则返回 FR_NO_FILESYSTEM(13)。
|
||||
* 本函数(以及 write_atomic / stat / remove)对**任何**非 FR_OK 一律转 -1,
|
||||
* 所以这些码的差异不影响对外语义——也正因如此,谁都不该去枚举错误码集合,
|
||||
* 除了自愈触发集那种"必须区分能不能靠删文件修好"的场合(见 sys_log_mcu.c
|
||||
* 的 s_is_selfheal_code)。
|
||||
* 函数说明:FIL 为**函数级 static**而非局部——本 FatFs 移植 FF_FS_TINY=0,
|
||||
* `FIL` 内含 `buf[FF_MAX_SS]`=512 B(Lib/FatFs/ff.h),一个局部对象就吃掉
|
||||
* 528 B 栈,是配置/syslog 调用链栈深的主要来源(2026-10-10 栈预算整改)。
|
||||
|
||||
+98
-33
@@ -31,6 +31,17 @@
|
||||
* adcTask(B1)等周期任务此时落日志会与 defaultTask 的存储初始化
|
||||
* 日志字节级交错(台架实测 [ADC_TASK] 首帧行与 [NAND_FTL] 行交错)。
|
||||
* 提供就绪查询供周期任务门控自身日志,不改直发路径本身。
|
||||
* 2026-10-24 王建锋 主机单测(test/host/console_log)照出的四条一并修复:
|
||||
* 冷却改判独立标志 s_con_cooling(旧写法拿 tick 值当"是否在冷却"哨兵,
|
||||
* 故障恰好落在 HAL_GetTick()==0 那一拍时冷却等于没记);
|
||||
* con_append 的"复核空位 + 整行拷贝 + 推进 head"收进同一临界区(旧写法
|
||||
* 判据在区外,多生产者可在环将满时同时过判据 -> head 越过容量把未发出
|
||||
* 的旧行静默覆盖,既不报错也不计丢弃);
|
||||
* drain 失败分诊:Instance==NULL(未初始化)不再走外设软复位自愈(那会
|
||||
* 在 HAL 未就绪时重配 RCC/GPIO),改记 dropped;僵尸通道仍自愈;
|
||||
* 三本账分开(docs §13-O2):新增 dbg_console_tx_timeout(),dropped 只记
|
||||
* 真丢的,tx_fault 仍只记软复位自愈次数;[ERR] dropped 行改为"发成功才
|
||||
* 销账",通道不可用期间不再把丢弃证据一起冲掉。
|
||||
*/
|
||||
|
||||
#include "console_log.h"
|
||||
@@ -89,10 +100,12 @@
|
||||
static uint8_t s_con_ring[DBG_CON_RING_SIZE];
|
||||
static volatile uint16_t s_con_head; /* 生产者写入位置 */
|
||||
static volatile uint16_t s_con_tail; /* 写者读取位置 */
|
||||
static volatile uint32_t s_con_drop; /* 因缓冲满丢弃的行数 */
|
||||
static volatile uint32_t s_con_drop; /* 真没落地的丢弃数(一次一行/一块) */
|
||||
static volatile uint32_t s_con_tx_fault; /* TX 失败(僵尸通道)次数,均经外设软复位自愈 */
|
||||
static volatile uint8_t s_con_tx_fault_log; /* TX 故障留痕待上报(由写者任务在 drain 之外补记 run.log) */
|
||||
static volatile uint32_t s_con_cool_until; /* 故障冷却截止 tick(0 = 无冷却) */
|
||||
static volatile uint32_t s_con_cool_until; /* 故障冷却截止 tick(只存时刻,不再兼作哨兵) */
|
||||
static volatile uint8_t s_con_cooling; /* 1 = 正在故障冷却(独立标志:tick 恰好等于 0 的那一拍也必须算在冷却) */
|
||||
static volatile uint32_t s_con_tx_timeout; /* 发送块超时次数(写者排空所见,与丢弃账分开) */
|
||||
static volatile uint8_t s_con_draining; /* 1 = 处于 drain 中(重入保护) */
|
||||
static volatile uint8_t s_con_writer_up; /* 1 = 写者已注册(此后走环形缓冲) */
|
||||
static volatile uint8_t s_con_dwt_up; /* 1 = DWT CYCCNT 已使能(硬上界基准可用) */
|
||||
@@ -179,7 +192,7 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) {
|
||||
|
||||
con_dwt_ensure();
|
||||
if (huart1.Instance == NULL) {
|
||||
return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位 */
|
||||
return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位(不是超时,不记超时账) */
|
||||
}
|
||||
if (s_con_dwt_up == 0U) {
|
||||
return con_tx_counted(huart1.Instance, p_buf, len);
|
||||
@@ -190,14 +203,14 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) {
|
||||
for (i = 0U; i < len; i++) {
|
||||
while ((u->SR & USART_SR_TXE) == 0U) {
|
||||
if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) {
|
||||
return HAL_TIMEOUT; /* 有界:本块放弃(调用方自愈) */
|
||||
return HAL_TIMEOUT; /* 有界:本块放弃(记账由调用方按返回值做) */
|
||||
}
|
||||
}
|
||||
u->DR = (uint16_t)p_buf[i];
|
||||
}
|
||||
while ((u->SR & USART_SR_TC) == 0U) {
|
||||
if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) {
|
||||
return HAL_TIMEOUT;
|
||||
return HAL_TIMEOUT; /* TC 超时:字节已交给发送器,调用方分账 */
|
||||
}
|
||||
}
|
||||
return HAL_OK;
|
||||
@@ -281,13 +294,16 @@ void dbg_console_drain(void) {
|
||||
|
||||
/* 故障冷却:僵尸通道(ORE 重锁)秒内不自愈,冷却期内不发送——
|
||||
* 本轮 drain 直接让位主循环,保住喂狗节奏(gen-288 停喂误复位的
|
||||
* 直接诱因 = 背靠连发的块超时链)。无符号差值跨 49.7 天回绕安全 */
|
||||
if (s_con_cool_until != 0U) {
|
||||
* 直接诱因 = 背靠连发的块超时链)。是否在冷却看 s_con_cooling 独立标志,
|
||||
* **不再拿 tick 值当哨兵**(2026-10-24 实测:故障恰好落在 tick==0 那一拍时,旧写法
|
||||
* 记出的 cool_until==0 等于没记冷却,下一轮立刻再耗一次 100ms 块超时)。
|
||||
* 无符号差值跨 49.7 天回绕安全 */
|
||||
if (s_con_cooling != 0U) {
|
||||
if ((uint32_t)(t0 - s_con_cool_until) < DBG_CON_FAULT_COOL_MS) {
|
||||
s_con_draining = 0U;
|
||||
return;
|
||||
}
|
||||
s_con_cool_until = 0U;
|
||||
s_con_cooling = 0U;
|
||||
}
|
||||
|
||||
while ((used = con_used()) != 0U) {
|
||||
@@ -309,12 +325,28 @@ void dbg_console_drain(void) {
|
||||
taskEXIT_CRITICAL();
|
||||
|
||||
if (st != HAL_OK) {
|
||||
if (st == HAL_ERROR) {
|
||||
/* 2026-10-24 修复:USART1 **未初始化**(huart1.Instance == NULL,
|
||||
* 只可能出现在 HAL_Init 早期或断言/故障上报路径)与"僵尸通道"是两回事:
|
||||
* 这里**绝不做外设重初始化**——那会在 HAL 尚未就绪时跑 MspInit 重配
|
||||
* RCC/GPIO;也不计 tx_fault(那本账的语义是"软复位自愈次数")。
|
||||
* 本块字节确实一个都没上线 -> 计入 dropped(只记真丢的),
|
||||
* 末尾的 [ERR] dropped 行负责留痕;本轮到此为止(失败即止)。 */
|
||||
s_con_drop++;
|
||||
break;
|
||||
}
|
||||
/* 僵尸通道:软复位自愈 + 进入发送冷却(ORE 毫秒级重锁,
|
||||
* 继续背靠连发只会把主循环耗在块超时链上);本块丢弃
|
||||
* (行已出环,不可重放) */
|
||||
* (行已出环,不可重放),本块在超时账与丢弃账上各记一笔 */
|
||||
s_con_tx_timeout++; /* 新增账:块超时次数(TXE 与 TC 两条硬上界
|
||||
* 都算,口径 = 写者排空时观察到的块超时) */
|
||||
con_uart_reinit();
|
||||
s_con_tx_fault++;
|
||||
s_con_drop++; /* 确认不了的块算真丢(一次丢弃事件),
|
||||
* 旧写法只涨 tx_fault -> 账上看不出日志在丢,
|
||||
* docs §13-O2 记的正是这条 */
|
||||
s_con_cool_until = (uint32_t)HAL_GetTick();
|
||||
s_con_cooling = 1U;
|
||||
if (s_con_tx_fault == 1U) {
|
||||
static const char note[] = "\r\n[WARN] CONSOLE tx fault: UART reinit (self-healed)\r\n";
|
||||
|
||||
@@ -344,14 +376,21 @@ void dbg_console_drain(void) {
|
||||
uint32_t d = s_con_drop;
|
||||
int n;
|
||||
|
||||
s_con_drop = 0U;
|
||||
n = snprintf(note, sizeof(note), "\r\n[ERR] CONSOLE dropped %lu lines\r\n",
|
||||
(unsigned long)d);
|
||||
if (n > 0) {
|
||||
if (n > (int)sizeof(note) - 1) {
|
||||
n = (int)sizeof(note) - 1;
|
||||
}
|
||||
dbg_console_write_raw(note, (unsigned)n);
|
||||
/* 只有**真发出去了才销账**(2026-10-24 一并修正):旧写法先把计数清零
|
||||
* 再裸发,通道不可用/僵尸时那一发也失败 -> 丢弃证据被一起冲掉,[ERR] 行
|
||||
* 永远出不来("永不静默"变成"永不显示")。留到下一轮或串口恢复后补发。 */
|
||||
if (con_tx_bounded((const uint8_t *)note, (uint16_t)n) == HAL_OK) {
|
||||
/* 按"本次报出去的这条"扣账而不是清零:期间生产者新丢的行留下轮报。
|
||||
* 递减只发生在这一处(单写者 + s_con_draining 重入保护),生产者只增,
|
||||
* 故 s_con_drop >= d 恒成立,无需再写下溢分支(写了也永远测不到)。 */
|
||||
s_con_drop -= d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,6 +399,8 @@ void dbg_console_drain(void) {
|
||||
* 函数功能:把一段文本放入控制台(写者已注册)/直接发出(未注册或中断上下文)
|
||||
* 返回值:0 - 已入缓冲或已直发;-1 - 缓冲满丢弃(计入 dbg_console_dropped)
|
||||
* 函数说明:生产者不持锁等待过久:最多 DBG_CON_WAIT_MS,仍满则丢弃该行;
|
||||
* "复核空位 -> 整行拷贝 -> 推进 head"是一个原子的临界区动作;
|
||||
* 环满等待(vTaskDelay / DWT 判据)一律在临界区之外,区内绝不睡眠。
|
||||
* 写者自身记录日志时先排空缓冲,避免自等。
|
||||
* 等待上界优先用 DWT 真实时间(CON_APPEND_WAIT_CYC,时间源无关)——
|
||||
* tick 停摆时 vTaskDelay 本身也会停,故不能用它作唯一判据(round 8b)。
|
||||
@@ -392,10 +433,37 @@ static int con_append(const char *buf, uint16_t len) {
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
uint16_t used = con_used();
|
||||
uint16_t free_space = (uint16_t)((DBG_CON_RING_SIZE - 1U) - used);
|
||||
uint16_t free_space;
|
||||
int fitted = 0;
|
||||
|
||||
/* 复核空位 + 整行拷贝 + 推进 head **必须在同一个临界区内**(2026-10-24 修复):
|
||||
* 旧写法先出区判 free_space >= len 再进区写,两个生产者可在环 >95% 满时同时过
|
||||
* 判据,head 合计越过容量 -> con_used() 回绕变小 -> 后写的行覆盖尚未发出的旧行,
|
||||
* 且既不报错也不计丢弃(静默损坏)。本区最长拷 192B(168MHz 亚微秒级),相对
|
||||
* 115200 波特下一行约 16ms 可忽略;等待(vTaskDelay/DWT 判据)刻意留在区外。 */
|
||||
taskENTER_CRITICAL();
|
||||
free_space = (uint16_t)((DBG_CON_RING_SIZE - 1U) - con_used());
|
||||
if (free_space >= len) {
|
||||
uint16_t head = s_con_head;
|
||||
uint16_t first = (uint16_t)(DBG_CON_RING_SIZE - head);
|
||||
uint16_t i;
|
||||
|
||||
if (first > len) {
|
||||
first = len;
|
||||
}
|
||||
for (i = 0U; i < first; i++) {
|
||||
s_con_ring[head + i] = (uint8_t)buf[i];
|
||||
}
|
||||
for (i = first; i < len; i++) {
|
||||
s_con_ring[(uint16_t)((uint16_t)(head + i) & (DBG_CON_RING_SIZE - 1U))] =
|
||||
(uint8_t)buf[i];
|
||||
}
|
||||
s_con_head = (uint16_t)((uint16_t)(head + len) & (DBG_CON_RING_SIZE - 1U));
|
||||
fitted = 1;
|
||||
}
|
||||
taskEXIT_CRITICAL();
|
||||
|
||||
if (fitted != 0) {
|
||||
break;
|
||||
}
|
||||
if (have_dwt != 0U) {
|
||||
@@ -411,26 +479,6 @@ static int con_append(const char *buf, uint16_t len) {
|
||||
waited++;
|
||||
}
|
||||
|
||||
taskENTER_CRITICAL();
|
||||
{
|
||||
uint16_t head = s_con_head;
|
||||
uint16_t first = (uint16_t)(DBG_CON_RING_SIZE - head);
|
||||
uint16_t i;
|
||||
|
||||
if (first > len) {
|
||||
first = len;
|
||||
}
|
||||
for (i = 0U; i < first; i++) {
|
||||
s_con_ring[head + i] = (uint8_t)buf[i];
|
||||
}
|
||||
for (i = first; i < len; i++) {
|
||||
s_con_ring[(uint16_t)((uint16_t)(head + i) & (DBG_CON_RING_SIZE - 1U))] =
|
||||
(uint8_t)buf[i];
|
||||
}
|
||||
s_con_head = (uint16_t)((uint16_t)(head + len) & (DBG_CON_RING_SIZE - 1U));
|
||||
}
|
||||
taskEXIT_CRITICAL();
|
||||
|
||||
if (sys_evtHandle != NULL) {
|
||||
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_CONSOLE_TX); /* 唤醒写者 */
|
||||
}
|
||||
@@ -472,6 +520,23 @@ uint32_t dbg_console_tx_fault(void) {
|
||||
return s_con_tx_fault;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:已发生的发送块超时次数(诊断用)
|
||||
* 返回值:块超时累计次数 uint32_t
|
||||
* 限定条件:无(任意上下文可读,含 ISR)
|
||||
* 函数说明:三本账分开(docs/子系统设计/控制台单写者日志设计说明.md §13-O2)——
|
||||
* dbg_console_dropped() = **真没落地**的丢弃数(缓冲满丢的一行,或一个
|
||||
* 没能确认送达而出环的块;[ERR] 行报的就是它);
|
||||
* dbg_console_tx_fault() = 外设软复位**自愈次数**(语义不变);
|
||||
* dbg_console_tx_timeout() = 本账:写者排空时观察到的**块超时**次数
|
||||
* (TXE 与 TC 两条硬上界都算)。
|
||||
* 裸发路径(内核前 / ISR / 两条告警行自身)的超时不记本账——那些上下文里
|
||||
* 没有"块"的概念,也只可能发生在启动极早期,混进来只会让运维读数失去意义。
|
||||
* UART 物理失效但字节已发出的情形只涨本账,不污染另两本。 */
|
||||
uint32_t dbg_console_tx_timeout(void) {
|
||||
return s_con_tx_timeout;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取走"控制台 TX 故障待上报"标志(读后清)
|
||||
* 返回值:1 - 需要补记一条 run.log 证据,0 - 无
|
||||
|
||||
@@ -39,8 +39,13 @@ extern "C" {
|
||||
* (仅假设 defaultTask 在日志),其余任务此时落日志会与
|
||||
* defaultTask 的存储初始化日志字节级交错(2026-10-10 台架实测)
|
||||
* dbg_console_drain - 写者任务排空缓冲并发送(单次有界;TX 失败自动软复位自愈)
|
||||
* dbg_console_dropped - 因缓冲满丢弃的行数(诊断)
|
||||
* dbg_console_tx_fault - 因僵尸通道软复位自愈的次数(诊断)
|
||||
* dbg_console_dropped - 真没落地的丢弃数:缓冲满丢的一行,或一个没能确认送达
|
||||
* 而出环的块(诊断;[ERR] CONSOLE dropped 行报的就是它)
|
||||
* dbg_console_tx_fault - 因僵尸通道软复位自愈的次数(诊断,语义不变)
|
||||
* dbg_console_tx_timeout - 写者排空时观察到的块超时次数(TXE / TC 两条硬上界都算)。
|
||||
* 三本账分开:看日志有没有在丢 -> dropped;看外设自愈了几次
|
||||
* -> tx_fault;看 UART 物理失效(含"字节已发出但 TC 不置位")
|
||||
* -> tx_timeout。别把三本账混成一个数(docs §13-O2 )
|
||||
* dbg_console_tx_fault_log_take
|
||||
* - 取走"TX 故障待上报"标志(读后清)。**必须**由写者任务在
|
||||
* dbg_console_drain() 之外调用后自行 syslog_post——本模块
|
||||
@@ -55,6 +60,7 @@ uint8_t dbg_console_writer_ready(void);
|
||||
void dbg_console_drain(void);
|
||||
uint32_t dbg_console_dropped(void);
|
||||
uint32_t dbg_console_tx_fault(void);
|
||||
uint32_t dbg_console_tx_timeout(void);
|
||||
uint8_t dbg_console_tx_fault_log_take(void);
|
||||
|
||||
/*
|
||||
|
||||
+51
-5
@@ -3,6 +3,16 @@
|
||||
* 模块功能:实现 soe_store.h——protectTask 投递(取时标 + soe_rec_build +
|
||||
* SPSC 待发环 / 消息队列,零 FS);defaultTask(syslog_drain 尾部)
|
||||
* 排空落 /log/soe.log(64B 定长,容量满删除重建)。
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-12 王建锋 创建初始版本(B9,设计见 docs/算法/继电保护功能复现规划.md §3.4)
|
||||
* v1.1 2026-10-24 王建锋 仅注释/文档更正(D 项,无行为改动):s_soe_append
|
||||
* 函数头原写"打开/写入结构性错误 → unlink 重试一次
|
||||
* (同 run.log)",与实现不符且方向错误——实现里
|
||||
* f_open 失败直接返回 -1、不 unlink,这是**有意**的:
|
||||
* SOE 是事件序列,打开失败就删文件等于销毁已发生的
|
||||
* 事件。改为如实描述"仅写失败才重试一次",并补上
|
||||
* 与 sys_log_mcu.c s_is_selfheal_code 同源的逐码表
|
||||
* (名字/参数类错误码两侧都绝不 unlink)
|
||||
* 创建日期:2026-10-12(B9)
|
||||
*/
|
||||
|
||||
@@ -88,6 +98,30 @@ void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* ── 自愈触发集逐码依据(与 sys_log_mcu.c 的同名资料块同源;2026-10-24 逐条核对
|
||||
* Lib/FatFs/ff.c)─────────────────────────────────────────────────────────
|
||||
* 进触发集(run.log 侧允许 unlink 重建一次):介质/结构类 FR_DISK_ERR(1) /
|
||||
* FR_INT_ERR(2) / FR_NO_FILESYSTEM(13) / FR_DENIED(7)。本模块**只在写失败路径**
|
||||
* 上碰自己的文件——打开失败连这些码也不 unlink。
|
||||
* 不算自愈:FR_NO_FILE(4)——带 FA_OPEN_APPEND 打开时文件会被直接建出来;返回本
|
||||
* 码的唯一现实场景是同名对象是目录(ff.c:3854),unlink 修不好它。
|
||||
* 两侧都绝不 unlink:名字/参数类 FR_INVALID_NAME(6) / FR_NO_PATH(5) /
|
||||
* FR_INVALID_OBJECT(9),以及 FR_NOT_READY(3) / FR_NOT_ENABLED(12) /
|
||||
* FR_TIMEOUT(15)——同名同因,删了也修不好,反而每轮丢一份历史(§10.1 第 10 条
|
||||
* 严禁的正是这种归集)。
|
||||
* 写失败路径不接这套判定:f_write 的返回码里不存在名字/参数类(能走到那里说明
|
||||
* 已经打开成功),而簇分配失败是 FR_OK + 短写、根本没有错误码可判,只能无条件
|
||||
* 重试一次。
|
||||
* 为什么 SOE 比 run.log 保守(两侧资料块各写一份、互指,改动必须同步):run.log
|
||||
* 是滚动运行日志,坏了重建损失有限;SOE 是**事件序列**,文件里是一条条已经发生
|
||||
* 过的保护事件,"打开失败就删整个历史文件"等于**销毁事件证据**——SOE 的存在就
|
||||
* 是为了留这些事件,删了比写不进去更糟。正确出路是等 syslog_mcu_mounted() 每 2s
|
||||
* 重试补目录 / 等卷恢复,条目留在待发环与队列里,不丢。
|
||||
* (run.log 侧的同一张表 + 同一理由见 App/log/sys_log_mcu.c 的同名资料块,测试
|
||||
* 侧两侧对齐由 TC-SYSLOG-101/102 与 TC-SOESTORE-022/022b 分别钉住。)
|
||||
*/
|
||||
|
||||
/*
|
||||
* 函数功能:追加一条 64B SOE 记录到 /log/soe.log
|
||||
* 入口参数:rec - 完整记录(SOE_REC_SIZE 字节) const uint8_t *
|
||||
@@ -98,11 +132,20 @@ void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value) {
|
||||
* ② FA_WRITE|FA_OPEN_APPEND(本移植 OPEN_APPEND 不含 WRITE,
|
||||
* 缺写位 f_write 恒 FR_DENIED,2026-09-20 实证);
|
||||
* ③ 判失败必须校验写回字节数(FR_OK+短写 = 失败,簇分配无错误码);
|
||||
* ④ 容量满 unlink 重建;打开/写入结构性错误 → unlink 重试一次
|
||||
* (L1 文件级自愈,同 run.log)。
|
||||
* ④ 容量满 unlink 重建;**仅写入失败(含 FR_OK+短写)才 unlink 重试
|
||||
* 一次**——f_open 失败一律直接返回 -1、不 unlink。
|
||||
* (2026-10-24 定案:本条旧注释写的是"打开/写入结构性错误都 unlink
|
||||
* 重试一次(同 run.log)",与实际实现不符,也与本模块的语义相反:
|
||||
* run.log 是**滚动运行日志**,坏了重建损失有限;SOE 是**事件序列**,
|
||||
* 文件里是已经发生的事件,打开失败就把整个历史文件删掉等于销毁
|
||||
* 证据,方向是错的。正确出路是让 syslog_mcu_mounted() 每 2s 重试
|
||||
* 补目录/等卷恢复,条目留在待发环与队列里,不丢。)
|
||||
* ⑤ 自愈触发集与 sys_log_mcu.c 的 s_is_selfheal_code() 同源,但本模块取
|
||||
* 更保守的子集:打开失败**任何错误码都不 unlink**,只有写失败才重试
|
||||
* 一次。逐码依据与两侧差异见上方同名资料块。
|
||||
* ⚠ 本模块**没有**任何"卷损坏"归类/重格式化路径:unlink 失败、
|
||||
* 名字/参数类错误码一律只留痕(存储架构 §10.1 教训),条目丢弃
|
||||
* 计数,绝不升级成卷操作。
|
||||
* 计数,绝不升级成卷操作(全模块无 f_mount/f_mkfs/f_opendir)。
|
||||
*/
|
||||
static int s_soe_append(const uint8_t *rec) {
|
||||
static FIL f; /* static:FF_FS_TINY=0 → FIL=528B 栈;
|
||||
@@ -129,8 +172,9 @@ static int s_soe_append(const uint8_t *rec) {
|
||||
fr = f_write(&f, rec, SOE_REC_SIZE, &bw);
|
||||
(void)f_close(&f);
|
||||
if ((fr != FR_OK) || (bw != SOE_REC_SIZE)) {
|
||||
/* L1 文件级自愈:陈旧/损坏条目 → 删除重建、重试一次(同 run.log;
|
||||
* unlink 返回值刻意不检查——失败也只是重试失败 → 留痕,不升级) */
|
||||
/* L1 文件级自愈:**只有写失败**才删除重建、重试一次(打开失败走下面
|
||||
* 的 else,不删——见函数头 ④)。unlink 返回值刻意不检查:失败也只是
|
||||
* 重试失败 → 留痕计数,绝不升级成卷操作。 */
|
||||
(void)f_unlink(SOE_PATH);
|
||||
bw = 0u;
|
||||
fr = f_open(&f, SOE_PATH, FA_WRITE | FA_OPEN_APPEND);
|
||||
@@ -143,6 +187,8 @@ static int s_soe_append(const uint8_t *rec) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* 打开失败:只留痕、**不 unlink**(SOE 是事件序列,删文件 = 销毁历史;
|
||||
* 名字/参数类错误码 unlink 也修不好,见函数头的逐码表) */
|
||||
rc = -1;
|
||||
}
|
||||
if (rc == 0) {
|
||||
|
||||
+33
-6
@@ -7,6 +7,13 @@
|
||||
* 创建日期:2026-09-14
|
||||
* 修改记录:
|
||||
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4)
|
||||
* v1.1 2026-10-24 王建锋 仅契约注释更正,与 App/log/sys_log_mcu.c 的 C/F 两项
|
||||
* 整改同步(无签名、无行为改动):
|
||||
* syslog_get_record 写死容量要求(ts_len > SYSLOG_TS_LEN、
|
||||
* desc_len > SYSLOG_DESC_MAX)并明确"容量不足一律 -1、
|
||||
* 不截断、不写调用方缓冲"——旧注释像是"给少了会被截断",
|
||||
* 实际是整条读不到;syslog_post 写明必须假设丢弃存在、
|
||||
* 且丢弃留痕已按通道节流(节流后无丢弃计数)
|
||||
*/
|
||||
|
||||
#ifndef SYS_LOG_H
|
||||
@@ -58,9 +65,21 @@ typedef enum {
|
||||
/*
|
||||
* 函数功能:投递一条日志(任意上下文;非阻塞,队列满则丢弃)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* desc - 描述串(UTF-8,≤SYSLOG_DESC_MAX 字节;超长截断) const char *
|
||||
* desc - 描述串(UTF-8;超过 SYSLOG_DESC_MAX 的部分丢弃,落盘只留前 28 字节)
|
||||
* 本参数**投递时即拷贝进待发条目**:调用方缓冲区可立即复用,
|
||||
* 不要求生命周期覆盖到落盘(2026-10-24 台架实证后由「只存指针」
|
||||
* 改为条目内嵌定长缓冲,理由见 sys_log_mcu.c 的 syslog_entry_t 注释)
|
||||
* 指针本身不可为 NULL const char *
|
||||
* 返回值:无
|
||||
* 限定条件:MCU 端 FreeRTOS 启动前调用走静态待发数组(由 defaultTask 挂载后落盘)
|
||||
* 函数说明:调用方**必须假设投递可能被丢弃**——MCU 后端(sys_log_mcu.c)队列 8 槽、
|
||||
* 启动前待发数组 8 槽,卷被 ftpTask 长期占用时 defaultTask 会把整批回投
|
||||
* 队列(队列因此可长时间处于满态),此时本函数直接丢弃该条,并只在
|
||||
* "转入故障"的那一次留痕(丢弃留痕按正常→故障节流,F 项整改 2026-10-24;
|
||||
* 节流后**丢弃总数没有计数**,要计量得另加计数器)。必须留住的证据走
|
||||
* sys_log_evidence(),不要靠 syslog_post 重试。
|
||||
* 模拟器后端(simulator/sim/sim_syslog.c)是单线程直写宿主文件、**没有
|
||||
* 队列与丢弃**,因此上面这段满态语义只约束 MCU 端。
|
||||
*/
|
||||
void syslog_post(syslog_id_t id, const char *desc);
|
||||
|
||||
@@ -75,11 +94,19 @@ uint32_t syslog_count(syslog_id_t id);
|
||||
* 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* idx - 记录序号(0 起) uint32_t
|
||||
* p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节;不可为 NULL) char *
|
||||
* ts_len- p_ts 容量 size_t
|
||||
* p_desc- 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节;不可为 NULL) char *
|
||||
* desc_len - p_desc 容量 size_t
|
||||
* 返回值:0 - 成功;-1 - 越界/读失败 int
|
||||
* p_ts - 时间戳输出缓冲(不可为 NULL) char *
|
||||
* ts_len- p_ts 容量,必须 > SYSLOG_TS_LEN(即 ≥ SYSLOG_TS_LEN+1 = 20 字节) size_t
|
||||
* p_desc- 描述输出缓冲(不可为 NULL) char *
|
||||
* desc_len - p_desc 容量,必须 > SYSLOG_DESC_MAX(即 ≥ 29 字节) size_t
|
||||
* 返回值:0 - 成功;-1 - 越界 / 读失败 / **缓冲容量不足** int
|
||||
* 限定条件:容量不足一律返回 -1,**不做静默截断、也不写调用方一个字节**
|
||||
* (C 项整改 2026-10-24;旧注释只写"≥…+1 字节"像是"给少了会被截断",
|
||||
* 照它给 28 字节缓冲的结果是整条读不到而不是读到 27 字符)。
|
||||
* 描述字段在写入侧已截到 SYSLOG_DESC_MAX,所以按上面的最小容量给缓冲
|
||||
* 就一定装得下,读侧不需要第二道截断。
|
||||
* MCU 后端与模拟器后端(sim_syslog.c)在这条守卫上取值相同,行为一致;
|
||||
* 模拟器侧那份同样不可达的截断支还留在文件里(未列入本轮改动范围)。
|
||||
* 调用方:uiTask 的日志页(行缓冲 32B)与 SYSLOG 取证打印。
|
||||
*/
|
||||
int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len);
|
||||
|
||||
|
||||
+189
-28
@@ -16,6 +16,24 @@
|
||||
* v1.3 2026-10-02 王建锋 新增 sys_log_evidence():把看门狗停喂现场
|
||||
* 证据落 /log/wdev.log(掉电不丢)+ flush 诱导
|
||||
* (round 7 纯取证,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)
|
||||
* v1.5 2026-10-24 王建锋 主机单测批次照出的缺陷一起修,本文件内 5 条
|
||||
* (A/B/C/D/F;E 在 App/cfg/cfg_file_store_mcu.c):
|
||||
* A 卷忙整批静默丢日志——syslog_drain 批量路径补
|
||||
* rc==-2 分支把 done 置 0,整批回投队列(不计故障、
|
||||
* 不打日志),与 -3 慢路径遇 -2 的处置对齐;
|
||||
* B 同分支文案 "drop N entries" 与行为相反(其实全
|
||||
* 部回投),改成如实描述并按"正常→故障"节流一条;
|
||||
* C 删掉 syslog_get_record 里永不可达的截断支,容量
|
||||
* 要求写进函数头与 sys_log.h(宁可拒绝不静默截断);
|
||||
* D 自愈触发集按语义拆类(新增 s_is_selfheal_code),
|
||||
* 把 FR_INVALID_NAME / FR_NO_PATH / FR_NO_FILE /
|
||||
* FR_INVALID_OBJECT 移出 unlink 重建集合——旧集合在
|
||||
* 名字/参数类错误下每轮删一次自己的历史日志且永不
|
||||
* 自愈,违反存储架构 §10.1 第 10 条
|
||||
* F syslog_post 的丢弃留痕按通道节流(队列满 / 启动前
|
||||
* 数组满各立 volatile 抑制位,与 B 的 drain 侧那位分
|
||||
* 开):卷被长期占用 + A 项回投让队列常驻满时,串口
|
||||
* 不再按投递频率刷 "queue full, drop"
|
||||
* v1.4 2026-10-04 王建锋 P2-9:syslog_mcu_mounted() 改为幂等 + 卷锁保护;
|
||||
* defaultTask 每 2s 复查,建 /log 曾失败时自动重试
|
||||
* (原"仅调用一次"→ 失败后 drain 永久 no-op)。
|
||||
@@ -51,20 +69,44 @@ static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_M
|
||||
#define SYSLOG_BATCH_MAX 8u
|
||||
static uint8_t s_batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE];
|
||||
|
||||
/* 待发条目:desc 为调用方静态串(Flash 字面量),只存指针不拷贝 */
|
||||
/* 待发条目:**投递时就把描述拷进条目**(B 方案整改,2026-10-24 台架实证)。
|
||||
* 旧实现只存 `const char *`,隐含契约「desc 必须是生命周期覆盖到落盘的静态期串」,
|
||||
* 而该契约从未写进 sys_log.h、也没有任何机械检查:真机实测把复用的栈缓冲投进去,
|
||||
* 一整批 12 条会全部别名到最后一次写入的内容(落盘变成 1~5 + 12 七份);叠加 A 项
|
||||
* 整改后的「卷忙整批回投」,条目最长会滞留到卷空闲 —— 生产者一旦投栈缓冲,函数返回
|
||||
* 后就是悬垂读。故改为条目内嵌定长缓冲,长度按 SYSLOG_DESC_MAX 截断。
|
||||
* RAM 代价(实测见 docs/子系统设计/运行与自检记录设计说明.md §4.4):s_preboot 与
|
||||
* s_batch_src 各 8 条由 8 B 变 32 B(.bss 合计 +384 B),消息队列存储由堆多要 192 B
|
||||
* (FreeRTOS 堆已迁 CCM)。 */
|
||||
typedef struct {
|
||||
uint8_t id;
|
||||
const char *desc;
|
||||
uint8_t id;
|
||||
char desc[SYSLOG_DESC_MAX + 1u];
|
||||
} syslog_entry_t;
|
||||
|
||||
static syslog_entry_t s_preboot[SYSLOG_QUEUE_MAX]; /* 启动前待发数组(RTOS 前单生产者/后单消费者,免锁) */
|
||||
static syslog_entry_t s_batch_src[SYSLOG_BATCH_MAX]; /* 本批条目的原始来源(N-24:未落盘的尾部要回投队列,故必须留住 id + desc 指针);
|
||||
static syslog_entry_t s_batch_src[SYSLOG_BATCH_MAX]; /* 本批条目的原始来源(N-24:未落盘的尾部要回投队列,故必须留住 id + desc 副本);
|
||||
* 唯一写者 defaultTask,走静态不吃栈 */
|
||||
static volatile uint8_t s_preboot_cnt; /* 数组条目数 */
|
||||
static osMessageQueueId_t s_queue; /* 运行期消息队列(syslog_drain 首次调用时惰性创建) */
|
||||
static const osMessageQueueAttr_t s_queue_attr = { .name = "syslog" };
|
||||
static volatile uint8_t s_fs_ready; /* 卷挂载 + /log 目录就绪后置位 */
|
||||
static volatile uint8_t s_mount_fail_reported; /* P2-9:建目录失败日志去重(成功复位) */
|
||||
/* B 项整改(2026-10-24):syslog_drain 批量写失败"整批回投"这条留痕的节流位——
|
||||
* 只在"从正常转入故障"的那一次打一条,成功一轮后复位。与 s_append_many 内部的
|
||||
* s_batch_fault 各管一层:那位压的是"这个文件写坏了",本位压的是"这批条目回投了"。
|
||||
* 唯一写者 defaultTask,免锁。 */
|
||||
static uint8_t s_requeue_reported[SYSLOG_COUNT];
|
||||
|
||||
/* F 项整改(2026-10-24,Lead 批准并入本轮):syslog_post 的丢弃留痕节流。
|
||||
* 场景:卷被 ftpTask 长期占用 -> A 项修复后整批回投 -> 队列常驻满 -> 此后每一条
|
||||
* 日志投递都会"队列满 + 打一条 DBG_ERROR",串口按业务投递频率刷屏(日志本身
|
||||
* 变成负载)。与上面 s_requeue_reported **分开**是两个必然:那位归 defaultTask
|
||||
* (唯一写者,免锁),本位由**任意上下文**的投递者读写,且恢复时机是"下一次投递
|
||||
* 成功"而不是"下一批落盘成功"。两个位各管一个通道:队列 / 启动前待发数组。
|
||||
* volatile + 单字节:Cortex-M 上对齐字节读写原子;跨任务竞态的最坏后果是"同一轮
|
||||
* 故障多打一行"或"少打一行",不会漏丢条目、也不会长时间静默(下一次成功会复位)。 */
|
||||
static volatile uint8_t s_post_qfull_reported; /* 队列满丢弃:已留痕? */
|
||||
static volatile uint8_t s_post_pboot_full_reported;/* 启动前数组满丢弃:已留痕? */
|
||||
|
||||
/* N-24(2026-10-15,报告 N-16 读侧):8 条滑动读窗口 + 写者代数。
|
||||
* 窗口 512 B 走静态(不吃任务栈;defaultTask/uiTask 最坏链见 stack_report.py,
|
||||
@@ -80,6 +122,72 @@ static volatile uint32_t s_rd_gen[SYSLOG_COUNT]; /* 每次成功落盘 / unli
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* ── 自愈触发集逐码依据(配套下方 s_is_selfheal_code;2026-10-24 逐条核对
|
||||
* Lib/FatFs/ff.c,行号即该文件)────────────────────────────────────────
|
||||
* 旧实现把 FR_INVALID_NAME / FR_NO_PATH / FR_NO_FILE / FR_INVALID_OBJECT 一并
|
||||
* 放进触发集,违反《STM32F4-Base 存储架构说明》§10.1 第 10 条:名字/参数类错误
|
||||
* 码严禁归入"卷损坏"处置集——这类码下 unlink 自己的文件必然同样失败(同名同因),
|
||||
* 后果是每轮排空删一次自己的历史日志、且永远修不好(run.log 1024 条 /
|
||||
* selftest.log 全是被删对象)。
|
||||
*
|
||||
* 进触发集(删了这个文件真能修好):
|
||||
* FR_DISK_ERR(1) 低层 I/O 失败(ff.c:1683/1739/1785 等)——陈旧簇链或坏
|
||||
* 目录项,unlink 后重建换新链有效;
|
||||
* FR_INT_ERR(2) 内部一致性断言失败(ff.c:1379/1460/3869)——本文件的目录
|
||||
* 项/簇号已不自洽,只能废弃重建;
|
||||
* FR_NO_FILESYSTEM(13) 卷上找不到有效 FAT(ff.c:3483 起)——本模块**不**做任何
|
||||
* 卷级动作(全模块无 f_mount/f_mkfs),只对本文件废弃重建
|
||||
* 一次并留痕,卷怎么办归挂载门;
|
||||
* FR_DENIED(7) 两条真实成因都能被 unlink 治好:目标被置只读属性
|
||||
* (ff.c:3808/3859),以及目录区无空目录项(ff.c:1850 由
|
||||
* FR_NO_FILE 改写而来)——后者删掉自己这个文件正好腾出一
|
||||
* 个目录项。陷阱一那类"缺写位"的 FR_DENIED 出现在 f_write
|
||||
* 上,不走本判定。
|
||||
*
|
||||
* 不进触发集(一律立即失败,不 unlink、不重开、不计卷损坏):
|
||||
* FR_INVALID_NAME(6) 8.3 短名违规 / 非法字符 / 以点开头(非 LFN 配置下的
|
||||
* create_name)——unlink 同名同因失败,永不自愈;
|
||||
* FR_NO_PATH(5) 中间路径段缺失(ff.c:3144/4357)= /log 目录不在:重建
|
||||
* 文件不解决目录,补目录归 syslog_mcu_mounted() 的每 2s 重试
|
||||
* (见该函数注释里的幂等改造说明),两侧各管一层;
|
||||
* FR_NO_FILE(4) 本模块用 FA_WRITE|FA_OPEN_APPEND(含 FA_OPEN_ALWAYS),
|
||||
* "文件不存在"会被直接创建出来;返回本码的唯一现实场景是
|
||||
* 同名对象是目录(ff.c:3854),unlink 一个目录修不好它;
|
||||
* FR_INVALID_OBJECT(9) 仅 ff.c:3774 的 !fp 分支,即句柄指针为 NULL——本模块用
|
||||
* 函数级 static FIL,地址恒非 0,属不可达码;
|
||||
* FR_NOT_READY(3) / FR_NOT_ENABLED(12) / FR_TIMEOUT(15)——卷未挂载、无工作区、
|
||||
* FF_FS_REENTRANT 取卷超时,属挂载门与并发范畴,与"这个
|
||||
* 文件坏了"无关。
|
||||
*
|
||||
* 两点边界,免得后人当成漏改:
|
||||
* 1) f_write 之后那条"失败/短写 -> unlink 重建重试一次"**不接**本判定:其前提
|
||||
* 是已经打开成功,名字/参数类错误到不了那里;而簇分配失败是 FR_OK + 短写
|
||||
* (根本没有错误码可判),必须无条件重试。
|
||||
* 2) App/log/soe_store.c 对同一张表取**更保守的子集**:那边**打开失败连介质类
|
||||
* 码也不 unlink**。原因不是它漏改,而是两个文件的对象不同——run.log 是滚动
|
||||
* 运行日志,文件坏了重建损失有限;soe.log 是**事件序列**,里面是一条条已经
|
||||
* 发生过的保护事件,"打开失败就删掉整个文件"等于**销毁事件证据**(SOE 的存
|
||||
* 在即目的,删了比写不进去更糟)。所以 SOE 侧的出路是等卷/目录恢复(条目留
|
||||
* 在待发环与队列里,不丢),只有"已经打开成功之后写失败"才允许废弃重建。
|
||||
* 两侧资料块必须同步修改、互指(彼处在 App/log/soe_store.c 的同名资料块)。
|
||||
*/
|
||||
/*
|
||||
* 函数功能:判定一个 f_open 返回码是否属于"把自己的日志文件删掉重建一次"能修好
|
||||
* 的那一类,即 L1 文件级自愈的触发集(D 项整改,逐码依据见上方资料块)
|
||||
* 入口参数:fr - f_open 的返回码 FRESULT
|
||||
* 返回值:1 - 属触发集,可 unlink 后重开一次;0 - 不属,一律立即失败、不 unlink、
|
||||
* 不计卷损坏 int
|
||||
* 限定条件:纯分类、无任何副作用;调用点只有 s_append_rec() 与 s_append_many()
|
||||
* 的"打开失败"分支
|
||||
* 函数说明:拆成函数是为了两处调用点共用同一张表,避免历史上"改一处漏一处"
|
||||
*/
|
||||
static int s_is_selfheal_code(FRESULT fr) {
|
||||
return ((fr == FR_DISK_ERR) || (fr == FR_INT_ERR) ||
|
||||
(fr == FR_NO_FILESYSTEM) || (fr == FR_DENIED)) ? 1 : 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* 函数功能:格式化一条 64 B 定长记录(时间戳 + 空格 + epoch + 空格 + 描述)
|
||||
* 入口参数:id - 日志 ID(当前不参与编码,仅为接口对称) syslog_id_t
|
||||
@@ -155,10 +263,9 @@ static int s_append_rec(syslog_id_t id, const uint8_t *p_rec) {
|
||||
* FA_OPEN_APPEND(0x30) 不含 FA_WRITE(0x02),不带写位打开的文件 f_write
|
||||
* 恒返回 FR_DENIED(7)——2026-09-20 台架实测 /log 写失败 ×5/开机 真根因 */
|
||||
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
|
||||
if (fr != FR_OK && (fr == FR_DENIED || fr == FR_INVALID_OBJECT ||
|
||||
fr == FR_INT_ERR || fr == FR_DISK_ERR ||
|
||||
fr == FR_NO_FILE || fr == FR_NO_PATH ||
|
||||
fr == FR_INVALID_NAME || fr == FR_NO_FILESYSTEM)) {
|
||||
/* D 项整改(2026-10-24):只有"删了这个文件能修好"的介质/结构类错误码才
|
||||
* 进这里;名字/参数类一律不 unlink —— 判据见 s_is_selfheal_code() */
|
||||
if ((fr != FR_OK) && (s_is_selfheal_code(fr) != 0)) {
|
||||
(void)f_unlink(s_paths[id]);
|
||||
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
|
||||
}
|
||||
@@ -255,10 +362,9 @@ static int s_append_many(syslog_id_t id, const uint8_t *p_buf, uint32_t n) {
|
||||
total = (UINT)(n * SYSLOG_REC_SIZE);
|
||||
|
||||
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
|
||||
if (fr != FR_OK && (fr == FR_DENIED || fr == FR_INVALID_OBJECT ||
|
||||
fr == FR_INT_ERR || fr == FR_DISK_ERR ||
|
||||
fr == FR_NO_FILE || fr == FR_NO_PATH ||
|
||||
fr == FR_INVALID_NAME || fr == FR_NO_FILESYSTEM)) {
|
||||
/* D 项整改(2026-10-24):只有"删了这个文件能修好"的介质/结构类错误码才
|
||||
* 进这里;名字/参数类一律不 unlink —— 判据见 s_is_selfheal_code() */
|
||||
if ((fr != FR_OK) && (s_is_selfheal_code(fr) != 0)) {
|
||||
(void)f_unlink(s_paths[id]);
|
||||
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
|
||||
}
|
||||
@@ -524,19 +630,36 @@ void syslog_drain(void) {
|
||||
break; /* 写失败:该条已丢,尾部回投 */
|
||||
}
|
||||
}
|
||||
} else if (rc == -1) {
|
||||
} else if (rc == -2) {
|
||||
/* A 项整改(2026-10-24,主机单测照出):卷忙 = **整批回投队列**,
|
||||
* 静默顺延——不计故障、不打日志(本函数每 2s 排空一次,一打就刷屏),
|
||||
* 与上面 -3 慢路径遇 -2 的处置对齐。旧实现在这里没有分支:done 保持 n
|
||||
* → 下面的回投循环空转 → 最多 8 条既不落盘也不回投,静默丢失。 */
|
||||
done = 0u;
|
||||
DBG_ERROR("drop %lu entries id=%d", (unsigned long)n, (int)bid);
|
||||
} else if (rc == -1) {
|
||||
/* B 项整改(2026-10-24):旧文案写 "drop %lu entries",但本分支
|
||||
* done = 0 → 整批其实**全部回投**、一条没丢(下轮重试),文案与行为
|
||||
* 相反会把排障方向带错;改成如实描述,并加"正常→故障"节流(持续故障
|
||||
* 时每 2s 一条会自己刷成新一轮刷屏,真丢另有下面 hold-over drop 兜底)。 */
|
||||
done = 0u;
|
||||
if (s_requeue_reported[bid] == 0u) {
|
||||
s_requeue_reported[bid] = 1u;
|
||||
DBG_ERROR("write fail, requeue %lu entries id=%d",
|
||||
(unsigned long)n, (int)bid);
|
||||
}
|
||||
}
|
||||
/* rc == -2(卷忙):整批回投队列,静默顺延,不计故障 */
|
||||
|
||||
for (i = done; i < n; i++) {
|
||||
if (osMessageQueuePut(s_queue, &s_batch_src[i], 0u, 0u) != osOK) {
|
||||
DBG_ERROR("hold-over drop (queue full): id=%d", (int)s_batch_src[i].id);
|
||||
}
|
||||
}
|
||||
/* 本批消耗的 preboot 条目:已落盘的删除;未落盘的已转投队列,同样删除
|
||||
* (否则会重复落盘) */
|
||||
if (rc == 0) {
|
||||
s_requeue_reported[bid] = 0u; /* 恢复正常:允许下一次故障重新留痕 */
|
||||
}
|
||||
/* 本批消耗的 preboot 条目:已落盘的删除;未落盘的(-1/-2/-3 尾部)已在
|
||||
* 上面转投队列,同样从数组删除——**由队列接手重投**,不是丢弃
|
||||
* (A 项整改前 -2 走不到回投,这里删掉就等于吞掉) */
|
||||
if (n_pre > 0u) {
|
||||
memmove(&s_preboot[0], &s_preboot[n_pre],
|
||||
sizeof(syslog_entry_t) * (size_t)(s_preboot_cnt - n_pre));
|
||||
@@ -558,26 +681,58 @@ void syslog_drain(void) {
|
||||
* desc - 描述串(UTF-8,静态存储期,超长截断) const char *
|
||||
* 返回值:无
|
||||
* 限定条件:FreeRTOS 启动前走静态待发数组;desc 必须为静态/常量串
|
||||
* 函数说明:丢弃留痕按"正常→故障"节流(F 项整改 2026-10-24):每个通道只在
|
||||
* 转入故障的那一次打一条,恢复(下一次成功收下)后复位。
|
||||
* **丢弃语义本身没变**(队列满/数组满仍然是丢),变的只有留痕次数——
|
||||
* 卷被 FTP 长期占用时 A 项修复让整批回投、队列常驻满,若这里不节流,
|
||||
* 串口就按业务投递频率刷"queue full, drop",日志自己变成负载。
|
||||
* 抑制位是 volatile 单字节、由任意上下文读写:竞态最坏只多打/少打一行。
|
||||
*/
|
||||
void syslog_post(syslog_id_t id, const char *desc) {
|
||||
syslog_entry_t e;
|
||||
size_t dl;
|
||||
|
||||
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) {
|
||||
return;
|
||||
}
|
||||
e.id = (uint8_t)id;
|
||||
e.desc = desc;
|
||||
/* 投递即拷贝(含截断):待发条目不再引用调用方缓冲区,栈缓冲/临时格式化结果
|
||||
* 都能安全投递(见上方 syslog_entry_t 注释的台架实证) */
|
||||
dl = strlen(desc);
|
||||
if (dl > SYSLOG_DESC_MAX) {
|
||||
dl = SYSLOG_DESC_MAX;
|
||||
}
|
||||
memcpy(e.desc, desc, dl);
|
||||
e.desc[dl] = '\0';
|
||||
if (s_queue != NULL && osKernelGetState() == osKernelRunning) {
|
||||
if (osMessageQueuePut(s_queue, &e, 0u, 0u) != osOK) {
|
||||
DBG_ERROR("queue full, drop: %s", desc);
|
||||
/* F 项:只报"从正常转入故障"那一次;期间条目照常丢弃(丢弃语义不变,
|
||||
* 变的只是留痕次数)。下一次投递成功即复位。 */
|
||||
if (s_post_qfull_reported == 0u) {
|
||||
s_post_qfull_reported = 1u;
|
||||
DBG_ERROR("queue full, drop: %s", desc);
|
||||
}
|
||||
} else {
|
||||
/* 恢复正常:清抑制位,下一次故障重新留痕。这里刻意无条件写
|
||||
* (曾试成"仅置位时才清",被 cppcheck duplicateConditionalAssign
|
||||
* 判成等价赋值并拦提交闸;本函数上方已做过一次条目拷贝,多一条
|
||||
* STR 可以忽略,别为它加 inline-suppr)。 */
|
||||
s_post_qfull_reported = 0u;
|
||||
}
|
||||
} else {
|
||||
uint8_t n = s_preboot_cnt;
|
||||
if (n < SYSLOG_QUEUE_MAX) {
|
||||
s_preboot[n] = e;
|
||||
s_preboot_cnt = n + 1u;
|
||||
} else {
|
||||
s_post_pboot_full_reported = 0u; /* 恢复正常:同上,无条件复位 */
|
||||
} else if (s_post_pboot_full_reported == 0u) {
|
||||
/* 同一纪律的另一通道:启动前数组只有 SYSLOG_QUEUE_MAX 槽,挂载慢 +
|
||||
* 开机事件密集时会在整个"未就绪窗口"里每条投递打一行(比队列通道更早
|
||||
* 发生、也更密集)。数组重新收下第一条即视为恢复。 */
|
||||
s_post_pboot_full_reported = 1u;
|
||||
DBG_ERROR("preboot full, drop: %s", desc);
|
||||
} else {
|
||||
/* 故障期间静默,条目照常丢弃 */
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -603,11 +758,14 @@ uint32_t syslog_count(syslog_id_t id) {
|
||||
* 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* idx - 记录序号(0 起) uint32_t
|
||||
* p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节,不可为 NULL) char *
|
||||
* ts_len - p_ts 容量 size_t
|
||||
* p_desc - 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节,不可为 NULL) char *
|
||||
* desc_len - p_desc 容量 size_t
|
||||
* 返回值:0 - 成功;-1 - 越界/读失败 int
|
||||
* p_ts - 时间戳输出缓冲(不可为 NULL) char *
|
||||
* ts_len - p_ts 容量,**必须 > SYSLOG_TS_LEN**(即至少 20 字节) size_t
|
||||
* p_desc - 描述输出缓冲(不可为 NULL) char *
|
||||
* desc_len - p_desc 容量,**必须 > SYSLOG_DESC_MAX**(即至少 29 字节) size_t
|
||||
* 返回值:0 - 成功;-1 - 越界/读失败/缓冲容量不足(不截断、不写入调用方缓冲) int
|
||||
* 限定条件:容量不足一律拒绝而不是截断(C 项整改 2026-10-24;理由见函数体注释)。
|
||||
* 记录里的描述字段本身已在写入侧截到 SYSLOG_DESC_MAX,所以按上面 minimum
|
||||
* 给缓冲就一定装得下,读侧无需第二道截断。
|
||||
*/
|
||||
int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len) {
|
||||
uint8_t rec[SYSLOG_REC_SIZE];
|
||||
@@ -624,9 +782,12 @@ int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, c
|
||||
while (dl < SYSLOG_DESC_MAX && rec[SYSLOG_DESC_OFFS + dl] != '\0') {
|
||||
dl++;
|
||||
}
|
||||
if (dl >= desc_len) {
|
||||
dl = desc_len - 1u;
|
||||
}
|
||||
/* C 项整改(2026-10-24):这里原来还有一句 "if (dl >= desc_len) dl = desc_len
|
||||
* - 1u"——**永不可达**:上面的守卫已把 desc_len <= SYSLOG_DESC_MAX 全部挡掉,
|
||||
* 而 dl 由定长记录的内容决定、恒 <= SYSLOG_DESC_MAX。删掉它,容量要求改由
|
||||
* 函数头与 sys_log.h 写死:**宁可整条拒绝,也不做静默截断**(UI 拿到截断串
|
||||
* 会显示成一条被删短过的日志,比读不到更难查)。desc_len 的校验仍保留,
|
||||
* 它是"调用方给够缓冲"这条契约的执行点,不是截断开关。 */
|
||||
memcpy(p_desc, rec + SYSLOG_DESC_OFFS, dl);
|
||||
p_desc[dl] = '\0';
|
||||
return 0;
|
||||
|
||||
@@ -545,6 +545,10 @@ static int cmd_pasv(lftpd_client_t *client, const char *arg)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* s_addr 是网络序整数(本层唯一约定,见 net_inet_addr 的声明处),这里
|
||||
* 先转主机序再按高字节到低字节拆 = 线序顺位,PASV 227 的 IP 文本因此正确
|
||||
* (TRAP-NET-21 已定案"双重反转恰好正确,勿误判";2026-10-24 字节序统一后复核:
|
||||
* 本行不属于被统一掉的那一族,语义未变,勿"顺手改对"成直接拆 s_addr) */
|
||||
uint32_t ip = net_htonl(local_addr.sin_addr.s_addr);
|
||||
send_simple_response(client->socket, 227, STATUS_227,
|
||||
(ip >> 24) & 0xff,
|
||||
|
||||
@@ -78,6 +78,14 @@ static volatile uint32_t s_net_generation = 0; /* 网络代次号:网络
|
||||
*/
|
||||
static osMessageQueueId_t s_net_msg_queue;
|
||||
|
||||
/*
|
||||
* 本层执行任务句柄(= net_init 的调用者,NET 层为 netTask、硬件层为 testTask)
|
||||
* 与 net_pending_capture_owner() 同一时刻、同一语义捕获,判据也一致。
|
||||
* 用途见 net_ctx_is_owner_task():给"绕过消息队列直调芯片"的接口把危险面关掉。
|
||||
* 捕获前恒 NULL(net_init 未跑)——那时任何 socket 都不存在,判空放行即可。
|
||||
*/
|
||||
static TaskHandle_t s_net_owner_task;
|
||||
|
||||
/*
|
||||
* 函数功能:绑定消息队列句柄(本模块是该句柄的唯一使用者)
|
||||
* 入口参数:queue_id - 已创建的消息队列句柄
|
||||
@@ -87,6 +95,30 @@ static osMessageQueueId_t s_net_msg_queue;
|
||||
* 真出现两处绑定时,说明有人新加了调用点,此时保留先绑的那个并把后来者
|
||||
* 拒掉,比静默换句柄(已入队的请求会留在旧队列里永不处理)更容易暴露。
|
||||
*/
|
||||
/*
|
||||
* 函数功能:判当前上下文是否本层执行任务(netTask / 硬件层的 testTask)
|
||||
* 入口参数:无
|
||||
* 出口参数:无
|
||||
* 返回值:1 - 是本层执行任务(或未捕获,见下);0 - 明确不是(应拒绝直调芯片)
|
||||
* 限定条件:net_init 之后判据才有效
|
||||
* 函数说明:给 net_sendto / net_recvfrom 这类**绕过消息队列、在调用方上下文里
|
||||
* 直接 net_poll + 读写芯片缓冲**的接口把危险面结构性关掉:非执行任务
|
||||
* 调用会与本层的 net_poll 抢同一根 SPI2(TRAP-NET-14 的根因形态),
|
||||
* 而 SPI 事务没有跨任务互斥(ch395f_spi_begin/end 只是片选与分频)。
|
||||
* 刻意**不做**成"排队改写":recvfrom 要把源地址交回调用方,按 AGENTS
|
||||
* 不能塞进按值拷贝的 net_msg_t,真要串行化得设计 per-task 结果槽,
|
||||
* 那是需要真机回归的独立工程(2026-10-24 定案:本轮先把危险面关掉,串行化留后续)。
|
||||
* 未捕获(net_init 未跑)时返回 1:那时任何 socket 都不存在,
|
||||
* 调用只会先在参数检查上拿到 BADF,不构成绕过。
|
||||
*/
|
||||
static uint8_t net_ctx_is_owner_task(void) {
|
||||
if (s_net_owner_task == NULL) {
|
||||
return 1U;
|
||||
}
|
||||
/* 内核未启动/中断上下文里句柄为 NULL:与执行任务不等 → 判不是(不得碰 SPI) */
|
||||
return (xTaskGetCurrentTaskHandle() == s_net_owner_task) ? 1U : 0U;
|
||||
}
|
||||
|
||||
int net_msg_queue_adopt(osMessageQueueId_t queue_id) {
|
||||
if (queue_id == NULL) {
|
||||
return -1;
|
||||
@@ -253,6 +285,9 @@ static bool s_mac_is_zero(const uint8_t *p_mac) {
|
||||
net_set_errno(NET_OK);
|
||||
s_net_initialized = 0;
|
||||
net_pending_capture_owner(); /* 执行任务 = net_init 调用者(NET 层 = netTask / 硬件层 = testTask) */
|
||||
/* 同一时刻捕获本层自己的执行任务句柄(net_ctx_is_owner_task 的判据;
|
||||
* 必须与上面同源,否则两张守卫会各认一个"谁是 netTask") */
|
||||
s_net_owner_task = xTaskGetCurrentTaskHandle();
|
||||
|
||||
/* 检查 CH395F 芯片 */
|
||||
if (ch395f_check_exist() != CH395F_STATUS_OK) {
|
||||
@@ -1561,14 +1596,16 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a
|
||||
p_sock->remote_ip = p_addr_in->sin_addr.s_addr;
|
||||
p_sock->remote_port = net_ntohs(p_addr_in->sin_port);
|
||||
|
||||
/* 转换 IP 为数组格式(大端/网络字节序,与 ch395f set_des_ip 期望一致,
|
||||
* 同 Phase2 phase2_tc201 的 test_parse_ip 结果 [192,168,1,2]) */
|
||||
p_sock->remote_ip_arr[0] = (uint8_t)(p_sock->remote_ip >> 24);
|
||||
/* 最高字节 → [0](大端序) */
|
||||
p_sock->remote_ip_arr[1] = (uint8_t)(p_sock->remote_ip >> 16);
|
||||
p_sock->remote_ip_arr[2] = (uint8_t)(p_sock->remote_ip >> 8);
|
||||
/* 最低字节 → [3] */
|
||||
p_sock->remote_ip_arr[3] = (uint8_t)(p_sock->remote_ip);
|
||||
/* 转换 IP 为数组格式(芯片要的是**线序**数组:[0]=第一段,与 ch395f_set_des_ip
|
||||
* 期望一致,同 Phase2 phase2_tc201 的 test_parse_ip 结果 [192,168,1,2])。
|
||||
* s_addr 是网络序整数,其**内存字节顺位**就是线序(2026-10-24 定案:全仓 s_addr 只此一族),
|
||||
* 所以这里按字节顺位取、不做数值右移:旧写法 remote_ip_arr[0] = remote_ip >> 24
|
||||
* 取的是数值的最高字节,只配得上当时那套「第一段在最高字节」的 inet_addr;
|
||||
* inet_addr 已改成 POSIX 网络序后,右移写法会把地址拆成 [100,1,168,192] 反序,
|
||||
* 表现为「连谁都不知去哪了」。用 memcpy 取顺位与 net_events.c 填 remote_ip_arr
|
||||
* 的写法同一形制(那边一直就是线序顺位),两端不再各说一套。 */
|
||||
(void)memcpy(p_sock->remote_ip_arr, &p_sock->remote_ip,
|
||||
sizeof(p_sock->remote_ip_arr));
|
||||
|
||||
/* 配置 CH395F Socket:先 OPEN 再写源/目的端口(避免 open 复位寄存器) */
|
||||
/* 设置协议类型为 TCP */
|
||||
@@ -2429,27 +2466,34 @@ int net_sendto(int sockfd, const void *buf, int len, int flags,
|
||||
return NET_ERR_RETURN_1(NET_ERR_BADF);
|
||||
}
|
||||
|
||||
/* 缓冲区无效 → INVAL */
|
||||
/* 缓冲区无效 → INVAL(本块必须留在守卫之前:参数错误先报参数错误) */
|
||||
if (buf == NULL || len <= 0) {
|
||||
return NET_ERR_RETURN_1(NET_ERR_INVAL);
|
||||
}
|
||||
|
||||
/* netTask 上下文守卫:本接口在本任务上下文里直接 net_poll + 读写芯片缓冲,
|
||||
* 非执行任务调用会与本层 net_poll 抢同一根 SPI2(TRAP-NET-14)。放在参数
|
||||
* 校验**之后**:越界/未在用/参数非法这些正常错误路径照旧先报,守卫不吞它们。
|
||||
* 判据与留痕见 net_ctx_is_owner_task()。 */
|
||||
if (net_ctx_is_owner_task() == 0U) {
|
||||
DBG_ERROR("sendto from non-exec task: refused (SPI race, use netTask)");
|
||||
return NET_ERR_RETURN_1(NET_ERR_NOT_IN_NETTASK);
|
||||
}
|
||||
|
||||
/* UDP 模式:设置目标地址 */
|
||||
if (dest_addr != NULL && addrlen >= (int)sizeof(struct net_sockaddr_in)) {
|
||||
/* 强转为 IPv4 地址结构体 */
|
||||
p_addr_in = (const struct net_sockaddr_in *)dest_addr;
|
||||
|
||||
/* 转换 IP 为大端数组(同 TCP 客户端,set_des_ip 期望大端) */
|
||||
/* 大端 IP 数组 */
|
||||
/* 转换 IP 为线序数组(同 TCP 客户端:set_des_ip 要 [0]=第一段)。
|
||||
* s_addr 的内存字节顺位即线序(2026-10-24 定案:全仓 s_addr 只此一族),按顺位取,
|
||||
* 不再按数值右移取最高字节——理由与 net_connect_locked 内同一处相同 */
|
||||
/* 线序 IP 数组 */
|
||||
uint8_t ip_arr[4];
|
||||
/* 目的 IP(网络序 32 位) */
|
||||
uint32_t ip = p_addr_in->sin_addr.s_addr;
|
||||
/* 最高字节 → [0](大端序) */
|
||||
ip_arr[0] = (uint8_t)(ip >> 24);
|
||||
ip_arr[1] = (uint8_t)(ip >> 16);
|
||||
ip_arr[2] = (uint8_t)(ip >> 8);
|
||||
/* 最低字节 → [3] */
|
||||
ip_arr[3] = (uint8_t)(ip);
|
||||
/* 字节顺位拷贝:ip_arr[i] = s_addr 的第 i 个内存字节 */
|
||||
(void)memcpy(ip_arr, &ip, sizeof(ip_arr));
|
||||
|
||||
/* 设置目的 IP */
|
||||
ch395f_set_des_ip(sockfd, ip_arr);
|
||||
@@ -2557,6 +2601,13 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags,
|
||||
return NET_ERR_RETURN_1(NET_ERR_INVAL);
|
||||
}
|
||||
|
||||
/* netTask 上下文守卫:本接口自旋 net_poll + 读芯片缓冲,同样只允许执行任务调;
|
||||
* 位置与 net_sendto 同一原则(参数校验之后、任何芯片事务之前) */
|
||||
if (net_ctx_is_owner_task() == 0U) {
|
||||
DBG_ERROR("recvfrom from non-exec task: refused (SPI race, use netTask)");
|
||||
return NET_ERR_RETURN_1(NET_ERR_NOT_IN_NETTASK);
|
||||
}
|
||||
|
||||
/* 轮询直到:有数据 / 非阻塞(阻塞 = 无限等待,无默认预算) */
|
||||
while (1) {
|
||||
/* 轮询一次:更新状态机与可读标志 */
|
||||
@@ -2586,7 +2637,10 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags,
|
||||
/* CH395F UDP 头 [0-1] 数据长度(LE) [2-3] src_port(LE) [4-7] src_ip(§9.2.4 表) */
|
||||
/* 源端口(小端 2 字节 → 主机序 → 网络序) */
|
||||
p_addr_in->sin_port = net_htons((uint16_t)(header[2] | (header[3] << 8)));
|
||||
/* 源 IP(小端 4 字节组装为网络序) */
|
||||
/* 源 IP:芯片给的是线序字节([4]=第一段),按"首段落在最低
|
||||
* 字节"组装即得网络序整数——与本层 s_addr 唯一约定同族
|
||||
* (2026-10-24 定案:全仓 s_addr 只此一族),所以这里**不能再加一次换端**。
|
||||
* 源 IP(小端 4 字节组装为网络序) */
|
||||
p_addr_in->sin_addr.s_addr = ((uint32_t)header[4]) |
|
||||
((uint32_t)header[5] << 8) |
|
||||
((uint32_t)header[6] << 16) |
|
||||
@@ -2819,11 +2873,20 @@ int net_sock_state(int sockfd) {
|
||||
* 出口参数:无
|
||||
* 返回值:网络字节序的 32 位 IP 地址(0 表示参数无效) uint32_t
|
||||
* 限定条件:无
|
||||
* 函数说明:1. 逐段解析十进制数与并小端组装为 32 位值
|
||||
* 2. 转换结果经 NET_HTONL 转为网络字节序
|
||||
* 函数说明:1. 逐段解析并按「第一段在主机序最高字节」累加,再经 NET_HTONL 转网络序,
|
||||
* 使结果满足 POSIX in_addr_t 的不变量:&s_addr 的第 0 个字节 == 点分第一段。
|
||||
* 本文件内 s_addr 只有这一族(2026-10-24:两套 IP 字节序约定统一为 POSIX 网络序):net_inet_ntoa 的
|
||||
* 「先 NTOHL、再高段在前」正是本函数的解析逆运算;net_getsockname、
|
||||
* net_getpeername、net_recvfrom 填的 s_addr 也同族,故三者可直接相等比较。
|
||||
* 2. 小端主机(STM32F407 与 PC 模拟器均是)上 "192.168.1.100" 的数值为
|
||||
* 0x6401A8C0(内存字节 C0 A8 01 64)。旧实现在小端累加后又多做一次
|
||||
* HTONL,得到 0xC0A80164,与 ntoa 不互逆——那是同一文件里的第二套约定,
|
||||
* 已连同其消费者(connect/sendto 的 des_ip 拆装)一起改为本族。
|
||||
* 3. 段值超过 255 时按低 8 位截断(旧实现会把溢出位溅进相邻段,更不可预期);
|
||||
* 非法字符仍按 0 处理,失败返回值维持 0,不改 NET_INADDR_NONE 的语义。
|
||||
*/
|
||||
uint32_t net_inet_addr(const char *cp) {
|
||||
/* 组装出的 32 位地址(主机序,末转网络序) */
|
||||
/* 主机序累加值(第一段最终落在最高字节) */
|
||||
uint32_t addr = 0;
|
||||
/* 段循环索引 */
|
||||
int i;
|
||||
@@ -2842,15 +2905,15 @@ uint32_t net_inet_addr(const char *cp) {
|
||||
val = val * 10 + (*cp - '0');
|
||||
cp++;
|
||||
}
|
||||
/* 本段放入对应字节位(段 0 = 最低字节,小端组装) */
|
||||
addr |= (val << (i * 8));
|
||||
/* 高位先入:第 0 段左移三次后落在最高字节 */
|
||||
addr = (addr << 8) | (val & 0xFFU);
|
||||
/* 跳过分隔符点(末段可无点) */
|
||||
if (*cp == '.') {
|
||||
cp++;
|
||||
}
|
||||
}
|
||||
|
||||
/* 转网络字节序后返回 */
|
||||
/* 转网络字节序后返回(此时最低地址字节 = 第一段) */
|
||||
return NET_HTONL(addr);
|
||||
}
|
||||
|
||||
@@ -2863,8 +2926,15 @@ uint32_t net_inet_addr(const char *cp) {
|
||||
* 限定条件:buf 非空且长度足够(>= 16 字节)
|
||||
* 函数说明:1. 先经 NET_NTOHL 转为主机字节序再格式化输出
|
||||
* 2. ⚠ 段序(E13 定案 修复):NTOHL 后主机序数值的高字节才是
|
||||
* 点分第一段(192.168.1.100 → 0xC0A80164 → "192.168.1.100")。
|
||||
* 原实现低段在前,显示成字节反转的 "100.1.168.192"
|
||||
* 点分第一段。**入参必须是网络序整数**(&addr 的第 0 字节 = 第一段,
|
||||
* 小端上 192.168.1.100 = 0x6401A8C0,见 net_inet_addr 的约定说明),
|
||||
* 于是 0x6401A8C0 → NTOHL → 0xC0A80164 → 高字节 0xC0 = 192 首段 ✓。
|
||||
* 原实现低段在前,同一入参会显示成字节反转的 "100.1.168.192"。
|
||||
* 3. ⚠ 本注释旧版举的例子写成「192.168.1.100 → 0xC0A80164 → 同串」,
|
||||
* 那个 0xC0A80164 取的是**已废弃的另一族**(当时 net_inet_addr 的返回
|
||||
* 值),按本函数的真实算法它打印出来恰是 "100.1.168.192"。两套 s_addr
|
||||
* 约定能在同一文件里长期共存而不被发现,源头就是这行错例(2026-10-24
|
||||
* 2026-10-24 修正;函数行为未变,只把例子改对)
|
||||
*/
|
||||
char *net_inet_ntoa(uint32_t addr, char *buf) {
|
||||
/* 网络序 → 主机序 */
|
||||
@@ -3076,7 +3146,9 @@ int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen) {
|
||||
p_addr_in->sin_family = NET_AF_INET;
|
||||
/* 本地端口(主机序 → 网络序) */
|
||||
p_addr_in->sin_port = net_htons(p_sock->local_port);
|
||||
/* 本地 IP:由 s_local_ip 数组小端组装为 32 位网络序值 */
|
||||
/* 本地 IP:s_local_ip[] 是线序数组([0]=第一段),按"首段落在最低字节"
|
||||
* 组装即得网络序整数 —— 与 net_inet_addr / net_inet_ntoa 同族(2026-10-24 定案),
|
||||
* 故 getsockname 的结果可直接与 inet_addr 的返回值相等比较 */
|
||||
p_addr_in->sin_addr.s_addr = ((uint32_t)s_local_ip[0]) |
|
||||
((uint32_t)s_local_ip[1] << 8) |
|
||||
((uint32_t)s_local_ip[2] << 16) |
|
||||
|
||||
@@ -324,6 +324,8 @@ int net_setblocking(int sockfd, int on);
|
||||
* 3. 用本接口把等待切成 ≤timeout_ms 的片,调用方自行累计总预算:总超时语义不变、单次占用有界
|
||||
* 4. 超时用独立返回码 -2(而非靠 errno 区分)→ 无跨任务 errno 竞态
|
||||
* 5. ⚠ IWDG required_mask 内任务必须走本接口或 net_select(见 net_recv 契约)
|
||||
* 6. 本接口**走消息队列**、任意任务可调用(与 net_recv 同一条路径),
|
||||
* 与 net_recvfrom(仅限执行任务、直调芯片)不是一回事,勿混
|
||||
*/
|
||||
int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms);
|
||||
|
||||
@@ -335,9 +337,26 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms);
|
||||
* flags - 通常为 0
|
||||
* dest_addr - 目标地址
|
||||
* addrlen - 地址长度
|
||||
* 返回值:实际发送字节数,失败返回 -1
|
||||
* 限定条件:socket 已创建,指针非空
|
||||
* 函数说明:设置目标地址后调用 CH395F 发送
|
||||
* 返回值:实际发送字节数 int > 0
|
||||
* -1 失败,精确码经本任务错误码槽给出(net_get_errno()):
|
||||
* NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_TIMEDOUT /
|
||||
* NET_ERR_NOT_IN_NETTASK(调用上下文不对,见「限定条件」)
|
||||
* 限定条件:**仅限本层执行任务(net_init 的调用者,NET 层 = netTask)调用**;
|
||||
* socket 已创建且为 DGRAM;指针非空
|
||||
* 函数说明:1. 设置目标地址后调用 CH395F 发送
|
||||
* 2. ⚠ 本接口**不走消息队列**:它在调用方上下文里直接 net_poll +
|
||||
* 写芯片缓冲,与 net_send/net_recv/net_connect 那一族(入队交 netTask
|
||||
* 串行执行)不是同一条路径。别的任务调用它 = 与本层的 net_poll 抢
|
||||
* 同一根 SPI2(TRAP-NET-14 的根因形态),故加上下文守卫直接拒掉,
|
||||
* 回 NET_ERR_NOT_IN_NETTASK。**该码重试永远无意义**(既非忙也非超时):
|
||||
* 要么把调用挪进执行任务,要么改用走队列的 net_send——没有第三条。
|
||||
* 3. 「把它改成走消息队列」是已知后续项,未做的原因:UDP 的目的地址
|
||||
* 与源地址回传都要穿过 net_msg_t,而 net_msg_t 按值拷入队列、
|
||||
* 错误码只搭任务通知值回来(AGENTS 明文「勿给 net_msg_t 加回传字段」),
|
||||
* 真要串行化得先设计 per-task 结果槽并配真机回归(2026-10-24 定案:
|
||||
* 本轮只把危险面关掉,串行化仍未做)。
|
||||
* 4. 全仓当前**无生产调用者**(只有 test/board 与模拟器),正因如此
|
||||
* 这条守卫要在有人开始用之前就在。
|
||||
*/
|
||||
int net_sendto(int sockfd, const void *buf, int len, int flags,
|
||||
const struct net_sockaddr *dest_addr, int addrlen);
|
||||
@@ -350,8 +369,12 @@ int net_sendto(int sockfd, const void *buf, int len, int flags,
|
||||
* flags - 通常为 0;NET_MSG_DONTWAIT 单次强制非阻塞
|
||||
* src_addr - 输出:发送方地址
|
||||
* addrlen - 输入输出:地址长度
|
||||
* 返回值:实际接收字节数,失败返回 -1
|
||||
* 限定条件:socket 已创建,指针非空
|
||||
* 返回值:实际接收字节数(0 = 无数据可报的边界形态见说明) int >= 0
|
||||
* -1 失败,精确码经本任务错误码槽给出(net_get_errno()):
|
||||
* NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_WOULDBLOCK /
|
||||
* NET_ERR_NOT_IN_NETTASK(调用上下文不对,见「限定条件」)
|
||||
* 限定条件:**仅限本层执行任务(net_init 的调用者,NET 层 = netTask)调用**;
|
||||
* socket 已创建且为 DGRAM;指针非空
|
||||
* 函数说明:1. 读取 CH395F 接收缓冲区,获取对端地址
|
||||
* 2. 阻塞语义与 net_recv 同:blocking 属性下无数据
|
||||
* 无限等待,DONTWAIT / 非阻塞属性立即 NET_ERR_WOULDBLOCK;
|
||||
@@ -438,6 +461,16 @@ int net_sock_state(int sockfd);
|
||||
* 返回值:网络字节序 IP,失败返回 0
|
||||
* 限定条件:cp 指针非空
|
||||
* 函数说明:不支持域名解析
|
||||
*
|
||||
* s_addr 的**唯一约定**(2026-10-24 定案:两套 IP 字节序约定统一为 POSIX 网络序;全仓只此一族,勿再引入第二套):
|
||||
* 网络字节序整数 —— &s_addr 的第 0 个内存字节 == 点分第一段。
|
||||
* 小端主机(STM32F407 与 PC 模拟器都是)上 "192.168.1.100" 的数值 = 0x6401A8C0。
|
||||
* 与 net_inet_ntoa 互为逆运算;net_getsockname / net_getpeername / net_recvfrom
|
||||
* 填出来的 s_addr、以及 net_bind / net_connect / net_sendto 收进去的 s_addr
|
||||
* 全在同一族,故可直接做相等比较(旧实现里 inet_addr 与其余四个不互逆,
|
||||
* 曾造成「解析→存储→打印」链路输出反序 IP,见 TRAP-NET-21 与本函数实现注释)。
|
||||
* 另:本层 inet_addr 语义与 BSD 一致——失败回 0,故 "0.0.0.0" 与非法串同值;
|
||||
* 需要区分时用 net_inet_addr 的返回值再判,勿拿 NET_INADDR_NONE 当失败哨兵。
|
||||
*/
|
||||
uint32_t net_inet_addr(const char *cp);
|
||||
|
||||
|
||||
@@ -108,6 +108,15 @@ struct net_sockaddr_in {
|
||||
#define NET_ERR_TIMEDOUT -9 /* 操作超时 */
|
||||
#define NET_ERR_NOSPACE -10 /* 发送缓冲区无空间 */
|
||||
#define NET_ERR_BUSY -11 /* 操作未完成(CH395F BUSY) */
|
||||
/*
|
||||
* 调用方**上下文**不对(不是运行时故障):该接口只允许在 net_init 的执行任务
|
||||
* (NET 层 = netTask / 硬件层 = testTask)里调用,其它任务调用会与本层 net_poll
|
||||
* 抢同一根 SPI(TRAP-NET-14)。与上面各码的区别:BUSY/TIMEDOUT/NOSPACE 换个时刻
|
||||
* 重试有意义,本码**重试永远无意义**——要么改到执行任务里调,要么把动作交回
|
||||
* 消息队列(netMsgQueue),没有第三种。所以它不参与任何"可重试"判定,
|
||||
* 上层见到它应当直接当编程错误处理(详见 net_socket.h net_sendto/net_recvfrom)。
|
||||
*/
|
||||
#define NET_ERR_NOT_IN_NETTASK -12 /* 非执行任务上下文调用(重试无用) */
|
||||
|
||||
/*
|
||||
* Socket 状态枚举(**本层会话投影**,非芯片状态事实源)
|
||||
|
||||
@@ -123,7 +123,7 @@ flowchart LR
|
||||
| 参数 | 值 | 理由 |
|
||||
|------|-----|------|
|
||||
| 容量 `DBG_CON_RING_SIZE` | 4096B | 2 的幂 → `&(SIZE-1)` 回绕免除法;≈21 整行(192B/行)余量 |
|
||||
| head/tail | `uint16` + `volatile`,单生产者/单写者 | head = 生产者写位置,tail = 写者读位置 |
|
||||
| head/tail | `uint16` + `volatile`;**多生产者安全 / 单写者**(2026-10-24 更正:本表旧写「单生产者」,与本文 §3.1「生产者 = 全部任务 + 内核前主线程 + ISR」互相矛盾,现由代码侧统一——**复核空位 + 推进 head + 整行拷贝收进同一个临界区**,最长 192 B 拷贝在 168 MHz 下约亚微秒;`vTaskDelay` 与 DWT/tick 上界判据一律留在区外,区内绝不睡眠) | head = 生产者写位置(临界区内推进),tail = 写者读位置 |
|
||||
| 已用字节 | `(head - tail) & (SIZE-1)` | 无符号差自然回绕,免锁可读 |
|
||||
| 单行可接收上限 | `SIZE/2 = 2048B` | 保证"一行恒可入环";正常路径受 `DBG_CON_LINE_MAX=192B` 约束,不触发 |
|
||||
| 位置 | BSS(SRAM1,非 CCM) | CCM = FreeRTOS 堆,放 BSS 不占堆;轮询发送不依赖 DMA 可达性(CCM 的 DMA 不可达约束不适用) |
|
||||
@@ -331,7 +331,7 @@ sequenceDiagram
|
||||
| # | 项 | 说明 |
|
||||
|---|----|------|
|
||||
| O1 | 未注册期裸发无互斥 | 启动窗口(内核启动 → attach)`dbg_console_write_raw` 无互斥 → 多任务并发会**逐字节交错**(非旧 `HAL_BUSY` 丢整行)。2026-10-10 已固化"周期任务用 `dbg_console_writer_ready()` 门控"纪律;若未来开机日志密度上升,可升级为"内核前静态待发行 + attach 后统一补发"或给裸发加轻量互斥 |
|
||||
| O2 | `HAL_UART_Transmit` 超时未计数 | 超时块(UART 物理失效)丢弃但不入 `s_con_drop`;v2 可加独立超时计数 |
|
||||
| O2 | `HAL_UART_Transmit` 超时未计数 —— **已处置(2026-10-24,v1.5)** | 三本账已分离:`dbg_console_dropped()` = **真没落地**的数(缓冲满丢的一行,或没能确认送达而出环的一个块);`dbg_console_tx_fault()` = 外设**软复位自愈次数**(语义不变);新增 `dbg_console_tx_timeout()` = 写者排空时看到的**块超时次数**(TXE 与 TC 两条硬上界都算)。**口径边界**:裸发路径(内核前 / ISR / 两条告警行自身)的超时**不进**超时账——裸发没有「块」的概念,混进来读数失去意义。**尚未接线**:本读口暂无生产调用者;候选位置 `App/util/task_watchdog.c` 的 `STALL` 行是**被解析的证据行**,加字段要连解析端与台架判据一起改 → 列为后续项 |
|
||||
| O3 | `printf` 逐字节开销 | 遗留 `printf` 路径 = 逐字节临界区 + 逐字节事件置位;遗留调用点全部切 `DBG_*` 宏后开销自然消失(不改变机制) |
|
||||
| O4 | **ORE 僵尸未根治** | §12:现仅有缓解(250ms 预算 / 300ms 冷却 / DWT 超时),机制根源是 USART1 使能了 RX。根治需 `.ioc` 改 TX-only + CubeMX 重生成 → 已登记 `docs/任务规划/登记簿.md` **S-3** |
|
||||
|
||||
@@ -351,4 +351,5 @@ sequenceDiagram
|
||||
| v1.1 | 2026-09-25 | 文件拆分同步:写者任务体迁至 `App/task/default_task.c`(`Src/freertos.c` 只留 CubeMX 接线与崩溃钩子);§3.2 文件清单 / §4.3 / §6 / §11 位置引用更新 |
|
||||
| v1.2 | 2026-10-02 | 新增 §12 ORE 僵尸通道(台架 gen-288 实证:节奏性喂狗饥饿,非死等;缓解已做、TX-only 根治待办)+ §11 时间线两条 10-02 记录;§13 新增 O4;原 §12/§13 顺延为 §13/§14 |
|
||||
| v1.3 | 2026-10-10 | 栈链整改(**未抬栈**)三处:① 裸发改为纯寄存器轮询——DWT 不可用时走 `con_tx_counted`(计数上界),**不再回退 `HAL_UART_Transmit`**(tick 依赖不可信 + 其调用链污染所有任务最坏栈链);② 切断 `drain → syslog_post → write_line` **调用图成环**,改为置标志 + 写者任务在 drain 之外补记(新 API `dbg_console_tx_fault_log_take`);③ 行缓冲按模块可收紧(`dbg_log.h` 新增 `DBG_LINE_BUF_MAX`,深链模块 128)+ 时间戳缓冲并入行缓冲(32 → 24)。§4.4 / §5 / §6 / §7 / §8 同步;实测 defaultTask 静态最坏链 5312 → 2776 B |
|
||||
| v1.5 | 2026-10-24 | **主机单测批次照出的四条现状全部处置**(生产码在 `App/log/console_log.c`,用例在 `test/host/console_log/`):① 冷却判据改用独立 `s_con_cooling` 标志,不再拿 `HAL_GetTick()` 的 0 值当哨兵——旧写法在 tick==0 那一拍(开机头 1 ms,或 49.7 天回绕到 0)冷却等于没记;② `con_append` 的空位判据 / 推进 head / 整行拷贝收进同一临界区——旧实现是临界区外 check-then-act,多生产者可在环 >95% 满时双双过判据把 head 推过容量 → 静默覆盖且**既不打 [ERR] 也不计 dropped**;③ `Instance == NULL`(未初始化)与僵尸通道**分诊**,未初始化绝不 reinit(旧路径会在 HAL 未就绪时经 MspInit 重配 RCC/GPIO);④ 三本账,见本文 §13-O2。**dropped 的单位口径变了**:一次 = 一行(缓冲满)或一个块(没能确认送达 / 通道不可用),故 `[ERR] CONSOLE dropped N lines` 的 N **不恒等于行数**(为兼容 `@test` 标记匹配,文案未改词)。本文 §3.1 与 §4.1 的「单生产者 vs 全上下文生产者」矛盾一并消除 |
|
||||
| v1.4 | 2026-10-10 | 新增 `dbg_console_writer_ready()`(写者就绪查询)并固化"未注册期只有 defaultTask 记日志、周期任务用就绪门控"纪律;更正 §3.4 / §7 / §9-D5 / §13-O1 的"只丢不交错"旧结论(裸发已改寄存器级轮询、无 `gState` 互斥) |
|
||||
|
||||
@@ -116,6 +116,22 @@
|
||||
|
||||
### 4.4 写路径(单写者,调研结论落地)
|
||||
|
||||
**待发条目存副本而非指针(B 方案整改,2026-10-24,台架实证驱动)**:`syslog_entry_t` 原为
|
||||
`{ uint8_t id; const char *desc; }`——只存调用方指针,隐含「desc 必须活到落盘」的契约,
|
||||
但该契约既没写进 `sys_log.h`、也没有任何机械检查。一次可控台架实验把它照了出来:临时在
|
||||
testTask 挂投递钩子,复用**同一块缓冲**连投 12 条带序号记录,并与一次 8 MB FTP STOR(占住
|
||||
卷锁)重叠——落盘的 12 条变成「1~5 + 第 12 条七份」,即所有滞留条目别名到该缓冲的最后一次
|
||||
写入。危险不止于内容错:卷忙整批回投让条目最长滞留到卷空闲,生产者若投栈缓冲,函数返回后
|
||||
就是悬垂读。现改为条目内嵌定长缓冲 `char desc[SYSLOG_DESC_MAX + 1u]`,`syslog_post` 投递时按
|
||||
`SYSLOG_DESC_MAX` 截断拷贝并补终止符,调用方缓冲区可立即复用;`sys_log.h` 的参数说明同步
|
||||
改写。RAM 代价(`scripts/ram_report.py` 同口径前后各跑一次):`ZI 131,500 → 131,852 B`
|
||||
(+352 B,`RW_IRAM1` 57.4% → 57.7%)、`Code +60 B`、`RW` 不变;`RW_CCM` 静态不变,队列
|
||||
存储多要的 192 B 取自 FreeRTOS 堆池,烧录后串口水位行 `[STACK] heap free=15624
|
||||
min_ever=8464` 说明余量充足。回归由 **TC-SYSLOG-108**(投出去之后改写调用方缓冲,落盘必须
|
||||
仍是投递时的原文)与 **TC-SYSLOG-108b**(超长在条目内钳位)钉住。**为什么主机侧此前照不出
|
||||
来**:用例传的都是字符串字面量、静态存储期无限,正好绕开生命周期维度——要照出这类问题必须
|
||||
专门构造可变缓冲。
|
||||
|
||||
```
|
||||
任意上下文 syslog_post(id, desc)
|
||||
├─ FreeRTOS 未运行(app_main_init 期)→ 静态待发数组(8 槽,满则丢弃+计数)
|
||||
@@ -131,9 +147,12 @@ defaultTask(唯一写者,挂载成功后):
|
||||
|
||||
- **N-24(2026-10-15,报告 N-16 写侧):一次排空合并成"每文件一次 open/write/close"**。旧实现每条记录一轮,而 `f_close` 要回写目录项、`f_write` 要对目标扇区做读-改-写 → **每条日志 2~4 次 NAND 页读写**。现在同一文件的一批(≤8 条 = 512 B 静态缓冲 `s_batch`)合并成一次追加写:
|
||||
- **掉电语义不变**:批次仍在**同一次 `syslog_drain()` 调用内**写完并 `f_close`;`f_close` 内部 `sync_fs` → `disk_ioctl(CTRL_SYNC)` 会 `ftl_flush_cache()` 把 FTL 的单页写回缓存刷进 NAND(`Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` 的 `CTRL_SYNC` 分支)。落盘时机与旧实现每条 close 等价,**批次不跨调用驻留 RAM**:"写完即关"的路径关文件即落盘,残余只到"单次写事务进行中掉电/复位"为止(`../任务规划/登记簿.md` N-22:无空闲/掉电前周期 flush,2026-10-22 用户裁定暂不做)。
|
||||
- **条目不丢**:启动前数组按下标"看而不删",整批成功才删;队列条目在落盘失败(-2 卷忙 / -1 写失败 / -3 容量不足)时**回投队列**——旧实现 -2 会直接丢掉那一条。
|
||||
- **条目不丢**:启动前数组按下标"看而不删",整批成功才删;队列条目在落盘失败(-2 卷忙 / -1 写失败 / -3 容量不足)时**回投队列**。
|
||||
⚠ **2026-10-24 更正本条**:旧实现里 -2 只在**按条慢路径**被正确回投,**批量路径根本没有 -2 分支**(`done` 保持 `n` → 回投循环空转)→ 卷忙时一批(≤8 条)既不落盘也不回投、静默丢失,而本节长期写着"条目不丢"替它背书。现已补 `rc == -2` 分支整批回投(不计故障、不打日志、保序),由 `test/host/sys_log_mcu/` 的 TC-SYSLOG-035 组逐条钉住。**真机必验**:FTP 传输期间持续投日志,传输结束后条数 == 投递条数、顺序不乱。
|
||||
- **容量轮转仍只在按条慢路径做**:批量路径发现剩余容量装不下一批时返回 -3,退回逐条 `s_append()`("满了 unlink 重建"只保留一处实现,避免一批跨多轮重建)。
|
||||
- 自愈重试(L1 丢弃重建 + 重试一次)与错误去重(L2 只报第一次)判据与单条路径一致。
|
||||
- **自愈触发集分类(2026-10-24,代码侧唯一入口 `s_is_selfheal_code()`)**:只有"删了这个文件真能修好"的介质/结构类码才触发 unlink 重建——`FR_DISK_ERR` / `FR_INT_ERR` / `FR_NO_FILESYSTEM` / `FR_DENIED`;**名字/参数类严禁归入**(`FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT`,另显式排除 `FR_NOT_READY` / `FR_NOT_ENABLED` / `FR_TIMEOUT`)。同名同因下 unlink 必然同样失败,旧集合的真实后果是**每轮排空删一次自己的历史日志且永远修不好**,违反 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 10 条。`f_write` 之后的"失败/短写 → unlink 重试一次"**不接**本判定(能走到那里说明已打开成功;而簇分配失败是 `FR_OK` + 短写、根本没有码可判,必须无条件重试)。钉住:TC-SYSLOG-101 / 101b / 102 / 103;`App/log/soe_store.c` 侧同源但取**更保守子集**(TC-SOESTORE-022 / 022b)。
|
||||
- **留痕节流分两层、各自独立抑制位**:drain 侧"整批回投"只在正常→故障那一次报(`write fail, requeue %lu entries`,落盘成功一轮复位;旧文案写作 "drop N entries" 与实际行为相反——整批其实一条没丢);post 侧 `queue full, drop` / `preboot full, drop` 各一位(**丢弃语义不变,满则丢照旧**,只压留痕次数)。节流之后**没有丢弃计数**——要计数属新增对外可见信息,另列后续项。
|
||||
- 读路径与写路径并发安全:UI 页在 uiTask 经 `syslog_get_record()` 读记录(内部走 `s_read_raw()`,见 §4.5);读写两侧都取卷锁 try 语义(P2-10),FatFS 系统锁保护单次调用。
|
||||
- **存储未就绪/挂载失败**:待发条目滞留 RAM(下次上电丢失),串口打印告警——存储故障时本就无法持久化,属预期行为。
|
||||
|
||||
@@ -142,12 +161,13 @@ defaultTask(唯一写者,挂载成功后):
|
||||
```c
|
||||
uint32_t syslog_count(syslog_id_t id); /* = filesize/64 */
|
||||
int syslog_get_record(syslog_id_t id, uint32_t idx, char *ts, size_t ts_len,
|
||||
char *desc, size_t desc_len); /* 越界返回 -1 */
|
||||
char *desc, size_t desc_len); /* 越界 / 读失败 / 缓冲容量不足 均返回 -1 */
|
||||
```
|
||||
|
||||
- MCU:**8 条滑动读窗口**(N-24,报告 N-16 读侧)——`s_read_raw()` 先查窗口(同 id + 代数未变 + 落在区间内 → **零 I/O** 直接从 512 B 静态窗口返回);未命中才 `f_open(FA_READ) → f_lseek(窗口首条×64) → f_read(≤8×64B) → f_close` **一次取回连续 8 条**,再从中切出请求的那条。旧实现每读一条都要 open/lseek/read/close 一轮,UI 翻 20 条记录 = 20 次目录项/FAT 往返。
|
||||
- 失效机制:写者每次落盘后把 `s_rd_gen[id]` 自增,读者比对填充时记录的代数,不匹配就重填。对齐 32 位在 ARM 上原子,跨任务(uiTask 填、defaultTask 增)无需临界区;最坏情况多读一轮,不会读错。
|
||||
- **API 与调用方一律不变**(`syslog_get_record()` 签名照旧,`ui_page_runlog` / `ui_page_selftest` 零改动)。
|
||||
- **容量契约(2026-10-24 写死在 `App/log/sys_log.h`)**:`ts_len > SYSLOG_TS_LEN`、`desc_len > SYSLOG_DESC_MAX`,**不足一律 -1,既不静默截断也不写调用方一个字节**(描述在写入侧已截到 `SYSLOG_DESC_MAX`,按最小容量给就一定装得下,读侧不需要第二道截断)。旧实现里那句 `if (dl >= desc_len) dl = desc_len - 1` 由上面的守卫保证**永不可达**,已删除;它的存在还会误导调用方"给 28 字节缓冲能读到 27 字符"——实际是整条读不到。UI 侧现用 32 B 列缓冲,走允许路径、行为不变。钉住:TC-SYSLOG-057、057b(0x7A 哨兵逐字节回比)、098。
|
||||
- 模拟器:宿主文件同语义(UI 页双端零差异)。
|
||||
- UI 页每帧仅读当前窗口 ≤8 条记录(每次重绘 8×64B SPI 读,用户翻页触发,频率极低)。
|
||||
|
||||
|
||||
@@ -579,7 +579,8 @@ if (nand_ftl_format() == 0) {
|
||||
7. **单写者规则** — 存储由 **defaultTask 独占**:挂载 / `f_mkfs` / 卷健康探针 / `net_cfg_load` / syslog 落盘 / 配置引擎全收敛于它。netTask / uiTask **不得**直接触碰配置引擎或 FatFS。启用存储测试套件(`TEST_SUITE_STORAGE`)时存储所有权整体移交 testTask,defaultTask 必须**完全跳过**存储 bring-up(`App/task/default_task.c` 中 `#if !defined(TEST_SUITE_STORAGE)` 包裹,2026-09-25 自 `Src/freertos.c` 迁出)——否则后台文件写与测试的 `nand_ftl_deinit` 掉电模拟并发,互相踩坏 FTL/FatFs 状态(实测:mkfs 半途冻结 + BBT 被误写)。
|
||||
8. **内存布局与 DMA/CCM 约束** — FreeRTOS 堆(含全部任务栈)已迁至 **CCM `0x10000000`**(64KB,直连 M4 零等待),但 **DMA1/DMA2 物理不可达 CCM**。GD5F 驱动 `GD5F_DMA_THRESHOLD = 1024`:栈上的小缓冲(dhara meta 读 132B、`dhara_nand_is_free` 64B)一律走 **CPU 轮询**、绝不经 DMA;页级大块(2048)才走 DMA 且源/目的**全部是文件作用域静态缓冲**(落 SRAM)。新增任何 DMA 收发缓冲必须①文件作用域静态②大小≥阈值③确认落 SRAM。违反任一条 = 静默数据损坏(CCM 首轮扩容实验实证)。换料注意:克隆片可能缺 SRAM3(CCM),必须重测。
|
||||
9. **LVGL 绘制缓冲越界护栏** — LVGL 9.3.0 对 I1(1bpp) 整帧渲染会在其算出的 `stride*h`(160×160/8=3200) 之后**多写 1 个字节**,紧邻 `g_ui_draw_buf` 之后的 `.bss`(如 `ui_menu_view` 的 `s_frame_box`)指针最低字节被打成 `0xFF` → 野指针 → HardFault(`PC=lv_obj_get_scroll_y`)。`ui_port.c` 的 `g_ui_draw_buf` 已预留 `UI_DRAW_BUF_GUARD=32` 字节护栏吸收该越界写(逻辑尺寸仍按 `UI_FB_SIZE` 上报,渲染数学不变)。**勿删护栏、勿把缓冲缩到与邻接 `.bss` 紧邻**。Lib/LVGL 为第三方不改。
|
||||
10. **FatFs 严格 8.3 短名** — 本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**:主名 ≤8 字符、扩展 ≤3 字符、**不得以点开头**(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)。`.fschk` 会被 `ff.c` 的 `create_short` 把「点之后」当扩展域解析而超限 → `f_open` 恒返 `FR_INVALID_NAME`(6);这是**纯 RAM 里的名字判定,不碰盘**。`ff.c` `create_short` 全部拒绝点的地方已加 DEBUG 打印:`[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符`——非法名字调用看开机日志即可定位。自愈类逻辑**严禁**把名字/参数类错误码(`FR_INVALID_NAME` 等)归入"卷损坏"触发集(整卷重格式化不可逆)。补充:含冒号 `:` 的名字被 FatFs 先解析为**盘符前缀** → `f_open` 返回 `FR_INVALID_DRIVE`(11) 而非 `FR_INVALID_NAME`(6);两者都属合法拒绝(测试用例须同时接受)。
|
||||
10. **FatFs 严格 8.3 短名** — 本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**:主名 ≤8 字符、扩展 ≤3 字符、**不得以点开头**(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)。
|
||||
- **推论(2026-10-24 补齐,代码侧唯一入口 `s_is_selfheal_code()`)**:既然名字/参数类错误码在 8.3 配置下**必然重现**(同名同因),它们就**严禁**被归进任何"卷/文件坏了"的自愈触发集——否则自愈会退化成反复删自己的数据。本仓落点:`App/log/sys_log_mcu.c` 的触发集只收 `FR_DISK_ERR` / `FR_INT_ERR` / `FR_NO_FILESYSTEM` / `FR_DENIED`,`FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT` 一律立即失败(逐码依据 + `Lib/FatFs/ff.c` 行号写在该函数上方资料块;`App/log/soe_store.c` 侧同源取更保守子集并互指)。**本条拦下的真事**:旧集合含名字类码,后果是路径/卷异常期间**每轮排空删一次自己的历史日志且永远修不好**,由主机单测照出(TC-SYSLOG-101 / 102、TC-SOESTORE-022b),详见 `docs/测试说明/主机单测覆盖率口径.md` §9「第 1 轮修复处置」。。`.fschk` 会被 `ff.c` 的 `create_short` 把「点之后」当扩展域解析而超限 → `f_open` 恒返 `FR_INVALID_NAME`(6);这是**纯 RAM 里的名字判定,不碰盘**。`ff.c` `create_short` 全部拒绝点的地方已加 DEBUG 打印:`[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符`——非法名字调用看开机日志即可定位。自愈类逻辑**严禁**把名字/参数类错误码(`FR_INVALID_NAME` 等)归入"卷损坏"触发集(整卷重格式化不可逆)。补充:含冒号 `:` 的名字被 FatFs 先解析为**盘符前缀** → `f_open` 返回 `FR_INVALID_DRIVE`(11) 而非 `FR_INVALID_NAME`(6);两者都属合法拒绝(测试用例须同时接受)。
|
||||
11. **FA_OPEN_APPEND 陷阱 / 短写** — `FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(这是当年 `/log` 写失败的真根因);追加写必须写 `FA_WRITE | FA_OPEN_APPEND`(2026-09-20 台架实测,`Lib/FatFs` 移植版)。该移植的 `f_write` 在簇分配失败时返回 `FR_OK` + 短写(**没有错误码**)→ 判失败必须校验写回的字节数。标志位为**旧式编号**:`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(`ff.c` 内部定义),与上游 FatFs R0.14 不同,**勿照抄上游代码示例**。
|
||||
12. **崩溃可观测性(勿屏蔽)** — 已开启 `configCHECK_FOR_STACK_OVERFLOW=2` + `configUSE_MALLOC_FAILED_HOOK` + HardFault 现场报告(`Src/stm32f4xx_it.c`:裸 UART 打印 PC/LR/xPSR + CFSR/HFSR/BFAR/MMFAR 后复位,对照 `.map` 定位符号)。defaultTask **1024** words、testTask **2048** words 是**实测下限,勿降**。开机日志出现 `[ERR] STACK OVERFLOW task=` / `[ERR] HARDFAULT ...` / `MALLOC FAILED` 是精确定位线索,禁止屏蔽或降级。
|
||||
13. **BBT 恢复模式** — 运行期坏块会随 TC-STORAGE-03 注入累积;当介质被**误标脏**(如 DMA 误打 CCM 栈导致随机块被写坏并持久化)时,打开 `test/board/storage_test.h` 的 `ENABLE_STORAGE_RECOVERY_TESTS` 编译**恢复构建**,testTask 会执行 `gd5f2gq5ue_bbt_rebuild()`(出厂扫描清除运行期误标)+ `nand_ftl_format()` 清账后复位。恢复后容量回到基线(即扣除 BBT 保留池后的正常容量,非出厂裸容量)。
|
||||
|
||||
+301
-8
@@ -29,7 +29,7 @@
|
||||
| gcc / gcov / clang / llvm-cov / lcov / gcovr(**Windows 原生侧**) | PATH 上没有;再全盘扫 `Program Files*`、`C:\msys64`、`scoop`、choco、`.platformio`、`%LOCALAPPDATA%\Programs` → **0 处**(OpenCppCoverage 也没有) | Windows 侧走不了 gcov 这条最主流的路(但 WSL 里有,见 §2.1) |
|
||||
| `cl /coverage`、`cl /fsanitize=coverage` | **MSVC 14.51 已不认**:只报 `warning D9002: 忽略未知选项`,**然后照常编译成功** | ⚠ 最阴的一种:不报错、静默没有插桩 → 覆盖率恒为 0 或假数据,绝不可用 |
|
||||
| `Team Tools\Dynamic Code Coverage Tools\CodeCoverage.exe`(17.0) | 文件在,但 `collect` / `analyze` / `instrument` **一律只打印"用法: CodeCoverage <command>"** 并返回 -1 | 已 deprecated 且子命令不可用 |
|
||||
| **`Microsoft.CodeCoverage.Console.exe`**(mscoverage 18.x,随 VS 装在 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\`) | `collect -f cobertura` 直接吃 MSVC 原生 exe + PDB,产出标准 Cobertura XML;**子进程退出码原样透传**(实测子进程 `return 3` → `collect` 也返回 3) | ✅ **本脚本用它**("加载期动态插桩",不是编译期插桩) |
|
||||
| **`Microsoft.CodeCoverage.Console.exe`**(mscoverage 18.x,随 VS 装在 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console`) | `collect -f cobertura` 直接吃 MSVC 原生 exe + PDB,产出标准 Cobertura XML;**子进程退出码原样透传**(实测子进程 `return 3` → `collect` 也返回 3) | ✅ **本脚本用它**("加载期动态插桩",不是编译期插桩) |
|
||||
|
||||
### 2.1 GCC 到底行不行:能编、能跑、**能出分支覆盖**——结论是**不用**(换编译器就是换分母)
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
分支这一维——不值当。代价如实记在 §7 第 1 条:本口径的绿灯**只证明行与函数被测到**。
|
||||
|
||||
探测顺序:环境变量 `DTU_MS_COVERAGE`(设了就必须有效,**不静默回退**,与 `DTU_MSVC_CL` 同口径)
|
||||
→ 各 VS 安装根的 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\` → PATH。
|
||||
→ 各 VS 安装根的 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console` → PATH。
|
||||
找不到即以退出码 2 失败并打印上述现实,**不会**假装通过。
|
||||
|
||||
## 3. 怎么跑
|
||||
@@ -77,7 +77,7 @@
|
||||
退出码:`0` 全过且无回退 / `1` 有单测失败或覆盖率回退 / `2` 环境错误(无 cl、无 mscoverage、
|
||||
编译失败、XML 缺失)/ `3` 用法错误。全量实测约 **25 s**(16 套 × 编译 + 插桩运行)。
|
||||
|
||||
中间产物(exe / PDB / Cobertura XML)一律落 `%TEMP%\dtu_test_cov\<模块>\`,**不进仓库**,
|
||||
中间产物(exe / PDB / Cobertura XML)一律落 `%TEMP%\dtu_test_cov\<模块>`,**不进仓库**,
|
||||
所以本脚本不需要新增任何 `.gitignore` 规则。
|
||||
|
||||
## 4. 统计面:进什么、不进什么
|
||||
@@ -153,7 +153,7 @@
|
||||
7. **头文件里的代码归 `.h`。** 统计面含 `.h`(静态内联与被展开的行落在头文件上);当前 16 套
|
||||
几乎没触及 `.h`,表里看不到它们——不要以为头文件"天生 100%"。
|
||||
8. **依赖 `Microsoft.CodeCoverage.Console.exe`。** 探测顺序:`DTU_MS_COVERAGE`(设了就必须
|
||||
有效、**不静默回退**)→ 各 VS 安装根的 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\` →
|
||||
有效、**不静默回退**)→ 各 VS 安装根的 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console` →
|
||||
PATH;找不到即退出码 2(**不是 0**)。2026-10-24 起该结果直接红掉 `test-coverage` 钩子(用户裁定
|
||||
FAIL 语义),并已合成自证:把 `DTU_MS_COVERAGE` 指到一个不存在的文件 → 钩子 Failed、脚本退出码 2、
|
||||
打印可执行的 `[ENV]` 提示。
|
||||
@@ -208,10 +208,13 @@
|
||||
|
||||
**下一步(按性价比排序)**:
|
||||
|
||||
1. **扩大覆盖面**:按 **§9 的建桩优先级清单**吃。**P1 已吃完**(2026-10-24 合入主干,13 个 .c 全部
|
||||
进分母,`files_tested` 19 → 32);下一步是 **P2 的 5 个 / 2,226 行**(`net_socket.c` 1,251 行需分片、
|
||||
`sys_log_mcu`+`cfg_file_store_mcu`+`soe_store` 共用一套假 FatFs、`console_log`)。P3 的 29 个 /
|
||||
4,814 行**刻意不做**并已写明理由。比把已覆盖文件从 85% 推到 95% 值钱得多。
|
||||
1. **扩大覆盖面**:按 **§9 的建桩优先级清单**吃 —— **清单已吃穿**:P1 13 个 `.c`(`files_tested` 19 → 32)
|
||||
与 P2 5 个 `.c`(`files_tested` 32 → **37**,实测见 §9「P2 落地」)都已进分母,**剩下的 29 个 `.c`
|
||||
全部是 P3「刻意不做」**、每条都能在 P3 表里找到归属(本节终点判据就此达成)。于是这一条**没有下一批可排**,
|
||||
只剩两类事:① 把 §9 登记为本批「现状」的条目按用户裁定落成 fix(体量最大的一块是
|
||||
`App/log/sys_log_mcu.c` 卷忙时整批丢弃日志);② 真想把百分比再往上推,就只能回头补**已进分母文件**的
|
||||
未测分支(缺口最大的三块:`lftpd_io.c` 53.5%、`net_pending.c` 87.1%、`lftpd.c` 64.3%)——
|
||||
按 §5 那仍是自分数、不是 KPI,性价比低于 ①。
|
||||
2. **不做**:第二条覆盖率通道(gcc/WSL、`dotnet-coverage`、OpenCppCoverage 三条路都不走,用户裁定
|
||||
2026-10-23)。要重开这个裁定,前提是能同时给出"两份分母怎么对齐"的答案,否则只是多一本对不上的账。
|
||||
|
||||
@@ -415,6 +418,284 @@ uint16_t ch395f_link_change_count(void);
|
||||
3. **`console_log.c`(265)** —— 单写者契约 K1/K2 已经写成表,照抄成用例最省力:
|
||||
`docs/子系统设计/控制台单写者日志设计说明.md`。
|
||||
|
||||
### P2 落地(2026-10-24,5 个文件全部进分母,套件 28 → 33 全绿)
|
||||
|
||||
三个模块码并行落地(`net_socket` 分片吞、存储三件套共用新建假 FatFs、`console_log` 独立),
|
||||
**新增 5 套 659 条断言**:`net_socket` 420、`console_log` 104、`sys_log_mcu` 75、`cfg_file_store_mcu` 34、`soe_store` 26。
|
||||
|
||||
| 新纳入分母的 `.c` | 插桩行 / 函数 | 断言 | 套件目录 |
|
||||
|---|---|---|---|
|
||||
| `net_socket.c` | **881/883 = 99.8%**、52/52 | 420 | `test/host/net_socket/` |
|
||||
| `sys_log_mcu.c` | **310/313 = 99.0%**、12/12 | 75 | `test/host/sys_log_mcu/` |
|
||||
| `console_log.c` | **188/189 = 99.5%**、15/15 | 104 | `test/host/console_log/` |
|
||||
| `cfg_file_store_mcu.c` | **97/97 = 100%**、10/10 | 34 | `test/host/cfg_file_store_mcu/` |
|
||||
| `soe_store.c` | **78/79 = 98.7%**、4/4 | 26 | `test/host/soe_store/` |
|
||||
| **P2 合计** | **1,554/1,561 = 99.6%**、93/93 | 659 | — |
|
||||
|
||||
**账**:套件 28 → **33** 全绿、`files_tested` 32 → **37**、未触及 **112 → 107**(`.c` **34 → 29**、`.h` 仍 78)、
|
||||
分母 4,684 → **6,245** 插桩行,`line=4020/4684(85.8%)` → **`5574/6245(89.3%)`**、
|
||||
`func=300/333(90.1%)` → **`393/426(92.2%)`**。恒等式跟着更新且仍然咬合:`29 = 0(P1 剩) + 0(P2) + 29(P3)`,
|
||||
并且**本节终点判据(下面「顺序建议与记账」第 3 条)就此达成**:剩下的 29 条 `.c` 逐条都能在 P3 那张表里找到归属
|
||||
(芯片驱动 5 + UI 17 + 任务编排 6 + 内核前装配 1 = 29),没有一条是"既没进分母、又说不清为什么不测"的真窟窿。
|
||||
|
||||
⚠ **这批罕见地没有摊薄、反而 +3.5pp** —— 不是因为更勤奋,而是 5 个文件**全都以 ≥98.7% 进分母**
|
||||
(P1 那批是 `lftpd_io.c` 53.5%、`lftpd.c` 64.3% 这种"半份进分母")。**别把它读成"百分比才是 KPI"**:
|
||||
按 §5 口径这批该盯的仍是 `files_tested` 32→37 与未触及 `.c` 34→29 这两个数,89.3% 只是自分数。
|
||||
|
||||
**两套分母别再混用**(写给下次估工作量的人):本节清单里的"有效代码行"是**去注释/去空行**的静态计数,
|
||||
插桩分母只数 mscoverage 认为可执行的行——`net_socket.c` 清单 1,251 / 插桩 **883**、`sys_log_mcu.c` 458 / **313**、
|
||||
`console_log.c` 265 / **189**、`cfg_file_store_mcu.c` 129 / **97**、`soe_store.c` 123 / **79**。
|
||||
按 1,251 估进度会把 99.8% 读成 70.4%。另:`for (;;)` 的收尾大括号会被算进可执行行
|
||||
(`soe_store.c:202` 就是这种非真语句),别当成漏测去"补"。
|
||||
|
||||
**画像期两处口径被落地推翻**(就地更正,不回改上文原话):① "⚠ **没有 `ch395f.h`**——NET 域那批必须新建它"
|
||||
已由 P1 建好(`test/host/net_listener/stub/ch395f.h` 转发到共享假芯片);② "FatFs 侧**不建假头**,沿用真实
|
||||
`ff.h` + 测试单元内假 diskio"——P2 走的是**另一条并存的路**:`test/host/common/fatfs_fake.{h,c}` 以真实
|
||||
`Lib/FatFs/ff.h` 为**声明源**、在 `fatfs_fake.c` 里**定义** `f_*`(签名一漂移就编译期报错),
|
||||
提供内存卷 + 可注入返回码/短写/`FR_DENIED`。两条路的共同点是**都不自造假头**。假件**刻意只提供三个生产文件
|
||||
实际调用的那 9 个 `f_*`**(`f_close f_lseek f_mkdir f_open f_read f_rename f_stat f_unlink f_write`),
|
||||
不给 `f_mount/f_mkfs/f_opendir/f_readdir/f_sync` —— 于是"自愈逻辑把名字/参数类错误码升级成卷操作"这类违规
|
||||
会在**链接期**就响,而不是靠评审眼神(`docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 陷阱二的机器化兜底)。
|
||||
|
||||
**编号**:`TC-HOST-*` 族 22 → **27**(本批补 `NETSOCK`/`SYSLOG`/`CFGFST`/`SOESTORE`/`CONLOG`,
|
||||
游标分别 `TC-NETSOCK-545`/`TC-SYSLOG-100`/`TC-CFGFST-053`/`TC-SOESTORE-024`/`TC-CONLOG-135`,
|
||||
属主登记处 `test/host/<目录>/*`,`cursor_owner_only`/`allow_orphans`/`subitem_ok` 照 B 层口径)。
|
||||
**先补族再引号**这条链本批又走了一遍,而且踩在收口前:`net_socket` 起初自取两字母码 `TC-NS-*`(465 处),
|
||||
那会整批落进通用 `TC` 族(属主 `docs/测试说明/*_Test_Guide.md`、**无** `allow_orphans`)
|
||||
= 458 条台账外新账直接拦提交;正名为 `TC-NETSOCK-` 是 bytes 级替换(465 处 / 0 残留 / 兄弟目录 `git diff` 全空 /
|
||||
复跑 420 全绿)。**写死成纪律**:队友起手就按 Lead 给的模块码写号,两字母不可检索的码(`NS`)不要取。
|
||||
本批**未**给新套件单挂 `*-unit-test` 钩子(与 P1 那 12 套同口径):聚合闸 `test-coverage` 的触发面
|
||||
`^(App/|Drivers/BSP/|test/host/|…)` 已自动发现新模块并按基线守回退。
|
||||
|
||||
**假件常量必须与真头逐字对齐(本批最值钱的机制结论)**:共享假芯片里 `CH395F_SOCKET_OPEN` 曾是 `0x01U`
|
||||
(真头 `Drivers/BSP/CH395F/ch395f.h`:220 = **`0x05U`**、`docs/芯片手册/CH395F.md`:527)、
|
||||
`CH395F_TCP_ESTABLISHED` 曾是 `0x05U`(真头:229 = **`0x04U`**、手册:539)。
|
||||
**三套存量单测一条都没红**——host 编译里生产码看到的也是假宏,宏对宏自洽 → 错在"值层",
|
||||
表现正是"把断言写在假件规则上而门禁毫无反应"。修法不是改断言而是**加机械闸**:
|
||||
`test/host/net_socket/run_unit_test.py` 在编译之前做真头/假件同名 `CH395F_*` 常量的文本核对
|
||||
(真头 106 项 / 假件 32 项 / 同名 22 项,只比纯字面量取值),并做过负例验证:把值改回 `0x01U` →
|
||||
输出 `[DRIFT] CH395F_SOCKET_OPEN 真=0x05 假=0x01` 且 rc=1 拒绝编译。跨头 `_Static_assert` 做不到,
|
||||
文本核对是目前唯一能常驻的拦法。**推论**:任何"照抄真头常量"的共享假件都该配这样一根闸。
|
||||
|
||||
**两条 FatFs 实证陷阱都做了正反两面**:`FA_OPEN_APPEND` 不含 `FA_WRITE`(缺写位 → `f_write` 恒
|
||||
`FR_DENIED` 的对照保真断言 3 处)与"`FR_OK` + 短写"判失败必须校验写回字节数;短名违规**分档**
|
||||
(体 > 8 / 扩展 > 3 / 非法字符 / 以点开头 → `FR_INVALID_NAME`;含 `:` → `FR_INVALID_DRIVE`),
|
||||
`FF_USE_LFN=0` 下 `.fschk` 走的正是"扩展域 5 字符超限"这条真实路径;自愈只动自己的文件
|
||||
(反复失败 20 轮后 `/log` 目录与邻居文件未被触碰)。
|
||||
|
||||
**本批照出的 11 条现状(缺陷原文保留在此,处置结论见紧随其后的「第 1 轮修复处置」)**:
|
||||
|
||||
| # | 位置 | 现象与影响 | 钉住它的用例 |
|
||||
|---|---|---|---|
|
||||
| 1 | `App/log/sys_log_mcu.c:509~537` | **疑似数据丢失(建议按 P1 定级)**:批量路径 `done = n` 只有 `rc==-3`/`rc==-1` 两个分支会改它,**`rc==-2`(卷忙)没有分支** → 紧随其后的 `for (i = done; i < n; …)` 回投循环一次都不执行 → 最多 8 条日志既不落盘也不回投、不计故障、不打日志。同段 `:531` 注释与 `docs/子系统设计/运行与自检记录设计说明.md` §4.4 都写"条目不丢(旧实现 -2 丢那一条)"→ **现状比旧实现更糟**(旧丢 1 条、现一次丢 8 条)。对照:**逐条慢路径遇 `-2` 会正确回投尾部**,同一错误码在两条路径行为相反。触发面常见:卷锁 v1.2 之后 ftpTask 传文件期间 defaultTask 每 2 s 排空一次 | TC-SYSLOG-035 / 035b / 035c |
|
||||
| 2 | `App/log/sys_log_mcu.c:527~529` | `rc==-1` 打 `DBG_ERROR("drop %lu entries …")`,但该分支 `done=0` → 整批其实**全部回投**、一条没丢 → 文案与行为相反,且持续写故障时每轮 drain 都重投重试一遍 | TC-SYSLOG-036 |
|
||||
| 3 | `App/log/sys_log_mcu.c:616` vs `627~628` | 守卫写成 `desc_len <= SYSLOG_DESC_MAX` 就 `-1`,而 `dl` 恒 ≤ 28 → 截断分支**永不可达**。无害(宁可拒绝不越界),但"传小缓冲能拿到截断内容"的直觉是错的 | TC-SYSLOG-098 / 057 |
|
||||
| 4 | `Drivers/BSP/NET/net_socket.c:2825` / `:2869` / `:3080` | **同一文件两套 IP 字节序约定,`inet_addr` 与 `ntoa`/`getsockname` 不互逆**:`net_inet_addr` 小端组装后又 `NET_HTONL` → `0xC0A80164`;`net_inet_ntoa` 交换后高段在前 → 只能正确还原 `getsockname` 那族(`0x6401A8C0`);`net_connect_locked:1571` 用 `>>24` 取首段 → 只配 `inet_addr` 那族。**为什么长期不被看见**:`ntoa` 自己 `:2865~2867` 那段 E13 修复注释里的例子「192.168.1.100 → 0xC0A80164 → "192.168.1.100"」取的是**另一族**的值(交换宏下实算 `"100.1.168.192"`)。前提已核清:`Drivers/BSP/NET/net_types.h`:56 是 `#ifdef __BIG_ENDIAN` 才恒等,STM32 与 x86/MSVC 都走交换分支 → **不存在"主机语义≠目标语义"**。当前无调用者跨族(`lftpd.c`:864 走 `getpeername→ntoa` 同族恰好正确),属埋在库 API 面上的地雷;统一必须跨 `inet_addr`/`ntoa`/`getsockname`/`connect` 解析/`lftpd.c`:548 一起过 | TC-NETSOCK-100b / 100c / 100d、107f、430h(钉的是**具体数值**,统一时会五条一起红并互相指认,不会被误读成单点回归) |
|
||||
| 5 | `Drivers/BSP/NET/net_socket.c:2408` / `:2537` | **UDP 两个公开 API 绕过 netTask 串行化**:`net_sendto`/`net_recvfrom` 直接 `net_poll()`(`:2482`/`:2501`/`:2563`)+ `ch395f_write_send_buf`(`:2487`)/`ch395f_read_recv_buf`(`:2575`/`:2604`),**全程不经 `netMsgQueue`** → 与 AGENTS「所有 `net_*` 经 netTask 串行执行」及 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-14 正面冲突。全仓无生产调用者(只有 `test/board` 与模拟器)所以未爆。建议:改走消息队列,或在 `net_socket.h` 显式标注"未串行化,仅限 netTask" | TC-NETSOCK-440 |
|
||||
| 6 | `App/log/soe_store.c` 函数头 ④ | 注释写"打开/写入结构性错误 → `unlink` 重试一次(同 run.log)",实现里 `f_open` 失败**直接 return -1、不重试**(只有 `f_write` 失败才重试);且 `run.log` 侧 `s_append_rec` 的重试码集合还含 `FR_INVALID_NAME`/`FR_NO_PATH` → 两套自愈集合不一致。属"注释夸大实现"还是"实现少一半"待裁 | TC-SOESTORE-022 |
|
||||
| 7 | `App/cfg/cfg_file_store_mcu.c:50` | 函数头写"未挂载时 `f_open` 返回 `FR_NO_SYSTEM`"——**`FRESULT` 里没有这个码**(`Lib/FatFs/ff.h` 共 20 个码,未挂载实际是 `FR_NOT_READY`/`FR_NO_FILESYSTEM`)。只在注释里不参与编译,危害是后人照它写判据会写出永不成立的分支 | —(注释面) |
|
||||
| 8 | `App/log/console_log.c:317` vs `:285` | 冷却起点用 `s_con_cool_until = (uint32_t)HAL_GetTick()`,而判据拿 `!= 0U` 当哨兵 → 故障恰好落在 `HAL_GetTick()==0` 那一拍(开机头 1 ms,或 49.7 天回绕到 0)时**冷却等于没记**,下一轮 drain 立刻再耗一次 100 ms 块超时。撞的正是控制台域那条已登记的"背靠背块超时拖长主循环 → 喂狗节奏饥饿"。建议:另用一个"在冷却"标志,别拿 tick 值当哨兵 | TC-CONLOG-081 / 082(按现状钉) |
|
||||
| 9 | `App/log/console_log.c:394~412` | `con_append` 的**空位判据在 `taskENTER_CRITICAL` 之外**(check-then-act)。生产者按 `docs/子系统设计/控制台单写者日志设计说明.md` §3.1 是"全部任务 + 内核前主线程 + ISR",而同文档 §4.1 的 head/tail 行写作"单生产者/单写者"——**两处口径互斥,后者需要更正**。多生产者可在环 >95% 满时同时过判据 → `head` 各自推进后合计越过容量 → `con_used()` 回绕变小 → 写者少发一段,被覆盖的行**既不打 `[ERR]` 也不进 `dropped`**(静默损坏;索引恒取模,不会越界)。**这条没有用例、只报推断**:单线程宿主构造不出真抢占点(把注入点挂在 `taskENTER_CRITICAL` 上已在区内,不算抢占点) | —(无用例,仅登记) |
|
||||
| 10 | `App/log/console_log.c:181~183` + `:306~331` | `huart1.Instance == NULL`(HAL 早期 / 断言-故障上报路径的专用短路,返回 `HAL_ERROR`)在 drain 里被当成**僵尸通道**走 `con_uart_reinit()` → `MX_USART1_UART_Init()`(含 MspInit 的 RCC/GPIO 重配)。当前接线顺序(`MX_USART1_UART_Init` 早于 `dbg_console_writer_attach`)挡住了,但"未初始化"与"通道卡死"共用一条自愈支路,attach 时机一旦被提前就会在 HAL 未就绪时重配外设 | TC-CONLOG-103(钉现状) |
|
||||
|
||||
### 第 1 轮修复处置(2026-10-24,同日;生产码改动 + 用例翻正向)
|
||||
|
||||
用户裁定「全部修」。三名队友按域并行改生产码(写范围互斥、零越界),Lead 统一 `@build` 与跑闸。
|
||||
**`@build` 0 错误 0 警告**(本工作树首次全量构建):`Code=213720 RO-data=18860 RW-data=1204 ZI-data=131500`。
|
||||
全量闸:**33/33 套件全绿**、`line=5605/6275(89.3%)`、`func=396/429(92.3%)`、未触及仍 29 个 `.c`;
|
||||
与修复前逐文件比对**下降文件数 = 0**(改过生产码的三个文件覆盖随代码同步增长:
|
||||
`console_log.c` 188→199、`sys_log_mcu.c` 310→324、`net_socket.c` 881→887)。
|
||||
|
||||
| 原表条目 | 处置 | 钉住它的正向断言 |
|
||||
|---|---|---|
|
||||
| 1 卷忙整批丢日志 | **已修**:`syslog_drain` 批量路径补 `rc == -2` 分支把 `done` 置 0 → 整批回投队列;三条纪律逐条有断言(不计故障 / 不打日志 / 保序),慢路径(参照物)一字未动 | TC-SYSLOG-035、035a、035c、035d、104 |
|
||||
| 2 文案与行为相反 | **已修**:文案改 `write fail, requeue %lu entries`,并加「正常→故障」节流(`s_requeue_reported[]`,落盘成功一轮复位);真丢仍由 `hold-over drop` 兜底 | TC-SYSLOG-039、039b |
|
||||
| 3 截断死分支 | **已修**:删掉永不可达的截断支,容量契约写死在 `sys_log.h`(`ts_len > SYSLOG_TS_LEN`、`desc_len > SYSLOG_DESC_MAX`,不足**一律 -1 且不写调用方一个字节**);API 签名与返回码集合不变 | TC-SYSLOG-057、057b(0x7A 哨兵逐字节回比)、098 |
|
||||
| 6 soe_store 注释夸大实现 | **已修(改注释,实现不动)**:打开失败**任何错误码都不 unlink** 是有意的——soe.log 是事件序列,「打开失败就删文件」等于销毁事件证据;两侧资料块各写一份理由并**互指对方文件与测试号**,要求同步修改 | TC-SOESTORE-022 |
|
||||
| 7 FR_NO_SYSTEM 不存在 | **已修**:改成实际会返回的 `FR_NOT_READY(3)` / `FR_NO_FILESYSTEM(13)`,并写明「本层对任何非 FR_OK 一律转 -1,别去枚举错误码集合」,唯一例外指向自愈触发集 | 注释面(无行为改动) |
|
||||
| 4 IP 两套字节序 | **已修(统一到 POSIX 网络序)**:改 `net_inet_addr`(`&s_addr[0] == 第一段`,小端上 192.168.1.100 = `0x6401A8C0`)+ `net_connect_locked`/`net_sendto` 的 IP 数组改按内存字节顺位拆装(**进与出同时换族 → 芯片侧数组净效果不变**,由 550h 直接钉住)+ 模拟器 `net_socket_sim.c` 逐字同语义;`net_inet_ntoa` **函数体一字未动**,只把它注释里那个取了废弃族值的例子改对(就是那条隐蔽原因)。lftpd 两处经算术核对**确认无需改**(PASV 227 的双重反转仍成立),只加「勿顺手改对」的注释 | TC-NETSOCK-100 组(新增往返不变量 100g/100h、边界 100i/100j、截断 100k)、550h |
|
||||
| 5 UDP 绕过 netTask | **危险面已结构性关掉,串行化改造仍未做**(按 Lead 定案,未自升级为改队列):两个 UDP 入口加**执行任务上下文守卫**,非 netTask 调用回新码 `NET_ERR_NOT_IN_NETTASK = -12`(住 `net_types.h`,与 BUSY/TIMEDOUT 的本质差别 = **重试永远无意义**,不参与任何可重试判定);守卫位于参数校验之后、任何芯片事务之前;模拟器**刻意不加**并标注为有意的不对称(单线程 + Winsock 自身线程安全、没有那根 SPI)。**附带关掉第二条更隐蔽的危险**:非 netTask 在 recvfrom 里自旋期间,netTask 可经 close 清零控制块并把同一 sockfd 分给新会话 → 旧自旋者消费新会话数据报,这条路现在断了 | TC-NETSOCK-560 组(含「零芯片事务」与「守卫不吞正常错误路径」) |
|
||||
| 8 冷却哨兵与 tick 撞值 | **已修**:新增独立 `s_con_cooling` 判定「是否在冷却」,`s_con_cool_until` 只存截止时刻(无符号差跨回绕的写法保留) | TC-CONLOG-080~084、107、108 |
|
||||
| 9 环空位判据在临界区外 | **已修**:「复核空位 + 整行拷贝 + 推进 head」收进同一个临界区(最长 192 B、168 MHz 下亚微秒;`vTaskDelay` 与 DWT/tick 判据一律留区外,区内绝不睡眠)→ 多生产者结构性安全,写者仍唯一。**原「这条主机测不出来」的结论本轮作废**:桩里记临界区嵌套深度并把第二生产者精确塞进第一个生产者的临界区,再把环形缓冲的宏名成函数调用使每一次碰环都可断言 | TC-CONLOG-160~167(**167 是正控**,证明 160~166 不是空转) |
|
||||
| 10 未初始化被误当僵尸通道 | **已修**:drain 的失败处理先分诊——`Instance == NULL` 视为通道不可用(不 reinit、不计 tx_fault、不进冷却、只记 dropped);只有确认超时/僵尸通道才走软复位自愈 + 冷却 | TC-CONLOG-103~106 |
|
||||
| (撤销)非阻塞 recv 看不见 EOF | **误报,未改任何码**:`net_recv_locked:1885` 的非阻塞分支里 `CLOSED || session_dead` 已判 EOF(0),只有会话存活才 WOULDBLOCK。顺带核实 `net_recvfrom` **不存在** EOF 误判面(UDP 无连接,无数据只有 WOULDBLOCK 或继续自旋两条出口) | —(记一次「描述得很合理」的误报:修之前先读码,误报比漏报更贵) |
|
||||
|
||||
**修复过程中顺带照出的两条(原表没有,一并修掉)**:
|
||||
|
||||
- `syslog_post` 的 `queue full, drop` / `preboot full, drop` **无节流**——1 号修好之后「整批回投 → 队列常驻满」成为必然场景,不修等于**用一个新刷屏换掉一个静默丢**。已按同一「正常→故障」纪律加两个独立抑制位(**丢弃语义一字未改**,满则丢照旧)。TC-SYSLOG-041b/041d/041e/041f、105、106、107、107b、107c。
|
||||
- 自愈触发集里混着 `FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT`——**比原报告的 6 号严重**:这类码下 unlink 自己的文件必然同样失败(同名同因),后果是**每轮排空删一次自己的历史日志且永远修不好**,正是 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 10 条严禁的归集。已新增代码侧唯一入口 `s_is_selfheal_code()`:触发集收敛为 `{FR_DISK_ERR, FR_INT_ERR, FR_NO_FILESYSTEM, FR_DENIED}`,逐码依据(含 `Lib/FatFs/ff.c` 行号)与显式排除清单写在资料块里;`soe_store.c` 侧同源但**取更保守子集**。TC-SYSLOG-101/101b/102/103/103b、TC-SOESTORE-022b。
|
||||
|
||||
**本轮刻意没做(后续项,含理由)**:① UDP 改走 `netMsgQueue`(需先设计 per-task 结果槽:`net_msg_t` 按值拷入队列、错误码只搭任务通知值回传,AGENTS 明文禁止给 `net_msg_t` 加回传字段,且必须配真机回归);
|
||||
② `simulator/sim/sim_syslog.c:171` 那条与 3 号**同源的不可达截断支**(本轮编译验证不到它——模拟器走 CMake+VS,不在 `@build` 覆盖内,不交没编译过的改动);③ `syslog_post` 的**丢弃计数**(属新增对外可见信息,本轮没有实测频率支撑阈值);
|
||||
④ `dbg_console_tx_timeout()` 的**读端接线**(现无生产调用者;候选位置 `task_watchdog.c` 的 `STALL snap=… req=… h=… mg=… cf=… seg=…` 是**被解析的证据行**,加字段要连解析端与台架判据一起改,本批不碰台架);⑤ 给 104 加来源地址日志(现网 `iec104_serv.c` 根本不打印来源 IP、无 `getpeername/ntoa`,**没有输出点可看**)。
|
||||
|
||||
**必须上台架复验(本轮按指示未碰台架;主机测覆盖不到的部分逐条列出)**:
|
||||
|
||||
1. TCP 出向连接(唯一真正穿过被改的 `des_ip` 拆装路径):板级 `net_test_task` Phase 7(TC-NET-701/703/704,PC 侧起 server),判据 connect 能建立、PC 侧看到的源端口正确;Phase 2 的 `test_parse_ip` 期望 `[192,168,1,2]` 不变。
|
||||
2. FTP 被动模式取一个文件并抓 227 响应:括号内前 4 元组 == 设备实际 IP、后 2 元组解出的端口 == 实际拨的端口(验 lftpd 那处双重反转没被改坏)。
|
||||
3. FTP 对端日志 `connection received from <ip>:<port>`:ip 必须是 PC 地址且正序(验 `getpeername` → `ntoa` 同族)。
|
||||
4. UI 网络配置页改 IP 保存 → 回读一致、按新 IP 通、104/FTP 仍可连(代次重建链路;UI 显示取自配置字符串、不经 `s_addr`,列进来是兜住「配置字符串→芯片下发→回读」这条主机测不到的路)。
|
||||
5. FTP 传输期间持续投日志:传输结束后 `run.log`/`selftest.log` 条数 == 投递条数、顺序不乱、全程无 `write fail, requeue`;同窗口 `queue full, drop` 只应出现**一行**。
|
||||
6. 制造持续写故障(坏块/写保护):串口只在**转入故障那一刻**出一条,恢复后再故障要再出一条。
|
||||
7. 控制台灌高频日志把环逼到将满:不再出现半行/串行,且 `[ERR] CONSOLE dropped N lines` 与实际缺失数一致(**单位口径已变**:N 是「行或块」的计数,不恒等于行数);拔/短接 TX 造一次僵尸通道,核对 `dropped` / `tx_fault` / `tx_timeout` 各 +1。
|
||||
|
||||
**第 1 轮新踩到的坑(都有复现路径)**:
|
||||
|
||||
1. **`edit` 替换块会吞掉相邻代码**:在 `net_sendto` 插守卫时 `new_string` 漏写了紧随其后的 `buf == NULL → INVAL` 检查,那块被整段删掉,现场只表现为「sendto 空指针不报 INVAL」——**是上一节刚落的存量断言 440d 当场抓到的**。教训:`old_string` 覆盖多个语义块时 `new_string` 必须逐块回读,别信「我只加了一段」。
|
||||
2. **`read` 全文回写会截断大文件**:`console_log_test.c` 到约 54 KB 后用「read 全文 → 替换 → write」连续两次把尾部(含 `main()`)吃掉,报 `C1075` 才发现。**超过约 40 KB 的文件只能定点改**(`edit` 或直接 fs 读写),不要整文件回写。
|
||||
3. **块注释里替换把结尾的注释闭合符提前引出**:给 `console_log.h` 的 API 清单加说明时,替换文本自带了注释闭合符 → 注释中途关闭、后面中文裸露成代码,报 `C3872` / `C4138` 且**错误落在远处中文字符上**,第一眼完全看不出根因。改注释类替换宁可让 old/new 都不含行尾的注释闭合符。
|
||||
4. **假时钟不要只挂在单一计数点**:假内核的时基挂在 `net_poll` 必读的 GPIO 上,而 `net_init` 未跑时 `net_poll` 直接短路 → tick 永不前进 → 停滞超时永不命中,**连 40 万次跑飞保险也挂在同一计数点、同样失效**(进程真挂死)。凡「可能短路返回」的路径都要能推进时间或独立触发保险;那条用例最后改用 `recvfrom(DONTWAIT)` 测同一分支。
|
||||
5. **被 kill 的测试进程会占住 `tempfile\u003csuite\u003e.exe`**:下一个套件表现为 `LNK1169` / `LNK1104 无法打开文件`,看着像构建配置坏了,其实先 `taskkill` 那个 exe——这是「逐套 runner 固定共享路径」的又一处连带代价(与上面第 12 条同源)。
|
||||
6. **cppcheck 会把「仅置位时才清」的抑制位写法判成 `duplicateConditionalAssign`(style,拦提交闸)**:为省一次 volatile 写而写成「判非零再清零」会被判等价赋值。处置:**回退成无条件写 + 注释写明别再「优化」回去**,不要为一条 style 告警加 inline 抑制。
|
||||
7. **新增断言要用「逐条退回生产码」验一次牙**(变异自证):把四条修复逐条退回旧实现,失败集合单调增长 6 → 11 → 18 → 22 条、每步都是上一步的超集,改回即 127/127 全绿——**没有这一步,一条空转断言就要被交上去**。
|
||||
8. **任务书里给的临时缺陷标签会被编号门禁当自造编号拦**——本批 Lead 在任务书里用的两个「单字母+连字符+序号」临时标签被原样抄进生产注释,一次冒出 8 类台账外错误(连本文档举这个反例时把字面量写出来,都会被同一根闸再拦一次,故此处只能描述形态、不能给样例)。纪律:**Lead 给的内部标签不得写进代码**,注释只写行为(如「两套 IP 字节序统一为 POSIX 网络序」),要指用例就指已登记的 `TC-*` 号。
|
||||
|
||||
### 第 2 轮:测试字面量统一纯 ASCII(2026-10-24,单独一次提交)
|
||||
|
||||
用户裁定「统一」。口径来自 AGENTS 铁律「**字符串字面量必须纯 ASCII**」(无 BOM 文件里奇数个汉字会吞掉结束引号,
|
||||
偶数才侥幸通过)——**中文只写注释**。四名工人按目录分四组并行、写范围互斥,25 套 / 30 个 `.c` 全部改完。
|
||||
|
||||
**两套口径必须分清(本轮最大的方法论收获)**:
|
||||
|
||||
| 口径 | 做法 | 结果 | 用途 |
|
||||
|---|---|---|---|
|
||||
| 粗口径(普查) | 按行切引号,跳过以 `*` / `//` / `/*` 开头的行 | 34 文件 / **1,552** 行 | 判断"谁还没动" |
|
||||
| 严格口径(交付判据) | 状态机把块/行注释替换成**等宽空格**(保住行列)后再取字符串字面量判 `ord>127` | 30 文件 / **1,535** 行 | 交付与验收 |
|
||||
|
||||
差值 = **4 个文件的命中全在注释里**(`net_socket/ns_peers.c`、`net_socket/stub/ns_rtos.h`、
|
||||
`common/ch395f_fake.h`、`common/fatfs_fake.c`:行尾或块注释续行里的中文 + ASCII 引号)——**一处都不该改**,
|
||||
改了等于把中文从注释里搬走、正好违背口径。教训写死成纪律:**粗口径绝不能当交付判据**,否则要么白改注释、
|
||||
要么留下"这行为什么没改"的悬案(本批两名工人各自独立撞见并如实上报,没有一人硬凑数字)。
|
||||
|
||||
**Lead 独立终检(不采信自报)五条硬判据 → 30 文件漂移 0**:
|
||||
|
||||
1. 每个文件的 **TC 号集合**与 HEAD 逐字相同(防"改标签顺手改号"与"批量替换串位");
|
||||
2. **断言宏调用点数**(`CHECK`/`ASSERT` 等)与 HEAD 相同;
|
||||
3. **行数与 HEAD 完全相等**——这条专抓 `read` 全文回写把尾部(含 `main()`)吃掉的截断,第 1 轮真发生过两次;
|
||||
4. **剥注释后代码区非 ASCII 字符 = 0**(含字符字面量),残留必须归零;
|
||||
5. 字节卫生:BOM 与 HEAD 一致、无 CR、无 U+FFFD、末尾恰好一个换行。
|
||||
另用一条更硬的机械证明替代"眼看 diff":**逐行把两侧字面量整体替换成空串后,工作版与 HEAD 必须逐行相等**
|
||||
(`only string literals changed`)——任何落到引号外的差异当场失败。
|
||||
|
||||
**"没手滑动逻辑"的证据 = 覆盖率复量逐字不变**:各套 `--module` 与基线一致(`net_socket` 887/889·53/53、
|
||||
`console_log` 199/200·16/16、`sys_log_mcu` 324/326·13/13、`cfg_file_store_mcu` 97/97·10/10、
|
||||
`net_listener` 125/126、`lftpd` 304/473、`iec104_link` 367/405 …);每套 runner 的断言数改前后相同
|
||||
(如 `iec104` 97/97、`sys_clock` 100/100、`adc_input` 103/103、`net_cfg` 45/45)。**基线本轮不需要重登**(生产码零改动)。
|
||||
|
||||
⚠ **`@build` 对本轮没有覆盖力**:`test/host/**` 不进 Keil 编译面也不进模拟器 CMake,字面量改动只能靠
|
||||
16 个 `*-unit-test` 钩子 + `test-cppcheck` + `test-coverage` 这三根闸验证——**别把"编译过了"当测试面改动的证据**。
|
||||
|
||||
**本轮新手法(值得抄进任何"批量改标签"作业)**:
|
||||
|
||||
1. **TC 号逐字相等守卫**:从文件字节取原文(不手敲),提取原文与译文各自的 TC 号要求**逐字相等**,不等即拒绝写入。
|
||||
它拦下的是「测试标签与被测事实错位」——断言照样绿,却永久指向错误的号位(本轮真的拦下一次 092 被写成 093)。
|
||||
2. **批量替换的键必须是内容,不能是顺序/位置**:按位置对齐 HEAD 与工作版会在字面量数量不等时**串位污染**
|
||||
(本轮真实发生过:`net_inet_addr("255.255.255.255")` 被换成一条断言名)。正确做法是 HEAD 字节 + 按字面量文本精确匹配,
|
||||
**凡未命中映射且含非 ASCII 的字面量直接报错终止**,其余字节一字节不动。
|
||||
3. **自查至少要有一维能发现「自己多改了东西」**:残留计数 + 改动形状(不含引号的改动行 = 0)+ 行数与 HEAD 对比,三样缺一就漏。
|
||||
本轮暴露事故的正是"残留本该 0 却报 11"——只看 rc 与断言条数会一路放行到提交。
|
||||
4. **新串必须「不引入任何编号形态」**(本批真实翻车点):一句 `CHECK` 的英文标签里被写回了一条**已登记族之外**的「单字母+连字符+序号」编号,
|
||||
而 HEAD 那行早就在上一轮整改中把它换成了平实描述——**翻译等于把债重新引进来**。批量翻译的守卫除「原文与译文 TC 号逐字相等」之外,
|
||||
还必须断言「新串不得出现任何 已登记前缀族之外 的编号形态」(字母+连字符+数字一律拦)。
|
||||
⚠ 同时暴露一条验收口径的教训:Lead 的自动验收器只比对了 `TC-*` 形的 id 集合,**没覆盖其它已登记族**(L 系 / E 系 / N 系 / S 系 / TRAP 系编号),
|
||||
这一条是**编号门禁闸**拦下来的,不是我的验收器——「结构不变式」只覆盖自己想到的维度时,绝不能替代真闸。
|
||||
⚠ 写这条手法时,Lead 又把该编号的字面量当样例写进本文档、被同一根闸再拦一次(第 1 轮举临时标签反例时已中过一次)。**结论:门禁不看语境,文档举例只能描述形态、不能贴字面量。**
|
||||
5. **替换说明符不变量(比「禁止裸 `%`」更强)**:新旧字面量的**转换说明符序列必须逐位相同**(个数/顺序/宽度一致)。
|
||||
「禁止新增 `%`」只挡一半——丢了 `%` 是 UB(实参没被消费)、多了也是 UB(多消费栈垃圾),而逼人把 `%` 写成 `%%`
|
||||
等于把说明符偷偷变成字面文本,编译器最多给个 C4473 就放过。另注:断言名是当**实参**打印的,本身不会被二次解释,
|
||||
真正要防的是「标签被当格式串」的实现——批量改标签前顺手扫一眼宏定义。
|
||||
6. **生成脚本的脚本,转义层数会翻倍**:经模板字符串生成 Python 源码时正则里的 `\\s` 被吃成字面量反斜杠+s,正则永不匹配,
|
||||
于是 418 条断言名的 TC 前缀**全部静默丢失**。要么显式 `chr(92)`,要么像本 Lead 会话一样改用「JS 组装 JSON 规格 +
|
||||
Python 按规格带断言应用」,把转义面收敛到一处。
|
||||
|
||||
**协作口径(并行作业的板子纪律)**:
|
||||
|
||||
- **先改板再派工**:拆别人在做的范围时,必须先 `team_task_update` 收缩原任务写范围,再建新任务并派工。
|
||||
本轮 Lead(我)建 G3a 时板上已回"write scopes overlap with task-9"却照样派工,属于用队友的规避动作补自己的疏漏——
|
||||
工人-side 的补救是查 `mtime` 确认文件未被动过再让出,比外部检查及时,但这不该依赖个人自觉。
|
||||
- **多名 agent 共用 `%TEMP%`,通用文件名本身就是竞态**:本轮一名工人写 `r2_verify.py` 撞上队友同名脚本,
|
||||
是「文件未读不许改」的守卫阻止了覆盖;若当时改用直接 fs 写入就会**静默毁掉别人的工件**。纪律:临时文件一律带组前缀。
|
||||
- 任务书里的条数要写清是**字面量条数**还是**断言总数**(本轮 `adc_input` 89 条字面量 vs 103 条断言),
|
||||
否则工人会拿两套数互相印证出假结论。
|
||||
- **三个计数单位别混用**(本批全踩过):① **行** = 含非 ASCII 字面量的行数(Lead 终检口径,本轮 1,535);
|
||||
② **条** = 字面量出现次数(HEAD 全量 1,538;个别行含 2 条);③ **numstat 的 ± 行数** ≠ 替换条数——
|
||||
git 会在相邻改动块之间重排空行(实测 `adc_input` 出现 90 行对 / 89 条替换,多出的那对两侧皆空、净零)。
|
||||
同一批工作因此出现过 1,552 / 1,538 / 1,535 三个「都对」的数字——**报数前先声明用的是哪一个**。
|
||||
- **分母只算 `.c`/`.h`**:`test/host` 下的 runner 与桩脚本是 `.py`,HEAD 上有 44 条中文 docstring,
|
||||
属另一套口径(不经 ARMCC 代码页,翻它没有工程收益);混进分母会直接虚高普查数。
|
||||
- 取改前原始字节一律 `git show HEAD:<路径>`:**不要**用 `git checkout` 取版本(会丢弃未提交工作,是铁律),
|
||||
也别拿工作区文件当「改前基线」——队友可能正在改它。
|
||||
|
||||
|
||||
**后续项(本轮不做)**:① 把「剥注释词法扫描」收进 `scripts/` 当门禁(需用户点头,并先定 `test/` 是否纳入
|
||||
`coding-standard` 的口径——该钩子现在 `exclude` 了 `test/`);② `test/host/iec104/` 里「`TC-` 直接接三位序号、不带模块码」这类**简写形态**
|
||||
归族(属编号登记与可追溯性改动,牵 `id_registry.json`,不能顺手做);③ `runner .py` 的中文 docstring **不动**
|
||||
(口径是 `.c/.h`,且 Python 源不经 ARMCC 代码页,翻它无工程收益)。
|
||||
|
||||
**给下一批的可复用手法与坑**(完整清单写在各套件 `.c` 文件头,这里只留跨模块通用的):
|
||||
|
||||
|
||||
|
||||
1. **测试 TU 里 `#define fputc <改名符号>` 再纳入生产源**:`App/log/console_log.c`:492 的
|
||||
`int fputc(int, FILE*)` 与宿主 CRT 撞符号(`libucrt.lib(fputc.obj) error LNK2005` + `LNK1169`)。
|
||||
改名纳入后函数体与行覆盖一字不动;顺带实测确认**宿主 printf 不走用户 `fputc`**(测试输出不会被吞进环形缓冲)。
|
||||
2. **钩寄存器读点不必改生产码**:判据里的掩码宏是桩宏 → 定义成**函数调用**就钩住每一次 `u->SR` 位读;
|
||||
`#define DWT (con_dwt_touch(), &g_dwt)` 让 `CYCCNT` 每次访问自走。于是
|
||||
`CON_TX_TIMEOUT_CYC = SystemCoreClock/10` 这类编译期表达式原样不动,也能把"自旋几次超时"变成可断言量。
|
||||
⚠ 结构体成员访问本身(`u->SR`/`u->DR`)钩不到,别绕进去。
|
||||
3. **跑飞保险要挂在被测模块的天然计数点上**:`net_send_locked`/`net_sendto`/`net_recvfrom` 的等待环里
|
||||
**没有 `osDelay`**,只给 `osDelay` 加保险挡不住这些死循环(实测第一次跑就真卡死)。挂在 `net_poll()`
|
||||
每轮必读的 `HAL_GPIO_ReadPin`(INT#)上,附带让 `NET_SEND_SPIN_MS` 这类有界自旋预算**第一次可量测**。
|
||||
同一纪律的另一半:假内核对 `xTaskNotifyWait(portMAX_DELAY)` 泵 50 轮仍无通知就 `exit(2)` 打 `FATAL` ——
|
||||
它当场抓出一处把 `net_close` 返回语义判错的用例,而不是让门禁卡死。
|
||||
4. **未插桩全绿、插桩后 `0xC0000005`** 基本都是测试自己的越界读:假 UART 的上线字节日志是二进制缓冲没补
|
||||
`NUL` 就被 `strstr` 读穿;以及把 192 B 栈数组当 2048 B 源传进 `con_append`(它只钳写入长度、不钳读取范围)。
|
||||
守则:凡是把缓冲当 C 字符串读的地方先补 `NUL` 或改 `memcmp` 定长比。
|
||||
5. **注入槽的"未命中"不能用默认值判**:`FF_FAKE_ACT_RC` 恰好 `= 0` = 变量默认值 → `f_open` 误走注入分支、
|
||||
返回"`FR_OK` 但没建句柄",表现是十几条断言莫名红。要显式 `hit` 标志 + `act` 双条件。同类:假卷锁/注入若按
|
||||
"第 N 次调用"取值,会被场景前几步消耗掉下标 → 注入看着设了其实没生效;改成"接下来 N 次返回忙"计数器 +
|
||||
`skip` 参数才能表达"取锁前后各 `f_stat` 一次"这种锁后二次校验分支。
|
||||
6. **假卷每文件簇池必须大于生产单文件容量上限**(`run.log`/`soe.log` = 1024×64 = 65,536 B)。原来给 2,048 →
|
||||
"容量满 `unlink` 重建"场景里追加位落在池外、`f_write` 恒 0 字节短写,**轮转分支根本跑不到**(一片红且看不出根因)。
|
||||
7. **共享调用日志会被长轮询挤掉**:假芯片 `g_chip.log` 只有 96 格,一次 `sock_reset` 的几十轮把更早的顺序信息
|
||||
挤出可查范围 → 顺序断言假绿/假红。做顺序断言前先 `clear_log`、用 `log_find` 查。
|
||||
8. **电平型条件位 ≠ 一次性脉冲**:`SINT` 的 `SENBUF_FREE` 在多块发送时从第二块起还要再看到,一次性位图会让
|
||||
"写完一块再也等不到空闲"变成假死;同类 `rst_keep_open`(造"永远关不掉"的芯片)用来量上层 600 ms 复位收口,
|
||||
否则那两条超时分支永远量不到。**新旋钮必须默认关**,存量套件行为才逐字不变。
|
||||
9. **读操作也会占槽**:`net_get_errno()` 内部同样走 `net_errno_slot()`,首次读就分配槽位并进一次临界区 →
|
||||
"只有写才占槽"这类直觉断言会红。这是真实语义,不是缺陷。
|
||||
10. **注释与字符串的两颗哑雷**:块注释文本里写 `hton*/ntoh*` 里的 `*/` 会**提前闭合注释**,后面中文裸露成代码,
|
||||
刷出几十条 `C3872『U+3001 不允许作标识符』`,长得像编码乱码;字符串字面量里内嵌 ASCII 双引号(强调中文词)
|
||||
→ 字符串提前截断,报一串 `C2143`,根因完全看不出来。要引用一律全角「」。另:用 heredoc /
|
||||
`python - <<PY` 写 C 文件会吃掉 `\n`、`\xef` 这类转义(落盘成真换行)→ 一律直写文件再编译验证。
|
||||
11. **共享假件扩展的纪律**(本批验证有效):**只加不改**,且**不往既有状态结构体里加字段** —— 另开
|
||||
`g_fx`/`g_fxchip` 一份扩展世界,新增 `ch395f_fake_ext_reset()` 由既有 `ch395f_fake_reset()` 末尾调用,
|
||||
既有三套的复位面与断言面一字不动、新状态也不会场景间残留。改完必跑存量回归(本批 net_listener 34/34、
|
||||
net_events 34/34、net_pending 43/43、net_select 22/22;nand_port 149/149、lftpd_io 33/33、vol_lock 33/33、
|
||||
soe 32/32、cfg_file 151/151)。
|
||||
12. **覆盖率中间产物根目录是固定共享的** `%TEMP%/dtu_test_cov/<模块名>`:并行期每人只跑 `--module <自己模块>`
|
||||
(子目录互不冲突);**绝不加 `--clean`**(它 `rmtree` 整棵共享根,会把队友正在编译的目录连根删掉,
|
||||
表现为"莫名其妙的编译失败、重跑就好");**不带 `--module` 的全量闸只在收口时由 Lead 单跑**。
|
||||
另注:逐套 `run_unit_test.py` 写的是固定 `tempfile/<suite>_obj` 与 `<suite>.exe`,所以**同一套件同一时刻
|
||||
只能一人跑**(不同套件互不干扰)。
|
||||
|
||||
**两处口径问答(免得下次重新推导)**:① 子集数不能和并集基线直接比 —— `soe_store` 单套对
|
||||
`App/log/soe_rec.c` 只到 78/87(89.7%),而台账是全部 33 套的**并集** 81/87(93.1%);本批
|
||||
`--update-baseline` 前后逐文件比对**下降文件数 = 0**,所以那不是回退。判据只有一条:全量闸的并集 ≥ 基线。
|
||||
② `console_log.c` 与 `soe_store.c` 那 1~2 行未覆盖都不是漏测:`console_log.c:352` 是 `snprintf` 溢出后的
|
||||
钳位(`note[64]`,最长 42 B → 数学上到不了)、`soe_store.c:202` 是 `for (;;)` 收尾大括号。
|
||||
|
||||
### 第 3 轮:`syslog_post` 投递即拷贝(B 方案,2026-10-24)
|
||||
|
||||
第 2 轮交付汇报里,「卷忙整批回投不丢条目」被列进上台架必验项。本轮把它跑成,并在跑的过程中改掉一个真隐患。
|
||||
|
||||
- **正/负双向实证(台架,全程非破坏性)**:临时钩子从 testTask 以 2.5 s 间隔投 12 条带序号记录(远低于 8 槽队列容量,避开投递侧「满则丢」),窗口中段起一次 8 MB FTP STOR 抢卷,回读 `LOG/SELFTEST.LOG` 按 64 B 定长解析——**修复在位 12/12 全落、序号严格递增**;把 `-2` 支路临时改成 `rc == -2 && 0u` 之后**只落 5/12、6~12 全丢**(两轮独立复现完全一致,且这一轮没有任何 `queue full` 留痕,说明丢的路径确实是「卷忙整批丢」而不是投递侧溢出)。**负向对照是必要的**:若卷其实没被占住,「零丢失」就是空结论。
|
||||
- **顺手照出的隐患**:待发条目只存 `const char *`,复用栈缓冲投递时整批别名到最后一次写入(实测落盘「1~5 + 12 七份」)。已改为条目内嵌 `desc[SYSLOG_DESC_MAX + 1u]` 并在投递时截断拷贝,头文件参数契约同步改写;代价 `+352 B ZI / +60 B Code`。理由与实测细节 → `docs/子系统设计/运行与自检记录设计说明.md` §4.4。
|
||||
- **两次测法自误(教训,别重复)**:① 先按 700 ms 快投递测,测到的 34 条丢失其实是投递侧队列满(设计如此),根本没走到被整改的支路——**测错了对象**;② 钩子投完后 `return` 使 testTask 退出,约 10 s 后被看门狗复位,把还压在 RAM 队列里的条目一起带走——**污染了自己的计数**。两者都不是固件问题,根因都是没有先验证测量手段本身有效。
|
||||
- **假件护栏的一次正向记录**:条目由 8 B 变 32 B 后,`test/host/sys_log_mcu` 的假消息队列因 `MQ_MAXMSG`(原 24)拒收一切 `osMessageQueuePut`,从既有用例起整片失败——这种「尺寸变了就响」的断言比任何评审都可靠;该宏已抬到 40 并把现象写进注释。
|
||||
- 本轮改动面:`App/log/sys_log_mcu.c`(条目结构 + 投递拷贝)、`App/log/sys_log.h`(参数契约)、`test/host/sys_log_mcu/sys_log_mcu_test.c`(新增 2 条断言 + `MQ_MAXMSG`)、`scripts/id_registry.json`(游标 `TC-SYSLOG-108`)。
|
||||
- **台架与临时改动已全部还原**:钩子、负向对照、套件宏逐一反向删除(`git status` 归零),设备上 `BENCH.STS` / `BENCH2.STS` / `B5FILL.STS` 与含钩子记录的 `SELFTEST.LOG` 已删除,产品固件重建后重烧并回读校验通过。
|
||||
|
||||
### P3|**明确不做**(把理由写死,免得下次又被列回待办:29 个 `.c` = 4,814 行)
|
||||
|
||||
| 类别 | 体量 | 为什么不做 | 那它靠什么把关 |
|
||||
@@ -462,3 +743,15 @@ uint16_t ch395f_link_change_count(void);
|
||||
逻辑度=1−硬件符号密度 / 要桩的外部头 / 可复用现有桩 / fix 提交数),分为 **P1 13 个 2,617 行、
|
||||
P2 5 个 2,226 行、P3 29 个 4,814 行明确不做**(四条对得上:13+5+29=47、三段相加=9,657);
|
||||
§8 下一步第 1 条改为指向本节 |
|
||||
| 2026-10-24 | **P2 落地(三名队友并行,本分支)**:新增 5 套 **659 条断言**(`net_socket` 420 /
|
||||
`console_log` 104 / `sys_log_mcu` 75 / `cfg_file_store_mcu` 34 / `soe_store` 26),套件 28 → **33** 全绿、
|
||||
`files_tested` 32 → **37**、未触及 `.c` **34 → 29**(恒等式 `29 = 0(P1) + 0(P2) + 29(P3)`,清单吃穿)、
|
||||
分母 4,684 → **6,245**、`line` 85.8% → **89.3%**、`func` 90.1% → **92.2%**(这批**没有摊薄**:5 个文件
|
||||
全都以 ≥98.7% 进分母)。新建共享假 FatFs `test/host/common/fatfs_fake.{h,c}`、共享假 CH395F 纯扩展
|
||||
(两个与真头不符的常量改成真值 + 加真头/假件常量文本核对闸,负例验证过)。补 `TC-HOST-*` 五族
|
||||
(22 → 27)与 `scripts/test_coverage_baseline.json` 重登(逐文件比对:下降 0 个)。照出 **10 条现状**
|
||||
(重点:`sys_log_mcu.c:509~537` 卷忙整批丢日志、`net_socket.c` 同文件两套 IP 字节序、UDP 两个公开 API
|
||||
绕过 netTask、`console_log.c` 冷却哨兵与空位判据),**生产码一行未改**,逐条登记在 §9「P2 落地」表内待裁定;
|
||||
§8 第 1 条同步改为「清单已吃穿」 |
|
||||
| 2026-10-24 | **第 1 轮修复(同日续)**:用户裁定「全部修」,11 条现状里 **10 条已处置 + 1 条撤销**(`net_recv_locked` 那条是误报:非阻塞分支已能报 EOF,`:1885` 核过)。生产码改动跨 `sys_log_mcu.c`(整批回投 / 文案与节流 / 删不可达截断支 / 新增 `s_is_selfheal_code` 收敛自愈触发集 / post 两层留痕节流)、`soe_store.c`(注释更正 + 触发集同源更保守子集)、`cfg_file_store_mcu.c`(`FR_NO_SYSTEM` 不存在)、`sys_log.h`(读侧容量契约)、`net_socket.c/.h/net_types.h`(`s_addr` 统一到 POSIX 网络序 + UDP 两入口执行任务守卫,新码 `NET_ERR_NOT_IN_NETTASK = -12`)、`net_socket_sim.c`(同步 + 有意不对称)、`console_log.c/.h`(冷却独立标志 / 空位判据收进临界区 / 未初始化分诊 / 三本账 + 新读口)。**`@build` 0 错 0 警**(`Code=213720 RO=18860 RW=1204 ZI=131500`);全量闸 **33/33 全绿**、`5605/6275(89.3%)`、`396/429(92.3%)`、逐文件比对**下降 0**。设计文档同步:`控制台单写者日志设计说明.md`(§4.1 口径 + §13-O2 销项 + v1.5)、`运行与自检记录设计说明.md`(§4.4/§4.5 四条)、`STM32F4-Base存储架构说明.md`(§10.1 第 10 条推论)、`CH395F_Trap_Records.md`(TRAP-NET-21 后续)、`BSD_Socket_API_使用指南.md`(新增 §9.0 唯一约定)。本轮新增处置表、后续项 5 条、台架复验 7 条、新踩坑 8 条见 §9 |
|
||||
| 2026-10-24 | **第 2 轮:测试字面量统一纯 ASCII**(单独提交):用户裁定「统一」,口径 = AGENTS 铁律「字符串字面量必须纯 ASCII」(无 BOM 文件里奇数个汉字会吞掉结束引号),中文只写注释。四名工人四组互斥目录并行,25 套 / 30 个 `.c` 的非 ASCII 字面量全部改成等值英文。粗口径普查 34 文件/1,552 行、严格口径(剥注释后取字面量)30 文件/**1,535** 行——差值那 4 个文件的命中全在注释里(`ns_peers.c`、`net_socket/stub/ns_rtos.h`、`common/ch395f_fake.h`、`common/fatfs_fake.c`),按口径**不改**。**Lead 独立终检五条硬判据**(TC 号集合不变 / 断言宏计数不变 / **行数与 HEAD 相等** / 代码区非 ASCII=0 / 字节卫生)→ **30 文件漂移 0**;每套断言数与 `--module` 覆盖率复量逐字不变(`net_socket` 887/889·53/53、`console_log` 199/200·16/16、`sys_log_mcu` 324/326·13/13、`adc_input` 103/103、`sys_clock` 100/100…),生产码零改动故基线不需重登。⚠ `@build` 对 `test/host/**` 无覆盖力,本轮证据只能来自 `*-unit-test` + `test-cppcheck` + `test-coverage`。方法、两版事故与协作纪律见 §9「第 2 轮」小节 |
|
||||
|
||||
@@ -723,6 +723,26 @@ void event_callback_example(void)
|
||||
|
||||
网络协议使用**大端序**(网络字节序),而 STM32 使用**小端序**(主机字节序)。发送和接收数据时需要进行字节序转换。
|
||||
|
||||
### 9.0 `s_addr` 的唯一约定(2026-10-24 统一;全仓只此一族,勿再引入第二套)
|
||||
|
||||
**定义**:`sin_addr.s_addr` 一律存**网络字节序整数**,判据是 `&s_addr` 的**第 0 个内存字节 == 点分第一段**。
|
||||
小端主机(STM32F407 与 PC 模拟器都是)上 `"192.168.1.100"` 的数值 = **`0x6401A8C0`**(内存里是 `C0 A8 01 64`)。
|
||||
|
||||
| 方向 | 用哪个 API | 要点 |
|
||||
|---|---|---|
|
||||
| 字符串 → `s_addr` | `net_inet_addr()` | 失败返回 **0**(与 BSD 一致),故 `"0.0.0.0"` 与非法串同值;**别拿 `NET_INADDR_NONE` 当失败哨兵** |
|
||||
| `s_addr` → 字符串 | `net_inet_ntoa()` | 内部先 `NET_NTOHL` 再**高段在前**打印(TRAP-NET-21 定案) |
|
||||
|
||||
**不变量(有单测钉住)**:`net_inet_ntoa(net_inet_addr(s), buf)` 还原 `s`;`net_inet_addr(net_inet_ntoa(v, buf))` 还原 `v`。
|
||||
`net_getsockname` / `net_getpeername` / `net_recvfrom` 填出来的 `s_addr`,与喂进 `net_bind` / `net_connect` / `net_sendto`
|
||||
的 `s_addr` **同族、可直接相等比较**。历史背景(同一文件曾并存两套约定、`ntoa` 注释里的例子取了废弃族的值)
|
||||
见 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-21 的 2026-10-24 后续条目。
|
||||
|
||||
**UDP 直调限制**:`net_sendto` / `net_recvfrom` **不走消息队列**——它们在调用方上下文里直接 `net_poll()` + 读写芯片缓冲,
|
||||
因此**仅限本层执行任务(NET 层 = netTask)调用**;别的任务调用回 `NET_ERR_NOT_IN_NETTASK`(-12)。该码**不是运行时故障,
|
||||
重试永远无意义**:要么把调用挪进执行任务,要么改用走队列的 `net_send` / `net_recv`,没有第三条路。
|
||||
「把这两个接口改成经 `netMsgQueue` 串行化」是已知后续项,未做的理由见 `Drivers/BSP/NET/net_socket.h` 同名函数头。
|
||||
|
||||
### 9.1 端口号转换
|
||||
|
||||
```c
|
||||
|
||||
@@ -233,6 +233,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修
|
||||
- **根因(定案)**:① `net_inet_ntoa` 段序反转——`NET_NTOHL` 后主机数字的 **MSB 才是点分首段**,原实现低段在前;② lftpd 打印 `net_getsockname` = 本机监听地址(端口恒 21),对端地址应读 `net_getpeername`。PASV 227 消费点做了 `net_htonl` 再 MSB 拆,**双重反转恰好正确,勿误判**。
|
||||
- **修复**:`net_inet_ntoa` 段序改 MSB 首(MCU+模拟器同步);新增 `net_getpeername`(读 CONNECT 事件缓存,零 SPI);lftpd 改打印**对端** IP:port(台架判据:日志 `connection received from 192.168.1.x:<高位端口>`)。
|
||||
- **教训**:显示层字节序 bug 不影响协议本身(FTP 功能全程正常),但会系统性误导排障——**日志里的地址也要当被测对象核对**。
|
||||
- **后续(2026-10-24):同一文件里的`s_addr` 两套约定已统一**。修完段序之后,本条只解决了显示侧——当时 `net_inet_addr` 仍属**另一族**(小端累加后又 `NET_HTONL`,返回 `0xC0A80164`),而 `net_inet_ntoa` / `net_getsockname` 属 POSIX 网络序那族(`0x6401A8C0`),两者**不互逆**;按前者写的消费者(`net_connect_locked` 用 `>>24` 取首段)恰好自洽,所以一直没人撞上。现已统一到 **POSIX 网络序一族**(判据 `&s_addr[0] == 第一段`;小端上 192.168.1.100 = `0x6401A8C0`):改 `net_inet_addr` + 出向 IP 数组改按内存字节顺位拆装(**进与出同时换族 → 芯片侧数组净效果不变**)+ 模拟器 `simulator/sim/net_socket_sim.c` 逐字同步;`net_inet_ntoa` 函数体一字未动。**为什么它能长期不被发现**:本条"修复"行下方 `ntoa` 自己注释里的例子(`192.168.1.100 → 0xC0A80164`)**取的是废弃族的值**——注释替错误背书,比没有注释更糟。规范定义见 `docs/网络/BSD_Socket_API_使用指南.md` §9.0;往返不变量由 TC-NETSOCK-100g / 100h 钉住。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1549,6 +1549,116 @@
|
||||
"owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓",
|
||||
"cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌"
|
||||
},
|
||||
{
|
||||
"name": "TC-HOST-NETSOCK",
|
||||
"class": "TC",
|
||||
"title": "主机侧单测用例号:CH395F socket 栈核心(fd 表分配/跨上下文消息/错误码经通知值回传/监听门控)(net_socket)",
|
||||
"pattern": "\\bTC-NETSOCK-[0-9]{1,4}[a-z]?\\b",
|
||||
"digit_width": [
|
||||
2,
|
||||
4
|
||||
],
|
||||
"owner_docs_glob": [
|
||||
"test/host/net_socket/*"
|
||||
],
|
||||
"scope": "global",
|
||||
"frozen": false,
|
||||
"cursor": "TC-NETSOCK-560",
|
||||
"cursor_owner_only": true,
|
||||
"check_dangling": true,
|
||||
"allow_orphans": true,
|
||||
"subitem_ok": true,
|
||||
"owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓",
|
||||
"cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌"
|
||||
},
|
||||
{
|
||||
"name": "TC-HOST-SYSLOG",
|
||||
"class": "TC",
|
||||
"title": "主机侧单测用例号:运行与自检日志存储(定长记录 + 批量写 + 读窗口)(sys_log_mcu)",
|
||||
"pattern": "\\bTC-SYSLOG-[0-9]{1,4}[a-z]?\\b",
|
||||
"digit_width": [
|
||||
2,
|
||||
4
|
||||
],
|
||||
"owner_docs_glob": [
|
||||
"test/host/sys_log_mcu/*"
|
||||
],
|
||||
"scope": "global",
|
||||
"frozen": false,
|
||||
"cursor": "TC-SYSLOG-108",
|
||||
"cursor_owner_only": true,
|
||||
"check_dangling": true,
|
||||
"allow_orphans": true,
|
||||
"subitem_ok": true,
|
||||
"owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓",
|
||||
"cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌"
|
||||
},
|
||||
{
|
||||
"name": "TC-HOST-CFGFST",
|
||||
"class": "TC",
|
||||
"title": "主机侧单测用例号:配置文件存储引擎(落盘与回读校验)(cfg_file_store_mcu)",
|
||||
"pattern": "\\bTC-CFGFST-[0-9]{1,4}[a-z]?\\b",
|
||||
"digit_width": [
|
||||
2,
|
||||
4
|
||||
],
|
||||
"owner_docs_glob": [
|
||||
"test/host/cfg_file_store_mcu/*"
|
||||
],
|
||||
"scope": "global",
|
||||
"frozen": false,
|
||||
"cursor": "TC-CFGFST-053",
|
||||
"cursor_owner_only": true,
|
||||
"check_dangling": true,
|
||||
"allow_orphans": true,
|
||||
"subitem_ok": true,
|
||||
"owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓",
|
||||
"cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌"
|
||||
},
|
||||
{
|
||||
"name": "TC-HOST-SOESTORE",
|
||||
"class": "TC",
|
||||
"title": "主机侧单测用例号:SOE 事件顺序记录持久化(soe_store)",
|
||||
"pattern": "\\bTC-SOESTORE-[0-9]{1,4}[a-z]?\\b",
|
||||
"digit_width": [
|
||||
2,
|
||||
4
|
||||
],
|
||||
"owner_docs_glob": [
|
||||
"test/host/soe_store/*"
|
||||
],
|
||||
"scope": "global",
|
||||
"frozen": false,
|
||||
"cursor": "TC-SOESTORE-024",
|
||||
"cursor_owner_only": true,
|
||||
"check_dangling": true,
|
||||
"allow_orphans": true,
|
||||
"subitem_ok": true,
|
||||
"owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓",
|
||||
"cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌"
|
||||
},
|
||||
{
|
||||
"name": "TC-HOST-CONLOG",
|
||||
"class": "TC",
|
||||
"title": "主机侧单测用例号:控制台单写者日志(环形缓冲 + TX 软复位自愈)(console_log)",
|
||||
"pattern": "\\bTC-CONLOG-[0-9]{1,4}[a-z]?\\b",
|
||||
"digit_width": [
|
||||
2,
|
||||
4
|
||||
],
|
||||
"owner_docs_glob": [
|
||||
"test/host/console_log/*"
|
||||
],
|
||||
"scope": "global",
|
||||
"frozen": false,
|
||||
"cursor": "TC-CONLOG-167",
|
||||
"cursor_owner_only": true,
|
||||
"check_dangling": true,
|
||||
"allow_orphans": true,
|
||||
"subitem_ok": true,
|
||||
"owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓",
|
||||
"cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌"
|
||||
},
|
||||
{
|
||||
"name": "TC-HOST-ADCHEALTH",
|
||||
"class": "TC",
|
||||
|
||||
@@ -64,6 +64,14 @@
|
||||
"lines_covered": 572,
|
||||
"lines_valid": 652
|
||||
},
|
||||
"App/cfg/cfg_file_store_mcu.c": {
|
||||
"func_rate": 1.0,
|
||||
"funcs_covered": 10,
|
||||
"funcs_valid": 10,
|
||||
"line_rate": 1.0,
|
||||
"lines_covered": 97,
|
||||
"lines_valid": 97
|
||||
},
|
||||
"App/cfg/prot_set_store.c": {
|
||||
"func_rate": 1.0,
|
||||
"funcs_covered": 4,
|
||||
@@ -80,6 +88,14 @@
|
||||
"lines_covered": 17,
|
||||
"lines_valid": 17
|
||||
},
|
||||
"App/log/console_log.c": {
|
||||
"func_rate": 1.0,
|
||||
"funcs_covered": 16,
|
||||
"funcs_valid": 16,
|
||||
"line_rate": 0.995,
|
||||
"lines_covered": 199,
|
||||
"lines_valid": 200
|
||||
},
|
||||
"App/log/soe_rec.c": {
|
||||
"func_rate": 1.0,
|
||||
"funcs_covered": 7,
|
||||
@@ -88,6 +104,22 @@
|
||||
"lines_covered": 81,
|
||||
"lines_valid": 87
|
||||
},
|
||||
"App/log/soe_store.c": {
|
||||
"func_rate": 1.0,
|
||||
"funcs_covered": 4,
|
||||
"funcs_valid": 4,
|
||||
"line_rate": 0.9873,
|
||||
"lines_covered": 78,
|
||||
"lines_valid": 79
|
||||
},
|
||||
"App/log/sys_log_mcu.c": {
|
||||
"func_rate": 1.0,
|
||||
"funcs_covered": 13,
|
||||
"funcs_valid": 13,
|
||||
"line_rate": 0.9939,
|
||||
"lines_covered": 324,
|
||||
"lines_valid": 326
|
||||
},
|
||||
"App/protocol/iec104/iec104_asdu.c": {
|
||||
"func_rate": 1.0,
|
||||
"funcs_covered": 10,
|
||||
@@ -240,6 +272,14 @@
|
||||
"lines_covered": 100,
|
||||
"lines_valid": 117
|
||||
},
|
||||
"Drivers/BSP/NET/net_socket.c": {
|
||||
"func_rate": 1.0,
|
||||
"funcs_covered": 53,
|
||||
"funcs_valid": 53,
|
||||
"line_rate": 0.9978,
|
||||
"lines_covered": 887,
|
||||
"lines_valid": 889
|
||||
},
|
||||
"Drivers/BSP/NETCFG/ftp_cfg.c": {
|
||||
"func_rate": 1.0,
|
||||
"funcs_covered": 4,
|
||||
@@ -280,6 +320,20 @@
|
||||
"lines_total": 652,
|
||||
"ok": true
|
||||
},
|
||||
"cfg_file_store_mcu": {
|
||||
"files": {
|
||||
"App/cfg/cfg_file_store_mcu.c": 97
|
||||
},
|
||||
"lines_total": 97,
|
||||
"ok": true
|
||||
},
|
||||
"console_log": {
|
||||
"files": {
|
||||
"App/log/console_log.c": 200
|
||||
},
|
||||
"lines_total": 200,
|
||||
"ok": true
|
||||
},
|
||||
"freq": {
|
||||
"files": {
|
||||
"App/algo/freq.c": 130,
|
||||
@@ -390,6 +444,13 @@
|
||||
"lines_total": 117,
|
||||
"ok": true
|
||||
},
|
||||
"net_socket": {
|
||||
"files": {
|
||||
"Drivers/BSP/NET/net_socket.c": 889
|
||||
},
|
||||
"lines_total": 889,
|
||||
"ok": true
|
||||
},
|
||||
"phasor": {
|
||||
"files": {
|
||||
"App/algo/phasor.c": 55,
|
||||
@@ -439,6 +500,14 @@
|
||||
"lines_total": 190,
|
||||
"ok": true
|
||||
},
|
||||
"soe_store": {
|
||||
"files": {
|
||||
"App/log/soe_rec.c": 87,
|
||||
"App/log/soe_store.c": 79
|
||||
},
|
||||
"lines_total": 166,
|
||||
"ok": true
|
||||
},
|
||||
"sys_clock": {
|
||||
"files": {
|
||||
"App/util/sys_clock.c": 157
|
||||
@@ -446,6 +515,13 @@
|
||||
"lines_total": 157,
|
||||
"ok": true
|
||||
},
|
||||
"sys_log_mcu": {
|
||||
"files": {
|
||||
"App/log/sys_log_mcu.c": 326
|
||||
},
|
||||
"lines_total": 326,
|
||||
"ok": true
|
||||
},
|
||||
"task_prof": {
|
||||
"files": {
|
||||
"App/util/task_prof.c": 93
|
||||
@@ -486,9 +562,13 @@
|
||||
"App/algo/prot_set.c",
|
||||
"App/algo/sample_ring.c",
|
||||
"App/cfg/cfg_file.c",
|
||||
"App/cfg/cfg_file_store_mcu.c",
|
||||
"App/cfg/prot_set_store.c",
|
||||
"App/fs/vol_lock.c",
|
||||
"App/log/console_log.c",
|
||||
"App/log/soe_rec.c",
|
||||
"App/log/soe_store.c",
|
||||
"App/log/sys_log_mcu.c",
|
||||
"App/protocol/iec104/iec104_asdu.c",
|
||||
"App/protocol/iec104/iec104_link.c",
|
||||
"App/protocol/iec104/iec104_pts.c",
|
||||
@@ -508,27 +588,24 @@
|
||||
"Drivers/BSP/NET/net_listener.c",
|
||||
"Drivers/BSP/NET/net_pending.c",
|
||||
"Drivers/BSP/NET/net_select.c",
|
||||
"Drivers/BSP/NET/net_socket.c",
|
||||
"Drivers/BSP/NETCFG/ftp_cfg.c",
|
||||
"Drivers/BSP/NETCFG/net_cfg.c"
|
||||
],
|
||||
"totals": {
|
||||
"files_generated_excluded": 5,
|
||||
"files_tested": 32,
|
||||
"files_untested": 112,
|
||||
"func_rate": 0.9009,
|
||||
"funcs_covered": 300,
|
||||
"funcs_valid": 333,
|
||||
"line_rate": 0.8582,
|
||||
"lines_covered": 4020,
|
||||
"lines_valid": 4684,
|
||||
"modules_ok": 28,
|
||||
"modules_total": 28
|
||||
"files_tested": 37,
|
||||
"files_untested": 107,
|
||||
"func_rate": 0.9231,
|
||||
"funcs_covered": 396,
|
||||
"funcs_valid": 429,
|
||||
"line_rate": 0.8932,
|
||||
"lines_covered": 5605,
|
||||
"lines_valid": 6275,
|
||||
"modules_ok": 33,
|
||||
"modules_total": 33
|
||||
},
|
||||
"untested_sources": [
|
||||
{
|
||||
"code_lines": 1251,
|
||||
"path": "Drivers/BSP/NET/net_socket.c"
|
||||
},
|
||||
{
|
||||
"code_lines": 801,
|
||||
"path": "Drivers/BSP/CH395F/ch395f.c"
|
||||
@@ -537,10 +614,6 @@
|
||||
"code_lines": 689,
|
||||
"path": "Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c"
|
||||
},
|
||||
{
|
||||
"code_lines": 458,
|
||||
"path": "App/log/sys_log_mcu.c"
|
||||
},
|
||||
{
|
||||
"code_lines": 329,
|
||||
"path": "Drivers/BSP/UI/ui_menu_view.c"
|
||||
@@ -557,10 +630,6 @@
|
||||
"code_lines": 281,
|
||||
"path": "App/task/default_task.c"
|
||||
},
|
||||
{
|
||||
"code_lines": 265,
|
||||
"path": "App/log/console_log.c"
|
||||
},
|
||||
{
|
||||
"code_lines": 264,
|
||||
"path": "Drivers/BSP/SD2506/sd2506.c"
|
||||
@@ -593,24 +662,16 @@
|
||||
"code_lines": 135,
|
||||
"path": "App/app/app_main.c"
|
||||
},
|
||||
{
|
||||
"code_lines": 129,
|
||||
"path": "App/cfg/cfg_file_store_mcu.c"
|
||||
},
|
||||
{
|
||||
"code_lines": 126,
|
||||
"path": "Drivers/BSP/UI/ui_menu.c"
|
||||
},
|
||||
{
|
||||
"code_lines": 123,
|
||||
"path": "App/log/soe_store.c"
|
||||
},
|
||||
{
|
||||
"code_lines": 123,
|
||||
"path": "App/task/net_task.c"
|
||||
},
|
||||
{
|
||||
"code_lines": 100,
|
||||
"code_lines": 101,
|
||||
"path": "Drivers/BSP/NET/net_types.h"
|
||||
},
|
||||
{
|
||||
@@ -787,11 +848,11 @@
|
||||
},
|
||||
{
|
||||
"code_lines": 20,
|
||||
"path": "Drivers/BSP/UI/ui_page_dc.c"
|
||||
"path": "App/log/console_log.h"
|
||||
},
|
||||
{
|
||||
"code_lines": 19,
|
||||
"path": "App/log/console_log.h"
|
||||
"code_lines": 20,
|
||||
"path": "Drivers/BSP/UI/ui_page_dc.c"
|
||||
},
|
||||
{
|
||||
"code_lines": 19,
|
||||
|
||||
@@ -600,8 +600,16 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) {
|
||||
* dest_addr - 目标地址
|
||||
* addrlen - 地址长度
|
||||
* 出口参数:无
|
||||
* 返回值:实际发送字节数,-1 失败
|
||||
* 返回值:实际发送字节数,-1 失败(精确码见 net_get_errno)
|
||||
* 限定条件:socket 已创建,buf/dest_addr 非空
|
||||
* 函数说明:⚠ **MCU 端本接口另有上下文约束**:它不经消息队列、在调用方上下文里
|
||||
* 直接 net_poll + 写芯片缓冲,故 MCU 侧只允许 net_init 的执行任务调用,
|
||||
* 其他任务调用回 NET_ERR_NOT_IN_NETTASK(2026-10-24 定案,理由与
|
||||
* "为何不是改成走队列"见 net_socket.h 同名声明处)。
|
||||
* 本后端刻意**不加**同样的守卫:模拟器是单线程 + Winsock(自身线程安全),
|
||||
* 没有"与 net_poll 抢同一根 SPI"这条危险面,也没有 RTOS 任务句柄可比。
|
||||
* 所以这是**有意的不对称**,不是漏实现;若将来模拟器引入多线程,
|
||||
* 必须连同这条约束一起补上(按线程 ID 比 net_init 的调用线程)。
|
||||
*/
|
||||
int net_sendto(int sockfd, const void *buf, int len, int flags,
|
||||
const struct net_sockaddr *dest_addr, int addrlen) {
|
||||
@@ -643,8 +651,10 @@ int net_sendto(int sockfd, const void *buf, int len, int flags,
|
||||
* src_addr - 输出:发送方地址(可为 NULL)
|
||||
* addrlen - 输入输出:源地址长度(可为 NULL)
|
||||
* 出口参数:buf/src_addr/addrlen - 填充
|
||||
* 返回值:实际接收字节数,-1 失败
|
||||
* 返回值:实际接收字节数,-1 失败(精确码见 net_get_errno)
|
||||
* 限定条件:socket 已创建,buf 非空
|
||||
* 函数说明:MCU 端有"仅限执行任务调用"的上下文守卫(NET_ERR_NOT_IN_NETTASK),
|
||||
* 本后端按 net_sendto 处同一条理由不加——有意的不对称,勿当成漏实现
|
||||
*/
|
||||
int net_recvfrom(int sockfd, void *buf, int len, int flags,
|
||||
struct net_sockaddr *src_addr, int *addrlen) {
|
||||
@@ -803,7 +813,14 @@ int net_get_errno(void) {
|
||||
* 出口参数:无
|
||||
* 返回值:网络字节序 IP(参数无效返回 0)
|
||||
* 限定条件:无
|
||||
* 函数说明:逐段解析后按 NET_HTONL 转网络字节序(与 MCU 端实现一致)
|
||||
* 函数说明:与 MCU 端 net_inet_addr **逐字同语义**(2026-10-24:两套 IP 字节序约定统一为 POSIX 网络序):
|
||||
* 按「第一段在主机序最高字节」累加,再经 NET_HTONL 转网络序,结果满足
|
||||
* POSIX/Winsock 的 in_addr 不变量——&s_addr 的第 0 个内存字节 == 第一段。
|
||||
* 本模拟器的 to_ws_addr()/from_ws_addr() 是把 net_sockaddr_in **整块 memcpy**
|
||||
* 进 sockaddr_in 的,所以只有两边同族时才成立:旧实现(小端累加后再换端)
|
||||
* 交给 Winsock 的是反序地址,只有从不走 inet_addr 的路径(bind ANY、
|
||||
* getpeername)才没暴露。改完后本函数与 net_inet_ntoa 互逆、与 Winsock 直拷兼容。
|
||||
* 段值超 255 按低 8 位截断;失败返回 0(不引入 NET_INADDR_NONE 判定)。
|
||||
*/
|
||||
uint32_t net_inet_addr(const char *cp) {
|
||||
uint32_t addr = 0;
|
||||
@@ -819,7 +836,8 @@ uint32_t net_inet_addr(const char *cp) {
|
||||
val = val * 10 + (uint32_t)(*cp - '0');
|
||||
cp++;
|
||||
}
|
||||
addr |= (val << (i * 8));
|
||||
/* 高位先入:第 0 段左移三次后落在最高字节 */
|
||||
addr = (addr << 8) | (val & 0xFFU);
|
||||
if (*cp == '.') {
|
||||
cp++;
|
||||
}
|
||||
|
||||
@@ -213,20 +213,20 @@ static void tc_init(void) {
|
||||
(void)adc_input_poll(); /* 单拍无新帧 = 相位伪影,不计故障 */
|
||||
(void)adc_input_poll(); /* 连续第二拍无新帧 = 真停摆 */
|
||||
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-001 连续两拍无新帧 -> 停摆计数 = 1");
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-001 two rounds in a row with no new frame -> stall count = 1");
|
||||
CHECK((adc_input_raw(ADC_SEM_UA, &v) == 0) && (v == 4321),
|
||||
"TC-ADCIN-002 poll 后 UA 可取数且值正确");
|
||||
"TC-ADCIN-002 after poll UA is readable and the value is right");
|
||||
|
||||
CHECK(adc_input_init() == 0, "TC-ADCIN-003 adc_input_init 返回 0(映射自检通过)");
|
||||
CHECK(adc_input_init() == 0, "TC-ADCIN-003 adc_input_init returns 0 (mapping self-check passed)");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN,
|
||||
"TC-ADCIN-004 init 后 STM32 状态码 = NOT_RUN");
|
||||
CHECK(s_start_dma_calls == 1, "TC-ADCIN-003B init 挂载 DMA 恰好一次");
|
||||
CHECK(s_dma_len == (2u * ADC_INPUT_STM32_CH_NUM), "TC-ADCIN-003C DMA 长度 = 2 x 9(乒乓双缓冲)");
|
||||
CHECK(s_dma_buf != NULL, "TC-ADCIN-003D DMA 目的缓冲地址已交下来");
|
||||
CHECK(adc_input_stm32_seq() == 0u, "TC-ADCIN-005B init 后 DMA 完成计数 = 0");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-005 init 后 STM32 失败计数归零");
|
||||
"TC-ADCIN-004 after init the STM32 status code = NOT_RUN");
|
||||
CHECK(s_start_dma_calls == 1, "TC-ADCIN-003B init arms DMA exactly once");
|
||||
CHECK(s_dma_len == (2u * ADC_INPUT_STM32_CH_NUM), "TC-ADCIN-003C DMA length = 2 x 9 (ping-pong double buffer)");
|
||||
CHECK(s_dma_buf != NULL, "TC-ADCIN-003D the DMA destination buffer address was handed down");
|
||||
CHECK(adc_input_stm32_seq() == 0u, "TC-ADCIN-005B after init the DMA completion counter = 0");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-005 after init the STM32 failure counter is zeroed");
|
||||
CHECK((adc_input_raw(ADC_SEM_UA, &v) == -2) && (adc_input_raw(ADC_SEM_UB, &v) == -2),
|
||||
"TC-ADCIN-006 init 后 TPAFE / STM32 两侧均无有效数据");
|
||||
"TC-ADCIN-006 after init neither TPAFE nor STM32 side holds valid data");
|
||||
}
|
||||
|
||||
/* ===================== TC-ADCIN-010..01A:desc 映射表 ===================== */
|
||||
@@ -262,8 +262,8 @@ static void tc_desc(void) {
|
||||
CHECK((d != NULL) && (d->source == ADC_SRC_STM32) && (d->index == 5u),
|
||||
"TC-ADCIN-018 TEMP -> STM32 CH5");
|
||||
|
||||
CHECK(adc_input_desc(ADC_SEM_COUNT) == NULL, "TC-ADCIN-019 越界(ADC_SEM_COUNT) -> NULL");
|
||||
CHECK(adc_input_desc((adc_semantic_t)(-1)) == NULL, "TC-ADCIN-01A 负数语义 -> NULL");
|
||||
CHECK(adc_input_desc(ADC_SEM_COUNT) == NULL, "TC-ADCIN-019 out of range (ADC_SEM_COUNT) -> NULL");
|
||||
CHECK(adc_input_desc((adc_semantic_t)(-1)) == NULL, "TC-ADCIN-01A negative index -> NULL");
|
||||
}
|
||||
|
||||
/* ===================== TC-ADCIN-020:map_check ===================== */
|
||||
@@ -272,7 +272,7 @@ static void tc_map_check(void) {
|
||||
/* 真实映射表自洽:TPAFE 下标 < 8 且每个 STM32 通道号都出现在扫描 Rank 内。
|
||||
* 反向路径(表不自洽 -> -1)需在运行时改表,而 k_adc_map / k_stm32_rank_ch 均为
|
||||
* 编译期 static const、无运行时输入可使其不自洽 -> 结构上不可达(见文件末说明)。 */
|
||||
CHECK(map_check() == 0, "TC-ADCIN-020 映射表自洽(TPAFE 下标与 STM32 Rank 均在范围内)");
|
||||
CHECK(map_check() == 0, "TC-ADCIN-020 mapping table is self-consistent (TPAFE index and STM32 Rank both in range)");
|
||||
}
|
||||
|
||||
/* ===================== TC-ADCIN-021..02C:stm32_scan 成功路径 ===================== */
|
||||
@@ -284,22 +284,22 @@ static void tc_take_ok(void) {
|
||||
(void)adc_input_init();
|
||||
dma_beat(100); /* 前半区一帧:Rank i -> 100+i */
|
||||
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-021 stm32_take 取到新帧 -> 0");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-ADCIN-022 状态码 = RC_OK");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-023 成功不计入失败");
|
||||
CHECK(s_start_dma_calls == 1, "TC-ADCIN-024 DMA 只在 init 挂一次(每拍不重启)");
|
||||
CHECK(adc_input_stm32_seq() == 1u, "TC-ADCIN-025 DMA 完成计数 = 1");
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-021 stm32_take got a new frame -> 0");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-ADCIN-022 status code = RC_OK");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-023 a success is not counted as a failure");
|
||||
CHECK(s_start_dma_calls == 1, "TC-ADCIN-024 DMA is armed once at init (never restarted per round)");
|
||||
CHECK(adc_input_stm32_seq() == 1u, "TC-ADCIN-025 DMA completion counter = 1");
|
||||
|
||||
/* Rank -> 通道号映射:第 i 个 Rank = 100+i。
|
||||
* k_stm32_rank_ch = [CH0..CH7, CH14],故 CH3->103 / CH5->105 / CH6->106 / CH14->108。 */
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-ADCIN-026 UB 取到 CH14(rank9)=108");
|
||||
CHECK((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 103), "TC-ADCIN-027 UC 取到 CH3(rank4)=103");
|
||||
CHECK((adc_input_raw(ADC_SEM_IB, &v) == 0) && (v == 106), "TC-ADCIN-028 IB 取到 CH6(rank7)=106");
|
||||
CHECK((adc_input_raw(ADC_SEM_IC, &v) == 0) && (v == 101), "TC-ADCIN-029 IC 取到 CH1(rank2)=101");
|
||||
CHECK((adc_input_raw(ADC_SEM_IN, &v) == 0) && (v == 102), "TC-ADCIN-02A IN 取到 CH2(rank3)=102");
|
||||
CHECK((adc_input_raw(ADC_SEM_TEMP, &v) == 0) && (v == 105), "TC-ADCIN-02B TEMP 取到 CH5(rank6)=105");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-ADCIN-026 UB reads CH14(rank9)=108");
|
||||
CHECK((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 103), "TC-ADCIN-027 UC reads CH3(rank4)=103");
|
||||
CHECK((adc_input_raw(ADC_SEM_IB, &v) == 0) && (v == 106), "TC-ADCIN-028 IB reads CH6(rank7)=106");
|
||||
CHECK((adc_input_raw(ADC_SEM_IC, &v) == 0) && (v == 101), "TC-ADCIN-029 IC reads CH1(rank2)=101");
|
||||
CHECK((adc_input_raw(ADC_SEM_IN, &v) == 0) && (v == 102), "TC-ADCIN-02A IN reads CH2(rank3)=102");
|
||||
CHECK((adc_input_raw(ADC_SEM_TEMP, &v) == 0) && (v == 105), "TC-ADCIN-02B TEMP reads CH5(rank6)=105");
|
||||
|
||||
CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-02C stm32_take 不置 TPAFE 有效标志");
|
||||
CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-02C stm32_take does not set the TPAFE valid flag");
|
||||
}
|
||||
|
||||
/* ============ TC-ADCIN-080..085:乒乓双缓冲(读刚完成半区,不读正在写的半区) ============
|
||||
@@ -314,27 +314,27 @@ static void tc_dma_pingpong(void) {
|
||||
|
||||
dma_beat(100); /* 第 1 帧 -> 前半区(半传输回调) */
|
||||
dma_fill(1u, 900u); /* 模拟 DMA 正在写后半区(值 900 系) */
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-080 第一拍取帧成功");
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-080 first round fetch succeeded");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108),
|
||||
"TC-ADCIN-081 读的是刚完成的前半区(108),不是正在写的后半区");
|
||||
"TC-ADCIN-081 reads the just completed half (108), not the half being written");
|
||||
|
||||
dma_beat(200); /* 第 2 帧 -> 后半区(传输完成回调) */
|
||||
dma_fill(0u, 800u); /* 模拟 DMA 正在写前半区(值 800 系) */
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-082 第二拍取帧成功");
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-082 second round fetch succeeded");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 208),
|
||||
"TC-ADCIN-083 第二拍读到后半区(208)");
|
||||
CHECK(adc_input_stm32_seq() == 2u, "TC-ADCIN-084 DMA 完成计数 = 2");
|
||||
"TC-ADCIN-083 second round reads the later half (208)");
|
||||
CHECK(adc_input_stm32_seq() == 2u, "TC-ADCIN-084 DMA completion counter = 2");
|
||||
|
||||
/* 半区奇偶口径:seq=1 -> 前半区,seq=2 -> 后半区(与 dma_beat 独立推导,互相证伪) */
|
||||
CHECK(((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 203)), "TC-ADCIN-085 UC 同帧内一致(203)");
|
||||
CHECK(((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 203)), "TC-ADCIN-085 UC stays consistent within the frame (203)");
|
||||
|
||||
/* 消费者超拍:两拍都没取 -> 中间那帧已被乒乓缓冲覆盖 -> 记 1 点丢帧 */
|
||||
dma_beat(300);
|
||||
dma_beat(400);
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-086 超拍后仍取到最新帧");
|
||||
CHECK(adc_input_stm32_gaps() == 1u, "TC-ADCIN-087 seq 前进 2 -> 记 1 点丢帧");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 408), "TC-ADCIN-088 取到的是最新帧(408)");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-089 丢帧不计入停摆(两类故障分开)");
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-086 after skipping rounds the newest frame is still fetched");
|
||||
CHECK(adc_input_stm32_gaps() == 1u, "TC-ADCIN-087 seq advanced by 2 -> records 1 dropped frame");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 408), "TC-ADCIN-088 what came back is the newest frame (408)");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-089 a drop is not counted as a stall (two fault classes stay apart)");
|
||||
|
||||
/* 第二档:上面只跨了 1 帧(前进 2 记 1 点),"次数"与"点数"数值相同、钉不出单位。
|
||||
* 这里连投三帧不消费 = 一次跨 2 帧,必须记 2 点(累计 1+2=3),把 2026-10-21 改的
|
||||
@@ -355,18 +355,18 @@ static void tc_hw_errs(void) {
|
||||
|
||||
HAL_ADC_ErrorCallback(&hadc1);
|
||||
HAL_ADC_ErrorCallback(&hadc1);
|
||||
CHECK(adc_input_stm32_hw_errs() == 2u, "TC-ADCIN-090 错误回调累计 = 2");
|
||||
CHECK(adc_input_stm32_hw_errs() == 2u, "TC-ADCIN-090 error callback total = 2");
|
||||
|
||||
HAL_ADC_ErrorCallback(&hadc_other);
|
||||
CHECK(adc_input_stm32_hw_errs() == 2u, "TC-ADCIN-091 别的 ADC 句柄不得计入(hadc 守卫)");
|
||||
CHECK(adc_input_stm32_hw_errs() == 2u, "TC-ADCIN-091 other ADC handles must not count (hadc guard)");
|
||||
|
||||
HAL_ADC_ConvHalfCpltCallback(&hadc_other);
|
||||
HAL_ADC_ConvCpltCallback(&hadc_other);
|
||||
CHECK(adc_input_stm32_seq() == 0u, "TC-ADCIN-092 别的句柄的完成回调不得推进 seq");
|
||||
CHECK(adc_input_stm32_seq() == 0u, "TC-ADCIN-092 a completion callback from another handle must not advance seq");
|
||||
|
||||
HAL_ADC_ErrorCallback(&hadc1);
|
||||
CHECK(adc_input_stm32_hw_errs() == 3u, "TC-ADCIN-093 hw_errs 与 errs 分开计数、互不干扰");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-09A hw_errs 不污染 errs");
|
||||
CHECK(adc_input_stm32_hw_errs() == 3u, "TC-ADCIN-093 hw_errs and errs count separately and never interfere");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-09A hw_errs does not pollute errs");
|
||||
}
|
||||
|
||||
/* ===================== TC-ADCIN-030..034:DMA 挂载失败 ===================== */
|
||||
@@ -377,10 +377,10 @@ static void tc_init_dma_fail(void) {
|
||||
fakes_reset();
|
||||
s_start_dma_rc = HAL_ERROR;
|
||||
|
||||
CHECK(adc_input_init() == -2, "TC-ADCIN-030 HAL_ADC_Start_DMA 失败 -> init 返回 -2");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_START, "TC-ADCIN-031 状态码 = RC_START");
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-032 失败计数 +1");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-034 挂载失败后 STM32 侧无有效数据");
|
||||
CHECK(adc_input_init() == -2, "TC-ADCIN-030 HAL_ADC_Start_DMA failure -> init returns -2");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_START, "TC-ADCIN-031 status code = RC_START");
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-032 failure counter +1");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-034 after an arming failure the STM32 side holds no valid data");
|
||||
}
|
||||
|
||||
/* ===================== TC-ADCIN-035..03A:本拍无新帧(停摆) ===================== */
|
||||
@@ -392,21 +392,21 @@ static void tc_take_stall(void) {
|
||||
(void)adc_input_init();
|
||||
/* 没有任何 DMA 完成回调 = TIM3 未启动 / ADC 停摆 / DMA 出错 */
|
||||
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_STALL, "TC-ADCIN-035 无新帧 -> RC_STALL(-2)");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-ADCIN-036 状态码 = RC_STALL");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-037 单拍无新帧不计故障(相位伪影)");
|
||||
CHECK(adc_input_stm32_seq() == 0u, "TC-ADCIN-038 停摆期间 seq 不前进");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-039 停摆拍 STM32 侧无有效数据");
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_STALL, "TC-ADCIN-035 no new frame -> RC_STALL(-2)");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-ADCIN-036 status code = RC_STALL");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-037 a single round without a new frame is not a fault (phase artefact)");
|
||||
CHECK(adc_input_stm32_seq() == 0u, "TC-ADCIN-038 seq does not advance while stalled");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-039 a stalled round leaves the STM32 side without valid data");
|
||||
|
||||
(void)stm32_take(); /* 连续第二拍无新帧 = 真停摆 */
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-03A 连续两拍无新帧 -> 停摆计数 = 1");
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-03A two rounds in a row with no new frame -> stall count = 1");
|
||||
(void)stm32_take(); /* 第三拍仍无新帧 */
|
||||
CHECK(adc_input_stm32_errs() == 2u, "TC-ADCIN-03B 停摆持续 -> 每拍累计 = 2");
|
||||
CHECK(adc_input_stm32_gaps() == 0u, "TC-ADCIN-03C 停摆不计入丢帧(两类故障分开)");
|
||||
CHECK(adc_input_stm32_errs() == 2u, "TC-ADCIN-03B an ongoing stall accumulates every round = 2");
|
||||
CHECK(adc_input_stm32_gaps() == 0u, "TC-ADCIN-03C a stall is not counted as a drop (two fault classes stay apart)");
|
||||
|
||||
dma_beat(100); /* 恢复 */
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-03D 恢复后取帧成功");
|
||||
CHECK(adc_input_stm32_gaps() == 0u, "TC-ADCIN-03E 恢复拍前进 1 -> 不算丢帧");
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-03D after recovery the fetch succeeds");
|
||||
CHECK(adc_input_stm32_gaps() == 0u, "TC-ADCIN-03E the recovery round advances by 1 -> not a drop");
|
||||
}
|
||||
|
||||
/* ============ TC-ADCIN-070..078:停摆拍不得沿用上一拍旧值(陈旧帧) ============
|
||||
@@ -424,24 +424,24 @@ static void tc_take_stale(void) {
|
||||
fakes_reset();
|
||||
(void)adc_input_init();
|
||||
dma_beat(100);
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-070 前置:首拍成功");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-ADCIN-071 前置:UB 取到 108(有效态)");
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-070 precondition: first round succeeded");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-ADCIN-071 precondition: UB reads 108 (valid state)");
|
||||
|
||||
/* 第二拍没有任何 DMA 完成(TIM3 停 / ADC 停摆 / DMA 出错) */
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_STALL, "TC-ADCIN-072 第二拍无新帧");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-073 停摆拍不得沿用上一拍值 -> -2");
|
||||
CHECK(adc_input_raw(ADC_SEM_UC, &v) == -2, "TC-ADCIN-074 停摆拍连其它 STM32 通道也不得出数");
|
||||
CHECK(adc_input_raw(ADC_SEM_TEMP, &v) == -2, "TC-ADCIN-074B 同上(TEMP 亦 -2)");
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_STALL, "TC-ADCIN-072 second round has no new frame");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-073 a stalled round must not reuse the previous value -> -2");
|
||||
CHECK(adc_input_raw(ADC_SEM_UC, &v) == -2, "TC-ADCIN-074 a stalled round must not emit any other STM32 channel either");
|
||||
CHECK(adc_input_raw(ADC_SEM_TEMP, &v) == -2, "TC-ADCIN-074B same as above (TEMP also -2)");
|
||||
|
||||
dma_beat(200);
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-075 第三拍恢复成功");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 208), "TC-ADCIN-076 恢复拍重新可取数(自愈)");
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-075 third round recovers successfully");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 208), "TC-ADCIN-076 the recovery round can be read again (self-healing)");
|
||||
|
||||
/* --- 挂载失败支路:同样不得沿用(init 失败后整层无有效数据) --- */
|
||||
fakes_reset();
|
||||
s_start_dma_rc = HAL_ERROR;
|
||||
(void)adc_input_init();
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-078 挂载失败后不得取到任何值 -> -2");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-078 after an arming failure no value may be read -> -2");
|
||||
}
|
||||
|
||||
/* ===================== TC-ADCIN-040..045:raw 参数守卫与无数据 ===================== */
|
||||
@@ -453,11 +453,11 @@ static void tc_raw_guards(void) {
|
||||
(void)adc_input_init();
|
||||
|
||||
CHECK(adc_input_raw(ADC_SEM_UA, NULL) == -1, "TC-ADCIN-040 out=NULL -> -1");
|
||||
CHECK(adc_input_raw(ADC_SEM_COUNT, &v) == -1, "TC-ADCIN-041 ch 越界 -> -1");
|
||||
CHECK(adc_input_raw((adc_semantic_t)(-1), &v) == -1, "TC-ADCIN-042 ch 负数 -> -1");
|
||||
CHECK(v == 0, "TC-ADCIN-043 参数错时不写 out");
|
||||
CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-044 TPAFE 无有效帧 -> -2");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-045 STM32 无有效扫描 -> -2");
|
||||
CHECK(adc_input_raw(ADC_SEM_COUNT, &v) == -1, "TC-ADCIN-041 ch out of range -> -1");
|
||||
CHECK(adc_input_raw((adc_semantic_t)(-1), &v) == -1, "TC-ADCIN-042 ch negative -> -1");
|
||||
CHECK(v == 0, "TC-ADCIN-043 on an argument error out is not written");
|
||||
CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-044 TPAFE has no valid frame -> -2");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-045 STM32 has no valid scan -> -2");
|
||||
}
|
||||
|
||||
/* ===================== TC-ADCIN-046..04B:raw TPAFE 取数与补码 ===================== */
|
||||
@@ -478,13 +478,13 @@ static void tc_raw_tpafe(void) {
|
||||
s_fake_tpafe[7] = 1007;
|
||||
s_snapshot_rc = 1;
|
||||
|
||||
CHECK(adc_input_poll() == 1, "TC-ADCIN-046 poll 透传 TPAFE 快照码 1");
|
||||
CHECK(s_snapshot_count == (uint8_t)ADC_INPUT_TPAFE_CH_NUM, "TC-ADCIN-047 快照请求 8 通道");
|
||||
CHECK(s_snapshot_dst != NULL, "TC-ADCIN-048 快照目的缓冲非空");
|
||||
CHECK(adc_input_poll() == 1, "TC-ADCIN-046 poll passes through the TPAFE snapshot code 1");
|
||||
CHECK(s_snapshot_count == (uint8_t)ADC_INPUT_TPAFE_CH_NUM, "TC-ADCIN-047 snapshot requests 8 channels");
|
||||
CHECK(s_snapshot_dst != NULL, "TC-ADCIN-048 the snapshot destination buffer is not NULL");
|
||||
CHECK((adc_input_raw(ADC_SEM_UA, &v) == 0) && (v == 3006), "TC-ADCIN-049 UA -> buf[6]=3006");
|
||||
CHECK((adc_input_raw(ADC_SEM_UN, &v) == 0) && (v == 2005), "TC-ADCIN-04A UN -> buf[5]=2005");
|
||||
CHECK((adc_input_raw(ADC_SEM_IA, &v) == 0) && (v == -1234),
|
||||
"TC-ADCIN-04B IA -> buf[2]=-1234(补码负值原样保留)");
|
||||
"TC-ADCIN-04B IA -> buf[2]=-1234 (two's-complement negative kept as is)");
|
||||
}
|
||||
|
||||
/* ===================== TC-ADCIN-050..058:poll 契约 ===================== */
|
||||
@@ -497,16 +497,16 @@ static void tc_poll(void) {
|
||||
(void)adc_input_init();
|
||||
s_snapshot_rc = 0;
|
||||
dma_beat(100);
|
||||
CHECK(adc_input_poll() == 0, "TC-ADCIN-050 快照无新帧 -> poll 返回 0");
|
||||
CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-051 无新帧时 TPAFE 侧无效");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-ADCIN-052 同拍 STM32 取帧照常执行");
|
||||
CHECK(adc_input_poll() == 0, "TC-ADCIN-050 snapshot with no new frame -> poll returns 0");
|
||||
CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-051 without a new frame the TPAFE side is invalid");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-ADCIN-052 the STM32 fetch in the same round still runs");
|
||||
|
||||
/* --- 参数错(-1):返回 -1,TPAFE 无效 --- */
|
||||
fakes_reset();
|
||||
(void)adc_input_init();
|
||||
s_snapshot_rc = -1;
|
||||
CHECK(adc_input_poll() == -1, "TC-ADCIN-053 快照参数错 -> poll 返回 -1");
|
||||
CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-054 参数错时 TPAFE 侧无效");
|
||||
CHECK(adc_input_poll() == -1, "TC-ADCIN-053 snapshot argument error -> poll returns -1");
|
||||
CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-054 on an argument error the TPAFE side is invalid");
|
||||
|
||||
/* --- 已有有效帧后再"无新帧":保持上次有效值(不清零) --- */
|
||||
fakes_reset();
|
||||
@@ -517,7 +517,7 @@ static void tc_poll(void) {
|
||||
s_snapshot_rc = 0;
|
||||
(void)adc_input_poll();
|
||||
CHECK((adc_input_raw(ADC_SEM_UA, &v) == 0) && (v == 777),
|
||||
"TC-ADCIN-055 已有有效帧后无新帧不清零");
|
||||
"TC-ADCIN-055 an existing valid frame is not zeroed when no new frame arrives");
|
||||
|
||||
/* --- STM32 失败不阻断 TPAFE、不影响 poll 返回值 --- */
|
||||
fakes_reset();
|
||||
@@ -525,10 +525,10 @@ static void tc_poll(void) {
|
||||
s_fake_tpafe[2] = 55;
|
||||
s_snapshot_rc = 1;
|
||||
/* 不注入任何 DMA 完成 = STM32 侧停摆 */
|
||||
CHECK(adc_input_poll() == 1, "TC-ADCIN-056 STM32 停摆不影响 poll 返回值(仍为 1)");
|
||||
CHECK(adc_input_poll() == 1, "TC-ADCIN-056 an STM32 stall does not change the poll return value (still 1)");
|
||||
CHECK((adc_input_raw(ADC_SEM_IA, &v) == 0) && (v == 55),
|
||||
"TC-ADCIN-057 STM32 停摆不影响 TPAFE 取数");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-ADCIN-058 STM32 停摆另由状态码暴露");
|
||||
"TC-ADCIN-057 an STM32 stall does not affect TPAFE data");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-ADCIN-058 an STM32 stall is exposed through the status code instead");
|
||||
}
|
||||
|
||||
/* ===================== TC-ADCIN-060..066:rc / errs 观测 ===================== */
|
||||
@@ -537,24 +537,24 @@ static void tc_rc_errs(void) {
|
||||
fakes_reset();
|
||||
(void)adc_input_init();
|
||||
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN, "TC-ADCIN-060 init 后未扫描 = NOT_RUN(-3)");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-061 init 后失败计数 = 0");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN, "TC-ADCIN-060 after init, before any scan = NOT_RUN(-3)");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-061 after init the failure counter = 0");
|
||||
|
||||
dma_beat(100);
|
||||
(void)stm32_take(); /* 成功 */
|
||||
CHECK((adc_input_stm32_rc() == ADC_INPUT_RC_OK) && (adc_input_stm32_errs() == 0u),
|
||||
"TC-ADCIN-062 成功: rc=OK 且 errs 不变");
|
||||
"TC-ADCIN-062 success: rc=OK and errs unchanged");
|
||||
|
||||
(void)stm32_take(); /* 停摆 1(单拍 = 相位伪影,不计) */
|
||||
(void)stm32_take(); /* 停摆 2 -> 计 1 */
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-063 连续两拍停摆累计 = 1");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-ADCIN-064 rc = 最近一次(STALL)");
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-063 two consecutive stalled rounds accumulate = 1");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-ADCIN-064 rc = the most recent result (STALL)");
|
||||
|
||||
dma_beat(200);
|
||||
(void)stm32_take(); /* 恢复成功 */
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-ADCIN-065 恢复成功后 rc=OK");
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-066 errs 单调递增(不因成功回落)");
|
||||
CHECK(adc_input_stm32_seq() == 2u, "TC-ADCIN-067 seq 只由 DMA 完成回调推进");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-ADCIN-065 after a successful recovery rc=OK");
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-066 errs increases monotonically (a success never lowers it)");
|
||||
CHECK(adc_input_stm32_seq() == 2u, "TC-ADCIN-067 seq is advanced only by the DMA completion callback");
|
||||
}
|
||||
|
||||
/* ===================== main ===================== */
|
||||
|
||||
@@ -0,0 +1,668 @@
|
||||
/*
|
||||
* 模块名称:配置存储后端(cfg_file_store_mcu)主机侧单元测试(L0.5)
|
||||
* 模块功能:在 PC 上离线验证 App/cfg/cfg_file_store_mcu.c 的五个卷操作 + 三个堆操作:
|
||||
* cfg_store_read / cfg_store_write_atomic / cfg_store_stat /
|
||||
* cfg_store_remove / 两个路径 getter / alloc-free-heap_free
|
||||
* 重心放在**原子写**与它的 v1.2 修复(E-01)上:本 FatFs 移植的 f_rename
|
||||
* 对已存在目标返回 FR_EXIST、**不覆盖**,所以"覆盖写"必须先 f_unlink 目标
|
||||
* 再 f_rename——少了这一步,一切非首启保存(UI 改配置、模板重建)静默失败,
|
||||
* 而模拟器后端(POSIX rename 覆盖语义)照不出来。
|
||||
*
|
||||
* 被测对象:真实源文件 App/cfg/cfg_file_store_mcu.c(#include 原样纳入)。
|
||||
* 假件与桩:
|
||||
* test/host/common/fatfs_fake.{h,c} —— 假 FatFs 内存卷(真实 ff.h 声明;
|
||||
* 它的 f_rename **如实返回 FR_EXIST**,正是 E-01 的那颗雷)
|
||||
* stub/FreeRTOS.h —— 三个分配器原型(签名逐条对齐 portable.h:163/164/166)
|
||||
* 假卷锁 / 假堆(可注入分配失败)写在本文件里
|
||||
*
|
||||
* 为什么要主机侧单测(而不是只靠台架):
|
||||
* 1. **E-01 原子写**:unlink→rename 这个非原子窗口是"掉电时配置暂失、开机按首启
|
||||
* 重建工厂模板"的有意取舍。任何一次"顺手优化"(比如给 rename 加覆盖、或把
|
||||
* unlink 挪到 rename 之后)都会让配置保存静默失败或留下脏 TMP,而这在 POSIX
|
||||
* 后端上完全看不出来。→ 假卷把 FR_EXIST 原样喂进来。
|
||||
* 2. **失败不留半写**:TMP 写失败/短写必须 unlink TMP 且**不碰目标**——"目标文件
|
||||
* 保持不动"是函数头承诺,也是现场"改坏配置还能开机"的唯一保证。
|
||||
* 3. **卷锁 try 语义**:v1.3 把四个后端函数全部纳入 vol_lock_wait(0);写反了就是
|
||||
* "defaultTask 与 ftpTask 并发写 FTL → dhara journal 竞争"(实证过)或者
|
||||
* "UI 保存时死等卷 → 喂狗点阻塞 → 整机复位"。四个函数的 try/release 配对
|
||||
* 只能靠计数断言。
|
||||
* 4. **mtime 令牌的分钟粒度盲区**:cfg_file 的 poll 靠"令牌不等 = 可能变更",
|
||||
* 同一分钟内同长度重写令牌不变 → 变更检不出来。这是设计接受的限制,但必须
|
||||
* 钉在测试里,否则后人会以为令牌是内容哈希。
|
||||
* 5. **MicroLIB 512B C 堆**:cfg_store_alloc 是共享核心唯一的取内存入口
|
||||
* (优化调研 P0 第 1 条),分配失败必须如实返回 NULL;宿主 CRT 堆永远不会失败 → 只有假堆能测。
|
||||
*
|
||||
* 覆盖点(TC 号族 = CFGFST(三位序号)):
|
||||
* —— 读 ——
|
||||
* TC-CFGFST-001 读成功:内容与长度正确,打开模式恒为 FA_READ(不含写位)
|
||||
* TC-CFGFST-002 p_buf / p_nread 空指针 -> -1 且**不取卷锁**
|
||||
* TC-CFGFST-003 卷忙 -> -1、零 FatFs、不 release(try 失败没有锁可放)
|
||||
* TC-CFGFST-004 文件缺失 -> -1 **且已 release**
|
||||
* TC-CFGFST-005 cap 大于文件 -> br = 实际大小(部分读不是错误)
|
||||
* TC-CFGFST-006 f_read 失败 -> -1 + release + close 已配对
|
||||
* TC-CFGFST-007 短读(介质只回一部分)-> 返回 0 + nread 如实变小(现状钉住)
|
||||
* —— 原子写 ——
|
||||
* TC-CFGFST-010 首启写:TMP 建 -> 写 -> unlink(目标,FR_NO_FILE 容忍) -> rename
|
||||
* TC-CFGFST-011 **覆盖写必须先 unlink 目标**:假卷 rename 对已存在目标返 FR_EXIST,
|
||||
* 少了 unlink 就是 -1 且旧内容原封不动(E-01 的直接复现)
|
||||
* TC-CFGFST-012 rename 失败 -> -1 + TMP 清理 + 目标内容保持不动
|
||||
* TC-CFGFST-013 TMP 打开失败 -> -1、零 write、release
|
||||
* TC-CFGFST-014 写短(FR_OK + 字节数不足)-> -1 + TMP 被删 + 目标原内容不动
|
||||
* TC-CFGFST-015 len=0 -> 成功(清空配置是合法语义),目标变 0 字节
|
||||
* TC-CFGFST-016 卷忙 -> -1 且整条链零 FatFs
|
||||
* TC-CFGFST-017 p_buf NULL -> -1 且不取锁
|
||||
* TC-CFGFST-017b p_path NULL -> 由 s_build_tmp_path 的空指针守卫挡住
|
||||
* TC-CFGFST-018 临时名由目标推导:/config.ini -> /config.TMP;/cfg/a.ini ->
|
||||
* /cfg/config.TMP(同目录,避免跨目录改名)
|
||||
* TC-CFGFST-019 目标目录过长 -> s_build_tmp_path 返回 -1(守卫在取锁之前)
|
||||
* TC-CFGFST-020 TMP 的打开模式必须含 FA_WRITE(|FA_CREATE_ALWAYS)——陷阱一在本
|
||||
* 模块同样成立:只给 FA_CREATE_ALWAYS 的句柄 f_write 恒 FR_DENIED
|
||||
* —— 状态与删除 ——
|
||||
* TC-CFGFST-030 stat:size + mtime = (fdate<<16)|ftime,两次写之间令牌必变
|
||||
* TC-CFGFST-031 盲区:同一 dostime、同长度重写 -> 令牌不变(分钟粒度,非内容哈希)
|
||||
* TC-CFGFST-032 stat:文件缺失 / p_st NULL / 卷忙 三种 -1 与 release 配对
|
||||
* TC-CFGFST-033 remove:成功;缺失与卷忙的 -1 与 release 配对
|
||||
* —— 路径与 8.3 ——
|
||||
* TC-CFGFST-040 config_path / tmp_path 返回值在严格 8.3 卷上真的打得开
|
||||
* TC-CFGFST-041 短名违规分档:体>8 / 扩展>3 / 非法字符 / 点开头 -> FR_INVALID_NAME;
|
||||
* 含 ':' -> FR_INVALID_DRIVE(假件保真,也是"改名=静默失败"的防线)
|
||||
* TC-CFGFST-042 陷阱二:非法名只让本次操作失败,**不触发任何卷级动作**
|
||||
* (条目总数不变、目录不没、不留 TMP)
|
||||
* —— 堆 ——
|
||||
* TC-CFGFST-050 alloc/free 走 FreeRTOS 堆:字节数增减可观测、成对归还
|
||||
* TC-CFGFST-051 分配失败注入 -> cfg_store_alloc 返回 NULL(MicroLIB 512B 的形态)
|
||||
* TC-CFGFST-052 cfg_store_free(NULL) 安全(不计数、不崩)
|
||||
* TC-CFGFST-053 heap_free 原样转发 xPortGetFreeHeapSize
|
||||
*
|
||||
* 本套照出的现状 —— E 项已在 task-4(2026-10-24)修掉(纯注释更正,无行为改动):
|
||||
* cfg_file_store_mcu.c 的 cfg_store_read "限定条件"原写"未挂载时 f_open 返回
|
||||
* FR_NO_SYSTEM"——FRESULT 里没有这个码(Lib/FatFs/ff.h 枚举共 20 个:
|
||||
* FR_OK..FR_INVALID_PARAMETER)。实际未挂载返回 FR_NOT_READY(3),卷上无有效 FAT
|
||||
* 返回 FR_NO_FILESYSTEM(13)。本模块对任何非 FR_OK 一律转 -1,所以运行结果一直
|
||||
* 是对的,危害在"后人照它写判据会写出永不成立的分支"。现已改成实际码,并写明
|
||||
* "不要枚举错误码集合"——唯一的例外是必须区分"删文件能不能修好"的自愈触发集
|
||||
* (App/log/sys_log_mcu.c 的 s_is_selfheal_code)。
|
||||
* 本套不新增断言:这条是注释层的事实错误,测试无从观测(生产行为未变)。
|
||||
*
|
||||
* 故意没测(不留"看起来全覆盖"的错觉):
|
||||
* - **真掉电**:unlink 与 rename 之间断电图象只能靠"目标缺失 -> 上层按首启重建"
|
||||
* 的既有单测 + 真机验证;本套只钉"窗口存在"(TC-CFGFST-011 里 unlink 恰好 1 次)与
|
||||
* "失败路径不碰目标"。
|
||||
* - 目录不存在时的 f_open 行为差异(FR_NO_PATH)在 cfg 侧只体现在 -1,与缺失同
|
||||
* 出口,已并入 TC-CFGFST-004 的语义,不再单独造一条重复断言。
|
||||
* - 卷未挂载(真实 ff.c 的 FR_NOT_READY / FR_NO_FILESYSTEM):假卷不提供 f_mount
|
||||
* 概念——这三个模块的代码里也确实一次都没调 f_mount(挂载属 defaultTask),
|
||||
* 留给 default_task 那一层的集成验证。
|
||||
* - cfg_file.c 共享核心(928 行)不在本批:它有自己的存量单测口径。
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
*
|
||||
* 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2)
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ff.h"
|
||||
#include "fatfs_fake.h"
|
||||
#include "FreeRTOS.h" /* 桩:三个分配器原型 */
|
||||
#include "cfg_file_store.h" /* 真实头 */
|
||||
#include "vol_lock.h" /* 真实头 */
|
||||
|
||||
/* ===================== 断言框架 ===================== */
|
||||
|
||||
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)
|
||||
|
||||
#define P_CFG "/config.ini"
|
||||
#define P_TMP "/config.TMP"
|
||||
|
||||
/* ===================== 假件与生产源码纳入 ===================== */
|
||||
|
||||
#include "fatfs_fake.c"
|
||||
#include "cfg_file_store_mcu.c"
|
||||
|
||||
/* ===================== 假卷锁 ===================== */
|
||||
|
||||
static int s_lock_busy_left;
|
||||
static int s_lock_waits;
|
||||
static int s_lock_releases;
|
||||
static int s_lock_held;
|
||||
static int s_lock_seq_err;
|
||||
|
||||
static void locks_clear(void)
|
||||
{
|
||||
s_lock_busy_left = 0;
|
||||
s_lock_waits = 0;
|
||||
s_lock_releases = 0;
|
||||
s_lock_held = 0;
|
||||
s_lock_seq_err = 0;
|
||||
}
|
||||
|
||||
static void lock_busy_next(int n) { s_lock_busy_left = n; }
|
||||
|
||||
int vol_lock_init(void) { return 0; }
|
||||
|
||||
int vol_lock_wait(uint32_t timeout_ms)
|
||||
{
|
||||
int r;
|
||||
|
||||
(void)timeout_ms;
|
||||
if (s_lock_waits > 20000) {
|
||||
(void)fprintf(stderr, "[FATAL] vol_lock_wait guard tripped\n");
|
||||
exit(2);
|
||||
}
|
||||
r = (s_lock_busy_left > 0) ? 0 : 1;
|
||||
if (s_lock_busy_left > 0) {
|
||||
s_lock_busy_left--;
|
||||
}
|
||||
s_lock_waits++;
|
||||
if (r != 0) {
|
||||
s_lock_held = 1;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
void vol_lock_release(void)
|
||||
{
|
||||
s_lock_releases++;
|
||||
if (s_lock_held == 0) {
|
||||
s_lock_seq_err++; /* try 失败却 release = 释放了别人的锁 */
|
||||
}
|
||||
s_lock_held = 0;
|
||||
}
|
||||
|
||||
/* ===================== 假 FreeRTOS 堆(可注入失败) ===================== */
|
||||
|
||||
#define FAKE_HEAP_CAP 4096u
|
||||
|
||||
static uint8_t s_heap_pool[FAKE_HEAP_CAP];
|
||||
static uint32_t s_heap_used;
|
||||
static int s_heap_fail; /* 1 = 下次 pvPortMalloc 直接返回 NULL */
|
||||
static int s_heap_alloc_calls;
|
||||
static int s_heap_free_calls;
|
||||
static int s_heap_free_err; /* free 了指针之外的地址 = 分配器被写脏 */
|
||||
|
||||
static void heap_clear(void)
|
||||
{
|
||||
s_heap_used = 0u;
|
||||
s_heap_fail = 0;
|
||||
s_heap_alloc_calls = 0;
|
||||
s_heap_free_calls = 0;
|
||||
s_heap_free_err = 0;
|
||||
}
|
||||
|
||||
void *pvPortMalloc(size_t xWantedSize)
|
||||
{
|
||||
void *p;
|
||||
|
||||
if (s_heap_alloc_calls++ > 20000) {
|
||||
(void)fprintf(stderr, "[FATAL] pvPortMalloc guard tripped\n");
|
||||
exit(2);
|
||||
}
|
||||
if ((s_heap_fail != 0) || (xWantedSize == 0u)
|
||||
|| ((s_heap_used + (uint32_t)xWantedSize) > FAKE_HEAP_CAP)) {
|
||||
return NULL;
|
||||
}
|
||||
p = (void *)(&s_heap_pool[s_heap_used]);
|
||||
s_heap_used += (uint32_t)xWantedSize;
|
||||
return p;
|
||||
}
|
||||
|
||||
void vPortFree(void *pv)
|
||||
{
|
||||
if (s_heap_free_calls++ > 20000) {
|
||||
(void)fprintf(stderr, "[FATAL] vPortFree guard tripped\n");
|
||||
exit(2);
|
||||
}
|
||||
if (pv == NULL) {
|
||||
return;
|
||||
}
|
||||
if (((uint8_t *)pv < &s_heap_pool[0])
|
||||
|| ((uint8_t *)pv >= &s_heap_pool[FAKE_HEAP_CAP])) {
|
||||
s_heap_free_err++; /* 释放栈地址/野指针 */
|
||||
return;
|
||||
}
|
||||
/* 只支持栈式使用(本模块的实际用法:借一块、用完还) */
|
||||
s_heap_used = (uint32_t)((uint8_t *)pv - &s_heap_pool[0]);
|
||||
}
|
||||
|
||||
size_t xPortGetFreeHeapSize(void)
|
||||
{
|
||||
return (size_t)(FAKE_HEAP_CAP - s_heap_used);
|
||||
}
|
||||
|
||||
/* ===================== 复位 ===================== */
|
||||
|
||||
static void reset_all(void)
|
||||
{
|
||||
ff_fake_reset();
|
||||
locks_clear();
|
||||
heap_clear();
|
||||
ff_fake_set_dostime(0x5A21u, 0x6E40u);
|
||||
}
|
||||
|
||||
/* ===================== 读 ===================== */
|
||||
|
||||
static void tc_read(void)
|
||||
{
|
||||
uint8_t buf[64];
|
||||
size_t n = 999u;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-CFGFST-001..007 read path --\n");
|
||||
|
||||
/* 001 成功 + 只读模式 */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"net ip=192.168.1.100\n", 20u);
|
||||
ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 20u)
|
||||
&& (memcmp(buf, "net ip=192.168.1.100\n", 20u) == 0)
|
||||
&& (g_ff_obs.last_open_mode == (BYTE)FA_READ)
|
||||
&& (s_lock_waits == 1) && (s_lock_releases == 1) && (s_lock_seq_err == 0);
|
||||
CHECK(ok, "TC-CFGFST-001 read ok: content and length correct, opened with FA_READ only (no write bit)");
|
||||
|
||||
/* 002 空指针守卫在取锁之前 */
|
||||
reset_all();
|
||||
ok = (cfg_store_read(P_CFG, NULL, sizeof(buf), &n) == -1)
|
||||
&& (cfg_store_read(P_CFG, buf, sizeof(buf), NULL) == -1);
|
||||
CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
|
||||
"TC-CFGFST-002 NULL pointer -> -1 and no volume lock taken (guard order correct)");
|
||||
|
||||
/* 003 卷忙 */
|
||||
reset_all();
|
||||
lock_busy_next(1);
|
||||
ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == -1);
|
||||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_lock_releases == 0)
|
||||
&& (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-003 volume busy -> -1: zero FatFs calls, no release (never release a lock we do not hold)");
|
||||
|
||||
/* 004 文件缺失:必须已放锁 */
|
||||
reset_all();
|
||||
ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == -1);
|
||||
CHECK(ok && (s_lock_releases == 1) && (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-004 missing file -> -1 **and the volume lock is released** (leaking it paralyses every later volume call)");
|
||||
|
||||
/* 005 cap > 文件大小 */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"abc", 3u);
|
||||
n = 999u;
|
||||
ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 3u)
|
||||
&& (g_ff_obs.last_read_btr == 64u) && (g_ff_obs.last_read_br == 3u);
|
||||
CHECK(ok, "TC-CFGFST-005 asked 64 got 3 -> nread=3 and still success (a short read is not an error)");
|
||||
|
||||
/* 006 f_read 失败 */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"abc", 3u);
|
||||
ff_fake_inject_rc(P_CFG, FF_FAKE_OP_READ, FR_DISK_ERR, 1);
|
||||
ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == -1);
|
||||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_CLOSE] == 1)
|
||||
&& (s_lock_releases == 1) && (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-006 read failure -> -1, close paired, lock released");
|
||||
|
||||
/* 007 介质短读:现状是"返回 0 + nread 如实变小",调用方靠 nread 定长度 */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"0123456789", 10u);
|
||||
ff_fake_inject_read_short(P_CFG, 4u, 1);
|
||||
n = 999u;
|
||||
ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 4u);
|
||||
CHECK(ok, "TC-CFGFST-007 injected short read -> returns 0 with nread=4 (caller truncates by nread)");
|
||||
}
|
||||
|
||||
/* ===================== 原子写 ===================== */
|
||||
|
||||
static void tc_write_atomic(void)
|
||||
{
|
||||
uint8_t buf[64];
|
||||
size_t n;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-CFGFST-010..020 atomic write (core of E-01) --\n");
|
||||
|
||||
/* 010 首启写(目标不存在) */
|
||||
reset_all();
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"v1", 2u) == 0);
|
||||
CHECK(ok && (ff_fake_size(P_CFG) == 2u) && (ff_fake_exists(P_TMP) == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 1)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) /* unlink 目标:FR_NO_FILE 无害 */
|
||||
&& (strcmp(g_ff_obs.last_rename_old, "/CONFIG.TMP") == 0)
|
||||
&& (strcmp(g_ff_obs.last_rename_new, "/CONFIG.INI") == 0)
|
||||
&& (s_lock_releases == 1) && (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-010 first write: TMP -> rename -> target 2 bytes, no TMP left behind");
|
||||
|
||||
/* 011 覆盖写:假卷的 f_rename 对已存在目标返回 FR_EXIST —— 这就是 E-01 */
|
||||
reset_all();
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"old-content", 11u) == 0);
|
||||
{
|
||||
int unlinks_before = g_ff_obs.calls[FF_FAKE_OP_UNLINK];
|
||||
ok = ok && (cfg_store_write_atomic(P_CFG, (const uint8_t *)"new", 3u) == 0);
|
||||
CHECK(ok && (ff_fake_size(P_CFG) == 3u)
|
||||
&& (ff_fake_fetch(P_CFG, buf, 0u, sizeof(buf)) == 3u)
|
||||
&& (memcmp(buf, "new", 3u) == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlinks_before + 1),
|
||||
"TC-CFGFST-011 overwrite: unlink target exactly once then rename -> new content lands (E-01)");
|
||||
}
|
||||
/* 同一条的"反面":直接 rename 到已存在目标必败(证明上一步的 unlink 不是多余的) */
|
||||
{
|
||||
static FIL probe;
|
||||
FRESULT fr;
|
||||
|
||||
(void)ff_fake_mkfile(P_TMP, (const uint8_t *)"x", 1u);
|
||||
fr = f_rename(P_TMP, P_CFG);
|
||||
CHECK(fr == FR_EXIST,
|
||||
"TC-CFGFST-011b fake fidelity: f_rename returns FR_EXIST when the target exists, no overwrite");
|
||||
if (fr == FR_OK) {
|
||||
(void)f_close(&probe);
|
||||
}
|
||||
}
|
||||
|
||||
/* 012 rename 失败 -> TMP 清理 + 目标不动 */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"keep-me", 7u);
|
||||
ff_fake_inject_rc(P_TMP, FF_FAKE_OP_RENAME, FR_DENIED, 1); /* 键 = 旧路径 */
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"replace", 7u) == -1);
|
||||
/* 目标在 rename 之前已被 unlink(非原子窗口),所以这里钉的是:
|
||||
* TMP 一定被清掉、锁一定释放、绝不留半成品 */
|
||||
CHECK(ok && (ff_fake_exists(P_TMP) == 0) && (ff_fake_exists(P_CFG) == 0)
|
||||
&& (s_lock_releases == 2) && (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-012 rename failure -> -1, TMP cleaned, lock released (the unlink->rename window really exists)");
|
||||
|
||||
/* 013 TMP 打不开 */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"orig", 4u);
|
||||
ff_fake_inject_rc(P_TMP, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"new", 3u) == -1);
|
||||
CHECK(ok && (ff_fake_size(P_CFG) == 4u) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 0)
|
||||
&& (s_lock_releases == 1) && (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-013 TMP open failure -> -1 and **not one byte of the target changed**");
|
||||
|
||||
/* 014 短写 = 失败:TMP 被删、目标不动、不 rename */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"good-config", 11u);
|
||||
/* 本次要写 11 字节,注入"只回 5"才算真短 */
|
||||
ff_fake_inject_write_short(P_TMP, 5u, 1);
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"bad-content", 11u) == -1);
|
||||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 1)
|
||||
&& (ff_fake_exists(P_TMP) == 0)
|
||||
&& (ff_fake_size(P_CFG) == 11u)
|
||||
&& (ff_fake_fetch(P_CFG, buf, 0u, sizeof(buf)) == 11u)
|
||||
&& (memcmp(buf, "good-config", 11u) == 0)
|
||||
&& (s_lock_seq_err == 0),
|
||||
"TC-CFGFST-014 TMP short write is a failure -> TMP deleted, no rename, target keeps old content");
|
||||
|
||||
/* 015 len = 0 合法(清空配置) */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"x", 1u);
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"", 0u) == 0);
|
||||
n = 999u;
|
||||
CHECK(ok && (ff_fake_size(P_CFG) == 0u)
|
||||
&& (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 0u),
|
||||
"TC-CFGFST-015 writing 0 bytes succeeds (config cleared), read-back gives nread=0");
|
||||
|
||||
/* 016 卷忙:整条链零 I/O */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"orig", 4u);
|
||||
lock_busy_next(1);
|
||||
ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"new", 3u) == -1);
|
||||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0)
|
||||
&& (ff_fake_size(P_CFG) == 4u) && (s_lock_releases == 0),
|
||||
"TC-CFGFST-016 volume busy -> -1: no TMP created, target untouched, no release");
|
||||
|
||||
/* 017 p_buf NULL */
|
||||
reset_all();
|
||||
ok = (cfg_store_write_atomic(P_CFG, NULL, 3u) == -1);
|
||||
CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
|
||||
"TC-CFGFST-017 p_buf NULL -> -1 and no lock taken");
|
||||
|
||||
/* 017b 目标路径本身是 NULL:s_build_tmp_path 的空指针分支(读函数有守卫,
|
||||
* 写函数**没有** p_path 的显式守卫,全靠推导函数把它挡在取锁之前) */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(P_CFG, (const uint8_t *)"keep", 4u);
|
||||
ok = (cfg_store_write_atomic(NULL, (const uint8_t *)"x", 1u) == -1);
|
||||
CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
|
||||
&& (ff_fake_size(P_CFG) == 4u),
|
||||
"TC-CFGFST-017b p_path NULL -> -1: no lock, no file open, target untouched");
|
||||
|
||||
/* 018 临时名由目标推导(同目录) */
|
||||
reset_all();
|
||||
(void)ff_fake_mkdir_direct("/cfg");
|
||||
ok = (cfg_store_write_atomic("/cfg/a.ini", (const uint8_t *)"z", 1u) == 0);
|
||||
CHECK(ok && (strcmp(g_ff_obs.last_open_path, "/CFG/CONFIG.TMP") == 0)
|
||||
&& (ff_fake_exists("/cfg/config.TMP") == 0) /* 已被 rename 走,不是留档 */
|
||||
&& (strcmp(g_ff_obs.last_rename_new, "/CFG/A.INI") == 0)
|
||||
&& (ff_fake_size("/cfg/a.ini") == 1u),
|
||||
"TC-CFGFST-018 target /cfg/a.ini -> temp file lands in the same directory /cfg/config.TMP");
|
||||
CHECK(strcmp(cfg_store_tmp_path(), P_TMP) == 0,
|
||||
"TC-CFGFST-018b getter still reports the volume root /config.TMP (same source as s_path_tmp)");
|
||||
|
||||
/* 019 目录太长 -> 推导失败,且发生在取锁之前 */
|
||||
reset_all();
|
||||
ok = (cfg_store_write_atomic("/this/is/a/very/deep/dir/name/x.ini",
|
||||
(const uint8_t *)"y", 1u) == -1);
|
||||
CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0),
|
||||
"TC-CFGFST-019 temp name does not fit the buffer -> -1, no lock (never open a file with a truncated path)");
|
||||
|
||||
/* 020 陷阱一在本模块同样成立 */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"m", 1u);
|
||||
CHECK((g_ff_obs.last_open_mode & FA_WRITE) != 0u,
|
||||
"TC-CFGFST-020 the TMP handle must carry FA_WRITE (CREATE_ALWAYS here, not APPEND)");
|
||||
reset_all();
|
||||
{
|
||||
static FIL probe;
|
||||
UINT bw = 7u;
|
||||
FRESULT fr1;
|
||||
FRESULT fr2;
|
||||
|
||||
fr1 = f_open(&probe, P_TMP, FA_CREATE_ALWAYS); /* 故意漏掉 FA_WRITE */
|
||||
fr2 = f_write(&probe, "abc", 3u, &bw);
|
||||
(void)f_close(&probe);
|
||||
CHECK((fr1 == FR_OK) && (fr2 == FR_DENIED) && (bw == 0u),
|
||||
"TC-CFGFST-020b FA_CREATE_ALWAYS alone -> open succeeds but f_write is always FR_DENIED");
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== stat / remove ===================== */
|
||||
|
||||
static void tc_stat_remove(void)
|
||||
{
|
||||
cfg_file_stat_t st;
|
||||
uint32_t tok1;
|
||||
uint32_t tok2;
|
||||
int rel;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-CFGFST-030..033 stat token and remove --\n");
|
||||
|
||||
/* 030 size + 令牌 */
|
||||
reset_all();
|
||||
ff_fake_set_dostime(0x5A21u, 0x6E40u);
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"abcd", 4u);
|
||||
tok1 = ((0x5A21u << 16) | 0x6E40u);
|
||||
ok = (cfg_store_stat(P_CFG, &st) == 0) && (st.size == 4u) && (st.mtime == tok1);
|
||||
ff_fake_set_dostime(0x5A22u, 0x6E40u); /* 次日同一时刻 */
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"efgh", 4u);
|
||||
tok2 = ((0x5A22u << 16) | 0x6E40u);
|
||||
ok = ok && (cfg_store_stat(P_CFG, &st) == 0) && (st.mtime == tok2)
|
||||
&& (tok1 != tok2);
|
||||
CHECK(ok && (s_lock_seq_err == 0) && (s_lock_releases == s_lock_waits),
|
||||
"TC-CFGFST-030 token = (fdate<<16)|ftime, a date rollover must change the token");
|
||||
|
||||
/* 031 盲区:同一分钟、同长度重写 -> 令牌不变 */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"aaaa", 4u);
|
||||
(void)cfg_store_stat(P_CFG, &st);
|
||||
ok = (st.size == 4u);
|
||||
{
|
||||
uint32_t before = st.mtime;
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"bbbb", 4u);
|
||||
(void)cfg_store_stat(P_CFG, &st);
|
||||
CHECK(ok && (st.size == 4u) && (st.mtime == before),
|
||||
"TC-CFGFST-031 rewrite in the same minute with the same length -> token unchanged (poll cannot see it, accepted by design)");
|
||||
}
|
||||
|
||||
/* 032 stat 三种 -1 */
|
||||
reset_all();
|
||||
rel = s_lock_releases;
|
||||
ok = (cfg_store_stat(P_CFG, NULL) == -1) && (s_lock_releases == rel); /* 守卫在取锁前 */
|
||||
ok = ok && (cfg_store_stat(P_CFG, &st) == -1) && (s_lock_releases == rel + 1);
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"x", 1u);
|
||||
rel = s_lock_releases;
|
||||
lock_busy_next(1);
|
||||
ok = ok && (cfg_store_stat(P_CFG, &st) == -1) && (s_lock_releases == rel);
|
||||
rel = s_lock_releases;
|
||||
ff_fake_inject_rc(P_CFG, FF_FAKE_OP_STAT, FR_DISK_ERR, 1);
|
||||
ok = ok && (cfg_store_stat(P_CFG, &st) == -1) && (s_lock_releases == rel + 1)
|
||||
&& (s_lock_seq_err == 0);
|
||||
CHECK(ok, "TC-CFGFST-032 stat: lock pairing on all four exits (NULL guard / missing / busy / read error)");
|
||||
|
||||
/* 033 remove */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"x", 1u);
|
||||
rel = s_lock_releases;
|
||||
ok = (cfg_store_remove(P_CFG) == 0) && (ff_fake_exists(P_CFG) == 0)
|
||||
&& (s_lock_releases == rel + 1);
|
||||
ok = ok && (cfg_store_remove(P_CFG) == -1) /* 再删:文件已不在 */
|
||||
&& (s_lock_releases == rel + 2);
|
||||
lock_busy_next(1);
|
||||
ok = ok && (cfg_store_remove(P_CFG) == -1) && (s_lock_releases == rel + 2)
|
||||
&& (s_lock_seq_err == 0);
|
||||
CHECK(ok, "TC-CFGFST-033 remove: success / -1 when already gone / -1 when busy, each with lock pairing");
|
||||
}
|
||||
|
||||
/* ===================== 路径与 8.3(陷阱二) ===================== */
|
||||
|
||||
static void tc_names(void)
|
||||
{
|
||||
static FIL probe;
|
||||
uint8_t rbuf[16];
|
||||
size_t rn;
|
||||
cfg_file_stat_t st_dummy;
|
||||
uint32_t ents_before;
|
||||
int rel;
|
||||
FRESULT fr_body;
|
||||
FRESULT fr_ext;
|
||||
FRESULT fr_bad;
|
||||
FRESULT fr_dot;
|
||||
FRESULT fr_colon;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-CFGFST-040..042 path legality, name errors never escalate to volume ops --\n");
|
||||
|
||||
/* 040 生产路径真的可用(改名即静默失败的第一道防线) */
|
||||
reset_all();
|
||||
ok = (strcmp(cfg_store_config_path(), P_CFG) == 0)
|
||||
&& (f_open(&probe, cfg_store_config_path(), FA_WRITE | FA_CREATE_ALWAYS) == FR_OK)
|
||||
&& (f_close(&probe) == FR_OK)
|
||||
&& (f_open(&probe, cfg_store_tmp_path(), FA_WRITE | FA_CREATE_ALWAYS) == FR_OK)
|
||||
&& (f_close(&probe) == FR_OK);
|
||||
CHECK(ok, "TC-CFGFST-040 /config.ini and /config.TMP are valid 8.3 names and really get created on the volume");
|
||||
|
||||
/* 041 短名违规分档 */
|
||||
reset_all();
|
||||
fr_body = f_open(&probe, "/configlooooong.ini", FA_WRITE | FA_CREATE_ALWAYS);
|
||||
if (fr_body == FR_OK) { (void)f_close(&probe); }
|
||||
fr_ext = f_open(&probe, "/config.ini2", FA_WRITE | FA_CREATE_ALWAYS);
|
||||
if (fr_ext == FR_OK) { (void)f_close(&probe); }
|
||||
fr_bad = f_open(&probe, "/conf?ig.ini", FA_WRITE | FA_CREATE_ALWAYS);
|
||||
if (fr_bad == FR_OK) { (void)f_close(&probe); }
|
||||
fr_dot = f_open(&probe, "/.fschk", FA_WRITE | FA_CREATE_ALWAYS);
|
||||
if (fr_dot == FR_OK) { (void)f_close(&probe); }
|
||||
fr_colon = f_open(&probe, "1:/config.ini", FA_READ);
|
||||
if (fr_colon == FR_OK) { (void)f_close(&probe); }
|
||||
CHECK((fr_body == FR_INVALID_NAME) && (fr_ext == FR_INVALID_NAME)
|
||||
&& (fr_bad == FR_INVALID_NAME) && (fr_dot == FR_INVALID_NAME)
|
||||
&& (fr_colon == FR_INVALID_DRIVE),
|
||||
"TC-CFGFST-041 classes: body>8 / ext>3 / bad char / leading dot -> FR_INVALID_NAME; ':' -> FR_INVALID_DRIVE");
|
||||
|
||||
/* 042 名字类错误码绝不升级成卷操作:非法目标名走一遍原子写 */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"keep", 4u);
|
||||
ents_before = (uint32_t)ff_fake_count_entries();
|
||||
rn = 0u;
|
||||
rel = s_lock_releases;
|
||||
ok = (cfg_store_read("/.fschk", rbuf, sizeof(rbuf), &rn) == -1)
|
||||
&& (cfg_store_stat("/.fschk", &st_dummy) == -1)
|
||||
&& (cfg_store_remove("/.fschk") == -1);
|
||||
CHECK(ok && ((uint32_t)ff_fake_count_entries() == ents_before)
|
||||
&& (ff_fake_size(P_CFG) == 4u) && (s_lock_seq_err == 0)
|
||||
&& (s_lock_releases == rel + 3),
|
||||
"TC-CFGFST-042 an illegal name fails only this call: entry count unchanged, existing config intact, lock always released");
|
||||
/* 非法目标名进原子写:推导出的 TMP 也非法 -> 第一步开文件就失败,
|
||||
* 绝不能把已经存在的目标删掉 */
|
||||
reset_all();
|
||||
(void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"keep", 4u);
|
||||
ents_before = (uint32_t)ff_fake_count_entries();
|
||||
ok = (cfg_store_write_atomic("/badname-tooooo-long.ini", (const uint8_t *)"x", 1u) == -1);
|
||||
CHECK(ok && (ff_fake_size(P_CFG) == 4u)
|
||||
&& ((uint32_t)ff_fake_count_entries() == ents_before),
|
||||
"TC-CFGFST-042b an atomic write to an illegal target fails whole, **never touching the live config file**");
|
||||
}
|
||||
|
||||
/* ===================== 堆 ===================== */
|
||||
|
||||
static void tc_heap(void)
|
||||
{
|
||||
void *p;
|
||||
void *q;
|
||||
|
||||
printf("-- TC-CFGFST-050..053 allocation backend (P0 item 1 of the optimisation study) --\n");
|
||||
|
||||
/* 050 成对归还 */
|
||||
reset_all();
|
||||
p = cfg_store_alloc(256u);
|
||||
(void)cfg_store_free(p);
|
||||
CHECK((p != NULL) && (s_heap_alloc_calls == 1) && (s_heap_free_calls == 1)
|
||||
&& (s_heap_used == 0u) && (s_heap_free_err == 0),
|
||||
"TC-CFGFST-050 alloc/free go through the FreeRTOS heap and return in pairs (give back what was taken)");
|
||||
|
||||
/* 051 分配失败如实返回 NULL */
|
||||
reset_all();
|
||||
s_heap_fail = 1;
|
||||
p = cfg_store_alloc(2048u);
|
||||
q = cfg_store_alloc(8u);
|
||||
cfg_store_free(p); /* NULL 必须安全 */
|
||||
CHECK((p == NULL) && (q == NULL) && (s_heap_free_calls == 0),
|
||||
"TC-CFGFST-051 heap exhausted -> cfg_store_alloc returns NULL (the real MicroLIB 512B shape)");
|
||||
|
||||
/* 052 free(NULL) 安全 + 不计数 */
|
||||
reset_all();
|
||||
cfg_store_free(NULL);
|
||||
CHECK((s_heap_free_calls == 0) && (s_heap_free_err == 0),
|
||||
"TC-CFGFST-052 cfg_store_free(NULL) does not touch the allocator");
|
||||
|
||||
/* 053 heap_free 转发 */
|
||||
reset_all();
|
||||
(void)cfg_store_alloc(100u);
|
||||
CHECK(cfg_store_heap_free() == (FAKE_HEAP_CAP - 100u),
|
||||
"TC-CFGFST-053 cfg_store_heap_free forwards xPortGetFreeHeapSize verbatim");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== Cfg File Store Mcu Tests ===\n");
|
||||
|
||||
tc_read();
|
||||
tc_write_atomic();
|
||||
tc_stat_remove();
|
||||
tc_names();
|
||||
tc_heap();
|
||||
|
||||
printf("\n=== Cfg File Store Mcu Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""配置存储后端(cfg_file_store_mcu)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c(以
|
||||
#include 纳入**生产源** App/cfg/cfg_file_store_mcu.c,并纳入共享假卷
|
||||
test/host/common/fatfs_fake.c),在 PC 上离线跑读、原子写(E-01 的
|
||||
unlink->rename 全链)、状态令牌、删除、路径 8.3 合规、假堆分配,失败即非 0 退出。
|
||||
工具链与判定委托 test/host/common/unit_test_harness.py。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
1. **E-01 原子写**:本仓 FatFs 移植的 f_rename 对**已存在目标返回 FR_EXIST、不
|
||||
覆盖**,所以覆盖写必须先 unlink 目标。模拟器后端(POSIX rename 覆盖语义)
|
||||
永远照不出这一条 → 曾经"一切非首启保存静默失败"。假卷按真实 ff.c 返回
|
||||
FR_EXIST,把这条做成必然失败的断言。
|
||||
2. **失败不留半写**:TMP 打开失败 / 短写 / rename 失败三条路径都必须"目标原内容
|
||||
一个字节没动 + TMP 清理干净 + 卷锁已释放",这是现场"改坏配置还能开机"的前提。
|
||||
3. **卷锁 try 语义(v1.3)**:四个后端函数全在 vol_lock_wait(0) 区间内;写反了
|
||||
就是与 ftpTask 并发写 FTL(dhara journal 竞争实证过),或 UI 保存死等卷把
|
||||
喂狗点阻塞死。只能靠调用计数断言。
|
||||
4. **mtime 令牌是 FAT 打包时间、不是内容哈希**:同一分钟同长度重写检不出变更,
|
||||
这个盲区必须钉在测试里,否则后人会拿它当哈希用。
|
||||
5. **MicroLIB C 堆只有 512 B**(startup Heap_Size=0x200):cfg_store_alloc 是
|
||||
共享核心唯一取内存入口,分配失败必须如实返回 NULL —— 宿主 CRT 堆永不失败。
|
||||
|
||||
桩的边界:ff.h / cfg_file_store.h / vol_lock.h 用**真实头**;FreeRTOS.h 是桩
|
||||
(真实头会拉整棵 portmacro),三条原型逐字对齐 portable.h:163/164/166。
|
||||
FatFs 由 test/host/common/fatfs_fake.c 给 RAM 实现(**显式纳入,不用目录通配**,
|
||||
避免被卷进别人的构建);假卷锁与假堆强符号写在本测试单元里。
|
||||
|
||||
用法(仓库根目录):
|
||||
& '.venv\Scripts\python.exe' test/host/cfg_file_store_mcu/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
|
||||
|
||||
设计依据:docs/子系统设计/配置文件设计说明.md §6.2(mtime 令牌)、
|
||||
docs/架构设计/STM32F4-Base存储架构说明.md §10.1 第 10/11 条、
|
||||
docs/测试说明/主机单测覆盖率口径.md §9 P2 第 2 项
|
||||
创建日期:2026-10-24
|
||||
"""
|
||||
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 必须最先(覆盖 FreeRTOS.h),其余全用真实头。
|
||||
# 假卷 fatfs_fake.c 由测试单元 #include 纳入(同 ch395f_fake.c 惯例,显式列文件、不通配)。
|
||||
BUILD = TestBuild(
|
||||
name="Cfg File Store Mcu Tests",
|
||||
src=os.path.join(HERE, "cfg_file_store_mcu_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "test", "host", "common"),
|
||||
os.path.join(ROOT, "App", "cfg"),
|
||||
os.path.join(ROOT, "App", "fs"),
|
||||
os.path.join(ROOT, "Lib", "FatFs"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== Cfg File Store Mcu Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - FreeRTOS.h(cfg_file_store_mcu 套件专用)
|
||||
* 模块功能:App/cfg/cfg_file_store_mcu.c 只经这个头拿三个分配器原型
|
||||
* (pvPortMalloc / vPortFree / xPortGetFreeHeapSize),它自己不碰任何内核
|
||||
* API——卷是 vol_lock 的事、卷操作是 ff.h 的事。真实 FreeRTOS.h 会拉整棵
|
||||
* portmacro + 编译器抽象层,PC 上编不动,所以只给这三条原型。
|
||||
*
|
||||
* ⚠ 仅供 test/host/cfg_file_store_mcu/ 使用,不参与 MCU 构建。
|
||||
*
|
||||
* 签名逐条对齐真实头(改一处即编译期报错,这就是桩存在的意义):
|
||||
* Middlewares/Third_Party/FreeRTOS/Source/include/portable.h:163
|
||||
* void *pvPortMalloc( size_t xSize ) PRIVILEGED_FUNCTION;
|
||||
* 同文件 :164
|
||||
* void vPortFree( void *pv ) PRIVILEGED_FUNCTION;
|
||||
* 同文件 :166
|
||||
* size_t xPortGetFreeHeapSize( void ) PRIVILEGED_FUNCTION;
|
||||
* (真实实现 Middlewares/.../portable/MemMang/heap_4.c:315 定义 xPortGetFreeHeapSize)
|
||||
* PRIVILEGED_FUNCTION 在 pc 端口展开为空,桩里不写等价。
|
||||
*
|
||||
* 实现放在测试单元里,是一份**可注入失败的假堆**:真机上 MicroLIB 的 C 堆只有
|
||||
* 512 B(startup Heap_Size=0x200),配置引擎的 KB 级临时缓冲必然走 FreeRTOS 堆,
|
||||
* 堆见底时 pvPortMalloc 返回 NULL —— 这条分支模拟器(宿主 CRT 堆)永远走不到,
|
||||
* 只有假堆能稳定构造(cfg_store_alloc 是共享核心的唯一取内存入口,必须测)。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
#ifndef FREERTOS_H
|
||||
#define FREERTOS_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
void *pvPortMalloc(size_t xWantedSize);
|
||||
void vPortFree(void *pv);
|
||||
size_t xPortGetFreeHeapSize(void);
|
||||
|
||||
#endif /* FREERTOS_H */
|
||||
@@ -9,8 +9,13 @@
|
||||
|
||||
#include "ch395f_fake.h"
|
||||
|
||||
/* 时基:假复位机的最短沉降窗要用(与真实 main.h 同名同签名,各测试 TU 自带实现) */
|
||||
uint32_t HAL_GetTick(void);
|
||||
|
||||
ch395f_fake_world_t g_fake;
|
||||
ch395f_fake_obs_t g_chip;
|
||||
ch395f_fake_ext_t g_fx;
|
||||
ch395f_fake_ext_obs_t g_fxchip;
|
||||
|
||||
static void fake_log(char c)
|
||||
{
|
||||
@@ -35,6 +40,33 @@ void ch395f_fake_reset(void)
|
||||
g_fake.open_rc[i] = CH395F_ERR_SUCCESS;
|
||||
g_fake.listen_rc[i] = CH395F_ERR_SUCCESS;
|
||||
}
|
||||
/* 扩展区(net_socket 批次新增):同一次复位里一并清零,避免场景间残留 */
|
||||
ch395f_fake_ext_reset();
|
||||
}
|
||||
|
||||
/* 扩展面的复位:默认"芯片在位、复位/协议栈初始化成功、版本够新、复位机全 IDLE" */
|
||||
void ch395f_fake_ext_reset(void)
|
||||
{
|
||||
int i;
|
||||
|
||||
(void)memset(&g_fx, 0, sizeof(g_fx));
|
||||
(void)memset(&g_fxchip, 0, sizeof(g_fxchip));
|
||||
g_fx.exist_rc = (int)CH395F_STATUS_OK;
|
||||
g_fx.reset_rc = (int)CH395F_STATUS_OK;
|
||||
g_fx.init_rc = (int)CH395F_STATUS_OK;
|
||||
g_fx.version = 0x46U; /* 台架实测芯片版本(版本门控的默认通过值)*/
|
||||
g_fx.phy_status = 0x08U; /* PHY 100M_FULL(非 DISCONN,glob_event_feed 判 link up)*/
|
||||
for (i = 0; i < CH395F_FAKE_SOCK_MAX; i++) {
|
||||
g_fx.connect_rc[i] = CH395F_ERR_SUCCESS;
|
||||
g_fx.rst_state[i] = (uint8_t)CH395F_FAKE_RST_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_fake_push_glob(uint16_t v)
|
||||
{
|
||||
if (g_fx.glob_n < CH395F_FAKE_GLOB_QUEUE) {
|
||||
g_fx.glob_q[g_fx.glob_n++] = v;
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_fake_push_recv_len(uint8_t sock, uint16_t v)
|
||||
@@ -94,7 +126,9 @@ void ch395f_get_socket_status(uint8_t sock, uint8_t *status)
|
||||
}
|
||||
}
|
||||
|
||||
/* 整体读即清:返回后该 socket 的挂起位图清零(真实芯片没有按位清除手段) */
|
||||
/* 整体读即清:返回后该 socket 的挂起位图清零(真实芯片没有按位清除手段)。
|
||||
* 唯一新增:清完立刻回填**电平型**掩码 sint_sticky(默认全 0 → 与既有行为逐字相同,
|
||||
* net_listener / net_events / net_pending 三套不受影响)。 */
|
||||
uint8_t ch395f_get_sock_int_status(uint8_t sock)
|
||||
{
|
||||
uint8_t v = 0U;
|
||||
@@ -104,11 +138,37 @@ uint8_t ch395f_get_sock_int_status(uint8_t sock)
|
||||
fake_log('i');
|
||||
if (sock < CH395F_FAKE_SOCK_MAX) {
|
||||
v = g_fake.int_status[sock];
|
||||
g_fake.int_status[sock] = 0U;
|
||||
g_fake.int_status[sock] = g_fx.sint_sticky[sock];
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void ch395f_fake_clear_log(void)
|
||||
{
|
||||
(void)memset(g_chip.log, 0, sizeof(g_chip.log));
|
||||
g_chip.log_n = 0;
|
||||
}
|
||||
|
||||
int ch395f_fake_log_find(char c1)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < g_chip.log_n; i++) {
|
||||
if (g_chip.log[i] == c1) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ch395f_fake_set_sint_sticky(uint8_t sock, uint8_t mask)
|
||||
{
|
||||
if (sock < CH395F_FAKE_SOCK_MAX) {
|
||||
g_fx.sint_sticky[sock] = mask;
|
||||
g_fake.int_status[sock] |= mask;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t ch395f_get_recv_len(uint8_t sock)
|
||||
{
|
||||
uint16_t v;
|
||||
@@ -134,10 +194,33 @@ void ch395f_read_recv_buf(uint8_t sock, uint8_t *p_data, uint16_t len)
|
||||
g_chip.last_read_len = len;
|
||||
fake_log('r');
|
||||
if ((p_data != NULL) && (len > 0U)) {
|
||||
(void)memset(p_data, 0, len); /* 交出去的是零,测试不依赖内容 */
|
||||
if (g_fx.recv_bytes_n > 0U) {
|
||||
/* 已预置内容:按预置交付,不足部分补零(默认分支行为不变) */
|
||||
uint16_t n = (len < g_fx.recv_bytes_n) ? len : g_fx.recv_bytes_n;
|
||||
|
||||
(void)memcpy(p_data, g_fx.recv_bytes, n);
|
||||
if (n < len) {
|
||||
(void)memset((uint8_t *)p_data + n, 0, (size_t)(len - n));
|
||||
}
|
||||
} else {
|
||||
(void)memset(p_data, 0, len); /* 交出去的是零,测试不依赖内容 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_fake_set_recv_bytes(const uint8_t *p, uint16_t n)
|
||||
{
|
||||
uint16_t i;
|
||||
|
||||
if (n > (uint16_t)sizeof(g_fx.recv_bytes)) {
|
||||
n = (uint16_t)sizeof(g_fx.recv_bytes);
|
||||
}
|
||||
for (i = 0U; i < n; i++) {
|
||||
g_fx.recv_bytes[i] = (p != NULL) ? p[i] : (uint8_t)0xA5U;
|
||||
}
|
||||
g_fx.recv_bytes_n = n;
|
||||
}
|
||||
|
||||
void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len)
|
||||
{
|
||||
(void)p_data;
|
||||
@@ -215,3 +298,344 @@ uint16_t ch395f_link_change_count(void)
|
||||
{
|
||||
return g_fake.link_change;
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
* 扩展区实现(net_socket 主机单测批次;纯新增,上面既有实现一字未动)
|
||||
* 签名逐字抄自 Drivers/BSP/CH395F/ch395f.h。
|
||||
* ========================================================================= */
|
||||
|
||||
ch395f_status_t ch395f_check_exist(void)
|
||||
{
|
||||
g_fxchip.exist_calls++;
|
||||
fake_log('E');
|
||||
return (ch395f_status_t)g_fx.exist_rc;
|
||||
}
|
||||
|
||||
ch395f_status_t ch395f_reset(void)
|
||||
{
|
||||
g_fxchip.reset_calls++;
|
||||
fake_log('Z');
|
||||
return (ch395f_status_t)g_fx.reset_rc;
|
||||
}
|
||||
|
||||
ch395f_status_t ch395f_init(void)
|
||||
{
|
||||
g_fxchip.init_calls++;
|
||||
fake_log('n');
|
||||
return (ch395f_status_t)g_fx.init_rc;
|
||||
}
|
||||
|
||||
uint8_t ch395f_get_version(void)
|
||||
{
|
||||
return g_fx.version;
|
||||
}
|
||||
|
||||
void ch395f_set_fun_para(uint32_t para)
|
||||
{
|
||||
g_fxchip.set_fun_para_calls++;
|
||||
g_fx.fun_para = para;
|
||||
fake_log('F');
|
||||
}
|
||||
|
||||
void ch395f_set_tcp_mss(uint16_t mss)
|
||||
{
|
||||
g_fxchip.set_mss_calls++;
|
||||
g_fx.mss = mss;
|
||||
fake_log('Q');
|
||||
}
|
||||
|
||||
void ch395f_set_retrans_count(uint8_t cnt)
|
||||
{
|
||||
g_fxchip.set_retrans_calls++;
|
||||
g_fx.retrans_cnt = cnt;
|
||||
}
|
||||
|
||||
void ch395f_set_retrans_period(uint16_t period)
|
||||
{
|
||||
g_fx.retrans_period = period;
|
||||
}
|
||||
|
||||
void ch395f_set_keepalive_idle(uint32_t time)
|
||||
{
|
||||
g_fxchip.set_ka_calls++;
|
||||
g_fx.ka_idle = time;
|
||||
}
|
||||
|
||||
void ch395f_set_keepalive_intvl(uint32_t time)
|
||||
{
|
||||
g_fx.ka_intvl = time;
|
||||
}
|
||||
|
||||
void ch395f_set_keepalive_cnt(uint8_t cnt)
|
||||
{
|
||||
g_fx.ka_cnt = cnt;
|
||||
}
|
||||
|
||||
void ch395f_set_dhcp(uint8_t enable)
|
||||
{
|
||||
g_fxchip.set_dhcp_calls++;
|
||||
g_fx.dhcp_enable = enable;
|
||||
fake_log('D');
|
||||
}
|
||||
|
||||
void ch395f_ping_enable(uint8_t enable)
|
||||
{
|
||||
g_fxchip.ping_calls++;
|
||||
g_fx.ping_enable = enable;
|
||||
}
|
||||
|
||||
void ch395f_set_ip_addr(uint8_t *p_ip)
|
||||
{
|
||||
g_fxchip.set_ip_calls++;
|
||||
fake_log('I');
|
||||
if (p_ip != NULL) {
|
||||
(void)memcpy(g_fx.ip, p_ip, 4U);
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_set_gwip_addr(uint8_t *p_gwip)
|
||||
{
|
||||
g_fxchip.set_gw_calls++;
|
||||
fake_log('N');
|
||||
if (p_gwip != NULL) {
|
||||
(void)memcpy(g_fx.gw, p_gwip, 4U);
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_set_mask_addr(uint8_t *p_mask)
|
||||
{
|
||||
g_fxchip.set_mask_calls++;
|
||||
fake_log('K');
|
||||
if (p_mask != NULL) {
|
||||
(void)memcpy(g_fx.mask, p_mask, 4U);
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_set_mac_addr(uint8_t *p_mac)
|
||||
{
|
||||
g_fxchip.set_mac_calls++;
|
||||
fake_log('M');
|
||||
if (p_mac != NULL) {
|
||||
(void)memcpy(g_fx.mac, p_mac, 6U);
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_set_send_buf(uint8_t sock, uint8_t start_block, uint8_t block_count)
|
||||
{
|
||||
g_fxchip.set_send_buf_calls++;
|
||||
fake_log('b');
|
||||
if (sock < CH395F_FAKE_SOCK_MAX) {
|
||||
g_fx.send_blk_start[sock] = start_block;
|
||||
g_fx.send_blk_cnt[sock] = block_count;
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_set_recv_buf(uint8_t sock, uint8_t start_block, uint8_t block_count)
|
||||
{
|
||||
g_fxchip.set_recv_buf_calls++;
|
||||
fake_log('u');
|
||||
if (sock < CH395F_FAKE_SOCK_MAX) {
|
||||
g_fx.recv_blk_start[sock] = start_block;
|
||||
g_fx.recv_blk_cnt[sock] = block_count;
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_set_des_ip(uint8_t sock, uint8_t *p_ip)
|
||||
{
|
||||
g_fxchip.set_des_ip_calls++;
|
||||
fake_log('e');
|
||||
if ((sock < CH395F_FAKE_SOCK_MAX) && (p_ip != NULL)) {
|
||||
(void)memcpy(g_fx.des_ip[sock], p_ip, 4U);
|
||||
}
|
||||
}
|
||||
|
||||
void ch395f_set_des_port(uint8_t sock, uint16_t port)
|
||||
{
|
||||
g_fxchip.set_des_port_calls++;
|
||||
fake_log('q');
|
||||
if (sock < CH395F_FAKE_SOCK_MAX) {
|
||||
g_fx.des_port[sock] = port;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t ch395f_tcp_connect(uint8_t sock)
|
||||
{
|
||||
g_fxchip.connect_calls++;
|
||||
g_chip.last_sock = sock;
|
||||
fake_log('T');
|
||||
return (sock < CH395F_FAKE_SOCK_MAX) ? g_fx.connect_rc[sock] : 0x01U;
|
||||
}
|
||||
|
||||
uint8_t ch395f_get_phy_status(void)
|
||||
{
|
||||
fake_log('g');
|
||||
return g_fx.phy_status;
|
||||
}
|
||||
|
||||
/* 全局中断状态(0x19,2 字节):**整体读即清**,且含全部 Socket 位——
|
||||
* 假件按调用序号返回预置值(耗尽后回落最后一个),并单独计数(B3 唯一读者=net_poll) */
|
||||
uint16_t ch395f_get_glob_int_status_all(void)
|
||||
{
|
||||
uint16_t v = 0U;
|
||||
|
||||
g_fxchip.glob_calls++;
|
||||
fake_log('G');
|
||||
if (g_fx.glob_n == 0) {
|
||||
v = 0U;
|
||||
} else if (g_fx.glob_i < g_fx.glob_n) {
|
||||
v = g_fx.glob_q[g_fx.glob_i++];
|
||||
} else {
|
||||
v = g_fx.glob_q[g_fx.glob_n - 1];
|
||||
}
|
||||
g_fxchip.glob_bits = (int)v;
|
||||
return v;
|
||||
}
|
||||
|
||||
/* 与真实驱动同判据:只有 PHY_CHANGE 位才回读 PHY;链路电平变了才计一次变更 */
|
||||
void ch395f_glob_event_feed(uint16_t glob_bits)
|
||||
{
|
||||
uint8_t up;
|
||||
|
||||
if ((glob_bits & CH395F_GINT_STAT_PHY_CHANGE) == 0U) {
|
||||
return;
|
||||
}
|
||||
up = (ch395f_get_phy_status() != CH395F_PHY_DISCONN) ? 1U : 0U;
|
||||
if (up != g_fake.link_up) {
|
||||
g_fake.link_up = up;
|
||||
g_fake.link_change++;
|
||||
}
|
||||
}
|
||||
|
||||
/* 只读本 Socket 的 SINT(读即清),永不碰全局寄存器——与真实护栏原语同语义 */
|
||||
uint8_t ch395f_sock_events(uint8_t sn)
|
||||
{
|
||||
g_fxchip.sock_events_calls++;
|
||||
return ch395f_get_sock_int_status(sn);
|
||||
}
|
||||
|
||||
/* —— sock_reset 复位机(契约等价:四步 + 最短沉降窗 + 二段 CLOSE 补发)——
|
||||
* 与驱动一致的**可断言点**:begin 重复启动必须被拒;GRACEFUL 满 100ms、
|
||||
* QUARANTINE 满 400ms 才继续;quiescent 要连续 2 轮无事件;byte0 回 00H 才算完成;
|
||||
* 芯片 byte0=05H 且 TCP=00H 时补发第二段 CLOSE。
|
||||
* 不复制驱动的超时数值细节(那是驱动自己的单测面),本假件不自造失败:
|
||||
* 想测超时由测试把 cached_open 一直留成 OPEN,由上层 net_sock_reset_sync 的 600ms 收口。 */
|
||||
int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode)
|
||||
{
|
||||
g_fxchip.rst_begin_calls++;
|
||||
fake_log('X');
|
||||
if (sn >= CH395F_FAKE_SOCK_MAX) {
|
||||
return -1;
|
||||
}
|
||||
if (g_fx.rst_state[sn] != (uint8_t)CH395F_FAKE_RST_IDLE) {
|
||||
g_fxchip.rst_begin_reject++;
|
||||
return -1;
|
||||
}
|
||||
g_fx.rst_state[sn] = (uint8_t)CH395F_FAKE_RST_DRAINED;
|
||||
g_fx.rst_mode[sn] = (mode == CH395F_RESET_QUARANTINE) ? 1U : 0U;
|
||||
g_fx.rst_t0[sn] = HAL_GetTick();
|
||||
g_fx.rst_quiet[sn] = 0U;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ch395f_sock_reset_poll(uint8_t sn)
|
||||
{
|
||||
uint32_t now;
|
||||
uint32_t el;
|
||||
uint32_t need;
|
||||
|
||||
g_fxchip.rst_poll_calls++;
|
||||
fake_log('x');
|
||||
if (sn >= CH395F_FAKE_SOCK_MAX) {
|
||||
return -1;
|
||||
}
|
||||
if (g_fx.rst_state[sn] == (uint8_t)CH395F_FAKE_RST_IDLE) {
|
||||
return 0; /* 未启动/已完成:幂等 */
|
||||
}
|
||||
now = HAL_GetTick();
|
||||
el = now - g_fx.rst_t0[sn];
|
||||
|
||||
switch (g_fx.rst_state[sn]) {
|
||||
case CH395F_FAKE_RST_DRAINED:
|
||||
/* 第 1 步:读空 RX;第 2 步:CLOSE */
|
||||
if (ch395f_get_recv_len(sn) != 0U) {
|
||||
uint8_t scratch[8];
|
||||
g_fxchip.rst_rx_discard_calls++;
|
||||
ch395f_read_recv_buf(sn, scratch, 8U);
|
||||
}
|
||||
g_fxchip.rst_close_calls++;
|
||||
(void)ch395f_close_socket(sn);
|
||||
if (g_fx.rst_keep_open[sn] == 0U) {
|
||||
g_fake.cached_open[sn] = CH395F_SOCKET_CLOSED;
|
||||
g_fake.cached_tcp[sn] = CH395F_TCP_CLOSED;
|
||||
}
|
||||
g_fx.rst_state[sn] = (uint8_t)CH395F_FAKE_RST_MINWAIT;
|
||||
g_fx.rst_t0[sn] = now;
|
||||
break;
|
||||
case CH395F_FAKE_RST_MINWAIT:
|
||||
need = (g_fx.rst_mode[sn] != 0U) ? 400U : 100U;
|
||||
if (el >= need) {
|
||||
g_fx.rst_state[sn] = (uint8_t)CH395F_FAKE_RST_QUIET;
|
||||
g_fx.rst_t0[sn] = now;
|
||||
g_fx.rst_quiet[sn] = 0U;
|
||||
}
|
||||
break;
|
||||
case CH395F_FAKE_RST_QUIET:
|
||||
if (ch395f_sock_events(sn) == 0U) {
|
||||
g_fx.rst_quiet[sn]++;
|
||||
if (g_fx.rst_quiet[sn] >= CH395F_FAKE_RST_QUIESCENT) {
|
||||
g_fx.rst_state[sn] = (uint8_t)CH395F_FAKE_RST_WAITCLS;
|
||||
g_fx.rst_t0[sn] = now;
|
||||
}
|
||||
} else {
|
||||
g_fx.rst_quiet[sn] = 0U;
|
||||
}
|
||||
break;
|
||||
case CH395F_FAKE_RST_WAITCLS:
|
||||
if (ch395f_cmd_done(sn, CH395F_DONE_CLOSE) == 1) {
|
||||
g_fx.rst_state[sn] = (uint8_t)CH395F_FAKE_RST_IDLE;
|
||||
g_fxchip.rst_done[sn]++;
|
||||
return 0;
|
||||
}
|
||||
/* 二段 CLOSE 补发(LISTEN 态一次 CLOSE 只拆监听,byte0 恒 05H) */
|
||||
if ((g_fake.cached_open[sn] == CH395F_SOCKET_OPEN) &&
|
||||
(g_fake.cached_tcp[sn] == CH395F_TCP_CLOSED) &&
|
||||
(g_fx.rst_quiet[sn] != 0xFFU)) {
|
||||
g_fx.rst_quiet[sn] = 0xFFU;
|
||||
g_fxchip.rst_close_calls++;
|
||||
(void)ch395f_close_socket(sn);
|
||||
if (g_fx.rst_keep_open[sn] == 0U) {
|
||||
g_fake.cached_open[sn] = CH395F_SOCKET_CLOSED;
|
||||
g_fake.cached_tcp[sn] = CH395F_TCP_CLOSED;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
g_fx.rst_state[sn] = (uint8_t)CH395F_FAKE_RST_IDLE;
|
||||
g_fxchip.rst_fail[sn]++;
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ch395f_cmd_done(uint8_t sn, ch395f_done_what_t what)
|
||||
{
|
||||
ch395f_sock_state_t st;
|
||||
|
||||
g_fxchip.cmd_done_calls++;
|
||||
g_fxchip.last_cmd_done_what = (uint8_t)what;
|
||||
fake_log('z');
|
||||
if (sn >= CH395F_FAKE_SOCK_MAX) {
|
||||
return -1;
|
||||
}
|
||||
st = ch395f_get_sock_state(sn);
|
||||
switch (what) {
|
||||
case CH395F_DONE_OPEN:
|
||||
return (st.open == CH395F_SOCKET_OPEN) ? 1 : 0;
|
||||
case CH395F_DONE_LISTEN:
|
||||
return (st.open == CH395F_SOCKET_OPEN && st.tcp_state == CH395F_TCP_LISTEN) ? 1 : 0;
|
||||
case CH395F_DONE_CLOSE:
|
||||
return (st.open == CH395F_SOCKET_CLOSED) ? 1 : 0;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@
|
||||
#define CH395F_ERR_SUCCESS 0x00U
|
||||
#define CH395F_PROTO_TYPE_TCP 0x03U
|
||||
#define CH395F_SOCKET_CLOSED 0x00U
|
||||
#define CH395F_SOCKET_OPEN 0x01U
|
||||
#define CH395F_SOCKET_OPEN 0x05U /* 真头 ch395f.h:220 = 0x05U(05H SOCKET_OPEN,docs/芯片手册/CH395F.md:527)*/
|
||||
#define CH395F_TCP_CLOSED 0x00U
|
||||
#define CH395F_TCP_LISTEN 0x01U
|
||||
#define CH395F_TCP_ESTABLISHED 0x05U
|
||||
#define CH395F_TCP_ESTABLISHED 0x04U /* 真头 ch395f.h:229 = 0x04U(04H TCP_ESTABLISHED,docs/芯片手册/CH395F.md:539)*/
|
||||
|
||||
/* 发送/接收缓冲尺寸宏链:**与真实 ch395f.h 同构转抄**(2026-10-23 逐条核对)。
|
||||
* net_pending.h 里的 NET_SEND_CHUNK_MAX 由这两个宏推导,是"一次最多往芯片写多少"
|
||||
@@ -66,6 +66,68 @@
|
||||
#define CH395F_FAKE_SOCK_MAX 8 /* 与 NET_MAX_SOCKETS 同量级,测试自用 */
|
||||
#define CH395F_FAKE_QUEUE_LEN 24
|
||||
#define CH395F_FAKE_LOG_LEN 96
|
||||
#define CH395F_FAKE_GLOB_QUEUE 24
|
||||
|
||||
/* ===== 扩展区(2026-10-24,net_socket 主机单测批次;**纯新增**,上面既有宏/字段/函数
|
||||
* 一字未动,net_listener / net_events / net_pending 三套的语义不受影响)=====
|
||||
*
|
||||
* 追加的是 net_socket.c 这一层会调、而前三个模块不调的芯片原语(初始化下发类 +
|
||||
* 全局中断 + sock_reset 复位机 + cmd_done 判据)。签名/常量同样**逐字抄自真实头**
|
||||
* Drivers/BSP/CH395F/ch395f.h,改真头即编译期/断言期暴露漂移。
|
||||
*
|
||||
* 必须复刻的第三条芯片怪癖(前两套测不到、net_socket.c 却是主路):
|
||||
* 3. GET_GLOB_INT_STATUS_ALL(0x19,2 字节)**整体读即清**,且它是全局寄存器
|
||||
* (含全部 Socket 事件位)。AGENTS 模块约定 B3:唯一读者只能是 net_poll。
|
||||
* 假件按调用序号返回预置值,并单独计数,让"别处偷偷读全局"成为可断言的事。
|
||||
* 4. sock_reset 复位机(E4 定案四步:读空 RX → CLOSE → 排空事件(quiescent) →
|
||||
* 等 byte0=00H,含 LISTEN 态二段 CLOSE 补发)。这里做**契约等价**实现:
|
||||
* 阶段推进与最短等待(GRACEFUL 100ms / QUARANTINE 400ms)照抄驱动,
|
||||
* 但不复制驱动的超时数值细节(那属于驱动自己的单测面)。
|
||||
*/
|
||||
#define CH395F_ERR_BUSY 0x10U /* 真头 ch395f.h:170 */
|
||||
#define CH395F_ERR_MEM 0x11U /* 真头 ch395f.h:171 */
|
||||
#define CH395F_ERR_TIMEOUT 0x13U /* 真头 ch395f.h:173 */
|
||||
#define CH395F_ERR_VAL 0x19U /* 真头 ch395f.h:179 */
|
||||
#define CH395F_ERR_USE 0x1BU /* 真头 ch395f.h:181(open 已被占用)*/
|
||||
#define CH395F_PROTO_TYPE_UDP 0x02U /* 真头 ch395f.h:213 */
|
||||
#define CH395F_FUN_PARA_SOCKET_CLOSE (1UL << 3) /* 真头 ch395f.h:205 */
|
||||
#define CH395F_TCP_MSS 1024U /* 真头 ch395f.h:124 */
|
||||
#define CH395F_PHY_DISCONN 0x01U /* 真头 ch395f.h:191 */
|
||||
#define CH395F_GINT_STAT_PHY_CHANGE 0x04U /* 真头 ch395f.h:245 */
|
||||
#define CH395F_SOCK_STATE_NONE 0xFFU /* 真头 ch395f.h:762 */
|
||||
|
||||
#define CH395F_SINT_STAT_SENBUF_FREE 0x01U /* 真头 ch395f.h:263 */
|
||||
#define CH395F_SINT_STAT_SEND_OK 0x02U /* 真头 ch395f.h:264 */
|
||||
#define CH395F_SINT_STAT_RECV_OK 0x04U /* 真头 ch395f.h:265 */
|
||||
#define CH395F_SINT_STAT_CONNECT 0x08U /* 真头 ch395f.h:266 */
|
||||
#define CH395F_SINT_STAT_DISCONNECT 0x10U /* 真头 ch395f.h:267 */
|
||||
#define CH395F_SINT_STAT_SOCK_TIMEOUT 0x40U /* 真头 ch395f.h:269 */
|
||||
|
||||
typedef enum {
|
||||
CH395F_STATUS_OK = 0, /* 真头 ch395f.h:275 */
|
||||
CH395F_STATUS_ERROR = -1,
|
||||
CH395F_STATUS_TIMEOUT = -2,
|
||||
CH395F_STATUS_NOT_DETECTED = -3
|
||||
} ch395f_status_t;
|
||||
|
||||
typedef enum {
|
||||
CH395F_RESET_GRACEFUL = 0, /* 真头 :803:最短等待 100ms */
|
||||
CH395F_RESET_QUARANTINE /* 真头 :804:最短等待 400ms */
|
||||
} ch395f_reset_mode_t;
|
||||
|
||||
typedef enum {
|
||||
CH395F_DONE_OPEN = 0, /* 真头 :826:byte0 变 05H */
|
||||
CH395F_DONE_LISTEN, /* 真头 :827:byte0=05H 且 TCP=01H */
|
||||
CH395F_DONE_CLOSE /* 真头 :828:byte0=00H */
|
||||
} ch395f_done_what_t;
|
||||
|
||||
/* 复位机阶段(与驱动同名状态的**契约等价**简化,不代表驱动内部实现) */
|
||||
#define CH395F_FAKE_RST_IDLE 0
|
||||
#define CH395F_FAKE_RST_DRAINED 1 /* 已读空 RX 并发过 CLOSE */
|
||||
#define CH395F_FAKE_RST_MINWAIT 2 /* 等最短沉降窗(100/400ms) */
|
||||
#define CH395F_FAKE_RST_QUIET 3 /* 排空本 Socket 事件(连续 2 轮无位) */
|
||||
#define CH395F_FAKE_RST_WAITCLS 4 /* 等 byte0=00H(含二段 CLOSE 补发) */
|
||||
#define CH395F_FAKE_RST_QUIESCENT 2U
|
||||
|
||||
typedef struct {
|
||||
uint8_t open;
|
||||
@@ -111,7 +173,13 @@ typedef struct {
|
||||
/* 调用顺序日志(短名首字母):
|
||||
* '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 */
|
||||
* 'P'=set_proto_type 'w'=write_send_buf
|
||||
* 扩展区新增(net_socket 批次):'G'=读全局中断 'v'=sock_events(读即清)
|
||||
* 'X'=sock_reset_begin 'x'=sock_reset_poll 'z'=cmd_done 'T'=tcp_connect
|
||||
* 'I'=set_ip_addr 'N'=set_gwip 'K'=set_mask 'M'=set_mac 'D'=set_dhcp
|
||||
* 'F'=set_fun_para 'Q'=set_tcp_mss 'b'=set_send_buf 'u'=set_recv_buf
|
||||
* 'e'=set_des_ip 'q'=set_des_port 'g'=get_phy_status 'E'=check_exist
|
||||
* 'Z'=reset 'n'=init */
|
||||
char log[CH395F_FAKE_LOG_LEN];
|
||||
int log_n;
|
||||
} ch395f_fake_obs_t;
|
||||
@@ -143,4 +211,140 @@ 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);
|
||||
|
||||
/* ===== 扩展区第二部分:新增状态世界 + 新增假芯片原型 =====
|
||||
* ⚠ 刻意**不往既有 ch395f_fake_world_t / ch395f_fake_obs_t 里加字段**,而是另开一份
|
||||
* ch395f_fake_ext_t / ch395f_fake_ext_obs_t:既有三套单测的复位与断言面一字不动,
|
||||
* 新增状态由 ch395f_fake_reset() 调 ch395f_fake_ext_reset() 一并清零(场景间不残留)。
|
||||
*/
|
||||
typedef struct {
|
||||
/* —— 初始化/参数下发面(net_init 主路)—— */
|
||||
int exist_rc; /* ch395f_check_exist 返回(ch395f_status_t)*/
|
||||
int reset_rc; /* ch395f_reset 返回 */
|
||||
int init_rc; /* ch395f_init 返回 */
|
||||
uint8_t version; /* ch395f_get_version(版本门控 0x46 的用点位)*/
|
||||
uint8_t dhcp_enable; /* ch395f_set_dhcp */
|
||||
uint8_t ping_enable; /* ch395f_ping_enable */
|
||||
uint8_t ip[4]; /* ch395f_set_ip_addr */
|
||||
uint8_t gw[4]; /* ch395f_set_gwip_addr */
|
||||
uint8_t mask[4]; /* ch395f_set_mask_addr */
|
||||
uint8_t mac[6]; /* ch395f_set_mac_addr */
|
||||
uint32_t fun_para; /* ch395f_set_fun_para 最近一次实发值 */
|
||||
uint16_t mss; /* ch395f_set_tcp_mss */
|
||||
uint8_t retrans_cnt;
|
||||
uint16_t retrans_period;
|
||||
uint32_t ka_idle;
|
||||
uint32_t ka_intvl;
|
||||
uint8_t ka_cnt;
|
||||
uint8_t send_blk_start[CH395F_FAKE_SOCK_MAX]; /* ch395f_set_send_buf */
|
||||
uint8_t send_blk_cnt[CH395F_FAKE_SOCK_MAX];
|
||||
uint8_t recv_blk_start[CH395F_FAKE_SOCK_MAX]; /* ch395f_set_recv_buf */
|
||||
uint8_t recv_blk_cnt[CH395F_FAKE_SOCK_MAX];
|
||||
uint8_t des_ip[CH395F_FAKE_SOCK_MAX][4]; /* ch395f_set_des_ip */
|
||||
uint16_t des_port[CH395F_FAKE_SOCK_MAX]; /* ch395f_set_des_port */
|
||||
uint8_t connect_rc[CH395F_FAKE_SOCK_MAX]; /* ch395f_tcp_connect 返回 */
|
||||
/* —— 全局中断(0x19,整体读即清;唯一读者应为 net_poll)—— */
|
||||
uint16_t glob_q[CH395F_FAKE_GLOB_QUEUE];
|
||||
int glob_n;
|
||||
int glob_i;
|
||||
uint8_t phy_status; /* ch395f_get_phy_status(喂 glob_event_feed)*/
|
||||
/* —— sock_reset 复位机(契约等价,见文件头注释 4)—— */
|
||||
uint8_t rst_state[CH395F_FAKE_SOCK_MAX];
|
||||
uint8_t rst_mode[CH395F_FAKE_SOCK_MAX];
|
||||
uint32_t rst_t0[CH395F_FAKE_SOCK_MAX];
|
||||
uint8_t rst_quiet[CH395F_FAKE_SOCK_MAX]; /* 连续无事件轮数 */
|
||||
/* 1=复位机**不**替芯片把 cached_open 落回 CLOSED(造"永远关不掉"的芯片形态,
|
||||
* 用来量上层的复位超时收口)。默认 0 → 与既有行为一致。 */
|
||||
uint8_t rst_keep_open[CH395F_FAKE_SOCK_MAX];
|
||||
/* GET_RECV_BUF 的**脚本化内容**(默认长度 0 → 与既有"全零交付"逐字相同)。
|
||||
* UDP 报文头(长度/源端口/源 IP)只有内容可脚本化才能被断言:net_recvfrom 要
|
||||
* 从芯片头里拆出对端地址,全零内容下"拆对了"与"拆反了"看起来一模一样。 */
|
||||
uint8_t recv_bytes[128];
|
||||
uint16_t recv_bytes_n;
|
||||
/* SINT 的**电平型**来源(可脚本化):读即清之后立刻把这个掩码放回 int_status。
|
||||
* 默认 0 → 与既有假件行为逐字相同(读一次就没了)。真实芯片的 RECV_OK /
|
||||
* SENBUF_FREE 本就是"条件位":只要条件还在(RX 仍非空 / 发送缓冲仍空),下一轮
|
||||
* 就还会再报一次。net_socket.c 的多块发送要的就是这个稳态,没有它就只能
|
||||
* 造"写完一块就再也等不到空闲"的假死形态。 */
|
||||
uint8_t sint_sticky[CH395F_FAKE_SOCK_MAX];
|
||||
} ch395f_fake_ext_t;
|
||||
|
||||
typedef struct {
|
||||
int exist_calls;
|
||||
int reset_calls;
|
||||
int init_calls;
|
||||
int set_ip_calls;
|
||||
int set_gw_calls;
|
||||
int set_mask_calls;
|
||||
int set_mac_calls;
|
||||
int set_dhcp_calls;
|
||||
int ping_calls;
|
||||
int set_fun_para_calls;
|
||||
int set_mss_calls;
|
||||
int set_retrans_calls;
|
||||
int set_ka_calls;
|
||||
int set_send_buf_calls;
|
||||
int set_recv_buf_calls;
|
||||
int set_des_ip_calls;
|
||||
int set_des_port_calls;
|
||||
int connect_calls;
|
||||
int glob_calls; /* 读全局中断次数(B3:除 net_poll 外应为 0)*/
|
||||
int glob_bits; /* 最近一次读到的 16 位全局位图 */
|
||||
int sock_events_calls; /* ch395f_sock_events(只碰本 Socket 的护栏原语)*/
|
||||
int rst_begin_calls;
|
||||
int rst_begin_reject; /* begin 被拒次数(复位机已在运行)*/
|
||||
int rst_poll_calls;
|
||||
int rst_done[CH395F_FAKE_SOCK_MAX]; /* 复位机完成计数 */
|
||||
int rst_fail[CH395F_FAKE_SOCK_MAX]; /* 复位机失败计数 */
|
||||
int cmd_done_calls;
|
||||
uint8_t last_cmd_done_what;
|
||||
int rst_close_calls; /* 复位机内部发出的 CLOSE 命令(含二段补发)*/
|
||||
int rst_rx_discard_calls; /* 复位机 RX 排空事务次数 */
|
||||
} ch395f_fake_ext_obs_t;
|
||||
|
||||
extern ch395f_fake_ext_t g_fx;
|
||||
extern ch395f_fake_ext_obs_t g_fxchip;
|
||||
|
||||
/* 由 ch395f_fake_reset() 自动调用;单独暴露是为了让测试能只清扩展面 */
|
||||
void ch395f_fake_ext_reset(void);
|
||||
void ch395f_fake_push_glob(uint16_t v); /* 预置全局中断返回序列 */
|
||||
void ch395f_fake_set_sint_sticky(uint8_t sock, uint8_t mask);
|
||||
/* 预置 GET_RECV_BUF 交付的字节(n=0 撤销预置);p 可为 NULL(表示填 n 个 0xA5) */
|
||||
void ch395f_fake_set_recv_bytes(const uint8_t *p, uint16_t n);
|
||||
/* 顺序断言辅助:清空调用日志 / 找某个短名第一次出现的位置(-1=没调过)。
|
||||
* 刻意提供"清零日志",否则一次长轮询(如 sock_reset 的几十轮 'x')会把 96 格
|
||||
* 环形外的顺序信息挤掉,让后续场景的顺序断言失真。 */
|
||||
void ch395f_fake_clear_log(void);
|
||||
int ch395f_fake_log_find(char c);
|
||||
|
||||
/* —— 新增假芯片实现(签名与真实 ch395f.h 一字不改)—— */
|
||||
ch395f_status_t ch395f_check_exist(void);
|
||||
ch395f_status_t ch395f_reset(void);
|
||||
ch395f_status_t ch395f_init(void);
|
||||
uint8_t ch395f_get_version(void);
|
||||
void ch395f_set_fun_para(uint32_t para);
|
||||
void ch395f_set_tcp_mss(uint16_t mss);
|
||||
void ch395f_set_retrans_count(uint8_t cnt);
|
||||
void ch395f_set_retrans_period(uint16_t period);
|
||||
void ch395f_set_keepalive_idle(uint32_t time);
|
||||
void ch395f_set_keepalive_intvl(uint32_t time);
|
||||
void ch395f_set_keepalive_cnt(uint8_t cnt);
|
||||
void ch395f_set_dhcp(uint8_t enable);
|
||||
void ch395f_ping_enable(uint8_t enable);
|
||||
void ch395f_set_ip_addr(uint8_t *p_ip);
|
||||
void ch395f_set_gwip_addr(uint8_t *p_gwip);
|
||||
void ch395f_set_mask_addr(uint8_t *p_mask);
|
||||
void ch395f_set_mac_addr(uint8_t *p_mac);
|
||||
void ch395f_set_send_buf(uint8_t sock, uint8_t start_block, uint8_t block_count);
|
||||
void ch395f_set_recv_buf(uint8_t sock, uint8_t start_block, uint8_t block_count);
|
||||
void ch395f_set_des_ip(uint8_t sock, uint8_t *p_ip);
|
||||
void ch395f_set_des_port(uint8_t sock, uint16_t port);
|
||||
uint8_t ch395f_tcp_connect(uint8_t sock);
|
||||
uint8_t ch395f_get_phy_status(void);
|
||||
uint16_t ch395f_get_glob_int_status_all(void);
|
||||
void ch395f_glob_event_feed(uint16_t glob_bits);
|
||||
uint8_t ch395f_sock_events(uint8_t sn);
|
||||
int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode);
|
||||
int ch395f_sock_reset_poll(uint8_t sn);
|
||||
int ch395f_cmd_done(uint8_t sn, ch395f_done_what_t what);
|
||||
|
||||
#endif /* CH395F_FAKE_H */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 模块名称:假 FatFs 内存卷(存储域主机侧单测共享件)
|
||||
* 模块功能:给 App/log 与 App/cfg 的主机侧单测一个可脚本化的假文件系统——
|
||||
* 类型与常量取自**真实 Lib/FatFs/ff.h**(FRESULT / FIL / FILINFO / FA_*
|
||||
* 一字不改),本件在 fatfs_fake.c 里给出九个 f_* 原型的 RAM 实现:
|
||||
* f_open / f_read / f_write / f_lseek / f_close / f_stat / f_unlink /
|
||||
* f_mkdir / f_rename。
|
||||
*
|
||||
* 仅供 test/host/** 使用:不参与 MCU 构建,也不参与 simulator 构建。
|
||||
*
|
||||
* 为什么共享一份(照 test/host/common/ch395f_fake.{h,c} 的先例):
|
||||
* sys_log_mcu / soe_store / cfg_file_store_mcu 三块的成败**全部**由
|
||||
* 「这次 f_open 拿到什么码、f_write 写回多少字节」决定。三份假件各写一遍,
|
||||
* 漂移 = 「假件与被测代码同时错而无人报错」。签名与真实头同源 → 真实 API 一改
|
||||
* 就编译期报错(同 test/host/nand_port 的「真头 + 假 diskio」路子)。
|
||||
*
|
||||
* —— 必须复刻的三条本仓实证语义(docs/架构设计/STM32F4-Base存储架构说明.md §10.1)——
|
||||
* 1) FA_OPEN_APPEND(0x30) **不含 FA_WRITE(0x02)**:本移植 FA_OPEN_APPEND =
|
||||
* FA_OPEN_ALWAYS(0x10) | FA_SEEKEND(0x20),而 FA_SEEKEND 只定义在 ff.c:74、
|
||||
* ff.h 里没有。所以只给 FA_OPEN_APPEND 打开的文件**能打开、能创建**,但
|
||||
* f_write 恒返 FR_DENIED(7)——2026-09-20 台架「/log 写失败 x5/开机」的真根因
|
||||
* (§10.1 第 11 条)。假件按真实 ff.c 一样把 mode 原值存进 fp->flag 再判
|
||||
* flag & FA_WRITE,不做任何「便利放行」。
|
||||
* 2) f_write 簇分配失败 = **FR_OK + 短写**(无错误码,判失败只能校验写回字节数):
|
||||
* 真实 ff.c:4102 的 if (clst == 0) break; → 循环带着已写计数退出 → FR_OK。
|
||||
* 假件把「每文件固定后备簇池」当介质:写到装不下就返回 FR_OK + 截断字节数;
|
||||
* 另给定点注入,让「恰好短一字节」这种真机要撞坏块才有的形态可复现。
|
||||
* 3) FF_USE_LFN=0 → **严格 8.3 短名**:主名 <=8、扩展 <=3、非法字符集
|
||||
* * + ; < = > [ ] | 与 0x7F(ff.c create_name 非 LFN 分支逐字模拟)。
|
||||
* 「.fschk」的真实死法 = 开头那个点把游标直接推进扩展域、随后 5 个字符撑爆
|
||||
* 扩展域上限 3 → FR_INVALID_NAME(6);路径里任何位置出现 ':' 被 get_ldnumber
|
||||
* 当盘符前缀吃下 → FR_INVALID_DRIVE(11)。两者都是**纯 RAM 里的名字判定、
|
||||
* 不碰盘**(§10.1 第 10 条,含「测试用例须同时接受这两个码」的口径)。
|
||||
*
|
||||
* —— 其余与真实 ff.c 对齐的路径/错误码细则 ——
|
||||
* - 前导分隔符被剥掉、恒从卷根开始(FF_FS_RPATH=0,见 ff.c follow_path);
|
||||
* - 中间目录段不存在 → FR_NO_PATH;末段不存在且未给创建位 → FR_NO_FILE;
|
||||
* - 对目录本身 f_open(无创建位)→ FR_NO_FILE;
|
||||
* - FA_CREATE_NEW 撞上已存在 → FR_EXIST;FA_CREATE_ALWAYS → 截零 + 刷新时间戳;
|
||||
* - 路径剥完分隔符为空 → FR_INVALID_NAME(ff.c create_name 的 i==0 分支);
|
||||
* - f_stat:父目录在而对象不在 → FR_NO_FILE;父目录也不在 → FR_NO_PATH;
|
||||
* - f_unlink 不存在 → FR_NO_FILE;f_mkdir 已存在 → FR_EXIST;
|
||||
* - **f_rename 撞已存在的新名 → FR_EXIST、不覆盖**(ff.c「new name already in
|
||||
* use」,2026-09-22 台架 E-01 暴露的那条:缺了它,一切「目标存在的覆盖写」
|
||||
* 必然失败,而模拟器端 POSIX rename 的覆盖语义照不出来)。不复刻本条 =
|
||||
* cfg_store_write_atomic 里「先 unlink 再 rename」那一步永远测不出必要性;
|
||||
* - f_lseek 越过文件尾:只读句柄**钳到 objsize 且不报错**(ff.c 正常寻址分支),
|
||||
* 可写句柄才扩展文件;
|
||||
* - f_open 失败即作废句柄(ff.c「fp->obj.fs = 0」)→ 后续 f_write 拿
|
||||
* FR_INVALID_OBJECT,不会拿着脏句柄继续写;
|
||||
* - f_close 成功即释放句柄,并把打开期间的变更落成新的 fdate/ftime
|
||||
* (真实 ff.c 在 close 里用 get_fattime 回写目录项)。
|
||||
*
|
||||
* —— 脚本化模型(沿用本仓既有假件风格:nand_port 的 RAM 假芯片 / lftpd_io 的假堆)——
|
||||
* - 注入:按路径 + op 预置「接下来 N 次返回某码 / 只写回 n 字节 / 只读回 n 字节」;
|
||||
* path 传 NULL = 不限路径;
|
||||
* - 观测:逐 op 调用计数 + 最近一次入参(路径、mode、btw/bw、btr/br、lseek 偏移),
|
||||
* 让「这轮压根不该碰盘」与「打开模式必须带写位」成为可断言的事;
|
||||
* - 卷:条目直读直写(ff_fake_mkfile / ff_fake_mkfile_size / ff_fake_fetch)用来
|
||||
* 铺前置状态——「run.log 已经有 1024 条记录」不可能靠真写 65536 字节铺出来;
|
||||
* - 假时钟:ff_fake_set_dostime 控制 fdate/ftime,FAT 分钟粒度导致
|
||||
* 「同分钟内同长度重写不可检出」这件事只有它能测(cfg_store_stat 的 mtime 令牌);
|
||||
* - ff_fake_reset() 一次全清(卷 + 注入 + 观测 + 时钟)。
|
||||
*
|
||||
* 有意**不提供** f_mount / f_mkfs / f_opendir / f_readdir:三个被测模块一次都不该
|
||||
* 调它们,真调了就是链接期报错——比运行时断言更硬,也不会静默通过。
|
||||
*
|
||||
* 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2)
|
||||
*/
|
||||
|
||||
#ifndef FATFS_FAKE_H
|
||||
#define FATFS_FAKE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* 真实头在 Lib/FatFs/;本文件不重定义任何 FatFs 类型或宏 */
|
||||
#include "ff.h"
|
||||
|
||||
#define FF_FAKE_MAX_ENT 16u /* 卷内条目上限(文件 + 目录) */
|
||||
#define FF_FAKE_PATH_MAX 24u /* 归一化后路径上限("/log/selftest.log" = 17) */
|
||||
/* 每文件的后备簇池:必须**大于生产侧单文件容量上限**(run.log / soe.log
|
||||
* = 1024 条 x 64 B = 65536 B,见 sys_log.h:SYSLOG_RUN_MAX 与 soe_store.h:
|
||||
* SOE_STORE_MAX),否则测"容量满 unlink 重建"时追加位落在池外,f_write 恒
|
||||
* 返回 0 字节短写,轮转场景根本跑不到。写满仍然如实返回 FR_OK + 短写
|
||||
*(真实 ff.c 簇分配失败的同一形态)。16 条目 x 66 KB ≈ 1 MB 静态,主机侧无压力。 */
|
||||
#define FF_FAKE_CLUSTER 67584u
|
||||
#define FF_FAKE_INJ_MAX 8u /* 注入槽位数 */
|
||||
#define FF_FAKE_GUARD 50000 /* 跑飞保险:单个 f_* 调用数超此值直接 exit(2) */
|
||||
|
||||
/* 注入与计数的操作编号(fatfs_fake.c 内部同一套) */
|
||||
enum {
|
||||
FF_FAKE_OP_OPEN = 0,
|
||||
FF_FAKE_OP_READ,
|
||||
FF_FAKE_OP_WRITE,
|
||||
FF_FAKE_OP_LSEEK,
|
||||
FF_FAKE_OP_CLOSE,
|
||||
FF_FAKE_OP_STAT,
|
||||
FF_FAKE_OP_UNLINK,
|
||||
FF_FAKE_OP_MKDIR,
|
||||
FF_FAKE_OP_RENAME,
|
||||
FF_FAKE_OP_N
|
||||
};
|
||||
|
||||
/* 注入动作 */
|
||||
enum {
|
||||
FF_FAKE_ACT_RC = 0, /* 直接返回指定 FRESULT,不做任何数据搬运 */
|
||||
FF_FAKE_ACT_WSHORT, /* 仅 f_write:FR_OK 但只写回 n 字节 */
|
||||
FF_FAKE_ACT_RSHORT /* 仅 f_read :FR_OK 但只读回 n 字节 */
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
int calls[FF_FAKE_OP_N]; /* 逐 op 调用次数(含失败的调用) */
|
||||
BYTE last_open_mode; /* 最近一次 f_open 的 mode 原值(断言 FA_WRITE 位) */
|
||||
char last_open_path[FF_FAKE_PATH_MAX];
|
||||
char last_unlink_path[FF_FAKE_PATH_MAX];
|
||||
char last_mkdir_path[FF_FAKE_PATH_MAX];
|
||||
char last_rename_old[FF_FAKE_PATH_MAX];
|
||||
char last_rename_new[FF_FAKE_PATH_MAX];
|
||||
UINT last_write_btw; /* 请求写的字节数 */
|
||||
UINT last_write_bw; /* 实际写回的字节数(短写的断言面) */
|
||||
UINT last_read_btr;
|
||||
UINT last_read_br;
|
||||
FSIZE_t last_lseek_ofs;
|
||||
int last_open_rc; /* 最近一次 f_open 的返回码 */
|
||||
int last_write_rc; /* 最近一次 f_write 的返回码 */
|
||||
} ff_fake_obs_t;
|
||||
|
||||
extern ff_fake_obs_t g_ff_obs;
|
||||
|
||||
/* —— 世界控制 —— */
|
||||
void ff_fake_reset(void); /* 卷 + 注入 + 观测 + 时钟一次全清 */
|
||||
void ff_fake_set_dostime(uint16_t fdate, uint16_t ftime); /* 假 get_fattime() 的两个域 */
|
||||
|
||||
/* —— 故障注入(times = 生效次数;path = NULL 表示不限路径)—— */
|
||||
void ff_fake_inject_rc(const char *path, int op, FRESULT rc, int times);
|
||||
void ff_fake_inject_write_short(const char *path, UINT n, int times);
|
||||
void ff_fake_inject_read_short(const char *path, UINT n, int times);
|
||||
|
||||
/* —— 卷内容直读直写(绕开打开语义,用来铺前置状态)—— */
|
||||
int ff_fake_exists(const char *path); /* 0 无 / 1 文件 / 2 目录 */
|
||||
uint32_t ff_fake_size(const char *path); /* 无此文件返回 0xFFFFFFFF */
|
||||
uint32_t ff_fake_time_token(const char *path); /* (fdate<<16)|ftime,同 cfg_store_stat */
|
||||
uint32_t ff_fake_fetch(const char *path, uint8_t *p_buf, uint32_t off, uint32_t cap);
|
||||
int ff_fake_mkfile(const char *path, const uint8_t *p_data, uint32_t len);
|
||||
int ff_fake_mkfile_size(const char *path, uint32_t size); /* 只给逻辑长度(造容量满) */
|
||||
int ff_fake_mkdir_direct(const char *path); /* 无条件建目录 */
|
||||
int ff_fake_count_entries(void); /* 卷内条目数(含目录) */
|
||||
|
||||
#endif /* FATFS_FAKE_H */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""控制台单写者日志(console_log)主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/console_log/console_log_test.c(以 #include 纳入生产源
|
||||
App/log/console_log.c),在 PC 上离线验证单写者环形缓冲的全套契约:192B 行截断、
|
||||
256B 分块、跨环尾两段拷贝、写者未注册/ISR 上下文的无锁直发、缓冲满两条上界
|
||||
(DWT 周期 / 200 tick)与丢弃计数、[ERR] 告警行不经环(不递归)、TX 僵尸通道软复位
|
||||
自愈 + 故障即止 + 300ms 冷却(含无符号回绕)、DWT 使能失败退回 20000 次指令计数、
|
||||
huart1.Instance==NULL 短路、fputc 重定向、drain 250ms 预算。
|
||||
工具链与判定委托 test/host/common/unit_test_harness.py。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
本模块的坑全是"异常时刻 + 节奏":ORE 僵尸通道在软复位后毫秒级重锁、TIM7 停摆让
|
||||
HAL 的 tick 超时永不成立(round 8a 台架:唯一喂狗者死等 TXE -> IWDG 误复位)、
|
||||
背靠背块超时把主循环拉到 450~500ms 饿死喂狗(台架实测的停喂误复位)。这些在台架上
|
||||
要么一年一遇、要么现场已被复位冲掉;把 100/250/300ms 三条时间线做成显式量之后,
|
||||
299ms 与 300ms 的差别就是一行断言。详见
|
||||
docs/子系统设计/控制台单写者日志设计说明.md(G1~G5 / K1~K5 / §12 ORE 僵尸通道)。
|
||||
|
||||
桩的边界:console_log.h 与 sys_events.h 用真实头(SYS_EVT_CONSOLE_TX 位定义一改即
|
||||
编译期报错);usart.h 是桩但只暴露本模块访问的 huart1 + MX_USART1_UART_Init,
|
||||
USART SR 位号、DWT/CoreDebug 位掩码、HAL_StatusTypeDef 枚举值均照抄真实头,
|
||||
宿主编不动的那条 main.h -> stm32f4xx_hal.h -> core_cm4.h 链由 stub/stm32f4_fake.h
|
||||
以普通结构体替代(理由写在该文件头注释里)。
|
||||
⚠ 测试单元刻意不定义任何 syslog_* / DBG_* 符号:生产源哪天恢复"在 drain 里调日志
|
||||
子系统",本套件会以链接失败告警(那是设计上必须挡住的成环)。
|
||||
|
||||
用法(仓库根目录):
|
||||
python test/host/console_log/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误。
|
||||
|
||||
设计依据:docs/子系统设计/控制台单写者日志设计说明.md、docs/测试说明/主机单测覆盖率口径.md §9「P2 落地」清单第 3 条
|
||||
创建日期:2026-10-24
|
||||
"""
|
||||
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 必须最先(覆盖 usart.h / FreeRTOS.h / task.h / cmsis_os.h / cmsis_os2.h),
|
||||
# 其余是真实头目录(console_log.h 在 App/log,sys_events.h 在 App/app)。
|
||||
BUILD = TestBuild(
|
||||
name="Console Log Tests",
|
||||
src=os.path.join(HERE, "console_log_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "App", "log"),
|
||||
os.path.join(ROOT, "App", "app"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== Console Log Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - FreeRTOS.h(console_log 模块专用)
|
||||
* 模块功能:TaskHandle_t / TickType_t 与临界区两个宏(真实版来自 FreeRTOS.h +
|
||||
* portmacro.c 的 vPortEnterCritical/vPortExitCritical)。宿主单线程 ->
|
||||
* 临界区退化成计数,但**配对与层数照旧可断言**:本模块三处临界区
|
||||
* (整行入环、推进 tail、drain 每块一次)的原子性语义靠计数核对。
|
||||
* ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef FREERTOS_H
|
||||
#define FREERTOS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "stm32f4_fake.h"
|
||||
|
||||
#endif /* FREERTOS_H */
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - cmsis_os.h(console_log 模块专用)
|
||||
* 模块功能:空壳(真实那份是 CMSIS-RTOS v1 兼容层)。console_log.c include 它但
|
||||
* 一个 v1 API 都不用;事件组走的是 cmsis_os2.h 的 osEventFlagsSet。
|
||||
* ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef CMSIS_OS_H
|
||||
#define CMSIS_OS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#endif /* CMSIS_OS_H */
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - cmsis_os2.h(console_log 模块专用)
|
||||
* 模块功能:给真实 App/app/sys_events.h 一个能在宿主上编的最小内核类型面:
|
||||
* osEventFlagsId_t / osStatus_t / 事件位选项宏。sys_events.h 只需要
|
||||
* osEventFlagsId_t 来声明 sys_evtHandle,而 SYS_EVT_* 位定义全是真实值
|
||||
* —— 测试断言"入环后必须置 SYS_EVT_CONSOLE_TX(bit6)"用的就是真实宏。
|
||||
* osEventFlagsSet 原型与真实头逐字一致,实体在 stm32f4_fake.h 声明、
|
||||
* 由测试单元实现(计数 + 记录最后一次掩码)。
|
||||
* ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建(真实那份拉 FreeRTOS 内核头)。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef CMSIS_OS2_H
|
||||
#define CMSIS_OS2_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "stm32f4_fake.h"
|
||||
|
||||
#endif /* CMSIS_OS2_H */
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - 假 HAL/CMSIS 寄存器面 + 内核入口(console_log 模块专用)
|
||||
* 模块功能:让 App/log/console_log.c 在 PC 上编起来,并把它依赖的硬件/内核面做成
|
||||
* **可脚本控制**的模型:
|
||||
* ① USART1:TXE/TC 可注入「就绪 / 永远不就绪(僵尸通道)/ 前 N 次不就绪」;
|
||||
* 写进 DR 的字节被收进一条上线字节日志,并按「一次 TC 就绪 = 一块」记块长。
|
||||
* ② DWT CYCCNT:每次访问推进 con_dwt_step 个周期(= 给时间定价),
|
||||
* CTRL 的 CYCCNTENA 可脚本成「写不进去」以走 con_tx_counted 支路。
|
||||
* ③ HAL_GetTick:显式量 + 每次读自动步进。
|
||||
* ④ 内核入口:临界区计数、当前任务句柄、vTaskDelay(可挂回调 + 跑飞保险)、
|
||||
* __get_IPSR、osEventFlagsSet、NVIC/RCC 宏。
|
||||
* ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建(MCU 走真实 stm32f4xx*.h)。
|
||||
*
|
||||
* 为什么必须桩(换机复核依据):真实链是 usart.h -> main.h -> stm32f4xx_hal.h ->
|
||||
* stm32f4xx.h -> core_cm4.h,里面是 ARMCC 内联汇编与 __IO 寄存器绝对地址
|
||||
* (0xE0001000 一类),宿主 MSVC 既编不动也访问不了。本桩把同样的位语义搬到普通
|
||||
* 结构体上,字段名 / 位号 / 枚举值一律照真实头抄,生产代码写法一字不改即可编译:
|
||||
* USART_TypeDef.SR/DR、USART_SR_TXE=bit7、USART_SR_TC=bit6(stm32f4xx_hal_uart.h)
|
||||
* DWT_Type.CTRL/CYCCNT + DWT_CTRL_CYCCNTENA_Msk=bit0、CoreDebug_Type.DEMCR +
|
||||
* CoreDebug_DEMCR_TRCENA_Msk=bit24(core_cm4.h)
|
||||
* HAL_StatusTypeDef{OK=0,ERROR=1,BUSY=2,TIMEOUT=3}(stm32f4xx_hal_def.h)
|
||||
* HAL_UART_STATE_RESET=0x00、HAL_UART_ERROR_NONE=0x00、USART1_IRQn=7
|
||||
* UART_HandleTypeDef 只搬本模块访问的 4 个字段(Instance/gState/RxState/ErrorCode),
|
||||
* 余下(Init/Lock/...)刻意不搬:抄半份 HAL 结构只会更快与真实头漂移。
|
||||
*
|
||||
* ★ 两个手法(本桩唯一的技巧点,改之前先读懂):
|
||||
* a) 「读寄存器位」没法用结构体成员钩(u->SR 是成员访问,宏替换不了成员名),但生产
|
||||
* 代码的判据写作 (u->SR & USART_SR_TXE),而 **USART_SR_TXE 是本桩的宏** —— 把它
|
||||
* 定义成函数调用,于是每次读该位都进 con_uart_read_txe():在这里计数、判跑飞、按
|
||||
* 脚本决定就绪与否。SR 成员恒为全 1,就绪完全由钩子返回值决定(返回掩码=就绪,
|
||||
* 返回 0=不就绪,(全1 & 0)==0 让生产代码继续等)。TC 同理。
|
||||
* b) DWT 同理:#define DWT (con_dwt_touch(), &g_con_dwt) —— 每次 DWT-> 访问先走一遍
|
||||
* touch()(CYCCNT += step、计数判跑飞、按需让 CYCCNTENA 位落不住),再把地址给出去。
|
||||
* 真实硬件 CYCCNT 每周期 +1,这里「每次访问 +step」是同一个模型、只是步长交给测试。
|
||||
* 于是 CON_TX_TIMEOUT_CYC = SystemCoreClock/10 这类编译期表达式**不必改生产代码**
|
||||
* 就能在 1 次自旋内成立(step 取 = 上界值即可)。
|
||||
* c) DR 的写不做钩子(写就是写进真成员)。「哪个字节真正上线了」由 a) 的就绪事件推定:
|
||||
* 生产代码每写一个 DR 之前必然刚读到一次就绪的 TXE(两个发送循环都是这个结构),
|
||||
* 所以「上次就绪之后写进 DR 的那个字节」= 此刻离开发送寄存器上线的字节 ——
|
||||
* 与真实「TXE 空 -> 装载 -> 移位」时序一致,不是桩自己发明的规则。
|
||||
* 由此还能照出超时路径的真实后果:TXE 卡死时最后那个字节**没有**被写进 DR,
|
||||
* 也就不会出现在上线日志里(半个块丢失、丢在哪个字节可查)。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef STM32F4_FAKE_H
|
||||
#define STM32F4_FAKE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ===== 与真实头同值的位 / 枚举(来源见文件头) ===== */
|
||||
#define USART_SR_TXE_Msk (0x0080U) /* USART1->SR bit7 发送寄存器空 */
|
||||
#define USART_SR_TC_Msk (0x0040U) /* USART1->SR bit6 发送完成 */
|
||||
#define DWT_CTRL_CYCCNTENA_Msk (0x00000001U)
|
||||
#define CoreDebug_DEMCR_TRCENA_Msk (0x01000000U)
|
||||
#define USART1_IRQn 7
|
||||
|
||||
#define HAL_UART_STATE_RESET 0x00U
|
||||
#define HAL_UART_STATE_READY 0x20U
|
||||
#define HAL_UART_ERROR_NONE 0x00U
|
||||
|
||||
typedef enum {
|
||||
HAL_OK = 0x00U,
|
||||
HAL_ERROR = 0x01U,
|
||||
HAL_BUSY = 0x02U,
|
||||
HAL_TIMEOUT = 0x03U
|
||||
} HAL_StatusTypeDef;
|
||||
|
||||
/* 主机没有真周期钟。CON_TX_TIMEOUT_CYC = SystemCoreClock/10 与
|
||||
* CON_APPEND_WAIT_CYC = SystemCoreClock/5 都是编译期表达式,取值原样保留
|
||||
* 168 MHz(与 MCU 端 SystemCoreClock 一致),时间快慢由 con_dwt_step 决定。 */
|
||||
extern uint32_t SystemCoreClock;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ① 假 USART1 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
typedef struct {
|
||||
volatile uint32_t SR;
|
||||
volatile uint32_t DR;
|
||||
} USART_TypeDef;
|
||||
|
||||
typedef struct {
|
||||
USART_TypeDef *Instance;
|
||||
volatile uint32_t gState;
|
||||
volatile uint32_t RxState;
|
||||
volatile uint32_t ErrorCode;
|
||||
} UART_HandleTypeDef;
|
||||
|
||||
extern USART_TypeDef g_con_uart; /* 寄存器实体(单 TU,非 static) */
|
||||
|
||||
#define CON_UART_TX_MAX 20000U /* 上线字节日志容量 */
|
||||
#define CON_UART_BLOCK_MAX 64U /* 块长记录容量 */
|
||||
|
||||
extern uint32_t con_uart_tx_n; /* 已上线字节数 */
|
||||
extern uint8_t con_uart_tx[CON_UART_TX_MAX];
|
||||
extern uint32_t con_uart_block_n; /* 已完成的块数 */
|
||||
extern uint32_t con_uart_block_len[CON_UART_BLOCK_MAX];
|
||||
extern uint32_t con_uart_cur_len; /* 当前块已上线字节数 */
|
||||
extern uint32_t con_uart_txe_reads;
|
||||
extern uint32_t con_uart_tc_reads;
|
||||
extern int con_uart_txe_stuck; /* 1 = TXE 永不就绪(僵尸通道) */
|
||||
extern int con_uart_txe_after; /* 前 N 次 TXE 读**不**就绪,之后就绪 */
|
||||
extern int con_uart_txe_ready_left; /* >=0 = 只准就绪 N 次,之后卡死 */
|
||||
extern int con_uart_tc_after; /* 前 N 次 TC 读不就绪,之后就绪 */
|
||||
extern int con_uart_tc_stuck; /* 1 = TC 永不就绪 */
|
||||
extern int con_uart_pending; /* 1 = 有个字节已进 DR 待收 */
|
||||
|
||||
void con_uart_reset(void);
|
||||
|
||||
static void con_uart_tx_put(uint8_t b) {
|
||||
if (con_uart_tx_n + 1U >= CON_UART_TX_MAX) {
|
||||
printf("[FATAL] fake USART: tx log overflow (>=%u)\n", CON_UART_TX_MAX);
|
||||
exit(2);
|
||||
}
|
||||
con_uart_tx[con_uart_tx_n++] = b;
|
||||
/* 每次都在尾部补一个 NUL:让 strstr 一类字符串函数不可能读过缓冲末尾 */
|
||||
con_uart_tx[con_uart_tx_n < CON_UART_TX_MAX ? con_uart_tx_n : (CON_UART_TX_MAX - 1U)] = 0U;
|
||||
con_uart_cur_len++;
|
||||
}
|
||||
|
||||
/* 收走「上次就绪之后写进 DR 的字节」:它此刻才真正上线 */
|
||||
static void con_uart_take_byte(void) {
|
||||
if (con_uart_pending == 0) {
|
||||
return;
|
||||
}
|
||||
con_uart_pending = 0;
|
||||
con_uart_tx_put((uint8_t)(g_con_uart.DR & 0xFFU));
|
||||
}
|
||||
|
||||
static uint32_t con_uart_read_txe(void) {
|
||||
con_uart_take_byte();
|
||||
con_uart_txe_reads++;
|
||||
if (con_uart_txe_reads > 200000U) {
|
||||
printf("[FATAL] fake USART: TXE read %lu times (runaway)\n",
|
||||
(unsigned long)con_uart_txe_reads);
|
||||
exit(2);
|
||||
}
|
||||
if ((con_uart_txe_stuck != 0) || (con_uart_txe_after > 0)) {
|
||||
if (con_uart_txe_after > 0) {
|
||||
con_uart_txe_after--;
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
if (con_uart_txe_ready_left >= 0) {
|
||||
if (con_uart_txe_ready_left == 0) {
|
||||
return 0U; /* 配额用尽 -> 从这一字节起通道卡死 */
|
||||
}
|
||||
con_uart_txe_ready_left--;
|
||||
}
|
||||
con_uart_pending = 1;
|
||||
return USART_SR_TXE_Msk;
|
||||
}
|
||||
|
||||
static uint32_t con_uart_read_tc(void) {
|
||||
con_uart_take_byte();
|
||||
con_uart_tc_reads++;
|
||||
if (con_uart_tc_reads > 200000U) {
|
||||
printf("[FATAL] fake USART: TC read %lu times (runaway)\n",
|
||||
(unsigned long)con_uart_tc_reads);
|
||||
exit(2);
|
||||
}
|
||||
if (con_uart_tc_stuck != 0) {
|
||||
return 0U;
|
||||
}
|
||||
if (con_uart_tc_after > 0) { /* TC 抖动:等几轮才置位 */
|
||||
con_uart_tc_after--;
|
||||
return 0U;
|
||||
}
|
||||
if (con_uart_block_n >= CON_UART_BLOCK_MAX) {
|
||||
printf("[FATAL] fake USART: block log overflow (>=%u)\n", CON_UART_BLOCK_MAX);
|
||||
exit(2);
|
||||
}
|
||||
con_uart_block_len[con_uart_block_n++] = con_uart_cur_len;
|
||||
con_uart_cur_len = 0U;
|
||||
return USART_SR_TC_Msk;
|
||||
}
|
||||
|
||||
/* ★手法 a):判据里的掩码宏定义成函数调用 -> 每次读该位都进钩子 */
|
||||
#define USART_SR_TXE (con_uart_read_txe())
|
||||
#define USART_SR_TC (con_uart_read_tc())
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ② 假 DWT / CoreDebug */
|
||||
/* ------------------------------------------------------------------ */
|
||||
typedef struct {
|
||||
volatile uint32_t CTRL;
|
||||
volatile uint32_t CYCCNT;
|
||||
} DWT_Type;
|
||||
|
||||
typedef struct {
|
||||
volatile uint32_t DEMCR;
|
||||
} CoreDebug_Type;
|
||||
|
||||
extern DWT_Type g_con_dwt;
|
||||
extern CoreDebug_Type g_con_core_debug;
|
||||
extern uint32_t con_dwt_step; /* 每次 DWT 访问推进的周期数 */
|
||||
extern int con_dwt_ctrl_sticky; /* 0 = CYCCNTENA 写不进去 */
|
||||
extern uint32_t con_dwt_reads;
|
||||
|
||||
void con_dwt_reset(void);
|
||||
|
||||
/* ★手法 b):每次访问先「花时间」,再决定是否让使能位落得住 */
|
||||
static void con_dwt_touch(void) {
|
||||
con_dwt_reads++;
|
||||
if (con_dwt_reads > 200000U) {
|
||||
printf("[FATAL] fake DWT: accessed %lu times (runaway)\n",
|
||||
(unsigned long)con_dwt_reads);
|
||||
exit(2);
|
||||
}
|
||||
g_con_dwt.CYCCNT += con_dwt_step;
|
||||
if (con_dwt_ctrl_sticky == 0) {
|
||||
g_con_dwt.CTRL &= ~DWT_CTRL_CYCCNTENA_Msk;
|
||||
}
|
||||
}
|
||||
|
||||
#define DWT (con_dwt_touch(), &g_con_dwt)
|
||||
#define CoreDebug (&g_con_core_debug)
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ③ HAL 时基(真实原型 uint32_t HAL_GetTick(void),stm32f4xx_hal.c) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
extern uint32_t con_tick_now;
|
||||
extern uint32_t con_tick_step; /* 每次读之后自动推进的 ms(0 = 不动) */
|
||||
extern uint32_t con_tick_reads;
|
||||
uint32_t HAL_GetTick(void);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ④ 内核入口 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
typedef void *TaskHandle_t;
|
||||
typedef uint32_t TickType_t;
|
||||
typedef long BaseType_t;
|
||||
typedef unsigned long UBaseType_t;
|
||||
|
||||
extern TaskHandle_t con_task_self; /* xTaskGetCurrentTaskHandle 的返回值 */
|
||||
TaskHandle_t xTaskGetCurrentTaskHandle(void);
|
||||
|
||||
extern uint32_t con_delay_calls; /* vTaskDelay 次数 = 真的等过 */
|
||||
extern void (*con_delay_hook)(void); /* 每次 delay 跑一遍(可模拟别的任务排空) */
|
||||
void vTaskDelay(TickType_t ticks);
|
||||
|
||||
extern uint32_t con_crit_enter;
|
||||
extern uint32_t con_crit_exit;
|
||||
void con_crit_push(void);
|
||||
void con_crit_pop(void);
|
||||
uint32_t con_crit_depth(void);
|
||||
/* 进区回调:环空位竞态那条正向用例靠它把"第二个生产者"精确塞进第一个生产者的临界区里
|
||||
* (旧写法里判据在区外,塞进去的写入不会被复核到;新写法复核在区内,必然看到)。
|
||||
* 只在 con_crit_push 记完深度之后调用,不改任何深度值。 */
|
||||
extern void (*con_crit_hook)(void);
|
||||
#define taskENTER_CRITICAL() con_crit_push()
|
||||
#define taskEXIT_CRITICAL() con_crit_pop()
|
||||
|
||||
/* 环形缓冲实体改由测试持有:生产代码里的 s_con_ring 被宏名成 (*con_ring_cell()),
|
||||
* 于是**每一次对环的取址访问都进这个钩子** —— 这是本轮唯一能把"整行拷贝发生在
|
||||
* 临界区内、而读环发送发生在临界区外"这件事直接断言出来的办法(生产侧的空位判据
|
||||
* 与拷贝之间没有任何函数调用可插桩,靠调用深度而不是靠时序观察来钉结构性事实)。
|
||||
* 钩子只记事实不改行为:返回的就是那块 4096B 存储。 */
|
||||
/* 注意:**不要**在这里定义 DBG_CON_RING_SIZE(那是生产 console_log.c 的宏,
|
||||
* 重复定义会撞 C4005);环形存储用字面量 4096U,与生产的数组维度一致即兼容。 */
|
||||
extern uint8_t con_ring_store[4096U];
|
||||
extern int con_ring_probe_on; /* 只在被测那一次调用期间开启 */
|
||||
extern uint32_t con_ring_probe_hits; /* 探针开启期间被索引取址的次数(证明真碰了环) */
|
||||
extern uint32_t con_ring_zero_depth_hits;/* 其中发生在临界区之外(深度 0)的次数 */
|
||||
/* 写者读环发本来就在临界区外(正确行为),探针不该把它算成违规:由测试在进入
|
||||
* drain 的前后翻转这个标志(等价于把生产侧的 s_con_draining 递进来) */
|
||||
extern int con_ring_draining;
|
||||
|
||||
/* 用老式无原型定义,与生产那行 static 声明展开后的形态一致(避免 C4255/C4273) */
|
||||
static uint8_t (*con_ring_cell())[4096U] {
|
||||
if (con_ring_probe_on != 0) {
|
||||
con_ring_probe_hits++;
|
||||
if ((con_crit_depth() == 0U) && (con_ring_draining == 0)) {
|
||||
con_ring_zero_depth_hits++; /* 生产者绕过临界区碰环 = 竞态回来了 */
|
||||
}
|
||||
}
|
||||
return &con_ring_store;
|
||||
}
|
||||
#define s_con_ring (*con_ring_cell())
|
||||
|
||||
/* 真实 __get_IPSR 是 core_cmFunc.h 的静态内联;这里做成宏读一个可控量 */
|
||||
extern uint32_t con_ipsr; /* 0 = 线程模式;!=0 = 中断/异常上下文 */
|
||||
#define __get_IPSR() (con_ipsr)
|
||||
|
||||
/* 事件组:原型与真实 cmsis_os2.h 逐字一致 */
|
||||
typedef void *osEventFlagsId_t;
|
||||
typedef uint32_t osStatus_t;
|
||||
#define osFlagsWaitAny 0x00000000u
|
||||
#define osFlagsNoClear 0x00000001u
|
||||
|
||||
extern uint32_t con_evt_sets;
|
||||
extern uint32_t con_evt_flags;
|
||||
osStatus_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags);
|
||||
|
||||
/* NVIC / RCC:真实版是寄存器写。自愈路径的判据 = 重初始化做了几回、次序对不对 */
|
||||
extern uint32_t con_nvic_clears;
|
||||
extern uint32_t con_rcc_disables;
|
||||
extern uint32_t con_rcc_enables;
|
||||
#define __NVIC_ClearPendingIRQ(irqn) do { (void)(irqn); con_nvic_clears++; } while (0)
|
||||
#define __HAL_RCC_USART1_CLK_DISABLE() do { con_rcc_disables++; } while (0)
|
||||
#define __HAL_RCC_USART1_CLK_ENABLE() do { con_rcc_enables++; } while (0)
|
||||
|
||||
#endif /* STM32F4_FAKE_H */
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - task.h(console_log 模块专用)
|
||||
* 模块功能:本模块用到的两个内核入口,原型与真实 task.h 一致:
|
||||
* TaskHandle_t xTaskGetCurrentTaskHandle(void);
|
||||
* void vTaskDelay(const TickType_t xTicksToDelay);
|
||||
* vTaskDelay 在桩里**不睡**,只计数 + 跑回调(测试用回调模拟"别的任务
|
||||
* 把环形缓冲排空"),并带跑飞保险:超过 5000 次直接 exit(2)。
|
||||
* ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef TASK_H
|
||||
#define TASK_H
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
|
||||
#endif /* TASK_H */
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - usart.h(console_log 模块专用)
|
||||
* 模块功能:与真实 Inc/usart.h 对本模块可见的部分逐字一致:extern UART_HandleTypeDef
|
||||
* huart1; + void MX_USART1_UART_Init(void);(真实那份还声明 huart2~5 与
|
||||
* MX_USART2~5_UART_Init,本模块只碰 USART1,搬多出来的声明没有意义)。
|
||||
* ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建。
|
||||
* 真实 Inc/usart.h 的 include 链(main.h -> stm32f4xx_hal.h -> stm32f4xx.h ->
|
||||
* core_cm4.h)在宿主上编不动,寄存器面由 stm32f4_fake.h 提供(理由见该文件头)。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef USART_H
|
||||
#define USART_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "stm32f4_fake.h"
|
||||
|
||||
extern UART_HandleTypeDef huart1;
|
||||
void MX_USART1_UART_Init(void);
|
||||
|
||||
#endif /* USART_H */
|
||||
@@ -58,11 +58,11 @@ static const ftp_auth_creds_t s_creds = { "admin", DEF_HASH, 1u };
|
||||
|
||||
static void tc_user_match(void)
|
||||
{
|
||||
CHECK(ftp_auth_user_match("admin", "admin") == 1, "TC-FTPAUTH-001a 相等匹配");
|
||||
CHECK(ftp_auth_user_match("admin", "ADMIN") == 1, "TC-FTPAUTH-001b 大小写不敏感");
|
||||
CHECK(ftp_auth_user_match("admin", "root") == 0, "TC-FTPAUTH-001c 不等拒绝");
|
||||
CHECK(ftp_auth_user_match("admin", "admin2") == 0, "TC-FTPAUTH-001d 前缀不算匹配");
|
||||
CHECK(ftp_auth_user_match("admin", NULL) == 0, "TC-FTPAUTH-001e NULL 拒绝");
|
||||
CHECK(ftp_auth_user_match("admin", "admin") == 1, "TC-FTPAUTH-001a equal strings match");
|
||||
CHECK(ftp_auth_user_match("admin", "ADMIN") == 1, "TC-FTPAUTH-001b case-insensitive");
|
||||
CHECK(ftp_auth_user_match("admin", "root") == 0, "TC-FTPAUTH-001c mismatch rejected");
|
||||
CHECK(ftp_auth_user_match("admin", "admin2") == 0, "TC-FTPAUTH-001d prefix does not count as match");
|
||||
CHECK(ftp_auth_user_match("admin", NULL) == 0, "TC-FTPAUTH-001e NULL rejected");
|
||||
}
|
||||
|
||||
/* ===================== TC-FTPAUTH-002..004:基本判定 ===================== */
|
||||
@@ -73,17 +73,17 @@ static void tc_basic(void)
|
||||
|
||||
(void)memset(&st, 0, sizeof(st));
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, 1000u) == FTP_AUTH_OK,
|
||||
"TC-FTPAUTH-002 对用户名+对口令 → OK");
|
||||
CHECK(st.fail_count == 0u, "TC-FTPAUTH-002b 成功后计数为 0");
|
||||
"TC-FTPAUTH-002 right user + right password -> OK");
|
||||
CHECK(st.fail_count == 0u, "TC-FTPAUTH-002b counter is 0 after success");
|
||||
|
||||
(void)memset(&st, 0, sizeof(st));
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, 1000u) == FTP_AUTH_FAIL,
|
||||
"TC-FTPAUTH-003 错口令 → FAIL");
|
||||
CHECK(st.fail_count == 1u, "TC-FTPAUTH-003b 失败计数 1");
|
||||
"TC-FTPAUTH-003 wrong password -> FAIL");
|
||||
CHECK(st.fail_count == 1u, "TC-FTPAUTH-003b fail count = 1");
|
||||
|
||||
(void)memset(&st, 0, sizeof(st));
|
||||
CHECK(ftp_auth_check(&s_creds, 0u, "admin", &st, 1000u) == FTP_AUTH_FAIL,
|
||||
"TC-FTPAUTH-004 错用户名+对口令 → FAIL(哈希路径已走)");
|
||||
"TC-FTPAUTH-004 wrong user + right password -> FAIL (hash path taken)");
|
||||
}
|
||||
|
||||
/* ===================== TC-FTPAUTH-005..006:锁定与解锁 ===================== */
|
||||
@@ -96,22 +96,22 @@ static void tc_lock(void)
|
||||
(void)memset(&st, 0, sizeof(st));
|
||||
for (int i = 0; i < 5; i++) {
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, t) == FTP_AUTH_FAIL,
|
||||
"TC-FTPAUTH-005 第 1..5 次失败 → FAIL");
|
||||
"TC-FTPAUTH-005 attempts 1..5 -> FAIL");
|
||||
}
|
||||
CHECK(st.fail_count == 5u, "TC-FTPAUTH-005b 计数到达 FAIL_MAX");
|
||||
CHECK(st.fail_count == 5u, "TC-FTPAUTH-005b counter reaches FAIL_MAX");
|
||||
/* 锁定期内:对口令也 LOCKED,且不再计数 */
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, t + 1u) == FTP_AUTH_LOCKED,
|
||||
"TC-FTPAUTH-005c 锁定期内对口令 → LOCKED");
|
||||
CHECK(st.fail_count == 5u, "TC-FTPAUTH-005d LOCKED 不改变计数");
|
||||
"TC-FTPAUTH-005c right password during lock window -> LOCKED");
|
||||
CHECK(st.fail_count == 5u, "TC-FTPAUTH-005d LOCKED does not change the counter");
|
||||
/* 59999 tick:仍在窗内(差值 < 60000) */
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st,
|
||||
st.lock_until_tick - 1u) == FTP_AUTH_LOCKED,
|
||||
"TC-FTPAUTH-005e 窗内最后 1 tick 仍 LOCKED");
|
||||
"TC-FTPAUTH-005e last tick inside window still LOCKED");
|
||||
/* 期满:恰好到达 lock_until → 放行 */
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st,
|
||||
st.lock_until_tick) == FTP_AUTH_OK,
|
||||
"TC-FTPAUTH-006 期满对口令 → OK");
|
||||
CHECK(st.fail_count == 0u, "TC-FTPAUTH-006b 解锁成功后计数清零");
|
||||
"TC-FTPAUTH-006 right password after lock expiry -> OK");
|
||||
CHECK(st.fail_count == 0u, "TC-FTPAUTH-006b counter cleared after unlock then success");
|
||||
}
|
||||
|
||||
/* ===================== TC-FTPAUTH-007:成功清零计数 ===================== */
|
||||
@@ -126,16 +126,16 @@ static void tc_reset(void)
|
||||
(void)ftp_auth_check(&s_creds, 1u, "wrong", &st, t);
|
||||
}
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, t) == FTP_AUTH_OK,
|
||||
"TC-FTPAUTH-007a 3 败后成功 → OK");
|
||||
CHECK(st.fail_count == 0u, "TC-FTPAUTH-007b 成功清零");
|
||||
"TC-FTPAUTH-007a success after 3 failures -> OK");
|
||||
CHECK(st.fail_count == 0u, "TC-FTPAUTH-007b success clears the counter");
|
||||
for (int i = 0; i < 4; i++) {
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, t) == FTP_AUTH_FAIL,
|
||||
"TC-FTPAUTH-007c 清零后再 4 败仍 FAIL(未锁)");
|
||||
"TC-FTPAUTH-007c after clear, 4 more failures still FAIL (not locked)");
|
||||
}
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, t) == FTP_AUTH_FAIL,
|
||||
"TC-FTPAUTH-007d 第 5 败 FAIL 并上锁");
|
||||
"TC-FTPAUTH-007d 5th failure FAILs and arms the lock");
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, t + 1u) == FTP_AUTH_LOCKED,
|
||||
"TC-FTPAUTH-007e 连续 5 败后 LOCKED");
|
||||
"TC-FTPAUTH-007e LOCKED after 5 consecutive failures");
|
||||
}
|
||||
|
||||
/* ===================== TC-FTPAUTH-008:tick 32 位回绕 ===================== */
|
||||
@@ -150,10 +150,10 @@ static void tc_wraparound(void)
|
||||
st.lock_until_tick = 0xFFFFFFF0u;
|
||||
/* 回绕前 240 tick:仍在窗内 */
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, 0xFFFFFF00u) == FTP_AUTH_LOCKED,
|
||||
"TC-FTPAUTH-008a 回绕前窗内 → LOCKED");
|
||||
"TC-FTPAUTH-008a inside window before wraparound -> LOCKED");
|
||||
/* 回绕后 32 tick:now=0x10,(int32_t)(0x10-0xFFFFFFF0)=+32 → 期满 */
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, 0x00000010u) == FTP_AUTH_OK,
|
||||
"TC-FTPAUTH-008b 回绕后期满 → OK(有符号差抗回绕)");
|
||||
"TC-FTPAUTH-008b after wraparound window elapsed -> OK (signed diff resists wraparound)");
|
||||
}
|
||||
|
||||
/* ===================== TC-FTPAUTH-009..010:fail-closed 边界 ===================== */
|
||||
@@ -165,17 +165,17 @@ static void tc_fail_closed(void)
|
||||
|
||||
(void)memset(&st, 0, sizeof(st));
|
||||
CHECK(ftp_auth_check(&bad_creds, 1u, "admin", &st, 1000u) == FTP_AUTH_FAIL,
|
||||
"TC-FTPAUTH-009 非法 hex 配置 → 恒 FAIL(无人能登录,fail-closed)");
|
||||
"TC-FTPAUTH-009 invalid hex config -> always FAIL (nobody can log in, fail-closed)");
|
||||
|
||||
(void)memset(&st, 0, sizeof(st));
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "", &st, 1000u) == FTP_AUTH_FAIL,
|
||||
"TC-FTPAUTH-010a 空口令 → FAIL");
|
||||
"TC-FTPAUTH-010a empty password -> FAIL");
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, NULL, &st, 1000u) == FTP_AUTH_FAIL,
|
||||
"TC-FTPAUTH-010b NULL 口令 → FAIL");
|
||||
"TC-FTPAUTH-010b NULL password -> FAIL");
|
||||
CHECK(ftp_auth_check(NULL, 1u, "admin", &st, 1000u) == FTP_AUTH_FAIL,
|
||||
"TC-FTPAUTH-010c NULL creds → FAIL");
|
||||
"TC-FTPAUTH-010c NULL creds -> FAIL");
|
||||
CHECK(ftp_auth_check(&s_creds, 1u, "admin", NULL, 1000u) == FTP_AUTH_FAIL,
|
||||
"TC-FTPAUTH-010d NULL state → FAIL");
|
||||
"TC-FTPAUTH-010d NULL state -> FAIL");
|
||||
}
|
||||
|
||||
/* ===================== main ===================== */
|
||||
|
||||
@@ -148,9 +148,9 @@ static void tc_register(void)
|
||||
printf("-- TC-FTPCFG-003..005 register --\n");
|
||||
fake_reset();
|
||||
ftp_cfg_register();
|
||||
CHECK(g_reg_n == 1, "TC-FTPCFG-003a 首次注册一次");
|
||||
CHECK(g_reg_n == 1, "TC-FTPCFG-003a first register adds exactly one entry");
|
||||
ftp_cfg_register();
|
||||
CHECK(g_reg_n == 1, "TC-FTPCFG-003 重复 register 不重复登记(幂等)");
|
||||
CHECK(g_reg_n == 1, "TC-FTPCFG-003 repeated register never duplicates the entry (idempotent)");
|
||||
|
||||
CHECK((strcmp(g_reg.name, "ftp") == 0) &&
|
||||
(g_reg.data_size == sizeof(ftp_cfg_t)) &&
|
||||
@@ -158,7 +158,7 @@ static void tc_register(void)
|
||||
(g_reg.p_defaults != NULL) &&
|
||||
(g_reg.p_data == (const void *)&s_cfg) &&
|
||||
(g_reg.p_cb == NULL),
|
||||
"TC-FTPCFG-004 段名/尺寸/键数/默认值/**p_cb 必须为 NULL**");
|
||||
"TC-FTPCFG-004 section name/size/key count/defaults/**p_cb must be NULL**");
|
||||
|
||||
CHECK((strcmp(g_reg.p_keys[0].name, "user") == 0) &&
|
||||
(g_reg.p_keys[0].type == CFG_VAL_STR) &&
|
||||
@@ -170,7 +170,7 @@ static void tc_register(void)
|
||||
(strcmp(g_reg.p_keys[2].name, "write") == 0) &&
|
||||
(g_reg.p_keys[2].type == CFG_VAL_UINT) &&
|
||||
(g_reg.p_keys[2].len == 1u),
|
||||
"TC-FTPCFG-005 键表逐项:user(STR,16)/pwd_hash(STR,65)/write(UINT,1)");
|
||||
"TC-FTPCFG-005 key table item by item: user(STR,16)/pwd_hash(STR,65)/write(UINT,1)");
|
||||
}
|
||||
|
||||
/* ===================== 出厂默认与只读访问 ===================== */
|
||||
@@ -182,17 +182,17 @@ static void tc_defaults(void)
|
||||
printf("-- TC-FTPCFG-001..002,014 defaults --\n");
|
||||
/* 契约就是"恒非空"(静态常驻),单独一条断言;随后的显式守卫既让
|
||||
* test-cppcheck 不再报"冗余或空指针二选一",也让真出问题时报在 001a 上 */
|
||||
CHECK(p != NULL, "TC-FTPCFG-001a ftp_cfg_get 恒非空(静态常驻)");
|
||||
CHECK(p != NULL, "TC-FTPCFG-001a ftp_cfg_get never NULL (statically resident)");
|
||||
if (p == NULL) {
|
||||
return;
|
||||
}
|
||||
CHECK(strcmp(p->user, "admin") == 0, "TC-FTPCFG-001 未注册也可 get,user = admin");
|
||||
CHECK(strcmp(p->user, "admin") == 0, "TC-FTPCFG-001 get works even unregistered, user = admin");
|
||||
CHECK((strcmp(p->pwd_hash, FTP_CFG_DEF_HASH) == 0) && (p->write_en == 1u),
|
||||
"TC-FTPCFG-002 初值 = 出厂哈希 + write_en=1(维护工作流依赖)");
|
||||
"TC-FTPCFG-002 initial value = factory hash + write_en=1 (maintenance workflow relies on it)");
|
||||
CHECK((FTP_CFG_USER_MAX == 16u) && (FTP_CFG_HASH_MAX == 65u) &&
|
||||
(sizeof(p->user) == FTP_CFG_USER_MAX) &&
|
||||
(sizeof(p->pwd_hash) == FTP_CFG_HASH_MAX),
|
||||
"TC-FTPCFG-014 缓冲尺寸契约:user 16 / hash 65(64 hex + NUL)");
|
||||
"TC-FTPCFG-014 buffer size contract: user 16 / hash 65 (64 hex + NUL)");
|
||||
}
|
||||
|
||||
/* ===================== 默认口令判定(启动告警的依据) ===================== */
|
||||
@@ -200,17 +200,17 @@ static void tc_defaults(void)
|
||||
static void tc_default_pwd(void)
|
||||
{
|
||||
printf("-- TC-FTPCFG-006..009 default-password detection --\n");
|
||||
CHECK(ftp_cfg_is_default_pwd() == 1, "TC-FTPCFG-006 出厂态判为默认口令");
|
||||
CHECK(ftp_cfg_is_default_pwd() == 1, "TC-FTPCFG-006 factory state judged as default password");
|
||||
|
||||
set_hash("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
|
||||
CHECK(ftp_cfg_is_default_pwd() == 0, "TC-FTPCFG-007 改了口令 → 不再默认");
|
||||
CHECK(ftp_cfg_is_default_pwd() == 0, "TC-FTPCFG-007 password changed -> no longer default");
|
||||
|
||||
set_hash_upper();
|
||||
CHECK(ftp_cfg_is_default_pwd() == 1,
|
||||
"TC-FTPCFG-008 大写 hex 仍判默认(比较必须大小写不敏感)");
|
||||
"TC-FTPCFG-008 uppercase hex still judged default (comparison must be case-insensitive)");
|
||||
|
||||
set_hash("");
|
||||
CHECK(ftp_cfg_is_default_pwd() == 0, "TC-FTPCFG-009 空哈希不是默认口令");
|
||||
CHECK(ftp_cfg_is_default_pwd() == 0, "TC-FTPCFG-009 empty hash is not the default password");
|
||||
}
|
||||
|
||||
/* ===================== 静态比较函数(NULL / 折叠 / 前缀) ===================== */
|
||||
@@ -219,12 +219,12 @@ static void tc_ci_eq(void)
|
||||
{
|
||||
printf("-- TC-FTPCFG-010..013 case-insensitive compare --\n");
|
||||
CHECK(s_str_ci_eq("A_b1!Z", "a_b1!z") == 1,
|
||||
"TC-FTPCFG-010 只折叠 A-Z,数字/下划线/符号原样比较");
|
||||
CHECK(s_str_ci_eq("abc", "abcd") == 0, "TC-FTPCFG-011 前缀关系不算相等");
|
||||
"TC-FTPCFG-010 only folds A-Z; digits/underscore/symbols compared as-is");
|
||||
CHECK(s_str_ci_eq("abc", "abcd") == 0, "TC-FTPCFG-011 prefix relation is not equality");
|
||||
CHECK((s_str_ci_eq(NULL, "x") == 0) && (s_str_ci_eq("x", NULL) == 0),
|
||||
"TC-FTPCFG-012 任一为 NULL 判不等");
|
||||
"TC-FTPCFG-012 either side NULL -> not equal");
|
||||
CHECK(s_str_ci_eq(NULL, NULL) == 0,
|
||||
"TC-FTPCFG-013 两个 NULL 也判不等(fail-closed)");
|
||||
"TC-FTPCFG-013 both NULL also judged not equal (fail-closed)");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
@@ -315,17 +315,17 @@ static void tc_cp56(void)
|
||||
|
||||
iec104_epoch_ms_to_cp56(e1, b);
|
||||
rc = iec104_cp56_to_epoch_ms(b, &back);
|
||||
CHECK((rc == 0) && (back == e1), "TC-001 CP56 往返 2000-01-01");
|
||||
CHECK((rc == 0) && (back == e1), "TC-001 CP56 round trip on 2000-01-01");
|
||||
iec104_epoch_ms_to_cp56(e2, b);
|
||||
back = 0u;
|
||||
rc = iec104_cp56_to_epoch_ms(b, &back);
|
||||
CHECK((rc == 0) && (back == e2), "TC-001 CP56 往返闰日 2024-02-29");
|
||||
CHECK((rc == 0) && (back == e2), "TC-001 CP56 round trip on the leap day 2024-02-29");
|
||||
b[2] |= 0x80u;
|
||||
CHECK(iec104_cp56_to_epoch_ms(b, &back) == -2, "TC-001 IV → -2");
|
||||
CHECK(iec104_cp56_to_epoch_ms(b, &back) == -2, "TC-001 IV -> -2");
|
||||
iec104_epoch_ms_to_cp56(0u, b); /* 早于 2000 → 钳位 */
|
||||
back = 0u;
|
||||
(void)iec104_cp56_to_epoch_ms(b, &back);
|
||||
CHECK(back == e1, "TC-001 早于 2000 钳位");
|
||||
CHECK(back == e1, "TC-001 a value before 2000 is clamped");
|
||||
}
|
||||
|
||||
static void tc_ioa_float(void)
|
||||
@@ -334,12 +334,12 @@ static void tc_ioa_float(void)
|
||||
|
||||
iec104_put_ioa(b, 0x123456u);
|
||||
CHECK((iec104_get_ioa(b) == 0x123456u) && (b[0] == 0x56u) && (b[1] == 0x34u)
|
||||
&& (b[2] == 0x12u), "TC-002 IOA 3B 小端");
|
||||
&& (b[2] == 0x12u), "TC-002 IOA is 3 bytes little endian");
|
||||
iec104_put_float(b, 1.0f);
|
||||
CHECK((b[0] == 0x00u) && (b[1] == 0x00u) && (b[2] == 0x80u)
|
||||
&& (b[3] == 0x3Fu), "TC-002 float IEEE754 小端");
|
||||
&& (b[3] == 0x3Fu), "TC-002 float in IEEE754 little endian");
|
||||
iec104_put_float(b, -12.5f);
|
||||
CHECK(iec104_get_float(b) == -12.5f, "TC-002 float 往返");
|
||||
CHECK(iec104_get_float(b) == -12.5f, "TC-002 float round trip");
|
||||
}
|
||||
|
||||
static void tc_apci(void)
|
||||
@@ -351,9 +351,9 @@ static void tc_apci(void)
|
||||
static const uint8_t b[6] = { 0x68u, 4u, 0x07u, 0u, 0u, 0u };
|
||||
|
||||
iec104_link_on_rx(&g_lk, b, 3u, now);
|
||||
CHECK(cap_u_count(0x0Bu) == 0, "TC-003 半帧不触发");
|
||||
CHECK(cap_u_count(0x0Bu) == 0, "TC-003 a half frame triggers nothing");
|
||||
iec104_link_on_rx(&g_lk, &b[3], 3u, now);
|
||||
CHECK(cap_u_count(0x0Bu) == 1, "TC-003 半帧重组 → con");
|
||||
CHECK(cap_u_count(0x0Bu) == 1, "TC-003 a half frame is reassembled then con");
|
||||
}
|
||||
{
|
||||
static const uint8_t b[12] = {
|
||||
@@ -364,21 +364,21 @@ static void tc_apci(void)
|
||||
iec104_link_on_rx(&g_lk, b, 12u, now);
|
||||
CHECK((cap_u_count(0x83u) == 1)
|
||||
&& (iec104_link_state(&g_lk) == IEC104_ST_STOPPED),
|
||||
"TC-003 粘帧逐处 + STOPDT 生效");
|
||||
"TC-003 a coalesced frame is handled one by one and STOPDT takes effect");
|
||||
}
|
||||
reset_all(now);
|
||||
{
|
||||
static const uint8_t b[6] = { 0x69u, 4u, 0x07u, 0u, 0u, 0u };
|
||||
|
||||
iec104_link_on_rx(&g_lk, b, 6u, now);
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-003 坏起始符 → IDLE");
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-003 a bad start byte -> IDLE");
|
||||
}
|
||||
reset_all(now);
|
||||
{
|
||||
static const uint8_t b[6] = { 0x68u, 3u, 0x07u, 0u, 0u, 0u };
|
||||
|
||||
iec104_link_on_rx(&g_lk, b, 6u, now);
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-003 坏长度 → IDLE");
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-003 a bad length -> IDLE");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,10 +387,10 @@ static void tc_startdt(void)
|
||||
uint32_t now = 1000u;
|
||||
|
||||
reset_all(now);
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_OPEN, "TC-004 open → OPEN");
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_OPEN, "TC-004 open -> OPEN");
|
||||
handshake(now);
|
||||
CHECK((cap_u_count(0x0Bu) == 1)
|
||||
&& (iec104_link_state(&g_lk) == IEC104_ST_RUN), "TC-004 STARTDT → RUN");
|
||||
&& (iec104_link_state(&g_lk) == IEC104_ST_RUN), "TC-004 STARTDT -> RUN");
|
||||
}
|
||||
|
||||
static void tc_open_ignore(void)
|
||||
@@ -403,11 +403,11 @@ static void tc_open_ignore(void)
|
||||
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i_count() == 0) && (iec104_link_state(&g_lk) == IEC104_ST_OPEN),
|
||||
"TC-005 OPEN 态 I 帧忽略");
|
||||
"TC-005 an I frame in OPEN state is ignored");
|
||||
handshake(now);
|
||||
m_ns = 0u; /* OPEN 忽略不消费序号 → 主站仍从 0 起 */
|
||||
feed_i(a, n, now); /* ns=0 仍有效(序号未被消费) */
|
||||
CHECK(cap_i_count() == 1, "TC-005 RUN 后同 ns 正常应答");
|
||||
CHECK(cap_i_count() == 1, "TC-005 after RUN the same ns is answered normally");
|
||||
}
|
||||
|
||||
static void tc_interrogation(void)
|
||||
@@ -426,34 +426,34 @@ static void tc_interrogation(void)
|
||||
n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 100u)
|
||||
&& (c.cot == IEC104_COT_ACTCON), "TC-006 actcon 即发 COT7");
|
||||
&& (c.cot == IEC104_COT_ACTCON), "TC-006 actcon is sent right away, COT7");
|
||||
iec104_link_poll(&g_lk, now);
|
||||
CHECK(cap_i_count() == 5, "TC-006 首 poll 发满预算 4 分片");
|
||||
CHECK(cap_i_count() == 5, "TC-006 the first poll uses the whole budget of 4 frames");
|
||||
CHECK((cap_i(1, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_INIT)
|
||||
&& (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 4u)) && (c.ioa == 1u),
|
||||
"TC-006 YX 双点组 首召 COT=4");
|
||||
"TC-006 YX double-point group, first call COT=4");
|
||||
CHECK((cap_i(2, &c) == 0) && (c.type == 1u)
|
||||
&& (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 29u)) && (c.ioa == 5u),
|
||||
"TC-006 YX 单点组 29 点");
|
||||
"TC-006 YX single-point group with 29 points");
|
||||
CHECK((cap_i(3, &c) == 0) && (c.type == 13u)
|
||||
&& (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 8u)) && (c.ioa == 0x4011u)
|
||||
&& (c.obj_len == 40u), "TC-006 YC 组 8 点 40B");
|
||||
&& (c.obj_len == 40u), "TC-006 YC group of 8 points, 40 bytes");
|
||||
if (cap_i(3, &c) == 0) {
|
||||
CHECK((iec104_get_float(c.obj) == 111.0f) && (c.obj[4] == 0u),
|
||||
"TC-006 YC 值 111.0 QDS=0");
|
||||
CHECK(c.obj[9] == IEC104_QDS_IV, "TC-006 无效通道 QDS=IV");
|
||||
"TC-006 YC value 111.0 with QDS=0");
|
||||
CHECK(c.obj[9] == IEC104_QDS_IV, "TC-006 an invalid channel sets QDS=IV");
|
||||
}
|
||||
CHECK((cap_i(4, &c) == 0) && (c.ioa == 0x401Du) && (c.vsq == 1u),
|
||||
"TC-006 频率单点 SQ=0");
|
||||
"TC-006 frequency as a single point with SQ=0");
|
||||
iec104_link_poll(&g_lk, now + 100u);
|
||||
CHECK((cap_i_count() == 6) && (cap_i(5, &c) == 0) && (c.type == 100u)
|
||||
&& (c.cot == IEC104_COT_ACTTERM), "TC-006 actterm 收尾");
|
||||
&& (c.cot == IEC104_COT_ACTTERM), "TC-006 actterm closes the sequence");
|
||||
/* 二召:init 已清 → COT=2 */
|
||||
n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
|
||||
feed_i(a, n, now + 200u);
|
||||
iec104_link_poll(&g_lk, now + 200u);
|
||||
CHECK((cap_i(7, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_BG_SCAN),
|
||||
"TC-006 二召 COT=2");
|
||||
"TC-006 second call COT=2");
|
||||
/* RBT 位 → COT=4(actcon3 是第 12 个未确认帧 → 先 S 确认窗才开) */
|
||||
n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, IEC104_COT_BIT_RBT,
|
||||
1u, 0u, body, 1u);
|
||||
@@ -461,7 +461,7 @@ static void tc_interrogation(void)
|
||||
feed_s(12u, now + 300u);
|
||||
iec104_link_poll(&g_lk, now + 300u);
|
||||
CHECK((cap_i(12, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_INIT),
|
||||
"TC-006 RBT → COT=4");
|
||||
"TC-006 RBT -> COT=4");
|
||||
}
|
||||
|
||||
static void tc_cov_yx(void)
|
||||
@@ -473,33 +473,33 @@ static void tc_cov_yx(void)
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
iec104_link_poll(&g_lk, now); /* 建快照 */
|
||||
CHECK(cap_i_count() == 0, "TC-007 首 poll 无自发");
|
||||
CHECK(cap_i_count() == 0, "TC-007 the first poll sends nothing spontaneous");
|
||||
s_f_bits |= (1uLL << 15); /* YX16(idx15)动作 */
|
||||
iec104_link_poll(&g_lk, now + 100u);
|
||||
/* B9:单点 YX 自发 = M_SP_TB_1(30)带 CP56Time2a 时标(SOE 上送路径) */
|
||||
CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 30u)
|
||||
&& (c.cot == IEC104_COT_SPONT) && (c.ioa == 16u)
|
||||
&& (c.obj_len == 8u) && (c.obj[0] == 1u), "TC-007 YX16 变位 COT3 TB");
|
||||
&& (c.obj_len == 8u) && (c.obj[0] == 1u), "TC-007 a YX16 change of state sends COT3 with TB");
|
||||
if ((cap_i(0, &c) == 0) && (c.obj_len == 8u)) {
|
||||
CHECK(iec104_cp56_to_epoch_ms(&c.obj[1], &back) == 0,
|
||||
"TC-007 CP56 可解");
|
||||
CHECK(back == s_epoch_val, "TC-007 时标 = epoch_ms 源");
|
||||
"TC-007 the CP56 time tag decodes");
|
||||
CHECK(back == s_epoch_val, "TC-007 the time tag equals the epoch_ms source");
|
||||
}
|
||||
s_f_bits &= ~(1uLL << 15); /* 复归 */
|
||||
iec104_link_poll(&g_lk, now + 200u);
|
||||
CHECK((cap_i_count() == 2) && (cap_i(1, &c) == 0) && (c.type == 30u)
|
||||
&& (c.ioa == 16u) && (c.obj[0] == 0u), "TC-007 YX16 复归再变");
|
||||
&& (c.ioa == 16u) && (c.obj[0] == 0u), "TC-007 YX16 is reset and changes again");
|
||||
/* B9 IV 路径:时标源无效 → CP56 IV 位如实置位 */
|
||||
s_epoch_val = UINT64_MAX;
|
||||
s_f_bits |= (1uLL << 16); /* YX17(idx16)动作 */
|
||||
iec104_link_poll(&g_lk, now + 300u);
|
||||
CHECK((cap_i_count() == 3) && (cap_i(2, &c) == 0) && (c.type == 30u)
|
||||
&& (c.ioa == 17u) && ((c.obj[3] & 0x80u) != 0u),
|
||||
"TC-007 时钟无效 → CP56 IV 位");
|
||||
"TC-007 an invalid clock sets the CP56 IV bit");
|
||||
s_epoch_val = 1760234567000ULL;
|
||||
s_f_bits |= (1uLL << 0); /* idx0 无效位 → 不触发 */
|
||||
iec104_link_poll(&g_lk, now + 400u);
|
||||
CHECK(cap_i_count() == 3, "TC-007 无效位变位不触发");
|
||||
CHECK(cap_i_count() == 3, "TC-007 a change while the IV bit is set triggers nothing");
|
||||
}
|
||||
|
||||
static void tc_cov_yc(void)
|
||||
@@ -513,12 +513,12 @@ static void tc_cov_yc(void)
|
||||
iec104_link_poll(&g_lk, now); /* 快照 = 100 */
|
||||
s_f_yc[0] = 105.0f; /* 死区 10 内 */
|
||||
iec104_link_poll(&g_lk, now + 100u);
|
||||
CHECK(cap_i_count() == 0, "TC-008 死区内不上送");
|
||||
CHECK(cap_i_count() == 0, "TC-008 inside the deadband nothing is uploaded");
|
||||
s_f_yc[0] = 130.0f; /* 130-105=25 > 10 */
|
||||
iec104_link_poll(&g_lk, now + 200u);
|
||||
CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 13u)
|
||||
&& (c.cot == IEC104_COT_SPONT) && (c.ioa == 0x4011u)
|
||||
&& (iec104_get_float(c.obj) == 130.0f), "TC-008 越死区上送");
|
||||
&& (iec104_get_float(c.obj) == 130.0f), "TC-008 crossing the deadband uploads");
|
||||
}
|
||||
|
||||
static void feed_rd(uint32_t now)
|
||||
@@ -540,12 +540,12 @@ static void tc_k_window(void)
|
||||
for (i = 0; i < 12; i++) {
|
||||
feed_rd(now);
|
||||
}
|
||||
CHECK(cap_i_count() == 12, "TC-009 k=12 封顶");
|
||||
CHECK(cap_i_count() == 12, "TC-009 the k=12 window caps the outstanding frames");
|
||||
feed_rd(now);
|
||||
CHECK(cap_i_count() == 12, "TC-009 第 13 个被 k 窗抑制");
|
||||
CHECK(cap_i_count() == 12, "TC-009 the 13th frame is suppressed by the k window");
|
||||
feed_s(12u, now); /* 全确认 → 窗开 */
|
||||
feed_rd(now);
|
||||
CHECK(cap_i_count() == 13, "TC-009 S 确认后恢复发送");
|
||||
CHECK(cap_i_count() == 13, "TC-009 sending resumes after the S acknowledgement");
|
||||
}
|
||||
|
||||
static void tc_w_thresh(void)
|
||||
@@ -559,9 +559,9 @@ static void tc_w_thresh(void)
|
||||
for (i = 0; i < 7; i++) {
|
||||
feed_rd(now);
|
||||
}
|
||||
CHECK(cap_has_s(&nr) == 0, "TC-010 收 7 未发 S");
|
||||
CHECK(cap_has_s(&nr) == 0, "TC-010 after 7 received frames no S has been sent yet");
|
||||
feed_rd(now); /* 第 8 个 → w 阈 */
|
||||
CHECK((cap_has_s(&nr) == 1) && (nr == 8u), "TC-010 w=8 → S nr=8");
|
||||
CHECK((cap_has_s(&nr) == 1) && (nr == 8u), "TC-010 w=8 -> S with nr=8");
|
||||
}
|
||||
|
||||
static void tc_t2(void)
|
||||
@@ -573,9 +573,9 @@ static void tc_t2(void)
|
||||
handshake(now);
|
||||
feed_rd(now); /* rx_unacked=1,t2 起爆 */
|
||||
iec104_link_poll(&g_lk, now + 9999u);
|
||||
CHECK(cap_has_s(&nr) == 0, "TC-011 t2 未到期不发 S");
|
||||
CHECK(cap_has_s(&nr) == 0, "TC-011 no S before t2 expires");
|
||||
iec104_link_poll(&g_lk, now + 10001u);
|
||||
CHECK((cap_has_s(&nr) == 1) && (nr == 1u), "TC-011 t2 到期发 S");
|
||||
CHECK((cap_has_s(&nr) == 1) && (nr == 1u), "TC-011 S is sent once t2 expires");
|
||||
}
|
||||
|
||||
static void tc_t3_t1(void)
|
||||
@@ -585,19 +585,19 @@ static void tc_t3_t1(void)
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
iec104_link_poll(&g_lk, now + 19999u);
|
||||
CHECK(cap_u_count(0x43u) == 0, "TC-012 t3 未到期无 TESTFR");
|
||||
CHECK(cap_u_count(0x43u) == 0, "TC-012 no TESTFR before t3 expires");
|
||||
iec104_link_poll(&g_lk, now + 20001u);
|
||||
CHECK(cap_u_count(0x43u) == 1, "TC-012 t3 到期发 TESTFR act");
|
||||
CHECK(cap_u_count(0x43u) == 1, "TC-012 TESTFR act is sent once t3 expires");
|
||||
iec104_link_poll(&g_lk, now + 20001u + 15001u);
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-012 TESTFR 未答 t1 → IDLE");
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-012 an unanswered TESTFR hits t1 -> IDLE");
|
||||
/* con 清挂起 → 不误杀 */
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
iec104_link_poll(&g_lk, now + 20001u);
|
||||
CHECK(cap_u_count(0x43u) == 1, "TC-012 重连 t3 再触发");
|
||||
CHECK(cap_u_count(0x43u) == 1, "TC-012 after reconnect t3 fires again");
|
||||
feed_u(0x83u, now + 20100u); /* TESTFR con */
|
||||
iec104_link_poll(&g_lk, now + 35000u);
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_RUN, "TC-012 con 清挂起后 t1 不误杀");
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_RUN, "TC-012 once con clears the pending frame t1 does not kill it by mistake");
|
||||
}
|
||||
|
||||
static void tc_dc(void)
|
||||
@@ -614,13 +614,13 @@ static void tc_dc(void)
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 52u) && (c.cot == IEC104_COT_ACTCON)
|
||||
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-013 选择段 actcon");
|
||||
CHECK((s_f_yk_sel == 1u) && (s_f_yk_ioa == 0x600Au), "TC-013 选择段转发源");
|
||||
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-013 the select phase answers actcon");
|
||||
CHECK((s_f_yk_sel == 1u) && (s_f_yk_ioa == 0x600Au), "TC-013 the select phase forwards the source");
|
||||
body[0] = 0x02u; /* 执行 ON */
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u)
|
||||
&& (s_f_yk_sel == 0u), "TC-013 执行段 actcon");
|
||||
&& (s_f_yk_sel == 0u), "TC-013 the execute phase answers actcon");
|
||||
/* 失配:选 ON 执 OFF */
|
||||
body[0] = 0x82u;
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u);
|
||||
@@ -629,13 +629,13 @@ static void tc_dc(void)
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(3, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
|
||||
"TC-013 执行失配 PN");
|
||||
"TC-013 a mismatching execute phase returns PN");
|
||||
/* 拒绝类(YK1):选择段即 PN */
|
||||
body[0] = 0x82u;
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x6000u, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(4, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
|
||||
"TC-013 无出口硬件 YK 选择段 PN");
|
||||
"TC-013 a YK without output hardware returns PN in the select phase");
|
||||
}
|
||||
|
||||
/* TC-024(B7):YK12 全局软压板两段式转真(经真实板端源 + 原子封存) */
|
||||
@@ -649,21 +649,21 @@ static void tc_dc_yk12(void)
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 软压板默认投入");
|
||||
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 soft plates are engaged by default");
|
||||
body[0] = 0x81u; /* S=1, DPC=OFF:选择段 */
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(0, &c) == 0) && (c.cot == IEC104_COT_ACTCON)
|
||||
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-024 YK12 选择段 actcon");
|
||||
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 选择段不动作");
|
||||
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-024 the YK12 select phase answers actcon");
|
||||
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 the select phase performs no action");
|
||||
body[0] = 0x01u; /* 执行 OFF */
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u),
|
||||
"TC-024 YK12 执行段 actcon");
|
||||
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-024 执行段软压板转真退出");
|
||||
CHECK(prot_set_selfcheck() == 0, "TC-024 set_sealed 原子封存 → selfcheck 通过");
|
||||
CHECK(prot_set_dirty_peek() == 1u, "TC-024 置脏待持久化");
|
||||
"TC-024 the YK12 execute phase answers actcon");
|
||||
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-024 the execute phase drives the soft plate to the released state");
|
||||
CHECK(prot_set_selfcheck() == 0, "TC-024 set_sealed seals atomically and selfcheck passes");
|
||||
CHECK(prot_set_dirty_peek() == 1u, "TC-024 the dirty flag is set for persistence");
|
||||
body[0] = 0x82u; /* 再投入:选择 ON */
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
@@ -671,13 +671,13 @@ static void tc_dc_yk12(void)
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(3, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u),
|
||||
"TC-024 投入执行 actcon");
|
||||
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 软压板转真投入");
|
||||
"TC-024 the engage command executes with actcon");
|
||||
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 the soft plate reaches the engaged state");
|
||||
body[0] = 0x80u; /* DPC=00 非法:选择段即 PN */
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(4, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
|
||||
"TC-024 非法 DPC 选择段 PN");
|
||||
"TC-024 an invalid DPC returns PN in the select phase");
|
||||
}
|
||||
|
||||
static void tc_se(void)
|
||||
@@ -691,27 +691,27 @@ static void tc_se(void)
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
CHECK(addr == 0x8858u, "TC-014 OC1 电流定值通讯地址");
|
||||
CHECK(addr == 0x8858u, "TC-014 the OC1 current setting communication address");
|
||||
iec104_put_float(body, 5.0f);
|
||||
body[4] = IEC104_QDC_S; /* 选择段 */
|
||||
n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 50u)
|
||||
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-014 定值选择 actcon");
|
||||
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-014 selecting a setting answers actcon");
|
||||
body[4] = 0u; /* 执行段 */
|
||||
n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u),
|
||||
"TC-014 定值执行 actcon");
|
||||
CHECK(prot_set_get_f(PS_OC1_CUR) == 5.0f, "TC-014 定值已写入(真实 prot_set)");
|
||||
CHECK(prot_set_selfcheck() == 0, "TC-014 执行后 seal → selfcheck 通过");
|
||||
CHECK(prot_set_dirty_peek() == 1u, "TC-014 执行段置脏(B7 待落盘)");
|
||||
"TC-014 executing a setting answers actcon");
|
||||
CHECK(prot_set_get_f(PS_OC1_CUR) == 5.0f, "TC-014 the setting really was written (real prot_set)");
|
||||
CHECK(prot_set_selfcheck() == 0, "TC-014 sealing after execute keeps selfcheck passing");
|
||||
CHECK(prot_set_dirty_peek() == 1u, "TC-014 the execute phase sets dirty (B7, waiting to be persisted)");
|
||||
iec104_put_float(body, 100000.0f); /* 越上限 */
|
||||
body[4] = IEC104_QDC_S;
|
||||
n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(2, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
|
||||
"TC-014 越界选择段 PN");
|
||||
"TC-014 an out-of-range select phase returns PN");
|
||||
}
|
||||
|
||||
static void tc_rd(void)
|
||||
@@ -727,20 +727,20 @@ static void tc_rd(void)
|
||||
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_REQ)
|
||||
&& (iec104_get_float(c.obj) == 222.0f), "TC-015 C_RD YC 命中");
|
||||
&& (iec104_get_float(c.obj) == 222.0f), "TC-015 C_RD hits a YC group");
|
||||
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x8858u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(1, &c) == 0) && (c.type == 13u) && (c.ioa == 0x8858u),
|
||||
"TC-015 C_RD 定值命中");
|
||||
"TC-015 C_RD hits a setting");
|
||||
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x9999u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(2, &c) == 0) && (c.type == 102u)
|
||||
&& (c.cot == IEC104_COT_UNK_IOA) && ((c.oa & 0x80u) != 0u),
|
||||
"TC-015 未知 IOA COT14+EVI");
|
||||
"TC-015 an unknown IOA answers COT14 plus EVI");
|
||||
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(3, &c) == 0) && (c.cot == IEC104_COT_UNK_IOA),
|
||||
"TC-015 本地定值项(comm_addr=0)不可寻址(B7)");
|
||||
"TC-015 a local setting item (comm_addr=0) is not addressable (B7)");
|
||||
}
|
||||
|
||||
static void tc_cs(void)
|
||||
@@ -759,18 +759,18 @@ static void tc_cs(void)
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 103u)
|
||||
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u) && (s_clock_ms == e),
|
||||
"TC-016 C_CS 对时成功 epoch 精确");
|
||||
"TC-016 C_CS syncs the clock with an exact epoch");
|
||||
cp[2] |= 0x80u; /* IV */
|
||||
n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
|
||||
"TC-016 CP56 IV → PN");
|
||||
"TC-016 CP56 IV -> PN");
|
||||
(void)iec104_epoch_ms_to_cp56(e, cp);
|
||||
s_clock_rc = -1; /* 系统侧拒绝 */
|
||||
n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(2, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
|
||||
"TC-016 clock_sync 失败 → PN");
|
||||
"TC-016 a clock_sync failure -> PN");
|
||||
}
|
||||
|
||||
static void tc_err_resp(void)
|
||||
@@ -785,13 +785,13 @@ static void tc_err_resp(void)
|
||||
n = mk_asdu(a, 200u, 1u, IEC104_COT_ACT, 0u, 1u, 0u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 200u) && (c.cot == IEC104_COT_UNK_COT)
|
||||
&& ((c.oa & 0x80u) != 0u), "TC-017 未知类型 COT12+EVI");
|
||||
&& ((c.oa & 0x80u) != 0u), "TC-017 an unknown type answers COT12 plus EVI");
|
||||
m = iec104_asdu_put_hdr(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 2u);
|
||||
a[m] = IEC104_RQT_GROUP1;
|
||||
m++;
|
||||
feed_i(a, m, now); /* ca=2 ≠ 1 */
|
||||
CHECK((cap_i(1, &c) == 0) && (c.cot == IEC104_COT_UNK_CA)
|
||||
&& ((c.oa & 0x80u) != 0u), "TC-017 错公共地址 COT13+EVI");
|
||||
&& ((c.oa & 0x80u) != 0u), "TC-017 a wrong common address answers COT13 plus EVI");
|
||||
}
|
||||
|
||||
static void tc_stopdt(void)
|
||||
@@ -809,9 +809,9 @@ static void tc_stopdt(void)
|
||||
body[0] = IEC104_RQT_GROUP1;
|
||||
n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK(cap_i_count() == 0, "TC-018 STOPPED 态数据服务关");
|
||||
CHECK(cap_i_count() == 0, "TC-018 in STOPPED state the data service is off");
|
||||
feed_u(0x43u, now);
|
||||
CHECK(cap_u_count(0x83u) == 1, "TC-018 STOPPED 态 TESTFR 仍应答");
|
||||
CHECK(cap_u_count(0x83u) == 1, "TC-018 in STOPPED state TESTFR is still answered");
|
||||
}
|
||||
|
||||
static void tc_seq_err(void)
|
||||
@@ -825,7 +825,7 @@ static void tc_seq_err(void)
|
||||
m_ns = 5u; /* 期望 0,投 5 */
|
||||
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-019 序号错 → IDLE");
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-019 a sequence error -> IDLE");
|
||||
}
|
||||
|
||||
static void tc_pts(void)
|
||||
@@ -836,23 +836,23 @@ static void tc_pts(void)
|
||||
|
||||
reset_all(1000u);
|
||||
CHECK((iec104_pts_yc_count() == 9u) && (iec104_pts_yx_count() == 33u)
|
||||
&& (iec104_pts_yc_ioa(8u) == 0x401Du), "TC-020 表尺寸/IOA");
|
||||
&& (iec104_pts_yc_ioa(8u) == 0x401Du), "TC-020 table size and IOA layout");
|
||||
s_f_yc[0] = 7.5f;
|
||||
CHECK((iec104_pts_yc_read(0u, &v, &q) == 0) && (v == 7.5f) && (q == 0u),
|
||||
"TC-020 有效 YC");
|
||||
"TC-020 a valid YC");
|
||||
s_f_yc_rc[1] = -1;
|
||||
(void)iec104_pts_yc_read(1u, &v, &q);
|
||||
CHECK((v == 0.0f) && (q == IEC104_QDS_IV), "TC-020 无效通道 IV");
|
||||
CHECK((v == 0.0f) && (q == IEC104_QDS_IV), "TC-020 an invalid channel gives IV");
|
||||
s_f_yc_rc[2] = -2;
|
||||
(void)iec104_pts_yc_read(2u, &v, &q);
|
||||
CHECK(q == IEC104_QDS_NT, "TC-020 未就绪 NT");
|
||||
CHECK(q == IEC104_QDS_NT, "TC-020 not ready gives NT");
|
||||
iec104_pts_yx_read(0u, &d);
|
||||
CHECK(d == IEC104_DIQ_IV, "TC-020 无效遥信 IV");
|
||||
CHECK(d == IEC104_DIQ_IV, "TC-020 an invalid telemetry point gives IV");
|
||||
CHECK((iec104_pts_yx_is_dp(0u) == 1u) && (iec104_pts_yx_is_dp(4u) == 0u),
|
||||
"TC-020 is_dp 分界");
|
||||
"TC-020 the is_dp boundary");
|
||||
s_f_bits = (1uLL << 15);
|
||||
iec104_pts_yx_read(15u, &d);
|
||||
CHECK(d == 1u, "TC-020 有效遥信 ON");
|
||||
CHECK(d == 1u, "TC-020 a valid telemetry point gives ON");
|
||||
}
|
||||
|
||||
static void tc_ts_rp(void)
|
||||
@@ -884,10 +884,10 @@ static void tc_periodic(void)
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
iec104_link_poll(&g_lk, now + 60001u);
|
||||
CHECK(cap_i_count() == 2, "TC-022 周期上送 2 分片");
|
||||
CHECK(cap_i_count() == 2, "TC-022 the cyclic upload takes 2 frames");
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_PERIODIC)
|
||||
&& (c.ioa == 0x4011u), "TC-022 周期 YC 组 COT=1");
|
||||
CHECK((cap_i(1, &c) == 0) && (c.ioa == 0x401Du), "TC-022 周期频率点");
|
||||
&& (c.ioa == 0x4011u), "TC-022 the cyclic YC group uses COT=1");
|
||||
CHECK((cap_i(1, &c) == 0) && (c.ioa == 0x401Du), "TC-022 the cyclic frequency point");
|
||||
}
|
||||
|
||||
static void tc_cov_kfull(void)
|
||||
@@ -904,11 +904,11 @@ static void tc_cov_kfull(void)
|
||||
}
|
||||
s_f_bits |= (1uLL << 14); /* YX15 变位 */
|
||||
iec104_link_poll(&g_lk, now + 100u);
|
||||
CHECK(cap_i_count() == 12, "TC-023 k 满时自发发不出");
|
||||
CHECK(cap_i_count() == 12, "TC-023 with k full nothing spontaneous can go out");
|
||||
feed_s(12u, now + 200u); /* 窗开 */
|
||||
iec104_link_poll(&g_lk, now + 300u);
|
||||
CHECK((cap_i_count() == 13) && (cap_i(12, &c) == 0) && (c.ioa == 15u)
|
||||
&& (c.cot == IEC104_COT_SPONT), "TC-023 窗开后补发不丢事件");
|
||||
&& (c.cot == IEC104_COT_SPONT), "TC-023 once the window opens the backlogged event is sent, none lost");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
|
||||
@@ -221,7 +221,7 @@ void osDelay(uint32_t ticks)
|
||||
(void)ticks;
|
||||
g_n.delay_calls++;
|
||||
if (g_n.delay_calls > 500) {
|
||||
printf(" ABORT 跑飞(>500 轮 osDelay)\n");
|
||||
printf(" ABORT runaway (>500 osDelay rounds)\n");
|
||||
fflush(stdout);
|
||||
exit(2);
|
||||
}
|
||||
@@ -278,22 +278,22 @@ static void tc_bind_listen(void)
|
||||
(g_n.last_bind_family == NET_AF_INET) &&
|
||||
(g_n.last_bind_port_net == net_htons(IEC104_SERV_PORT)) &&
|
||||
(g_n.last_bind_addr == NET_INADDR_ANY),
|
||||
"TC-I104SRV-001 建监听:2404 端口过 htons、绑任意地址、记下监听号");
|
||||
"TC-I104SRV-001 build the listener: port 2404 through htons, bind to any address, remember the listen handle");
|
||||
|
||||
reset_all();
|
||||
g_n.socket_rc = -1;
|
||||
CHECK((s_bind_listen() == -1) && (g_n.bind_calls == 0) && (g_n.close_calls == 0),
|
||||
"TC-I104SRV-002 建不出 socket 就返回,不去 bind 也不关不存在的口");
|
||||
"TC-I104SRV-002 if no socket can be created return at once: no bind and no close of a handle that does not exist");
|
||||
|
||||
reset_all();
|
||||
g_n.bind_rc = -1;
|
||||
CHECK((s_bind_listen() == -1) && (g_n.close_calls == 1) && (g_n.listen_calls == 0),
|
||||
"TC-I104SRV-003 bind 失败要把刚建的口关掉(否则每次重试漏一个 socket 号)");
|
||||
"TC-I104SRV-003 a bind failure must close the socket just created (otherwise every retry leaks a handle)");
|
||||
|
||||
reset_all();
|
||||
g_n.listen_rc = -1;
|
||||
CHECK((s_bind_listen() == -1) && (g_n.close_calls == 1) && (s_listen == -1),
|
||||
"TC-I104SRV-004 listen 失败同样关口且不留下半个监听");
|
||||
"TC-I104SRV-004 a listen failure also closes it and leaves no half-built listener");
|
||||
}
|
||||
|
||||
/* ===================== 发送与连续短写判死 ===================== */
|
||||
@@ -307,25 +307,25 @@ static void tc_send(void)
|
||||
s_client = -1;
|
||||
s_send_apdu(NULL, apdu, 4u);
|
||||
CHECK((g_n.send_rc == 0) && (g_n.close_calls == 0) && (g_i4_error_count == 0),
|
||||
"TC-I104SRV-010 没连接时直接返回(不发 net_send、不报错)");
|
||||
"TC-I104SRV-010 with no connection it returns directly (no net_send, no error)");
|
||||
|
||||
reset_all();
|
||||
s_client = 7;
|
||||
g_n.send_rc = 4;
|
||||
s_send_apdu(NULL, apdu, 4u);
|
||||
CHECK((s_send_fail_run == 0u) && (s_conn_dead == 0u),
|
||||
"TC-I104SRV-011 完整发出把连败计数清零");
|
||||
"TC-I104SRV-011 a fully sent frame clears the consecutive-failure counter");
|
||||
|
||||
reset_all();
|
||||
s_client = 7;
|
||||
g_n.send_rc = 2; /* 短写:只发了 2/4 */
|
||||
s_send_apdu(NULL, apdu, 4u);
|
||||
CHECK((s_send_fail_run == 1u) && (s_conn_dead == 0u) && (g_i4_error_count == 1),
|
||||
"TC-I104SRV-012 第一次短写:留痕但不判死(拥塞很正常)");
|
||||
"TC-I104SRV-012 the first short write is logged but does not declare death (congestion is normal)");
|
||||
s_send_apdu(NULL, apdu, 4u);
|
||||
s_send_apdu(NULL, apdu, 4u);
|
||||
CHECK((s_send_fail_run == 3u) && (s_conn_dead == 1u),
|
||||
"TC-I104SRV-012b 连到 3 次才判死(IEC104_SEND_FAIL_MAX)");
|
||||
"TC-I104SRV-012b only the 3rd failure in a row declares death (IEC104_SEND_FAIL_MAX)");
|
||||
|
||||
reset_all();
|
||||
s_client = 7;
|
||||
@@ -338,7 +338,7 @@ static void tc_send(void)
|
||||
s_send_apdu(NULL, apdu, 4u);
|
||||
s_send_apdu(NULL, apdu, 4u);
|
||||
CHECK((s_conn_dead == 0u) && (s_send_fail_run == 2u),
|
||||
"TC-I104SRV-013 **成功即复位**:偶发拥塞不会被累计成误杀");
|
||||
"TC-I104SRV-013 success resets the counter: occasional congestion never adds up into a false kill");
|
||||
}
|
||||
|
||||
/* ===================== 时钟桥接边界 ===================== */
|
||||
@@ -348,22 +348,22 @@ static void tc_clock(void)
|
||||
printf("-- TC-I104SRV-020..023 clock bridge --\n");
|
||||
reset_all();
|
||||
CHECK(s_clock_sync(NULL, 0x100000000ULL * 1000ULL) == -1,
|
||||
"TC-I104SRV-020 秒数超过 32 位一律拒绝(截断写进 RTC 就是 2106 年后的乱值)");
|
||||
CHECK(g_clock_set_calls == 0, "TC-I104SRV-020b 拒绝时根本不该碰时钟");
|
||||
"TC-I104SRV-020 any second count beyond 32 bits is rejected (truncating it into the RTC means garbage past 2106)");
|
||||
CHECK(g_clock_set_calls == 0, "TC-I104SRV-020b when rejected the clock must not be touched at all");
|
||||
|
||||
reset_all();
|
||||
(void)s_clock_sync(NULL, 1700000123000ULL);
|
||||
CHECK((g_clock_set_calls == 1) && (g_clock_last_secs == 1700000123u),
|
||||
"TC-I104SRV-021 毫秒按整除换秒后写时钟");
|
||||
"TC-I104SRV-021 milliseconds become seconds by integer division before the clock is written");
|
||||
|
||||
reset_all();
|
||||
g_clock_val = UINT32_MAX; /* 未对时的哨兵值 */
|
||||
CHECK(s_epoch_ms(NULL) == UINT64_MAX,
|
||||
"TC-I104SRV-022 未对时回 UINT64_MAX 哨兵(绝不能把 1970 当成设备时间)");
|
||||
"TC-I104SRV-022 without time sync return the UINT64_MAX sentinel (1970 must never pass for device time)");
|
||||
|
||||
reset_all();
|
||||
g_clock_val = 1700000123u;
|
||||
CHECK(s_epoch_ms(NULL) == 1700000123000ULL, "TC-I104SRV-023 正常时对时给出毫秒");
|
||||
CHECK(s_epoch_ms(NULL) == 1700000123000ULL, "TC-I104SRV-023 with a synced clock the answer is in milliseconds");
|
||||
}
|
||||
|
||||
/* ===================== 代次重建与 accept 门控 ===================== */
|
||||
@@ -379,13 +379,13 @@ static void tc_generation(void)
|
||||
CHECK((g_n.close_calls == 2) && (g_n.last_closed_fd == 5) &&
|
||||
(s_client == -1) && (s_liveness_ctr == 0u) && (s_conn_dead == 0u) &&
|
||||
(g_lk_close == 1) && (s_gen == 101u) && (g_n.bind_calls == 1),
|
||||
"TC-I104SRV-030 代次一变就拆掉旧连接与旧监听并重建(新 IP 上端口必须继续服务)");
|
||||
"TC-I104SRV-030 a generation change tears down the old connection and listener and rebuilds them (the port must keep serving on the new IP)");
|
||||
|
||||
reset_all();
|
||||
s_listen = -1;
|
||||
s_serv_poll();
|
||||
CHECK((g_n.bind_calls == 1) && (s_listen == 5) && (g_n.accept_calls == 0),
|
||||
"TC-I104SRV-031 监听丢了就补建,这一轮不去 accept");
|
||||
"TC-I104SRV-031 a lost listener is rebuilt and this round does not accept");
|
||||
|
||||
reset_all();
|
||||
s_listen = 5;
|
||||
@@ -393,7 +393,7 @@ static void tc_generation(void)
|
||||
g_n.sock_exists = 1;
|
||||
s_serv_poll();
|
||||
CHECK((g_n.accept_calls == 0) && (s_listen == 5),
|
||||
"TC-I104SRV-032 监听门未开时不 accept(否则对着未就绪的号空转)");
|
||||
"TC-I104SRV-032 no accept while the listener gate is down (otherwise it spins on a handle that is not ready)");
|
||||
|
||||
reset_all();
|
||||
s_listen = 5;
|
||||
@@ -401,7 +401,7 @@ static void tc_generation(void)
|
||||
g_n.sock_exists = 0; /* 表项已不存在 */
|
||||
s_serv_poll();
|
||||
CHECK((s_listen == -1) && (g_n.accept_calls == 0),
|
||||
"TC-I104SRV-032b 表项已失效时把监听号丢掉,下一轮重建");
|
||||
"TC-I104SRV-032b once the slot is invalid the listen handle is dropped and rebuilt next round");
|
||||
|
||||
reset_all();
|
||||
s_listen = 5;
|
||||
@@ -413,7 +413,7 @@ static void tc_generation(void)
|
||||
CHECK((s_client == 9) && (g_lk_open == 1) && (g_lk_last_now == 5000u) &&
|
||||
(s_send_fail_run == 0u) && (s_conn_dead == 0u) && (s_liveness_ctr == 0u) &&
|
||||
(g_i4_info_count == 1),
|
||||
"TC-I104SRV-033 新连接:打开链路、三个失败计数全部复位");
|
||||
"TC-I104SRV-033 a new connection opens the link and resets all three failure counters");
|
||||
}
|
||||
|
||||
/* ===================== 活性分频、对端 FIN、错误丢弃 ===================== */
|
||||
@@ -428,7 +428,7 @@ static void tc_liveness(void)
|
||||
s_liveness_ctr = 0u;
|
||||
s_serv_poll();
|
||||
CHECK((g_n.recv_calls == 0) && (g_lk_poll == 1) && (s_liveness_ctr == 1u),
|
||||
"TC-I104SRV-034 无可读数据时按 32 分频探活,这一轮不收包(省 CPU 预算)");
|
||||
"TC-I104SRV-034 with nothing readable the liveness probe runs on a divide-by-32 tick and this round receives nothing (saves CPU budget)");
|
||||
|
||||
reset_all();
|
||||
s_listen = 5;
|
||||
@@ -441,7 +441,7 @@ static void tc_liveness(void)
|
||||
s_liveness_ctr = 31u; /* 下一轮正好到分频点 */
|
||||
s_serv_poll();
|
||||
CHECK((g_n.recv_calls == 1) && (g_lk_poll == 1) && (s_client == 9),
|
||||
"TC-I104SRV-035 分频到点才去收一次(TESTFR 等活性帧靠它发出去)");
|
||||
"TC-I104SRV-035 only when the divider fires does it receive once (liveness frames such as TESTFR depend on it going out)");
|
||||
|
||||
reset_all();
|
||||
s_listen = 5;
|
||||
@@ -451,7 +451,7 @@ static void tc_liveness(void)
|
||||
s_serv_poll();
|
||||
CHECK((g_n.last_closed_fd == 9) && (s_client == -1) && (g_lk_close == 1) &&
|
||||
(g_n.close_calls == 1) && (s_listen == 5) && (g_n.bind_calls == 0),
|
||||
"TC-I104SRV-036 对端正常断开:只关数据口、**监听必须留着**(单连接 socket 关错就是端口失联)");
|
||||
"TC-I104SRV-036 a normal peer disconnect closes only the data socket: the listener must stay (on a single-socket chip closing the wrong one kills the port)");
|
||||
|
||||
reset_all();
|
||||
s_listen = 5;
|
||||
@@ -460,7 +460,7 @@ static void tc_liveness(void)
|
||||
g_n.recv_rc = 0;
|
||||
s_serv_poll();
|
||||
CHECK((g_n.close_calls == 0) && (s_client == -1) && (g_lk_close == 1),
|
||||
"TC-I104SRV-036b 数据号与监听号同号时**不许关**(否则把监听一起毁掉)");
|
||||
"TC-I104SRV-036b when the data handle equals the listen handle it must NOT be closed (that would destroy the listener too)");
|
||||
|
||||
reset_all();
|
||||
s_listen = 5;
|
||||
@@ -470,7 +470,7 @@ static void tc_liveness(void)
|
||||
g_n.errno_val = NET_ERR_WOULDBLOCK; /* 空转:没数据可读 */
|
||||
s_serv_poll();
|
||||
CHECK((s_client == 9) && (g_lk_close == 0) && (g_i4_error_count == 0),
|
||||
"TC-I104SRV-037 WOULDBLOCK 是正常空转,不能当成错误丢连接");
|
||||
"TC-I104SRV-037 WOULDBLOCK is normal idling and must not drop the connection as an error");
|
||||
|
||||
reset_all();
|
||||
s_listen = 5;
|
||||
@@ -481,7 +481,7 @@ static void tc_liveness(void)
|
||||
s_serv_poll();
|
||||
CHECK((s_client == -1) && (s_listen == 5) && (g_i4_error_count == 1) &&
|
||||
(g_lk_close == 1) && (s_liveness_ctr == 0u),
|
||||
"TC-I104SRV-038 真错误:丢连接并**当场重建监听**(不留空窗)");
|
||||
"TC-I104SRV-038 a real error drops the connection and rebuilds the listener on the spot (no gap in service)");
|
||||
|
||||
reset_all();
|
||||
s_listen = 5;
|
||||
@@ -489,7 +489,7 @@ static void tc_liveness(void)
|
||||
s_conn_dead = 1u;
|
||||
s_service_link(1000u);
|
||||
CHECK((s_client == -1) && (g_n.close_calls == 1) && (g_i4_error_count == 1),
|
||||
"TC-I104SRV-039 判死后由 service_link 收口(发送失败线索单独成一条)");
|
||||
"TC-I104SRV-039 after death is declared service_link does the teardown (the send-failure path is a separate case)");
|
||||
|
||||
reset_all();
|
||||
s_listen = 5;
|
||||
@@ -497,7 +497,7 @@ static void tc_liveness(void)
|
||||
g_lk_state = IEC104_ST_IDLE; /* 链路层自己回到了未建立 */
|
||||
s_service_link(1000u);
|
||||
CHECK((s_client == -1) && (g_lk_close >= 1) && (s_listen == 5),
|
||||
"TC-I104SRV-040 协议状态掉回 IDLE 也要丢并重生(否则服务挂着但永不服务)");
|
||||
"TC-I104SRV-040 falling back to IDLE also drops and respawns the session (otherwise the service stays up but never serves)");
|
||||
|
||||
reset_all();
|
||||
s_listen = 9;
|
||||
@@ -508,7 +508,7 @@ static void tc_liveness(void)
|
||||
g_n.recv_rc = 0;
|
||||
s_serv_poll();
|
||||
CHECK((s_client == -1) && (s_listen == -1) && (g_n.close_calls == 0),
|
||||
"TC-I104SRV-041 表项没了又重建失败:丢掉假监听号,下一轮继续重试(数据号与监听同号时不许关)");
|
||||
"TC-I104SRV-041 slot gone and the rebuild failed: drop the stale listen handle and retry next round (never close it while it equals the data handle)");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
|
||||
@@ -334,7 +334,7 @@ void vTaskDelay(TickType_t t)
|
||||
(void)t;
|
||||
g_n.delay_calls++;
|
||||
if (g_n.delay_calls > 200) {
|
||||
printf(" ABORT 循环跑飞(>200 轮)\n");
|
||||
printf(" ABORT loop runaway (>200 rounds)\n");
|
||||
fflush(stdout);
|
||||
exit(2);
|
||||
}
|
||||
@@ -450,46 +450,46 @@ static void tc_gates(void)
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("220 ") == 1) && (rep_has("530 ") == 3) &&
|
||||
(g_n.accept_calls == 0),
|
||||
"TC-LFTPD-001 未登录只放行 USER/PASS/QUIT/FEAT/SYST/NOOP,其它一律 530");
|
||||
"TC-LFTPD-001 before login only USER/PASS/QUIT/FEAT/SYST/NOOP pass, everything else gets 530");
|
||||
|
||||
reset_all();
|
||||
feed("ABCDEFG", "NOOP", NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("500 ") == 1) && (rep_has("200 ") == 1),
|
||||
"TC-LFTPD-002 命令名 >=5 字符先拒(command_tmp 只有 5 格,防 memcpy 越界)");
|
||||
"TC-LFTPD-002 a command name of 5 characters or more is rejected first (command_tmp has only 5 slots, this keeps memcpy in bounds)");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1; /* 否则会被认证白名单先以 530 拦掉,测不到分派 */
|
||||
feed("FOO bar", NULL, NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("502 ") == 1) && (rep_has("530 ") == 0),
|
||||
"TC-LFTPD-003 认不出的命令回 502 而不是断开");
|
||||
"TC-LFTPD-003 an unknown command answers 502 instead of dropping the connection");
|
||||
|
||||
reset_all();
|
||||
feed("QUIT", "NOOP", NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("221 ") == 1) && (rep_has("200 ") == 0) && (g_n.line_i == 1),
|
||||
"TC-LFTPD-004 QUIT 之后不再读下一条(会话就此结束)");
|
||||
"TC-LFTPD-004 after QUIT no further command is read (the session ends here)");
|
||||
|
||||
reset_all();
|
||||
g_n.string_rc = -1; /* 220 都送不出去 */
|
||||
feed("NOOP", NULL, NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((g_n.line_i == 0) && (g_lg_info_count >= 1),
|
||||
"TC-LFTPD-005 欢迎未送达(对端已走)就直接收工,不空转读命令(该留痕按真实分级算 INFO)");
|
||||
"TC-LFTPD-005 if the welcome never got through (peer already gone) finish straight away instead of spinning on reads; that trace really is INFO");
|
||||
|
||||
reset_all();
|
||||
g_cli.data_socket = 33; /* 被动模式已开但客户端直接跑了 */
|
||||
feed("QUIT", NULL, NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((g_n.close_calls == 1) && (g_cli.data_socket == -1),
|
||||
"TC-LFTPD-006 退出时必须释放还开着的**数据监听**(单连接 socket 不能留残口)");
|
||||
"TC-LFTPD-006 on exit the still-open data listener must be released (a single-socket chip must not be left with a dangling socket)");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
feed("noop", NULL, NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK(rep_has("200 ") == 1, "TC-LFTPD-007 命令名大小写不敏感");
|
||||
CHECK(rep_has("200 ") == 1, "TC-LFTPD-007 command names are case insensitive");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
@@ -499,7 +499,7 @@ static void tc_gates(void)
|
||||
feed("LIST", NULL, NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("425 ") == 1) && (g_n.accept_calls == 0),
|
||||
"TC-LFTPD-010 没先开被动模式的数据命令一律 425,且不去 accept");
|
||||
"TC-LFTPD-010 a data command without passive mode set first gets 425 and never reaches accept");
|
||||
}
|
||||
|
||||
/* ===================== 被动模式与数据通道 ===================== */
|
||||
@@ -514,7 +514,7 @@ static void tc_passive(void)
|
||||
(void)cmd_pasv(&g_cli, NULL);
|
||||
CHECK((strcmp(rep(0), "227 Entering Passive Mode (192,168,1,1,19,136).\r\n") == 0) &&
|
||||
(g_cli.data_socket == 20) && (g_n.listen_calls == 1),
|
||||
"TC-LFTPD-011 227 六元组:IP 经 htonl 后按高字节到低字节(点分序)、端口高/低,并记下被动监听号");
|
||||
"TC-LFTPD-011 the 227 tuple: ip through htonl then high byte to low byte (dotted order), port high/low, and the passive listen handle is recorded");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
@@ -523,14 +523,14 @@ static void tc_passive(void)
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("425 ") == 1) && (g_n.close_calls == 1) &&
|
||||
(g_cli.data_socket == -1),
|
||||
"TC-LFTPD-012 拿不到本机 IP 时要把刚建的监听关掉(别留半个被动模式)");
|
||||
"TC-LFTPD-012 when the local ip is unavailable the listener just created must be closed (no half-passive mode left behind)");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
feed("EPSV", NULL, NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK(strcmp(rep(1), "229 Entering Extended Passive Mode (|||5000|).\r\n") == 0,
|
||||
"TC-LFTPD-013 EPSV 只报端口(IPv6 客户端自己沿用控制连接地址)");
|
||||
"TC-LFTPD-013 EPSV reports only the port (an IPv6 client reuses the control connection address)");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
@@ -540,7 +540,7 @@ static void tc_passive(void)
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("150 ") == 1) && (rep_has("425 ") == 1) &&
|
||||
(g_cli.data_socket == -1) && (g_n.close_calls == 1),
|
||||
"TC-LFTPD-014 数据 accept 失败:报 425 并**关掉监听复位**,不留给下一条命令");
|
||||
"TC-LFTPD-014 a failed data accept reports 425 and closes and resets the listener instead of leaving it to the next command");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
@@ -550,14 +550,14 @@ static void tc_passive(void)
|
||||
feed("PASV", "LIST", NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("226 ") == 1) && (g_n.data_write_rc == 0),
|
||||
"TC-LFTPD-015 LIST 成功:先 150、数据口写目录、再 226 收尾");
|
||||
"TC-LFTPD-015 LIST success: 150 first, the directory goes out on the data socket, then 226 closes it");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
g_io.opendir_rc = 1; /* 目录打不开 */
|
||||
feed("PASV", "LIST", NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK(rep_has("550 ") == 1, "TC-LFTPD-016 目录打不开时 LIST 回 550(不是 226)");
|
||||
CHECK(rep_has("550 ") == 1, "TC-LFTPD-016 when the directory cannot be opened LIST answers 550 (not 226)");
|
||||
}
|
||||
|
||||
/* ===================== 传输与写开关 ===================== */
|
||||
@@ -571,7 +571,7 @@ static void tc_transfer(void)
|
||||
feed("PASV", "RETR a.txt", NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("226 ") == 1) && (g_lg_info_count >= 1) && (g_wd_beats >= 1),
|
||||
"TC-LFTPD-017 RETR 成功:226 收尾、留 DOWNLOAD 记录,且传输循环有喂狗");
|
||||
"TC-LFTPD-017 RETR success: 226 at the end, a DOWNLOAD record is kept and the transfer loop feeds the watchdog");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
@@ -579,7 +579,7 @@ static void tc_transfer(void)
|
||||
feed("PASV", "RETR gone.txt", NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("450 ") == 1) && (g_lg_error_count >= 1),
|
||||
"TC-LFTPD-018 文件打不开回 450(传输失败与命令不合法要分开报)");
|
||||
"TC-LFTPD-018 a file that cannot be opened answers 450 (a transfer failure and an invalid command must be reported apart)");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
@@ -588,7 +588,7 @@ static void tc_transfer(void)
|
||||
feed("PASV", "STOR up.bin", NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((g_io.unlink_calls == 1) && (rep_has("450 ") == 1),
|
||||
"TC-LFTPD-019 STOR 中途失败必须**删掉半成品**(否则 Flash 上永久留个坏文件)");
|
||||
"TC-LFTPD-019 a STOR failing halfway must delete the partial file (otherwise a broken file stays on flash forever)");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
@@ -598,27 +598,27 @@ static void tc_transfer(void)
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("550 ") == 1) && (g_n.accept_calls == 0) &&
|
||||
(g_io.open_write_rc == 0) && (g_io.unlink_calls == 0),
|
||||
"TC-LFTPD-020 写开关关闭时 STOR 在碰文件之前就拒绝,且不开数据通道");
|
||||
"TC-LFTPD-020 with writes disabled STOR is refused before the file is touched and no data channel is opened");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
g_cfg.write_en = 0u;
|
||||
(void)cmd_dele(&g_cli, "a.txt");
|
||||
CHECK(rep_has("550 ") == 1, "TC-LFTPD-021 写开关同样挡住 DELE");
|
||||
CHECK(rep_has("550 ") == 1, "TC-LFTPD-021 the write switch blocks DELE as well");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
g_io.is_dir = 1;
|
||||
(void)cmd_dele(&g_cli, "sub");
|
||||
CHECK((rep_has("550 ") == 1) && (g_io.unlink_calls == 0),
|
||||
"TC-LFTPD-022 DELE 不许删目录(本实现只支持删文件)");
|
||||
"TC-LFTPD-022 DELE must not delete directories (this implementation only deletes files)");
|
||||
|
||||
reset_all();
|
||||
g_cli.auth_logged_in = 1;
|
||||
g_io.is_dir = 0;
|
||||
(void)cmd_dele(&g_cli, "a.txt");
|
||||
CHECK((g_io.unlink_calls == 1) && (rep_has("250 ") == 1),
|
||||
"TC-LFTPD-022b 删普通文件走 unlink 并回 250");
|
||||
"TC-LFTPD-022b deleting a regular file goes through unlink and answers 250");
|
||||
}
|
||||
|
||||
/* ===================== 目录与元信息 ===================== */
|
||||
@@ -629,58 +629,58 @@ static void tc_meta(void)
|
||||
reset_all();
|
||||
(void)cmd_cwd(&g_cli, "");
|
||||
CHECK((rep_has("550 ") == 1) && (strcmp(g_cli.directory, "/") == 0),
|
||||
"TC-LFTPD-023 CWD 空参数拒绝,当前目录不变");
|
||||
"TC-LFTPD-023 CWD with an empty argument is refused and the current directory stays");
|
||||
|
||||
reset_all();
|
||||
g_io.is_dir = 0;
|
||||
(void)cmd_cwd(&g_cli, "a.txt");
|
||||
CHECK(rep_has("550 ") == 1, "TC-LFTPD-023b CWD 到普通文件被拒(is_dir 必须真为目录)");
|
||||
CHECK(rep_has("550 ") == 1, "TC-LFTPD-023b CWD onto a regular file is refused (is_dir must really be a directory)");
|
||||
|
||||
reset_all();
|
||||
g_io.is_dir = 1;
|
||||
(void)cmd_cwd(&g_cli, "logs");
|
||||
CHECK((rep_has("250 ") == 1) && (strcmp(g_cli.directory, "//logs") == 0) &&
|
||||
(g_free_calls == 1),
|
||||
"TC-LFTPD-024 CWD 成功后更新当前目录并释放路径缓冲(不泄漏堆)");
|
||||
"TC-LFTPD-024 a successful CWD updates the current directory and frees the path buffer (no heap leak)");
|
||||
|
||||
reset_all();
|
||||
(void)cmd_cwd(&g_cli, "/");
|
||||
CHECK((rep_has("250 ") == 1) && (g_io.stat_rc == 0),
|
||||
"TC-LFTPD-024b 回到根目录免 stat 直接放行(根一定存在)");
|
||||
"TC-LFTPD-024b going back to the root skips stat and succeeds directly (the root always exists)");
|
||||
|
||||
reset_all();
|
||||
g_io.is_dir = 0;
|
||||
g_io.size = 12345u;
|
||||
(void)cmd_size(&g_cli, "up.bin");
|
||||
CHECK(strcmp(rep(0), "213 12345\r\n") == 0, "TC-LFTPD-025 SIZE 报字节数");
|
||||
CHECK(strcmp(rep(0), "213 12345\r\n") == 0, "TC-LFTPD-025 SIZE reports the byte count");
|
||||
|
||||
reset_all();
|
||||
g_io.stat_rc = -1;
|
||||
(void)cmd_size(&g_cli, "nope");
|
||||
CHECK(rep_has("550 ") == 1, "TC-LFTPD-025b 文件不存在时 SIZE 回 550");
|
||||
CHECK(rep_has("550 ") == 1, "TC-LFTPD-025b when the file is missing SIZE answers 550");
|
||||
|
||||
reset_all();
|
||||
(void)strcpy(g_cli.directory, "/logs");
|
||||
(void)cmd_pwd(&g_cli, NULL);
|
||||
CHECK(strcmp(rep(0), "257 \"/logs\"\r\n") == 0,
|
||||
"TC-LFTPD-026 PWD 用双引号包住目录(RFC 959 要求)");
|
||||
"TC-LFTPD-026 PWD wraps the directory in double quotes (required by RFC 959)");
|
||||
|
||||
reset_all();
|
||||
(void)cmd_syst(&g_cli, NULL);
|
||||
CHECK(strcmp(rep(0), "215 UNIX Type: L8\r\n") == 0,
|
||||
"TC-LFTPD-027 SYST 固定报 UNIX Type: L8(客户端据此决定模式)");
|
||||
"TC-LFTPD-027 SYST always reports UNIX Type: L8 (clients pick their mode from it)");
|
||||
|
||||
reset_all();
|
||||
(void)cmd_feat(&g_cli, NULL);
|
||||
CHECK((strncmp(rep(0), "211-", 4) == 0) && (strcmp(rep(1), "EPSV\r\n") == 0) &&
|
||||
(strcmp(rep(2), "PASV\r\n") == 0) && (strncmp(rep(5), "211 ", 4) == 0) &&
|
||||
(g_n.reply_n == 6),
|
||||
"TC-LFTPD-028 FEAT 多行:首行 211-、四条能力无状态码、末行 211 结束");
|
||||
"TC-LFTPD-028 FEAT multiline: first line 211-, four capability lines without a status code, last line 211 ends it");
|
||||
|
||||
reset_all();
|
||||
(void)cmd_type(&g_cli, "I");
|
||||
(void)cmd_noop(&g_cli, NULL);
|
||||
CHECK(rep_has("200 ") == 2, "TC-LFTPD-030 TYPE 与 NOOP 都回 200");
|
||||
CHECK(rep_has("200 ") == 2, "TC-LFTPD-030 both TYPE and NOOP answer 200");
|
||||
}
|
||||
|
||||
/* ===================== 登录序列 ===================== */
|
||||
@@ -693,7 +693,7 @@ static void tc_login(void)
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("331 ") == 1) && (rep_has("230 ") == 1) &&
|
||||
(g_cli.auth_logged_in == 1u) && (strcmp(g_cli.auth_user, "admin") == 0),
|
||||
"TC-LFTPD-031 USER 记下用户名与匹配位,PASS 过了就转已登录");
|
||||
"TC-LFTPD-031 USER records the user name and the match bit; a passing PASS switches to logged in");
|
||||
|
||||
reset_all();
|
||||
g_auth_res = FTP_AUTH_FAIL;
|
||||
@@ -701,7 +701,7 @@ static void tc_login(void)
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("530 ") == 1) && (g_cli.auth_logged_in == 0u) &&
|
||||
(g_lg_info_count >= 1),
|
||||
"TC-LFTPD-032 口令错:530 且**必须重新走 USER**(auth_user_ok 已清零)");
|
||||
"TC-LFTPD-032 a wrong password gives 530 and USER must be repeated (auth_user_ok was cleared)");
|
||||
|
||||
reset_all();
|
||||
g_auth_res = FTP_AUTH_LOCKED;
|
||||
@@ -709,7 +709,7 @@ static void tc_login(void)
|
||||
feed("USER admin", "PASS bad", NULL, NULL);
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK(rep_has("530 ") == 1,
|
||||
"TC-LFTPD-033 连错被锁时同样只回 530(不给攻击者区分提示)");
|
||||
"TC-LFTPD-033 while locked out after repeated failures it also answers only 530 (no distinguishing hint for an attacker)");
|
||||
|
||||
reset_all();
|
||||
g_cfg_present = 0; /* 配置读不到 */
|
||||
@@ -717,7 +717,7 @@ static void tc_login(void)
|
||||
(void)handle_control_channel(&g_cli);
|
||||
CHECK((rep_has("331 ") == 1) && (rep_has("530 ") == 1) &&
|
||||
(g_cli.auth_user_ok == 0u),
|
||||
"TC-LFTPD-034 配置缺失时 USER 无法匹配、PASS 直接 530(fail-closed)");
|
||||
"TC-LFTPD-034 with missing configuration USER cannot match and PASS answers 530 straight away (fail-closed)");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
|
||||
@@ -241,7 +241,7 @@ void vTaskDelay(TickType_t xTicksToDelay)
|
||||
if (g_net.delay_calls > 100) {
|
||||
/* 跑飞保险:本模块每条用例都必须在有限轮内退出(accept 序列要么成功、
|
||||
* 要么撤防、要么代次变化)。真跑飞就退出,绝不把挂死留给 pre-commit。 */
|
||||
printf(" ABORT accept/等待循环跑飞(>100 轮未退出)\n");
|
||||
printf(" ABORT accept/wait loop runaway (>100 rounds without exit)\n");
|
||||
fflush(stdout);
|
||||
exit(2);
|
||||
}
|
||||
@@ -262,32 +262,32 @@ static void tc_listen(void)
|
||||
net_reset(); logs_clear();
|
||||
g_net.socket_rc = -1;
|
||||
CHECK((lftpd_inet_listen(21) == -1) && (g_net.bind_calls == 0) &&
|
||||
(g_lfi_error_count == 1), "TC-LFTINET-001 建 socket 失败即返回,不再 bind");
|
||||
(g_lfi_error_count == 1), "TC-LFTINET-001 a socket creation failure returns at once, no bind attempt");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.bind_rc = -1;
|
||||
CHECK((lftpd_inet_listen(21) == -1) && (g_net.close_calls == 1) &&
|
||||
(g_net.listen_calls == 0), "TC-LFTINET-002 bind 失败要把 socket 关掉");
|
||||
(g_net.listen_calls == 0), "TC-LFTINET-002 a bind failure must close the socket");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.listen_rc = -1;
|
||||
CHECK((lftpd_inet_listen(21) == -1) && (g_net.close_calls == 1) &&
|
||||
(g_lfi_error_count == 1), "TC-LFTINET-003 listen 失败同样关socket 并留痕");
|
||||
(g_lfi_error_count == 1), "TC-LFTINET-003 a listen failure also closes the socket and logs it");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
CHECK((lftpd_inet_listen(21) == 3) && (g_net.last_domain == NET_AF_INET) &&
|
||||
(g_net.last_type == NET_SOCK_STREAM) &&
|
||||
(g_net.last_bind_port_net == net_htons(21u)) && (g_net.listen_calls == 1),
|
||||
"TC-LFTINET-004 流式监听口,且**端口过了 htons**(sin_port 是网络序)");
|
||||
"TC-LFTINET-004 a stream listener whose port went through htons (sin_port is network order)");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.getsock_rc = -1;
|
||||
CHECK((lftpd_inet_get_socket_port(3) == -1) && (g_lfi_error_count == 1),
|
||||
"TC-LFTINET-005 问不到名字就报 -1(不拿脏端口当结果)");
|
||||
"TC-LFTINET-005 a failing getsockname returns -1 (never report a stale port as the result)");
|
||||
net_reset(); logs_clear();
|
||||
g_net.getsock_port_net = net_htons(8888u);
|
||||
CHECK(lftpd_inet_get_socket_port(3) == 8888,
|
||||
"TC-LFTINET-006 端口回来要 ntohs 还原成宿主序");
|
||||
"TC-LFTINET-006 the port comes back through ntohs into host order");
|
||||
}
|
||||
|
||||
/* ===================== accept:喂狗、代次、撤防 ===================== */
|
||||
@@ -298,14 +298,14 @@ static void tc_accept(void)
|
||||
net_reset(); logs_clear();
|
||||
g_net.accept_seq[0] = 9; g_net.accept_n = 1;
|
||||
CHECK((lftpd_inet_accept(3) == 9) && (g_net.delay_calls == 0),
|
||||
"TC-LFTINET-010 一击即中不睡不等");
|
||||
"TC-LFTINET-010 an immediate hit neither sleeps nor waits");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.accept_seq[0] = -1; g_net.accept_seq[1] = -1; g_net.accept_seq[2] = 9;
|
||||
g_net.accept_n = 3;
|
||||
CHECK((lftpd_inet_accept(3) == 9) && (g_net.delay_calls == 2) &&
|
||||
(g_lfi_info_count == 0),
|
||||
"TC-LFTINET-011 每轮失败都让出一次 CPU(同段代码里也喂狗),直到成功");
|
||||
"TC-LFTINET-011 every failed round yields the CPU (and feeds the watchdog in the same block) until it succeeds");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.accept_seq[0] = -1; g_net.accept_seq[1] = -1; g_net.accept_seq[2] = -1;
|
||||
@@ -313,25 +313,25 @@ static void tc_accept(void)
|
||||
g_net.gen_flip_after = 3; /* 第 3 轮时网络重建 */
|
||||
CHECK((lftpd_inet_accept(3) == -1) && (g_lfi_info_count == 1) &&
|
||||
(g_net.accept_calls == 3),
|
||||
"TC-LFTINET-012 **代次一变立刻撤掉 accept**(否则新 IP 生效后监听还挂在旧代次)");
|
||||
"TC-LFTINET-012 a generation change aborts accept immediately (otherwise the listener stays on the old generation after the new IP takes effect)");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.accept_seq[0] = -1; g_net.accept_seq[1] = 9; g_net.accept_n = 2;
|
||||
CHECK((lftpd_inet_accept_timeout(3, 5000) == 9) && (g_net.delay_calls == 1),
|
||||
"TC-LFTINET-020 超时窗内等到数据通道");
|
||||
"TC-LFTINET-020 the data channel is accepted inside the timeout window");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.accept_seq[0] = -1; g_net.accept_seq[1] = -1; g_net.accept_n = 2;
|
||||
g_net.armed = 0; /* 监听门被撤(会话已放弃) */
|
||||
CHECK((lftpd_inet_accept_timeout(3, 5000) == -1) && (g_net.accept_calls == 1) &&
|
||||
(g_lfi_error_count == 1),
|
||||
"TC-LFTINET-021 监听门撤防立即放弃,不对着死监听耗满 5 s");
|
||||
"TC-LFTINET-021 lowering the listener gate gives up at once instead of burning the full 5 s on a dead listener");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.accept_seq[0] = -1; g_net.accept_n = 1;
|
||||
CHECK((lftpd_inet_accept_timeout(3, 300) == -1) && (g_net.delay_calls == 3) &&
|
||||
(g_lfi_info_count == 1),
|
||||
"TC-LFTINET-022 对端一直不开数据通道:按 100ms 步进等满后 INFO 收场");
|
||||
"TC-LFTINET-022 the peer never opens the data channel: step by 100ms to the limit and finish as INFO");
|
||||
}
|
||||
|
||||
/* ===================== read_line:分片、截断、预算 ===================== */
|
||||
@@ -347,38 +347,38 @@ static void tc_read_line(void)
|
||||
g_net.recv_seq[0] = 12; g_net.recv_n = 1;
|
||||
rc = lftpd_inet_read_line(3, b, sizeof(b));
|
||||
CHECK((rc == 0) && (strcmp(b, "USER admin") == 0),
|
||||
"TC-LFTINET-030 一次到位:CRLF 就地截成结束符(FTP 命令行的边界)");
|
||||
"TC-LFTINET-030 one shot: CRLF is cut into the terminator in place (the FTP command line boundary)");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
(void)strcpy(g_net.rx, "USER admin\r\n");
|
||||
g_net.recv_seq[0] = 4; g_net.recv_seq[1] = 8; g_net.recv_n = 2;
|
||||
rc = lftpd_inet_read_line(3, b, sizeof(b));
|
||||
CHECK((rc == 0) && (strcmp(b, "USER admin") == 0),
|
||||
"TC-LFTINET-031 分两片到达也要拼出整行(跨包命令行)");
|
||||
"TC-LFTINET-031 two arriving halves are stitched into one line (a command line spanning packets)");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.recv_seq[0] = 0; g_net.recv_n = 1;
|
||||
CHECK(lftpd_inet_read_line(3, b, sizeof(b)) == -1,
|
||||
"TC-LFTINET-032 读到 0(对端关闭)立刻失败,不进死循环");
|
||||
"TC-LFTINET-032 reading 0 (peer closed) fails at once and must not spin");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
(void)strcpy(g_net.rx, "NOOP\r\n");
|
||||
g_net.recv_seq[0] = -2; g_net.recv_seq[1] = 6; g_net.recv_n = 2;
|
||||
rc = lftpd_inet_read_line(3, b, sizeof(b));
|
||||
CHECK((rc == 0) && (strcmp(b, "NOOP") == 0),
|
||||
"TC-LFTINET-033 -2 表示这一片超时,继续等而不是当错误");
|
||||
"TC-LFTINET-033 -2 means this slice timed out: keep waiting instead of treating it as an error");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.recv_seq[0] = -5; g_net.recv_n = 1;
|
||||
CHECK(lftpd_inet_read_line(3, b, sizeof(b)) == -5,
|
||||
"TC-LFTINET-034 其它负值**原样返回**(错误码不许被吞成 -1)");
|
||||
"TC-LFTINET-034 any other negative value is returned verbatim (an error code must not be swallowed into -1)");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.recv_seq[0] = -2; g_net.recv_n = 1; /* 一直超时,每轮耗 200ms */
|
||||
rc = lftpd_inet_read_line(3, b, sizeof(b));
|
||||
CHECK((rc == -1) && (g_net.recv_calls == 150) &&
|
||||
(g_net.last_recv_timeout <= 200u),
|
||||
"TC-LFTINET-035 30 s 总预算按 200ms 分片累加,到点退出(饿不到看门狗)");
|
||||
"TC-LFTINET-035 the 30 s total budget accumulates in 200ms slices and exits on time (the watchdog never starves)");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
(void)memset(g_net.rx, 'A', 40); /* 没有 CRLF 的长行 */
|
||||
@@ -386,7 +386,7 @@ static void tc_read_line(void)
|
||||
(void)memset(b, 0, sizeof(b));
|
||||
rc = lftpd_inet_read_line(3, b, 8);
|
||||
CHECK((rc == -1) && (b[7] == '\0'),
|
||||
"TC-LFTINET-036 缓冲填满仍无行尾:失败且**不越界写**(留最后一格给结束符)");
|
||||
"TC-LFTINET-036 buffer full with no line end: fail without writing out of bounds (the last byte is reserved for the terminator)");
|
||||
}
|
||||
|
||||
/* ===================== 写与读 ===================== */
|
||||
@@ -400,45 +400,45 @@ static void tc_write_read(void)
|
||||
net_reset(); logs_clear();
|
||||
g_net.send_seq[0] = 12; g_net.send_n = 1;
|
||||
CHECK((lftpd_inet_write_string(3, msg) == 0) && (g_net.send_calls == 1),
|
||||
"TC-LFTINET-040 一次写完");
|
||||
"TC-LFTINET-040 everything written in a single call");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.send_seq[0] = 5; g_net.send_seq[1] = 7; g_net.send_n = 2;
|
||||
CHECK((lftpd_inet_write_string(3, msg) == 0) && (g_net.send_calls == 2) &&
|
||||
(g_net.send_off[1] == 5),
|
||||
"TC-LFTINET-041 短写后续传从**正确的偏移**接着写(少半条响应就是协议错乱)");
|
||||
"TC-LFTINET-041 after a short write the continuation resumes at the correct offset (half a response is a protocol break)");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.send_seq[0] = 0; g_net.send_n = 1;
|
||||
CHECK((lftpd_inet_write_string(3, msg) == -1) && (g_lfi_error_count == 1),
|
||||
"TC-LFTINET-042 写 0 字节按失败处理(不是当成成功写了 0 字节)");
|
||||
"TC-LFTINET-042 writing 0 bytes counts as a failure (not a successful zero-byte write)");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.send_seq[0] = -1; g_net.send_n = 1;
|
||||
CHECK(lftpd_inet_write(3, msg, 12) == -1, "TC-LFTINET-043 写失败原样回 -1");
|
||||
CHECK(lftpd_inet_write(3, msg, 12) == -1, "TC-LFTINET-043 a write failure returns -1 as is");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.send_seq[0] = 4; g_net.send_seq[1] = 6; g_net.send_n = 2;
|
||||
CHECK((lftpd_inet_write(3, msg, 10) == 10) && (g_net.send_calls == 2),
|
||||
"TC-LFTINET-050 二进制写也走短写续传,成功时回**请求长度**");
|
||||
"TC-LFTINET-050 binary writes also resume after short writes and return the requested length on success");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
(void)strcpy(g_net.rx, "STOR\r\n");
|
||||
g_net.recv_seq[0] = 4; g_net.recv_n = 1;
|
||||
CHECK(lftpd_inet_read(3, b, sizeof(b)) == 4, "TC-LFTINET-060 读到数据立刻交还");
|
||||
CHECK(lftpd_inet_read(3, b, sizeof(b)) == 4, "TC-LFTINET-060 data read is handed back immediately");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.recv_seq[0] = -1; g_net.recv_n = 1;
|
||||
CHECK(lftpd_inet_read(3, b, sizeof(b)) == -1, "TC-LFTINET-061 -1 直接失败返回");
|
||||
CHECK(lftpd_inet_read(3, b, sizeof(b)) == -1, "TC-LFTINET-061 -1 fails and returns directly");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
g_net.recv_seq[0] = -3; g_net.recv_n = 1;
|
||||
CHECK((lftpd_inet_read(3, b, sizeof(b)) == -1) && (g_net.recv_calls > 1),
|
||||
"TC-LFTINET-062 其它负值继续等,直到 30 s 预算耗尽");
|
||||
"TC-LFTINET-062 other negative values keep waiting until the 30 s budget is exhausted");
|
||||
|
||||
net_reset(); logs_clear();
|
||||
CHECK((lftpd_inet_close(3) == 0) && (g_net.close_calls == 1),
|
||||
"TC-LFTINET-070 close 就是转 net_close");
|
||||
"TC-LFTINET-070 close only forwards to net_close");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
|
||||
@@ -276,11 +276,11 @@ static void tc_fatfs_path(void)
|
||||
{
|
||||
printf("-- TC-LFTPIO-001..004 to_fatfs_path --\n");
|
||||
CHECK((strcmp(to_fatfs_path(NULL), "0:") == 0) &&
|
||||
(strcmp(to_fatfs_path(""), "0:") == 0), "TC-LFTPIO-001 NULL/空串 -> 0:");
|
||||
CHECK(strcmp(to_fatfs_path("/"), "0:") == 0, "TC-LFTPIO-002 根 -> 0:");
|
||||
(strcmp(to_fatfs_path(""), "0:") == 0), "TC-LFTPIO-001 NULL/empty string -> 0:");
|
||||
CHECK(strcmp(to_fatfs_path("/"), "0:") == 0, "TC-LFTPIO-002 root -> 0:");
|
||||
CHECK(strcmp(to_fatfs_path("/a/b.txt"), "a/b.txt") == 0,
|
||||
"TC-LFTPIO-003 去掉前导斜杠");
|
||||
CHECK(strcmp(to_fatfs_path("a.txt"), "a.txt") == 0, "TC-LFTPIO-004 原样返回");
|
||||
"TC-LFTPIO-003 the leading slash is stripped");
|
||||
CHECK(strcmp(to_fatfs_path("a.txt"), "a.txt") == 0, "TC-LFTPIO-004 returned unchanged");
|
||||
}
|
||||
|
||||
/* ===================== 路径规范化 ===================== */
|
||||
@@ -289,32 +289,32 @@ static void tc_canon(void)
|
||||
{
|
||||
printf("-- TC-LFTPIO-010..022 canonicalize --\n");
|
||||
reset_all();
|
||||
CHECK(strcmp(canon("/dir", "f.txt"), "/dir/f.txt") == 0, "TC-LFTPIO-010 相对拼接");
|
||||
CHECK(strcmp(canon("/dir", "/abs.txt"), "/abs.txt") == 0, "TC-LFTPIO-011 绝对忽略 base");
|
||||
CHECK(strcmp(canon("/a/b", "../c"), "/a/c") == 0, "TC-LFTPIO-012 上跳一级");
|
||||
CHECK(strcmp(canon("/dir", "f.txt"), "/dir/f.txt") == 0, "TC-LFTPIO-010 relative join");
|
||||
CHECK(strcmp(canon("/dir", "/abs.txt"), "/abs.txt") == 0, "TC-LFTPIO-011 an absolute name ignores base");
|
||||
CHECK(strcmp(canon("/a/b", "../c"), "/a/c") == 0, "TC-LFTPIO-012 go up one level");
|
||||
CHECK(strcmp(canon("/a", "../../x"), "/x") == 0,
|
||||
"TC-LFTPIO-013 上跳过头仍被钳在根内(安全属性)");
|
||||
CHECK(strcmp(canon("/a", "."), "/a") == 0, "TC-LFTPIO-014 丢弃 . 段");
|
||||
CHECK(strcmp(canon("/a", ".."), "/") == 0, "TC-LFTPIO-015 回退到根");
|
||||
CHECK(strcmp(canon("/a//", "b"), "/a/b") == 0, "TC-LFTPIO-016 段间多余斜杠被折叠");
|
||||
"TC-LFTPIO-013 over-shooting .. is still clamped inside the root (safety property)");
|
||||
CHECK(strcmp(canon("/a", "."), "/a") == 0, "TC-LFTPIO-014 a . segment is dropped");
|
||||
CHECK(strcmp(canon("/a", ".."), "/") == 0, "TC-LFTPIO-015 .. at the top falls back to the root");
|
||||
CHECK(strcmp(canon("/a//", "b"), "/a/b") == 0, "TC-LFTPIO-016 redundant slashes between segments are collapsed");
|
||||
CHECK(strcmp(canon("/dir", "//x"), "/x") == 0,
|
||||
"TC-LFTPIO-016b name 以斜杠开头按**绝对路径**处理(忽略 base,仍钳在根内)");
|
||||
CHECK(strcmp(canon(NULL, "x"), "/x") == 0, "TC-LFTPIO-017 base NULL 视作空串");
|
||||
CHECK(strcmp(canon("/a", NULL), "/a") == 0, "TC-LFTPIO-018 name NULL 视作空串");
|
||||
CHECK(strcmp(canon(NULL, NULL), "/") == 0, "TC-LFTPIO-019 两个 NULL -> /");
|
||||
CHECK(strcmp(canon("/", ".."), "/") == 0, "TC-LFTPIO-020 根上加 .. 仍是根");
|
||||
"TC-LFTPIO-016b a name starting with a slash is treated as an absolute path (base ignored, still clamped inside the root)");
|
||||
CHECK(strcmp(canon(NULL, "x"), "/x") == 0, "TC-LFTPIO-017 a NULL base counts as an empty string");
|
||||
CHECK(strcmp(canon("/a", NULL), "/a") == 0, "TC-LFTPIO-018 a NULL name counts as an empty string");
|
||||
CHECK(strcmp(canon(NULL, NULL), "/") == 0, "TC-LFTPIO-019 both NULL -> /");
|
||||
CHECK(strcmp(canon("/", ".."), "/") == 0, "TC-LFTPIO-020 .. on the root is still the root");
|
||||
|
||||
reset_all();
|
||||
g_fail_at = 1;
|
||||
CHECK(lftpd_io_canonicalize_path("/a", "b") == NULL,
|
||||
"TC-LFTPIO-021 首块分配失败返回 NULL");
|
||||
CHECK(g_alloc_calls == 1, "TC-LFTPIO-021b 失败后不再分配第二块");
|
||||
"TC-LFTPIO-021 the first allocation failing returns NULL");
|
||||
CHECK(g_alloc_calls == 1, "TC-LFTPIO-021b no second allocation is attempted after that failure");
|
||||
|
||||
reset_all();
|
||||
g_fail_at = 2;
|
||||
CHECK(lftpd_io_canonicalize_path("/a", "b") == NULL,
|
||||
"TC-LFTPIO-022 结果缓冲分配失败返回 NULL");
|
||||
CHECK(g_free_calls == 1, "TC-LFTPIO-022b 第一块已释放(不泄漏堆)");
|
||||
"TC-LFTPIO-022 the result buffer allocation failing returns NULL");
|
||||
CHECK(g_free_calls == 1, "TC-LFTPIO-022b the first block was freed (no heap leak)");
|
||||
}
|
||||
|
||||
/* ===================== 打开文件:守卫 / 卷锁 / 错误通道 ===================== */
|
||||
@@ -329,24 +329,24 @@ static void tc_open(void)
|
||||
rc = lftpd_io_open_read("/a/b.txt");
|
||||
CHECK((rc == 0) && (g_open_mode == FA_READ) &&
|
||||
(strcmp(g_open_path, "a/b.txt") == 0),
|
||||
"TC-LFTPIO-030 成功:FA_READ + 映射后的路径");
|
||||
"TC-LFTPIO-030 success: FA_READ plus the mapped path");
|
||||
|
||||
reset_all();
|
||||
s_file_open = 1;
|
||||
rc = lftpd_io_open_read("/x");
|
||||
CHECK((rc == -1) && (g_open_calls == 0) && (g_wait_calls == 0),
|
||||
"TC-LFTPIO-031 单文件槽守卫:已打开时不碰卷锁与 FatFs");
|
||||
"TC-LFTPIO-031 single-file slot guard: while a file is open it touches neither the volume lock nor FatFs");
|
||||
|
||||
reset_all();
|
||||
g_open_rc = FR_NO_FILE;
|
||||
rc = lftpd_io_open_read("/x");
|
||||
CHECK((rc == -1) && (g_release_calls == 1) && (g_lfio_error_count == 1),
|
||||
"TC-LFTPIO-032 打开失败:返回 -1 + 留痕 + 释放卷锁");
|
||||
"TC-LFTPIO-032 open failure: return -1 + log + release the volume lock");
|
||||
|
||||
reset_all();
|
||||
rc = lftpd_io_open_read("/x");
|
||||
CHECK((rc == 0) && (g_release_calls == 0) && (s_file_open == 1),
|
||||
"TC-LFTPIO-033 成功后卷锁不释放(传输期独占)");
|
||||
"TC-LFTPIO-033 the volume lock stays held after success (exclusive for the whole transfer)");
|
||||
|
||||
reset_all();
|
||||
g_wait_seq[0] = 0; /* 第一轮:卷被 defaultTask 占着 */
|
||||
@@ -354,22 +354,22 @@ static void tc_open(void)
|
||||
g_wait_seq[2] = 1; /* 第三轮:拿到 */
|
||||
rc = lftpd_io_open_read("/x");
|
||||
CHECK((rc == 0) && (g_beat_calls == 2) && (g_wait_calls == 3),
|
||||
"TC-LFTPIO-034 等锁循环每轮喂看门狗(漏喂 = 整机复位)");
|
||||
"TC-LFTPIO-034 the lock-wait loop feeds the watchdog every round (missing a feed resets the whole device)");
|
||||
|
||||
reset_all();
|
||||
rc = lftpd_io_open_write("/up.bin");
|
||||
CHECK((rc == 0) && (g_open_mode == (BYTE)(FA_WRITE | FA_CREATE_ALWAYS)),
|
||||
"TC-LFTPIO-035 open_write 用覆盖写模式");
|
||||
"TC-LFTPIO-035 open_write uses overwrite mode");
|
||||
|
||||
reset_all();
|
||||
s_file_open = 1;
|
||||
rc = lftpd_io_open_write("/up.bin");
|
||||
CHECK((rc == -1) && (g_open_calls == 0), "TC-LFTPIO-036 open_write 同样有槽守卫");
|
||||
CHECK((rc == -1) && (g_open_calls == 0), "TC-LFTPIO-036 open_write has the same slot guard");
|
||||
|
||||
reset_all();
|
||||
g_open_rc = FR_DISK_ERR;
|
||||
rc = lftpd_io_open_write("/up.bin");
|
||||
CHECK((rc == -1) && (g_release_calls == 1), "TC-LFTPIO-037 open_write 失败也释放卷锁");
|
||||
CHECK((rc == -1) && (g_release_calls == 1), "TC-LFTPIO-037 an open_write failure also releases the volume lock");
|
||||
}
|
||||
|
||||
/* ===================== trim ===================== */
|
||||
@@ -386,19 +386,19 @@ static void tc_trim(void)
|
||||
(void)strcpy(buf, " PASS x");
|
||||
r = lftpd_string_ltrim(buf);
|
||||
CHECK((r != buf) && (strcmp(r, "PASS x") == 0) && (buf[0] == ' '),
|
||||
"TC-LFTSTR-001 行首空白靠返回指针跳过,原缓冲不搬移");
|
||||
"TC-LFTSTR-001 leading blanks are skipped via the returned pointer, the buffer itself is not moved");
|
||||
|
||||
(void)strcpy(buf, "NOOP");
|
||||
r = lftpd_string_ltrim(buf);
|
||||
CHECK((r == buf) && (strcmp(buf, "NOOP") == 0), "TC-LFTSTR-002 无前导空白返回同一指针");
|
||||
CHECK((r == buf) && (strcmp(buf, "NOOP") == 0), "TC-LFTSTR-002 with no leading blanks the same pointer comes back");
|
||||
|
||||
(void)strcpy(buf, " USER admin ");
|
||||
r = lftpd_string_ltrim(buf);
|
||||
CHECK(strcmp(r, "USER admin ") == 0, "TC-LFTSTR-003 词中间空白保留");
|
||||
CHECK(strcmp(r, "USER admin ") == 0, "TC-LFTSTR-003 blanks inside the text are kept");
|
||||
|
||||
(void)strcpy(buf, " \t\r\n");
|
||||
r = lftpd_string_ltrim(buf);
|
||||
CHECK(r[0] == '\0', "TC-LFTSTR-004 全空白 -> 空串");
|
||||
CHECK(r[0] == '\0', "TC-LFTSTR-004 all blanks -> empty string");
|
||||
|
||||
/* 现状桩(不是期望行为):尾部循环从 strlen(p) 起步,那里是 '\0',
|
||||
* isspace(0)=0 立刻退出 —— **尾部空白/CRLF 实际从未被裁**。今天无害是因为
|
||||
@@ -411,7 +411,7 @@ static void tc_trim(void)
|
||||
(void)strcpy(buf, "LIST ");
|
||||
r = lftpd_string_ltrim(buf);
|
||||
CHECK(strcmp(r, "LIST ") == 0,
|
||||
"TC-LFTSTR-005 尾随空格必须保留(ltrim 语义,RFC 959 参数本体)");
|
||||
"TC-LFTSTR-005 trailing spaces must survive (ltrim semantics, they belong to the RFC 959 argument)");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
@@ -266,7 +266,7 @@ static void tc_defaults(void)
|
||||
CHECK(bytes_eq(p->ip, def_ip, 4u), "TC-NETCFG-001 ip = 192.168.1.100");
|
||||
CHECK(bytes_eq(p->mask, def_mask, 4u), "TC-NETCFG-002 mask = 255.255.255.0");
|
||||
CHECK(bytes_eq(p->gw, def_gw, 4u), "TC-NETCFG-003 gw = 192.168.1.1");
|
||||
CHECK(mac_eq(p->mac, zero), "TC-NETCFG-004 mac = 全零(芯片内置哨兵)");
|
||||
CHECK(mac_eq(p->mac, zero), "TC-NETCFG-004 mac = all zero (chip built-in sentinel)");
|
||||
}
|
||||
|
||||
/* ===================== 注册与 load 契约 ===================== */
|
||||
@@ -279,13 +279,13 @@ static void tc_register_load(void)
|
||||
fake_reset();
|
||||
rc = net_cfg_load();
|
||||
CHECK((rc == 0) && (g_reg_n == 1) && (strcmp(g_regs[0].name, "network") == 0),
|
||||
"TC-NETCFG-005 首次 load 注册 [network] 恰好一次");
|
||||
"TC-NETCFG-005 first load registers [network] exactly once");
|
||||
CHECK((g_ftp_reg_calls == 1) && (g_load_calls == 1),
|
||||
"TC-NETCFG-006 一并注册 [ftp],且注册在 load 之前");
|
||||
"TC-NETCFG-006 also registers [ftp], registered before load");
|
||||
|
||||
rc = net_cfg_load();
|
||||
CHECK((rc == 0) && (g_reg_n == 1) && (g_ftp_reg_calls == 2),
|
||||
"TC-NETCFG-007 重复 load 不重复注册 [network]");
|
||||
"TC-NETCFG-007 repeated load does not re-register [network]");
|
||||
|
||||
CHECK((strcmp(g_regs[0].name, "network") == 0) &&
|
||||
(g_regs[0].data_size == sizeof(net_cfg_t)) &&
|
||||
@@ -293,18 +293,18 @@ static void tc_register_load(void)
|
||||
(g_regs[0].p_defaults != NULL) &&
|
||||
(g_regs[0].p_cb != NULL) &&
|
||||
(g_regs[0].p_data == (const void *)&s_cfg),
|
||||
"TC-NETCFG-008 注册内容:尺寸/键数/默认值/回调/数据指针=RAM 副本");
|
||||
"TC-NETCFG-008 registration content: size/key count/defaults/callback/data pointer = RAM copy");
|
||||
|
||||
fake_reset();
|
||||
rc = net_cfg_load();
|
||||
CHECK((rc == 0) && (g_netcfg_info_count == 1) && (g_netcfg_error_count == 0),
|
||||
"TC-NETCFG-009 load 成功:返回 0 + 一条 INFO");
|
||||
"TC-NETCFG-009 load success: returns 0 + one INFO line");
|
||||
|
||||
fake_reset();
|
||||
g_load_rc = -1;
|
||||
rc = net_cfg_load();
|
||||
CHECK(rc == 0, "TC-NETCFG-010 引擎 -1 时仍返回 0(RAM 恒有效契约)");
|
||||
CHECK(g_netcfg_error_count == 1, "TC-NETCFG-011 load 失败必须留 ERROR");
|
||||
CHECK(rc == 0, "TC-NETCFG-010 engine -1 still returns 0 (RAM copy is always valid, by contract)");
|
||||
CHECK(g_netcfg_error_count == 1, "TC-NETCFG-011 load failure must leave an ERROR line");
|
||||
}
|
||||
|
||||
/* ===================== RAM 副本读写 ===================== */
|
||||
@@ -321,10 +321,10 @@ static void tc_set_get(void)
|
||||
memcpy(c.mac, mac, 6u);
|
||||
net_cfg_set(&c);
|
||||
CHECK(bytes_eq(net_cfg_get()->ip, ip, 4u) &&
|
||||
mac_eq(net_cfg_get()->mac, mac), "TC-NETCFG-012 set 被 get 读回");
|
||||
mac_eq(net_cfg_get()->mac, mac), "TC-NETCFG-012 set is read back by get");
|
||||
|
||||
net_cfg_set(NULL);
|
||||
CHECK(bytes_eq(net_cfg_get()->ip, ip, 4u), "TC-NETCFG-013 set(NULL) 不改写");
|
||||
CHECK(bytes_eq(net_cfg_get()->ip, ip, 4u), "TC-NETCFG-013 set(NULL) rewrites nothing");
|
||||
}
|
||||
|
||||
/* ===================== 直接保存 ===================== */
|
||||
@@ -336,13 +336,13 @@ static void tc_save_direct(void)
|
||||
printf("-- TC-NETCFG-014..015 save --\n");
|
||||
fake_reset();
|
||||
rc = net_cfg_save();
|
||||
CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-014 写盘成功返回 0");
|
||||
CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-014 write success returns 0");
|
||||
|
||||
fake_reset();
|
||||
g_save_rc = -1;
|
||||
rc = net_cfg_save();
|
||||
CHECK((rc == -1) && (g_netcfg_error_count == 1),
|
||||
"TC-NETCFG-015 写盘失败返回 -1 并留痕");
|
||||
"TC-NETCFG-015 write failure returns -1 and leaves a trace");
|
||||
}
|
||||
|
||||
/* ===================== 跨任务保存交接状态机 ===================== */
|
||||
@@ -357,40 +357,40 @@ static void tc_save_handoff(void)
|
||||
net_cfg_set_save_req_cb(NULL);
|
||||
s_save_pending = 0u; /* 白盒复位:上一组的 save() 不碰 pending/result,此处 */
|
||||
s_save_result = 0u; /* 只是防止将来有人调整后漏出脏态 */
|
||||
CHECK(net_cfg_save_state() == NET_CFG_SAVE_ST_IDLE, "TC-NETCFG-020 初始 IDLE");
|
||||
CHECK(net_cfg_save_state() == NET_CFG_SAVE_ST_IDLE, "TC-NETCFG-020 initial state IDLE");
|
||||
|
||||
CHECK((net_cfg_save_request() == 0) &&
|
||||
(net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT), "TC-NETCFG-021 投递后 WAIT");
|
||||
(net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT), "TC-NETCFG-021 WAIT after posting");
|
||||
|
||||
net_cfg_set_save_req_cb(req_cb);
|
||||
g_req_cb_calls = 0;
|
||||
(void)net_cfg_save_request();
|
||||
CHECK(g_req_cb_calls == 1, "TC-NETCFG-022 投递经回调唤醒属主任务");
|
||||
CHECK(g_req_cb_calls == 1, "TC-NETCFG-022 posting wakes the owner task through the callback");
|
||||
|
||||
net_cfg_set_save_req_cb(NULL);
|
||||
(void)net_cfg_save_request();
|
||||
CHECK((g_req_cb_calls == 1) &&
|
||||
(net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT),
|
||||
"TC-NETCFG-023 未注册回调时空操作,请求照样挂起");
|
||||
"TC-NETCFG-023 no callback registered -> no-op, request still pending");
|
||||
|
||||
g_save_rc = 0;
|
||||
g_save_calls = 0;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-024a 有请求时确实写盘一次");
|
||||
CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-024a with a pending request it really writes once");
|
||||
g_save_calls = 0;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == 0) && (g_save_calls == 0),
|
||||
"TC-NETCFG-024 无请求时空醒不写盘(2s 轮询不刷 Flash)");
|
||||
"TC-NETCFG-024 no request: empty wake does not write (2 s polling never flushes Flash)");
|
||||
|
||||
(void)net_cfg_save_request();
|
||||
net_cfg_set_apply_cb(apply_cb);
|
||||
g_apply_calls = 0;
|
||||
g_save_rc = 0;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == 0) && (s_save_pending == 0u), "TC-NETCFG-025 成功后 pending 清零");
|
||||
CHECK((rc == 0) && (s_save_pending == 0u), "TC-NETCFG-025 pending cleared after success");
|
||||
CHECK((net_cfg_save_state() == NET_CFG_SAVE_ST_OK) && (s_save_busy == 0u),
|
||||
"TC-NETCFG-026 成功后状态 OK 且写盘标志已清");
|
||||
CHECK(g_apply_calls == 1, "TC-NETCFG-027 成功后触发一次立即应用");
|
||||
"TC-NETCFG-026 state OK after success and the write flag is cleared");
|
||||
CHECK(g_apply_calls == 1, "TC-NETCFG-027 success triggers one immediate apply");
|
||||
|
||||
(void)net_cfg_save_request();
|
||||
g_save_hook = save_hook_probe;
|
||||
@@ -399,7 +399,7 @@ static void tc_save_handoff(void)
|
||||
g_save_rc = 0;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == 0) && (g_state_during_save == (int)NET_CFG_SAVE_ST_WAIT),
|
||||
"TC-NETCFG-028 写盘进行中状态仍是 WAIT");
|
||||
"TC-NETCFG-028 state stays WAIT while the write is in progress");
|
||||
|
||||
/* 写盘期间再投递一次:本轮不得把它当已处理,下轮必须真的再写 */
|
||||
g_requeue_during_save = 1;
|
||||
@@ -407,12 +407,12 @@ static void tc_save_handoff(void)
|
||||
g_save_calls = 0;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == 0) && (net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT),
|
||||
"TC-NETCFG-029a 写盘期间的新请求没丢");
|
||||
"TC-NETCFG-029a a new request during the write is not lost");
|
||||
g_requeue_during_save = 0;
|
||||
g_save_hook = NULL;
|
||||
g_save_calls = 0;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-029b 下一轮真的再写一次");
|
||||
CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-029b the next round really writes once more");
|
||||
|
||||
/* 写盘失败:置 FAIL 且**不得**触发应用(内存值没落盘,网络不能重建) */
|
||||
s_save_pending = 0u;
|
||||
@@ -423,20 +423,20 @@ static void tc_save_handoff(void)
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == -1) && (net_cfg_save_state() == NET_CFG_SAVE_ST_FAIL) &&
|
||||
(s_save_busy == 0u),
|
||||
"TC-NETCFG-030 失败置 FAIL 且写盘标志已清(否则 UI 永远卡 WAIT)");
|
||||
CHECK(g_apply_calls == 0, "TC-NETCFG-031 写盘失败不得触发立即应用");
|
||||
"TC-NETCFG-030 failure sets FAIL and clears the write flag (else UI sticks in WAIT forever)");
|
||||
CHECK(g_apply_calls == 0, "TC-NETCFG-031 write failure must not trigger an immediate apply");
|
||||
|
||||
net_cfg_set_apply_cb(NULL);
|
||||
g_apply_calls = 0;
|
||||
net_cfg_request_apply();
|
||||
CHECK(g_apply_calls == 0, "TC-NETCFG-032 未注册应用回调时是空操作");
|
||||
CHECK(g_apply_calls == 0, "TC-NETCFG-032 apply is a no-op while no callback is registered");
|
||||
|
||||
net_cfg_set_apply_cb(apply_cb);
|
||||
net_cfg_request_apply();
|
||||
CHECK(g_apply_calls == 1, "TC-NETCFG-033a 注册后可被触发");
|
||||
CHECK(g_apply_calls == 1, "TC-NETCFG-033a after registering it can be triggered");
|
||||
net_cfg_set_apply_cb(NULL);
|
||||
net_cfg_request_apply();
|
||||
CHECK(g_apply_calls == 1, "TC-NETCFG-033b 注销后不再被触发");
|
||||
CHECK(g_apply_calls == 1, "TC-NETCFG-033b after unregistering it is no longer triggered");
|
||||
}
|
||||
|
||||
/* ===================== MAC 三态优先级 ===================== */
|
||||
@@ -457,30 +457,30 @@ static void tc_mac_priority(void)
|
||||
net_cfg_set_active_mac(shadow);
|
||||
memset(out, 0xFF, sizeof(out));
|
||||
net_cfg_get_mac(out);
|
||||
CHECK(mac_eq(out, stored), "TC-NETCFG-040 存储非零优先于芯片影子");
|
||||
CHECK(mac_eq(out, stored), "TC-NETCFG-040 stored non-zero takes precedence over the chip shadow");
|
||||
|
||||
c = *net_cfg_get();
|
||||
memcpy(c.mac, zero, 6u);
|
||||
net_cfg_set(&c);
|
||||
memset(out, 0xFF, sizeof(out));
|
||||
net_cfg_get_mac(out);
|
||||
CHECK(mac_eq(out, shadow), "TC-NETCFG-041 存储全零则用芯片回写影子");
|
||||
CHECK(mac_eq(out, shadow), "TC-NETCFG-041 stored all zero -> use the chip shadow write-back");
|
||||
|
||||
s_active_valid = 0u; /* 白盒:回到"芯片还没回写"的上电初态 */
|
||||
memset(out, 0xFF, sizeof(out));
|
||||
net_cfg_get_mac(out);
|
||||
CHECK(mac_eq(out, zero), "TC-NETCFG-042 两者皆无 → 全零");
|
||||
CHECK(mac_eq(out, zero), "TC-NETCFG-042 neither available -> all zero");
|
||||
|
||||
net_cfg_set_active_mac(NULL);
|
||||
CHECK(s_active_valid == 0u, "TC-NETCFG-043 NULL 回写不得把影子置有效");
|
||||
CHECK(s_active_valid == 0u, "TC-NETCFG-043 NULL write-back must not mark the shadow valid");
|
||||
|
||||
net_cfg_set_active_mac(shadow);
|
||||
memset(out, 0xFF, sizeof(out));
|
||||
net_cfg_get_mac(out);
|
||||
CHECK(mac_eq(out, shadow), "TC-NETCFG-044 正常回写后影子生效");
|
||||
CHECK(mac_eq(out, shadow), "TC-NETCFG-044 shadow becomes active after a normal write-back");
|
||||
|
||||
net_cfg_get_mac(NULL);
|
||||
CHECK(1, "TC-NETCFG-045 get_mac(NULL) 安全返回");
|
||||
CHECK(1, "TC-NETCFG-045 get_mac(NULL) returns safely");
|
||||
}
|
||||
|
||||
/* ===================== IPv4 定长格式化 ===================== */
|
||||
@@ -500,33 +500,33 @@ static void tc_fmt_ipv4(void)
|
||||
/* 定长 = 4 段 × 3 位 + 3 个点 = 15(曾有误记为 11 的注释,已同日更正)
|
||||
* 钉死这个长度是 UI 光标偏移(段 i 起点 4i、点号 3/7/11)的前提 */
|
||||
CHECK((strcmp(buf, "192.168.001.100") == 0) && (strlen(buf) == 15u),
|
||||
"TC-NETCFG-050 定长 15 字符且每段补零");
|
||||
"TC-NETCFG-050 fixed 15 chars with zero padding in every segment");
|
||||
|
||||
memset(buf, 0x7A, sizeof(buf));
|
||||
net_cfg_fmt_ipv4(ip0, buf, sizeof(buf));
|
||||
CHECK(strcmp(buf, "000.000.000.000") == 0, "TC-NETCFG-051 全零也占满 15 字符");
|
||||
CHECK(strcmp(buf, "000.000.000.000") == 0, "TC-NETCFG-051 all zero still fills 15 chars");
|
||||
|
||||
memset(buf, 0x7A, sizeof(buf));
|
||||
net_cfg_fmt_ipv4(ipm, buf, sizeof(buf));
|
||||
CHECK(strcmp(buf, "255.255.255.255") == 0, "TC-NETCFG-052 255 边界");
|
||||
CHECK(strcmp(buf, "255.255.255.255") == 0, "TC-NETCFG-052 255 boundary");
|
||||
|
||||
memset(guard, 0x7A, sizeof(guard));
|
||||
memcpy(buf, guard, sizeof(buf));
|
||||
net_cfg_fmt_ipv4(NULL, buf, sizeof(buf));
|
||||
CHECK(memcmp(buf, guard, sizeof(buf)) == 0, "TC-NETCFG-053 p_ip NULL 不写缓冲");
|
||||
CHECK(memcmp(buf, guard, sizeof(buf)) == 0, "TC-NETCFG-053 p_ip NULL does not touch the buffer");
|
||||
|
||||
net_cfg_fmt_ipv4(ip1, NULL, 16u);
|
||||
CHECK(1, "TC-NETCFG-054 p_buf NULL 不崩");
|
||||
CHECK(1, "TC-NETCFG-054 p_buf NULL does not crash");
|
||||
|
||||
memset(guard, 0x7A, sizeof(guard));
|
||||
memcpy(buf, guard, sizeof(buf));
|
||||
net_cfg_fmt_ipv4(ip1, buf, 0u);
|
||||
CHECK(memcmp(buf, guard, sizeof(buf)) == 0, "TC-NETCFG-055 len 0 不写缓冲");
|
||||
CHECK(memcmp(buf, guard, sizeof(buf)) == 0, "TC-NETCFG-055 len 0 does not touch the buffer");
|
||||
|
||||
memset(buf, 0x7A, sizeof(buf));
|
||||
net_cfg_fmt_ipv4(ip1, buf, 5u);
|
||||
CHECK((strlen(buf) == 4u) && (buf[4] == '\0') && (buf[5] == (char)0x7A),
|
||||
"TC-NETCFG-056 len 不足:截断但必须 NUL 结尾且不越界");
|
||||
"TC-NETCFG-056 short len: truncate but must stay NUL-terminated and in bounds");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
@@ -217,16 +217,16 @@ static void tc_connect(void)
|
||||
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 对端六元组解析:ip 低字节在前拼 32 位、端口取第 5/6 字节");
|
||||
"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 监听口自接后转已连接并立刻可写");
|
||||
"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 建链即开 keepalive 并清一次挂起位(读即清)");
|
||||
"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 事件带 fd 与调用方 arg 回调给应用");
|
||||
"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);
|
||||
@@ -234,13 +234,13 @@ static void tc_connect(void)
|
||||
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 主动连接分支同样转已连接,但**不动 send_ready**(与自接有别)");
|
||||
"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 其它状态收到 CONNECT 中断一律忽略(不许凭空造会话)");
|
||||
"TC-NETEV-006 a CONNECT interrupt in any other state is ignored (never invent a session)");
|
||||
}
|
||||
|
||||
/* ===================== DISCONNECT ===================== */
|
||||
@@ -257,7 +257,7 @@ static void tc_disconnect(void)
|
||||
handle_disconnect_event(p);
|
||||
CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (g_ev_n == 0) &&
|
||||
(g_nev_error_count == 1),
|
||||
"TC-NETEV-010 芯片说还在连接:忽略这个陈旧 DISCONNECT(不许拆活会话)");
|
||||
"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);
|
||||
@@ -267,7 +267,7 @@ static void tc_disconnect(void)
|
||||
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 疑似误报(一条数据都没收就断):延后交给对账器,不立即拆");
|
||||
"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);
|
||||
@@ -280,7 +280,7 @@ static void tc_disconnect(void)
|
||||
(p->last_peer_port == 51234u) &&
|
||||
(p->state == NET_SOCK_STATE_CLOSED) &&
|
||||
(g_ev_kind[0] == (int)NET_EVENT_DISCONNECTED),
|
||||
"TC-NETEV-012 正常断开:优雅沉降 + 会话标死 + 记旧端口 + 发事件");
|
||||
"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);
|
||||
@@ -289,20 +289,20 @@ static void tc_disconnect(void)
|
||||
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 监听还在:陈旧监听 DISCONNECT 只留痕,不报 ERROR");
|
||||
"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 监听丢了也只留痕:重开交给对账器,事件层不越权");
|
||||
"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:报 ERROR 但仍要通知应用");
|
||||
"TC-NETEV-014 DISCONNECT in an unusual state: report ERROR but still notify the application");
|
||||
}
|
||||
|
||||
/* ===================== RECV / SEND / TIMEOUT ===================== */
|
||||
@@ -316,21 +316,21 @@ static void tc_recv_send_timeout(void)
|
||||
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 RECV 事件置可读并通知应用");
|
||||
"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 完成只发事件,字段留给写路径自己管");
|
||||
"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 连接超时:退回 CREATED 并通知应用");
|
||||
"TC-NETEV-030 connect timeout: fall back to CREATED and notify the application");
|
||||
|
||||
reset_all();
|
||||
p = mk(0, NET_SOCK_STATE_ESTABLISHED, 2404u);
|
||||
@@ -339,14 +339,14 @@ static void tc_recv_send_timeout(void)
|
||||
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 会话超时:隔离级沉降 + 标异常释放(net_pending 收尾据此报 CONNRESET)");
|
||||
"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 监听超时只留痕:不改状态,但仍要通知等待方");
|
||||
"TC-NETEV-032 listen timeout is only logged: state unchanged but waiters are still notified");
|
||||
}
|
||||
|
||||
/* ===================== 掉线自愈 ===================== */
|
||||
@@ -362,7 +362,7 @@ static void tc_lost_probe(void)
|
||||
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 芯片不在已连接态:只清标记,不自作主张建会话");
|
||||
"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);
|
||||
@@ -372,7 +372,7 @@ static void tc_lost_probe(void)
|
||||
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 对端口与上次相同:认定是自己没看到 CONNECT,不自接第二遍");
|
||||
"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 后什么都
|
||||
@@ -387,7 +387,7 @@ static void tc_lost_probe(void)
|
||||
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 丢了 CONNECT 中断时自愈:补一次 accept,会话不至于永久失联");
|
||||
"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);
|
||||
@@ -395,7 +395,7 @@ static void tc_lost_probe(void)
|
||||
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 现状:软件已 CLOSED 时自愈接不上(只留痕,不凭空造会话)");
|
||||
"TC-NETEV-042b current state: self-heal cannot accept once the software side is CLOSED (logged only, no invented session)");
|
||||
}
|
||||
|
||||
/* ===================== 周期对账 ===================== */
|
||||
@@ -419,10 +419,10 @@ static void tc_reconcile(void)
|
||||
infos = g_nev_info_count;
|
||||
recon_step(1000u); /* priming:链路计数记基线 */
|
||||
CHECK(g_nev_info_count == infos,
|
||||
"TC-NETEV-051a 上电第一趟不因链路计数打日志(0xFFFF 哨兵只记基线)");
|
||||
"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 周期闸门:同一时刻不重复查芯片");
|
||||
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);
|
||||
@@ -434,17 +434,17 @@ static void tc_reconcile(void)
|
||||
recon_step(1300u);
|
||||
CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (g_ev_n == 0) &&
|
||||
(g_nev_info_count == infos),
|
||||
"TC-NETEV-052 会话死亡要**连观 4 次**才动手:前三次保持原状");
|
||||
"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 第 4 次稳定后走断开收尾");
|
||||
"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 动作后 1000ms 冷却内不再重复折腾(哪怕稳定计数够了)");
|
||||
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);
|
||||
@@ -452,11 +452,11 @@ static void tc_reconcile(void)
|
||||
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 芯片偷偷重开监听:需要 2 次稳定,第一次只累计");
|
||||
"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 第 2 次稳定:重同步成监听并**取消两条挂起**(否则任务永远等不到回)");
|
||||
"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);
|
||||
@@ -466,7 +466,7 @@ static void tc_reconcile(void)
|
||||
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 连接完成但中断丢了:1 次即补 accept");
|
||||
"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);
|
||||
@@ -476,7 +476,7 @@ static void tc_reconcile(void)
|
||||
recon_step(5300u);
|
||||
CHECK((g_req_n == 1) && (g_req_sock == 4) &&
|
||||
(g_req_reason == (int)NET_LISTENER_REQ_RECON),
|
||||
"TC-NETEV-055 监听丢了:2 次稳定后请求重开监听(不自已在事件层里开)");
|
||||
"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);
|
||||
@@ -487,7 +487,7 @@ static void tc_reconcile(void)
|
||||
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 重同步:软件 CLOSED 而芯片在 LISTEN -> 直接跟芯片,不多余重开");
|
||||
"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);
|
||||
@@ -495,13 +495,13 @@ static void tc_reconcile(void)
|
||||
recon_step(7500u);
|
||||
recon_step(7600u);
|
||||
CHECK((g_req_n == 1) && (g_req_sock == 6),
|
||||
"TC-NETEV-057 软件 CLOSED 且芯片也不在监听:请求重开(重建交回监听层)");
|
||||
"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 不在用的槽位直接跳过(不产生任何动作)");
|
||||
CHECK(g_nev_info_count == infos, "TC-NETEV-058 unused slots are skipped outright (no action at all)");
|
||||
|
||||
/* 链路日志:s_link_last 初值 0xFFFF 是哨兵——第一次看到计数只记基线、不打日志
|
||||
* (否则每次上电都会冒一条"链路 up"的假事件);此后计数一变就必须留痕。
|
||||
@@ -511,7 +511,7 @@ static void tc_reconcile(void)
|
||||
infos = g_nev_info_count;
|
||||
recon_step(9800u);
|
||||
CHECK(g_nev_info_count == (infos + 1),
|
||||
"TC-NETEV-051b 链路变更计数一变就留痕(基线那趟已由 051a 验证不打)");
|
||||
"TC-NETEV-051b any change of the link-change counter is logged (the baseline pass is already covered by 051a)");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
|
||||
@@ -194,14 +194,14 @@ static void tc_bounds(void)
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 0) && (g_chip.close_calls == 0) &&
|
||||
(g_nlis_error_count == 0),
|
||||
"TC-NETLIS-001 越界 fd 静默返回,不打扰芯片");
|
||||
"TC-NETLIS-001 an out-of-range fd returns silently without touching the chip");
|
||||
|
||||
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 不覆盖首个");
|
||||
"TC-NETLIS-019 request de-duplication: a later reason must not overwrite the first one");
|
||||
}
|
||||
|
||||
/* ===================== 该不该重建 ===================== */
|
||||
@@ -215,12 +215,12 @@ static void tc_should_or_not(void)
|
||||
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");
|
||||
"TC-NETLIS-010 while the listener is alive nothing is rebuilt and the request is cleared");
|
||||
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 上一步确实清掉了请求(否则这里会被去重吞掉)");
|
||||
"TC-NETLIS-010b the previous step really cleared the request (otherwise de-duplication would swallow it here)");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
@@ -228,23 +228,23 @@ static void tc_should_or_not(void)
|
||||
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 未结束时让路但保留请求");
|
||||
"TC-NETLIS-011 a pending recv yields the way but keeps the request");
|
||||
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 同样挡路");
|
||||
"TC-NETLIS-011b a pending send blocks the same way");
|
||||
g_send_pending[0] = 0;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 1) && (s_relisten_req[0] == 0u),
|
||||
"TC-NETLIS-011c 挂起结束后自动补上重建");
|
||||
"TC-NETLIS-011c once the pending op ends the rebuild happens automatically");
|
||||
|
||||
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 重建成功后不重复重建");
|
||||
CHECK(g_chip.open_calls == 1, "TC-NETLIS-013 a successful rebuild is not repeated");
|
||||
}
|
||||
|
||||
static void tc_giveup_conditions(void)
|
||||
@@ -254,7 +254,7 @@ static void tc_giveup_conditions(void)
|
||||
int cleared[5];
|
||||
int errs[5];
|
||||
|
||||
printf("-- TC-NETLIS-012 五种放弃条件 --\n");
|
||||
printf("-- TC-NETLIS-012 the five give-up conditions --\n");
|
||||
reset_all();
|
||||
for (i = 0; i < 5; i++) {
|
||||
uint8_t before_open = (uint8_t)g_chip.open_calls;
|
||||
@@ -276,19 +276,19 @@ static void tc_giveup_conditions(void)
|
||||
}
|
||||
CHECK((opened[0] == 0) && (opened[1] == 0) && (opened[2] == 0) &&
|
||||
(opened[3] == 0) && (opened[4] == 0),
|
||||
"TC-NETLIS-012 五种条件下都不碰芯片");
|
||||
"TC-NETLIS-012 none of the five conditions touches the chip");
|
||||
CHECK((cleared[0] == 1) && (cleared[1] == 1) && (cleared[2] == 1) &&
|
||||
(cleared[3] == 1) && (cleared[4] == 1),
|
||||
"TC-NETLIS-012b 放弃即清请求(不留在表里反复白试)");
|
||||
"TC-NETLIS-012b giving up also clears the request (no repeated futile attempts left in the table)");
|
||||
CHECK((errs[0] == 0) && (errs[2] == 0) && (errs[4] == 0),
|
||||
"TC-NETLIS-012c 放弃是静默的(不该报 ERROR,那是对账器的活)");
|
||||
"TC-NETLIS-012c giving up is silent (no ERROR here, that is the reconciler's job)");
|
||||
|
||||
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 直接放弃");
|
||||
"TC-NETLIS-026 a missing table entry is given up directly by the tick");
|
||||
/* 注:net_relisten_locked() 里的 NET_ERR_BADF 分支因此从 tick **不可达**
|
||||
* (net_listener.c:112 那段只在 p_sock 非空时才可能被调到)——留痕:它是
|
||||
* 内部兜底,不是活路径;将来若新增调用点,要重新审视这条。 */
|
||||
@@ -308,13 +308,13 @@ static void tc_backoff_and_budget(void)
|
||||
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 预算");
|
||||
"TC-NETLIS-014 open failure: close back + BUSY + 30ms backoff + a 2000ms budget armed on the first round");
|
||||
g_now = 1020u;
|
||||
net_listener_tick();
|
||||
CHECK(g_chip.open_calls == 1, "TC-NETLIS-014b 退避未到不重试(不空转打芯片)");
|
||||
CHECK(g_chip.open_calls == 1, "TC-NETLIS-014b no retry before the backoff expires (no spinning on the chip)");
|
||||
g_now = 1030u;
|
||||
net_listener_tick();
|
||||
CHECK(g_chip.open_calls == 2, "TC-NETLIS-014c 退避到点再试一次");
|
||||
CHECK(g_chip.open_calls == 2, "TC-NETLIS-014c once the backoff expires it tries exactly once more");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
@@ -323,7 +323,7 @@ static void tc_backoff_and_budget(void)
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.listen_calls == 1) && (g_chip.close_calls == 1) &&
|
||||
(s_relisten_req[0] != 0u),
|
||||
"TC-NETLIS-015 listen 失败也要关断(不留下开了一半的 socket)");
|
||||
"TC-NETLIS-015 a listen failure must also close (no half-open socket left behind)");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
@@ -336,7 +336,7 @@ static void tc_backoff_and_budget(void)
|
||||
net_listener_tick();
|
||||
CHECK((g_nlis_error_count == 1) && (s_relisten_req[0] == 0u) &&
|
||||
(g_chip.open_calls == 1),
|
||||
"TC-NETLIS-018 预算耗尽:报一次 ERROR 交对账器,之后不再自己试");
|
||||
"TC-NETLIS-018 budget exhausted: report ERROR once and hand over to the reconciler, never retry on its own");
|
||||
}
|
||||
|
||||
/* ===================== 成功重建的簿记 ===================== */
|
||||
@@ -363,22 +363,22 @@ static void tc_bookkeeping(void)
|
||||
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 重建后回到干净监听态");
|
||||
"TC-NETLIS-016 after the rebuild the state is a clean listener again");
|
||||
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,其余对端痕迹清零");
|
||||
"TC-NETLIS-016b the old peer port goes into last_peer_port and the other peer traces are zeroed");
|
||||
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 源端口与协议取自表项本身(监听端口不能漂)");
|
||||
"TC-NETLIS-017 the source port and protocol come from the table entry itself (the listening port must not drift)");
|
||||
}
|
||||
|
||||
/* ===================== 沉降窗:隔离 400 / 优雅 100 ===================== */
|
||||
|
||||
static void tc_settle_windows(void)
|
||||
{
|
||||
printf("-- TC-NETLIS-020/021 沉降窗 --\n");
|
||||
printf("-- TC-NETLIS-020/021 settle windows --\n");
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; /* 芯片关不掉 */
|
||||
@@ -390,11 +390,11 @@ static void tc_settle_windows(void)
|
||||
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");
|
||||
"TC-NETLIS-020 if it will not close, wait: the first pass records start plus an 800ms logging line and returns BUSY");
|
||||
g_now = 1030u;
|
||||
net_listener_tick();
|
||||
CHECK(g_nlis_info_count == 1,
|
||||
"TC-NETLIS-020b 等待期间**只在第一次**打日志(否则每 30ms 刷一条)");
|
||||
"TC-NETLIS-020b while waiting the log is written only the first time (otherwise one line every 30ms)");
|
||||
|
||||
/* 边界必须在**独立场景**里测:放行判定与 30ms 退避共用 s_relisten_next,
|
||||
* 上一轮等待会把 next 设成"当次 + 30",于是同一条链里"恰好 400ms"那一次 tick
|
||||
@@ -411,7 +411,7 @@ static void tc_settle_windows(void)
|
||||
g_now = 1399u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 0) && (s_relisten_quarantine[0] != 0u),
|
||||
"TC-NETLIS-021 隔离级 hold=400ms:第 399ms 仍不放行");
|
||||
"TC-NETLIS-021 quarantine level hold=400ms: at 399ms it still does not proceed");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
@@ -424,7 +424,7 @@ static void tc_settle_windows(void)
|
||||
g_now = 1400u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 1) && (s_relisten_quarantine[0] == 0u),
|
||||
"TC-NETLIS-021b 第 400ms 放行并清隔离(独立场景,不受退避槽遮挡)");
|
||||
"TC-NETLIS-021b at 400ms it proceeds and clears the quarantine (standalone scenario, not masked by a backoff slot)");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
@@ -433,10 +433,10 @@ static void tc_settle_windows(void)
|
||||
g_now = 1000u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.open_calls == 0) && (s_relisten_settle_start[0] == 1000u),
|
||||
"TC-NETLIS-021c 优雅级同样先等(0 < 100ms)");
|
||||
"TC-NETLIS-021c the graceful level also waits first (0 < 100ms)");
|
||||
g_now = 1099u;
|
||||
net_listener_tick();
|
||||
CHECK(g_chip.open_calls == 0, "TC-NETLIS-021c 优雅级 hold=100ms:第 99ms 不放行");
|
||||
CHECK(g_chip.open_calls == 0, "TC-NETLIS-021c graceful level hold=100ms: at 99ms it does not proceed");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
@@ -446,12 +446,12 @@ static void tc_settle_windows(void)
|
||||
net_listener_tick();
|
||||
g_now = 1100u;
|
||||
net_listener_tick();
|
||||
CHECK(g_chip.open_calls == 1, "TC-NETLIS-021d 优雅级第 100ms 放行(比隔离级快 4 倍)");
|
||||
CHECK(g_chip.open_calls == 1, "TC-NETLIS-021d the graceful level proceeds at 100ms (4x faster than the quarantine level)");
|
||||
}
|
||||
|
||||
static void tc_cap_is_trace_only(void)
|
||||
{
|
||||
printf("-- TC-NETLIS-022 800ms 上限只留痕 --\n");
|
||||
printf("-- TC-NETLIS-022 the 800ms ceiling only logs --\n");
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; /* 永远关不掉 */
|
||||
@@ -464,15 +464,15 @@ static void tc_cap_is_trace_only(void)
|
||||
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 只**留痕一次**,既不重建也**不放弃请求**");
|
||||
"TC-NETLIS-022 reaching 800ms logs exactly once, rebuilds nothing and does not give up the request");
|
||||
g_now = 1830u;
|
||||
net_listener_tick();
|
||||
CHECK(g_nlis_info_count == 2,
|
||||
"TC-NETLIS-022b 留痕只打一次(之后继续默默等,不刷屏)");
|
||||
"TC-NETLIS-022b the log line is emitted only once (it keeps waiting quietly afterwards, no flood)");
|
||||
g_now = 3000u;
|
||||
net_listener_tick();
|
||||
CHECK((g_nlis_error_count == 1) && (s_relisten_req[0] == 0u),
|
||||
"TC-NETLIS-022c 最终由 2000ms 预算接手:报 ERROR 并交出请求");
|
||||
"TC-NETLIS-022c the 2000ms budget takes over in the end: report ERROR and release the request");
|
||||
}
|
||||
|
||||
/* ===================== 芯片交互:清理、顺序、读即清 ===================== */
|
||||
@@ -488,7 +488,7 @@ static void tc_chip_interaction(void)
|
||||
g_now = 1000u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.read_calls == 16) && (g_chip.last_read_len == 256u),
|
||||
"TC-NETLIS-023 清接收区:单次钳到 256 字节、最多 16 轮(有守卫不死循环)");
|
||||
"TC-NETLIS-023 clearing the receive area: clamped to 256 bytes per call and at most 16 rounds (a guard prevents an infinite loop)");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
@@ -499,7 +499,7 @@ static void tc_chip_interaction(void)
|
||||
g_now = 1000u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.read_calls == 1) && (g_chip.last_read_len == 5u),
|
||||
"TC-NETLIS-023b 读到空立刻停手(不硬凑 16 轮)");
|
||||
"TC-NETLIS-023b an empty read stops it immediately (never padding up to 16 rounds)");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
@@ -510,7 +510,7 @@ static void tc_chip_interaction(void)
|
||||
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 顺序:清接收 → 关断 → 读即清 → 读状态(不可换)");
|
||||
"TC-NETLIS-024 order: clear recv -> close -> read-to-clear -> read status (fixed, must not be reordered)");
|
||||
|
||||
reset_all();
|
||||
mk_closed_listen(0, 2404);
|
||||
@@ -521,7 +521,7 @@ static void tc_chip_interaction(void)
|
||||
g_now = 1000u;
|
||||
net_listener_tick();
|
||||
CHECK((g_chip.int_status_calls >= 1) && (g_fake.int_status[0] == 0u),
|
||||
"TC-NETLIS-025 读即清真的清零(复刻芯片无按位清除的怪癖)");
|
||||
"TC-NETLIS-025 read-to-clear really clears everything (mirrors the chip having no per-bit clear)");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
@@ -168,24 +168,24 @@ static void tc_recv_register(void)
|
||||
printf("-- TC-NETPEND-001..006 recv register --\n");
|
||||
reset_all();
|
||||
rc = net_pending_register(-1, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj);
|
||||
CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-001 下越界 fd 报 BADF");
|
||||
CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-001 an fd below range reports BADF");
|
||||
rc = net_pending_register(NET_MAX_SOCKETS, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj);
|
||||
CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-001b 上越界同样拦下");
|
||||
CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-001b an fd above range is rejected the same way");
|
||||
|
||||
reset_all();
|
||||
rc = net_pending_register(0, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj);
|
||||
CHECK(rc == NET_PENDING_REGISTERED, "TC-NETPEND-005 注册成功回 0x70000000 标记");
|
||||
CHECK(rc == NET_PENDING_REGISTERED, "TC-NETPEND-005 a successful registration returns the 0x70000000 marker");
|
||||
rc = net_pending_register(0, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj);
|
||||
CHECK((rc == -1) && (g_errno_last == NET_ERR_BUSY),
|
||||
"TC-NETPEND-002 同 fd 已有挂起就拒绝(一条 socket 只挂一个 recv)");
|
||||
"TC-NETPEND-002 a pending op on the same fd is refused (one recv per socket at most)");
|
||||
|
||||
reset_all();
|
||||
rc = net_pending_register(0, g_rbuf, 10, 100u, NULL);
|
||||
CHECK((rc == -1) && (g_errno_last == NET_ERR_BUSY),
|
||||
"TC-NETPEND-003 caller 为空拒绝(没有回传目标就别挂起)");
|
||||
"TC-NETPEND-003 a NULL caller is refused (nothing to report back to, so do not suspend)");
|
||||
rc = net_pending_register(0, g_rbuf, 10, 100u, (TaskHandle_t)&g_owner_obj);
|
||||
CHECK((rc == -1) && (g_errno_last == NET_ERR_BUSY),
|
||||
"TC-NETPEND-004 属主任务自己发起被拒(自己等自己 = 死锁 netTask)");
|
||||
"TC-NETPEND-004 the owning task starting it is refused (waiting for itself would deadlock netTask)");
|
||||
|
||||
reset_all();
|
||||
/* 必须先把槽位置成"在用且已建立":否则 tick 第一条兜底(in_use == 0 →
|
||||
@@ -193,11 +193,11 @@ static void tc_recv_register(void)
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
(void)net_pending_register(0, g_rbuf, 10, 0u, (TaskHandle_t)&g_caller_obj);
|
||||
CHECK((s_net_pending[0].infinite == 1u) && (s_net_pending[0].deadline == 0u),
|
||||
"TC-NETPEND-006 to_ms=0 记为无限等(不置 deadline)");
|
||||
"TC-NETPEND-006 to_ms=0 is recorded as an infinite wait (no deadline armed)");
|
||||
g_now = 999999u;
|
||||
net_pending_tick(g_socks);
|
||||
CHECK((s_net_pending[0].active == 1u) && (g_notify_n == 0),
|
||||
"TC-NETPEND-006b 无限等不被时间掐掉(也不会偷偷发通知)");
|
||||
"TC-NETPEND-006b an infinite wait is never cut off by time (and no notification is sent behind the scenes)");
|
||||
}
|
||||
|
||||
static void tc_recv_paths(void)
|
||||
@@ -213,7 +213,7 @@ static void tc_recv_paths(void)
|
||||
(g_notify_target[0] == (void *)&g_caller_obj) &&
|
||||
(g_socks[0].recv_bytes == 5u) && (g_socks[0].readable == 0u) &&
|
||||
(s_net_pending[0].active == 0u),
|
||||
"TC-NETPEND-007 数据到达:字节数经通知值回传给发起任务");
|
||||
"TC-NETPEND-007 data arrived: the byte count is returned to the calling task through the notification value");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
@@ -222,7 +222,7 @@ static void tc_recv_paths(void)
|
||||
ch395f_fake_push_recv_len(0, 100u);
|
||||
net_pending_tick(g_socks);
|
||||
CHECK((g_notify_value[0] == 10u) && (g_chip.last_read_len == 10u),
|
||||
"TC-NETPEND-008 读量钳在调用方缓冲内(绝不越界写别人的 buf)");
|
||||
"TC-NETPEND-008 the read amount is clamped into the caller buffer (never writing past someone else's buf)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
@@ -231,7 +231,7 @@ static void tc_recv_paths(void)
|
||||
net_pending_tick(g_socks);
|
||||
CHECK((g_notify_n == 0) && (g_socks[0].readable == 0u) &&
|
||||
(s_net_pending[0].active == 1u),
|
||||
"TC-NETPEND-009 空 readable:清标志继续等,不误交 0 字节");
|
||||
"TC-NETPEND-009 an empty readable: clear the flag and keep waiting instead of handing over 0 bytes");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
@@ -240,14 +240,14 @@ static void tc_recv_paths(void)
|
||||
net_pending_tick(g_socks);
|
||||
CHECK((g_notify_value[0] == (uint32_t)NET_ERR_TIMEDOUT) &&
|
||||
(g_errno_last == NET_ERR_TIMEDOUT) && (s_net_pending[0].active == 0u),
|
||||
"TC-NETPEND-010 超时:错误码同时进通知值与 errno(跨上下文拿得到)");
|
||||
"TC-NETPEND-010 timeout: the error code goes into both the notification value and errno (available across contexts)");
|
||||
|
||||
reset_all();
|
||||
(void)net_pending_register(0, g_rbuf, 10, 500u, (TaskHandle_t)&g_caller_obj);
|
||||
g_socks[0].in_use = 0u;
|
||||
net_pending_tick(g_socks);
|
||||
CHECK(g_notify_value[0] == (uint32_t)NET_ERR_CONNRESET,
|
||||
"TC-NETPEND-011 槽位消失报 CONNRESET(不能让任务熬到超时才发现)");
|
||||
"TC-NETPEND-011 a vanished slot reports CONNRESET (the task must not have to survive until the timeout to notice)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_CLOSED);
|
||||
@@ -256,7 +256,7 @@ static void tc_recv_paths(void)
|
||||
net_pending_tick(g_socks);
|
||||
CHECK((g_notify_value[0] == 12u) && (g_socks[0].recv_bytes == 12u) &&
|
||||
(g_lis_req_n == 0),
|
||||
"TC-NETPEND-012 关闭但有残留:先把最后一包读走再收尾(不丢尾包)");
|
||||
"TC-NETPEND-012 closed with leftovers: drain the last packet before finishing (no tail data lost)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_CLOSED);
|
||||
@@ -264,7 +264,7 @@ static void tc_recv_paths(void)
|
||||
net_pending_tick(g_socks);
|
||||
CHECK((g_notify_value[0] == 0u) && (g_lis_req_n == 1) && (g_lis_req_sock == 0) &&
|
||||
(g_lis_req_reason == (int)NET_LISTENER_REQ_PENDING),
|
||||
"TC-NETPEND-013 干净结束:报 0 并请求重开监听(单连接 socket 必须回 LISTEN)");
|
||||
"TC-NETPEND-013 a clean end reports 0 and requests a listener reopen (a single-connection socket must return to LISTEN)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_CLOSED);
|
||||
@@ -272,7 +272,7 @@ static void tc_recv_paths(void)
|
||||
(void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj);
|
||||
net_pending_tick(g_socks);
|
||||
CHECK((g_notify_value[0] == 0u) && (g_lis_req_n == 0),
|
||||
"TC-NETPEND-013b 不自动重开的 socket 不会被偷偷要求重建监听");
|
||||
"TC-NETPEND-013b a socket with auto-relisten off is never asked to rebuild the listener behind its back");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_CLOSED);
|
||||
@@ -280,7 +280,7 @@ static void tc_recv_paths(void)
|
||||
(void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj);
|
||||
net_pending_tick(g_socks);
|
||||
CHECK(g_notify_value[0] == (uint32_t)NET_ERR_CONNRESET,
|
||||
"TC-NETPEND-014 异常释放报 CONNRESET 而不是 0(调用方要能分辨好坏收尾)");
|
||||
"TC-NETPEND-014 an abnormal release reports CONNRESET instead of 0 (the caller must tell a good end from a bad one)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
@@ -288,7 +288,7 @@ static void tc_recv_paths(void)
|
||||
(void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj);
|
||||
net_pending_tick(g_socks);
|
||||
CHECK((g_notify_n == 1) && (g_lis_req_n == 1),
|
||||
"TC-NETPEND-015 session_dead 与 CLOSED 同路(状态滞后时仍能收口)");
|
||||
"TC-NETPEND-015 session_dead takes the same path as CLOSED (a stale state still gets a proper close)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
@@ -296,9 +296,9 @@ static void tc_recv_paths(void)
|
||||
net_pending_cancel(0, NET_ERR_CONNRESET);
|
||||
CHECK((g_notify_value[0] == (uint32_t)NET_ERR_CONNRESET) &&
|
||||
(g_errno_last == NET_ERR_CONNRESET) && (s_net_pending[0].active == 0u),
|
||||
"TC-NETPEND-016 取消:错误码经通知值送达并解除挂起");
|
||||
"TC-NETPEND-016 cancel: the error code is delivered through the notification value and the suspension is released");
|
||||
net_pending_cancel(0, NET_ERR_CONNRESET);
|
||||
CHECK(g_notify_n == 1, "TC-NETPEND-016b 重复取消幂等(不给同一任务发两次通知)");
|
||||
CHECK(g_notify_n == 1, "TC-NETPEND-016b a repeated cancel is idempotent (the same task is never notified twice)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
@@ -308,14 +308,14 @@ static void tc_recv_paths(void)
|
||||
net_pending_cancel_all(NET_ERR_CONNRESET);
|
||||
CHECK((g_notify_n == 2) && (s_net_pending[0].active == 0u) &&
|
||||
(s_net_pending[2].active == 0u),
|
||||
"TC-NETPEND-017 cancel_all 只通知挂着的那两个(网络整体复位时用)");
|
||||
"TC-NETPEND-017 cancel_all notifies only the two suspended ops (used on a full network reset)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
(void)net_pending_register(0, g_rbuf, 10, 500u, (TaskHandle_t)&g_caller_obj);
|
||||
net_pending_clear(0);
|
||||
CHECK((s_net_pending[0].active == 0u) && (g_notify_n == 0),
|
||||
"TC-NETPEND-018 clear 只解除挂起、不发通知(不给调用方假答复)");
|
||||
"TC-NETPEND-018 clear only releases the suspension and sends no notification (the caller gets no fake answer)");
|
||||
}
|
||||
|
||||
static void tc_settle_close(void)
|
||||
@@ -328,7 +328,7 @@ static void tc_settle_close(void)
|
||||
before = g_chip.read_calls + g_chip.get_status_calls;
|
||||
net_pending_settle_close(0, &g_socks[0]);
|
||||
CHECK((g_chip.read_calls + g_chip.get_status_calls) == before,
|
||||
"TC-NETPEND-019 没挂起时直接返回(不打扰芯片)");
|
||||
"TC-NETPEND-019 with nothing suspended it returns directly (the chip is not disturbed)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_CLOSED);
|
||||
@@ -337,14 +337,14 @@ static void tc_settle_close(void)
|
||||
net_pending_settle_close(0, &g_socks[0]);
|
||||
CHECK((g_notify_value[0] == 7u) && (g_socks[0].recv_bytes == 7u) &&
|
||||
(g_socks[0].readable == 0u),
|
||||
"TC-NETPEND-020 收尾时把剩余字节读走并交还字节数(收口不丢数据)");
|
||||
"TC-NETPEND-020 the teardown drains the remaining bytes and returns their count (closing loses no data)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_CLOSED);
|
||||
(void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj);
|
||||
net_pending_settle_close(0, &g_socks[0]);
|
||||
CHECK(g_notify_value[0] == (uint32_t)NET_ERR_CONNRESET,
|
||||
"TC-NETPEND-021 无残留则报 CONNRESET(关闭收尾不该被当成读到 0 字节)");
|
||||
"TC-NETPEND-021 with nothing left it reports CONNRESET (a close teardown must not look like a zero-byte read)");
|
||||
}
|
||||
|
||||
static void tc_send_paths(void)
|
||||
@@ -354,31 +354,31 @@ static void tc_send_paths(void)
|
||||
printf("-- TC-NETPEND-030..040 send pending --\n");
|
||||
reset_all();
|
||||
rc = net_send_pending_register(-1, g_wbuf, 100, 0, (TaskHandle_t)&g_caller_obj);
|
||||
CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-030 越界 fd 报 BADF");
|
||||
CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-030 an out-of-range fd reports BADF");
|
||||
rc = net_send_pending_register(0, NULL, 100, 0, (TaskHandle_t)&g_caller_obj);
|
||||
CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030b buf 为空报 INVAL");
|
||||
CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030b a NULL buffer reports INVAL");
|
||||
rc = net_send_pending_register(0, g_wbuf, 0, 0, (TaskHandle_t)&g_caller_obj);
|
||||
CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030c len 非正报 INVAL");
|
||||
CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030c a non-positive length reports INVAL");
|
||||
rc = net_send_pending_register(0, g_wbuf, 100, -1, (TaskHandle_t)&g_caller_obj);
|
||||
CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030d sent 为负报 INVAL");
|
||||
CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030d a negative sent count reports INVAL");
|
||||
rc = net_send_pending_register(0, g_wbuf, 100, 100, (TaskHandle_t)&g_caller_obj);
|
||||
CHECK((g_errno_last == NET_ERR_INVAL) && (s_net_send[0].active == 0u),
|
||||
"TC-NETPEND-030e sent>=len 报 INVAL(已发完的不必挂)");
|
||||
"TC-NETPEND-030e sent >= len reports INVAL (nothing left to send, no need to suspend)");
|
||||
rc = net_send_pending_register(0, g_wbuf, 100, 50, NULL);
|
||||
CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031 caller 为空报 BUSY");
|
||||
CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031 a NULL caller reports BUSY");
|
||||
rc = net_send_pending_register(0, g_wbuf, 100, 50, (TaskHandle_t)&g_owner_obj);
|
||||
CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031b 属主自己发报 BUSY");
|
||||
CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031b the owning task sending it itself reports BUSY");
|
||||
rc = net_send_pending_register(0, g_wbuf, 100, 50, (TaskHandle_t)&g_caller_obj);
|
||||
CHECK(rc == NET_PENDING_REGISTERED, "TC-NETPEND-032 挂起发送注册成功");
|
||||
CHECK(rc == NET_PENDING_REGISTERED, "TC-NETPEND-032 a suspended send registers successfully");
|
||||
rc = net_send_pending_register(0, g_wbuf, 100, 50, (TaskHandle_t)&g_caller_obj);
|
||||
CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031c 同 fd 只允许一条挂起发送");
|
||||
CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031c only one suspended send per fd is allowed");
|
||||
|
||||
reset_all();
|
||||
(void)net_send_pending_register(0, g_wbuf, 3000, 700, (TaskHandle_t)&g_caller_obj);
|
||||
g_socks[0].in_use = 0u;
|
||||
net_send_pending_tick(g_socks);
|
||||
CHECK((g_notify_n == 1) && (g_notify_value[0] == 700u),
|
||||
"TC-NETPEND-033 槽位消失回**已发字节数**(不是错误码,调用方据此算进度)");
|
||||
"TC-NETPEND-033 a vanished slot returns the number of bytes already sent (not an error code; the caller computes progress from it)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_CLOSED);
|
||||
@@ -386,7 +386,7 @@ static void tc_send_paths(void)
|
||||
net_send_pending_tick(g_socks);
|
||||
CHECK((g_npd_error_count == 1) && (g_errno_last == NET_ERR_CONNRESET) &&
|
||||
(g_notify_value[0] == 700u),
|
||||
"TC-NETPEND-034 会话塌了:留 ERROR + 报 CONNRESET,但仍把已发字节交回去");
|
||||
"TC-NETPEND-034 a collapsed session logs ERROR + reports CONNRESET yet still hands back the bytes already sent");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
@@ -397,7 +397,7 @@ static void tc_send_paths(void)
|
||||
(g_chip.last_read_len == (uint16_t)NET_SEND_CHUNK_MAX) &&
|
||||
(g_socks[0].send_bytes == (uint32_t)NET_SEND_CHUNK_MAX) &&
|
||||
(g_socks[0].send_ready == 0u) && (s_net_send[0].active == 1u),
|
||||
"TC-NETPEND-035 单次写入钳在 NET_SEND_CHUNK_MAX(芯片发送缓冲上界)");
|
||||
"TC-NETPEND-035 a single write is clamped to NET_SEND_CHUNK_MAX (the chip send buffer upper bound)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
@@ -410,7 +410,7 @@ static void tc_send_paths(void)
|
||||
net_send_pending_tick(g_socks);
|
||||
CHECK((g_notify_value[0] == 3000u) && (s_net_send[0].active == 0u) &&
|
||||
(g_chip.write_calls == 3),
|
||||
"TC-NETPEND-036 分三块发完 3000 字节后交还总长度");
|
||||
"TC-NETPEND-036 3000 bytes finish in three chunks and the total length is returned");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
@@ -423,7 +423,7 @@ static void tc_send_paths(void)
|
||||
net_send_pending_tick(g_socks);
|
||||
CHECK((g_npd_error_count == 0) && (s_net_send[0].active == 1u) &&
|
||||
(g_chip.write_calls == 2),
|
||||
"TC-NETPEND-038 每写一轮续期 deadline:还在推进就不该被 1000ms 掐死");
|
||||
"TC-NETPEND-038 every written round renews the deadline: while it progresses it must not be killed at 1000ms");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
@@ -432,7 +432,7 @@ static void tc_send_paths(void)
|
||||
net_send_pending_tick(g_socks);
|
||||
CHECK((g_npd_error_count == 1) && (g_errno_last == NET_ERR_TIMEDOUT) &&
|
||||
(g_notify_value[0] == 0u),
|
||||
"TC-NETPEND-037 停滞超时报 TIMEDOUT(对端不收时不能永久占着发送通道)");
|
||||
"TC-NETPEND-037 a stalled wait times out with TIMEDOUT (a peer that never reads must not hold the send path forever)");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
@@ -440,16 +440,16 @@ static void tc_send_paths(void)
|
||||
net_send_pending_cancel(0, NET_ERR_CONNRESET);
|
||||
CHECK((g_npd_error_count == 1) && (g_errno_last == NET_ERR_CONNRESET) &&
|
||||
(g_notify_value[0] == 40u) && (s_net_send[0].active == 0u),
|
||||
"TC-NETPEND-039 取消挂起发送:留痕 + 交还已发字节");
|
||||
"TC-NETPEND-039 cancelling a suspended send logs it and hands back the bytes already sent");
|
||||
net_send_pending_cancel(0, NET_ERR_CONNRESET);
|
||||
CHECK(g_notify_n == 1, "TC-NETPEND-039b 重复取消幂等");
|
||||
CHECK(g_notify_n == 1, "TC-NETPEND-039b a repeated cancel is idempotent");
|
||||
|
||||
reset_all();
|
||||
mk_sock(0, NET_SOCK_STATE_ESTABLISHED);
|
||||
(void)net_send_pending_register(0, g_wbuf, 100, 40, (TaskHandle_t)&g_caller_obj);
|
||||
net_send_pending_clear(0);
|
||||
CHECK((s_net_send[0].active == 0u) && (g_notify_n == 0),
|
||||
"TC-NETPEND-040 clear 不发通知(与 recv 侧同一条纪律)");
|
||||
"TC-NETPEND-040 clear sends no notification (the same discipline as the recv side)");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
|
||||
@@ -106,7 +106,7 @@ void osDelay(uint32_t ticks)
|
||||
/* 保险:本模块每条用例都必须在有限轮内退出(要么就绪、要么超时、要么由
|
||||
* g_release_after 造事件)。跑飞就说明用例或实现出了问题——直接退出,
|
||||
* 绝不能把挂死留给 pre-commit / CI(上一次就是这么卡住 exe 文件名的)。 */
|
||||
printf(" ABORT 轮询跑飞(>500 轮未退出)\n");
|
||||
printf(" ABORT poll runaway (>500 rounds without exit)\n");
|
||||
fflush(stdout);
|
||||
exit(2);
|
||||
}
|
||||
@@ -147,9 +147,9 @@ static void tc_bounds(void)
|
||||
reset_all();
|
||||
NET_FD_ZERO(&r);
|
||||
CHECK(net_select(0, &r, NULL, NULL, NULL) == -1, "TC-NETSEL-001 nfds=0 -> -1");
|
||||
CHECK(net_select(-1, &r, NULL, NULL, NULL) == -1, "TC-NETSEL-002 nfds 为负 -> -1");
|
||||
CHECK(net_select(-1, &r, NULL, NULL, NULL) == -1, "TC-NETSEL-002 negative nfds -> -1");
|
||||
CHECK(net_select(NET_MAX_SOCKETS + 1, &r, NULL, NULL, NULL) == -1,
|
||||
"TC-NETSEL-003 nfds 越界 -> -1(不许扫出表外)");
|
||||
"TC-NETSEL-003 nfds out of range -> -1 (must not scan past the table)");
|
||||
}
|
||||
|
||||
/* ===================== 就绪位与集合回写 ===================== */
|
||||
@@ -169,14 +169,14 @@ static void tc_ready_sets(void)
|
||||
NET_FD_SET(1, &r);
|
||||
rc = net_select(4, &r, NULL, NULL, NULL);
|
||||
CHECK((rc == 1) && NET_FD_ISSET(1, &r) && !NET_FD_ISSET(0, &r),
|
||||
"TC-NETSEL-004 回写只保留就绪位,未就绪的 fd0 被清掉");
|
||||
"TC-NETSEL-004 write-back keeps only the ready bits; not-ready fd0 is cleared");
|
||||
|
||||
reset_all();
|
||||
make_sock(2, NET_SOCK_STATE_ESTABLISHED, 1u, 0u);
|
||||
NET_FD_ZERO(&r);
|
||||
NET_FD_SET(2, &r);
|
||||
rc = net_select(3, &r, NULL, NULL, NULL); /* write/except 传 NULL */
|
||||
CHECK((rc == 1) && (NET_FD_ISSET(2, &r)), "TC-NETSEL-005 其余集合可为 NULL");
|
||||
CHECK((rc == 1) && (NET_FD_ISSET(2, &r)), "TC-NETSEL-005 the other sets may be NULL");
|
||||
|
||||
/* 注意:timeout 传 NULL 时本函数会一直等到有事件(见 008 的现状说明),
|
||||
* 所以这类用例必须让被请求的 fd 真的就绪,否则就是用例自己写死的死循环。 */
|
||||
@@ -187,7 +187,7 @@ static void tc_ready_sets(void)
|
||||
NET_FD_SET(2, &w);
|
||||
rc = net_select(3, NULL, &w, NULL, NULL);
|
||||
CHECK((rc == 1) && NET_FD_ISSET(2, &w) && (r.fd_bits == 0u),
|
||||
"TC-NETSEL-005b 只回写调用方真正请求过的集合");
|
||||
"TC-NETSEL-005b only the sets the caller actually requested get written back");
|
||||
}
|
||||
|
||||
/* ===================== 超时算术 ===================== */
|
||||
@@ -205,7 +205,7 @@ static void tc_timeout(void)
|
||||
NET_FD_SET(0, &r);
|
||||
rc = net_select(1, &r, NULL, NULL, NULL); /* timeout = NULL:无限等 */
|
||||
CHECK((rc == 1) && (g_delay_calls == 3) && NET_FD_ISSET(0, &r),
|
||||
"TC-NETSEL-006 无限等被对端事件唤醒(第 3 轮)");
|
||||
"TC-NETSEL-006 infinite wait wakes on a peer event (round 3)");
|
||||
|
||||
reset_all();
|
||||
make_sock(0, NET_SOCK_STATE_ESTABLISHED, 0u, 0u);
|
||||
@@ -215,7 +215,7 @@ static void tc_timeout(void)
|
||||
tv.tv_usec = 1999; /* 应为 1ms,不是 2ms */
|
||||
rc = net_select(1, &r, NULL, NULL, &tv);
|
||||
CHECK((rc == 0) && (g_delay_calls == 0),
|
||||
"TC-NETSEL-007 tv_usec 整除截断:1999us 当 1ms,首轮即退");
|
||||
"TC-NETSEL-007 tv_usec integer truncation: 1999us counts as 1ms, exits on the first round");
|
||||
|
||||
/* 已修(2026-10-23):原来这里写的是 (timeout_ms > 0) && (已用时 >= timeout_ms),
|
||||
* timeout = {0,0} 换算出 0 后**永不超时**,只能干等事件——而 POSIX 语义是
|
||||
@@ -229,7 +229,7 @@ static void tc_timeout(void)
|
||||
tv.tv_usec = 0;
|
||||
rc = net_select(1, &r, NULL, NULL, &tv);
|
||||
CHECK((rc == 0) && (g_delay_calls == 0) && !NET_FD_ISSET(0, &r),
|
||||
"TC-NETSEL-008 timeout={0,0} 只查一轮立即返回(与 poll 的 0 对齐)");
|
||||
"TC-NETSEL-008 timeout={0,0} checks one round and returns at once (aligned with poll timeout 0)");
|
||||
}
|
||||
|
||||
/* ===================== 状态准入 ===================== */
|
||||
@@ -250,7 +250,7 @@ static void tc_state_admission(void)
|
||||
tv.tv_usec = 1000;
|
||||
rc = net_select(1, &r, NULL, NULL, &tv);
|
||||
CHECK((rc == 0) && !NET_FD_ISSET(0, &r),
|
||||
"TC-NETSEL-009 监听态不可读(accept 语义不在 select 这层)");
|
||||
"TC-NETSEL-009 listen state is not readable (accept semantics are not in the select layer)");
|
||||
|
||||
reset_all();
|
||||
make_sock(0, NET_SOCK_STATE_ESTABLISHED, 0u, 1u);
|
||||
@@ -258,7 +258,7 @@ static void tc_state_admission(void)
|
||||
NET_FD_SET(0, &w);
|
||||
rc = net_select(1, NULL, &w, NULL, &tv);
|
||||
CHECK((rc == 1) && NET_FD_ISSET(0, &w),
|
||||
"TC-NETSEL-010 已建立 + send_ready 才可写");
|
||||
"TC-NETSEL-010 established + send_ready is what makes it writable");
|
||||
|
||||
reset_all();
|
||||
make_sock(0, NET_SOCK_STATE_CONNECTING, 0u, 1u);
|
||||
@@ -266,7 +266,7 @@ static void tc_state_admission(void)
|
||||
NET_FD_SET(0, &w);
|
||||
rc = net_select(1, NULL, &w, NULL, &tv);
|
||||
CHECK((rc == 0) && !NET_FD_ISSET(0, &w),
|
||||
"TC-NETSEL-011 连接中不可写(send_ready 不作数)");
|
||||
"TC-NETSEL-011 connecting state is not writable (send_ready does not count)");
|
||||
|
||||
/* 已修(2026-10-23):异常判定原先排在 p_sock == NULL → continue 之后,而
|
||||
* check_socket_error() 只有"表项为空 / fd 越界"才返回 1、越界的 i 又不在本循环
|
||||
@@ -279,14 +279,14 @@ static void tc_state_admission(void)
|
||||
NET_FD_SET(0, &r);
|
||||
rc = net_select(1, NULL, NULL, &r, &tv);
|
||||
CHECK((rc == 1) && NET_FD_ISSET(0, &r),
|
||||
"TC-NETSEL-012 表项不存在的 fd 计入异常集合");
|
||||
"TC-NETSEL-012 an fd with no table entry counts into the exception set");
|
||||
reset_all();
|
||||
g_sock_valid[0] = 0u;
|
||||
NET_FD_ZERO(&r);
|
||||
NET_FD_SET(0, &r); /* 只请求可读 */
|
||||
rc = net_select(1, &r, NULL, NULL, &tv);
|
||||
CHECK((rc == 0) && !NET_FD_ISSET(0, &r) && (g_delay_calls == 0),
|
||||
"TC-NETSEL-012b 未请求异常集时不因缺失 fd 计数(不误伤只查可读的调用方)");
|
||||
"TC-NETSEL-012b a missing fd is not counted when the exception set was not requested");
|
||||
reset_all();
|
||||
g_sock_valid[0] = 0u;
|
||||
NET_FD_ZERO(&r);
|
||||
@@ -295,7 +295,7 @@ static void tc_state_admission(void)
|
||||
NET_FD_SET(0, &w); /* 请求异常:应该置位 */
|
||||
rc = net_select(1, &r, NULL, &w, &tv);
|
||||
CHECK((rc == 1) && NET_FD_ISSET(0, &w) && !NET_FD_ISSET(0, &r),
|
||||
"TC-NETSEL-012c 异常位只落到被请求的那个集合,不污染可读集合");
|
||||
"TC-NETSEL-012c the exception bit lands only in the requested set, never polluting the read set");
|
||||
}
|
||||
|
||||
/* ===================== poll 语义 ===================== */
|
||||
@@ -308,7 +308,7 @@ static void tc_poll(void)
|
||||
printf("-- TC-NETSEL-013..019 poll --\n");
|
||||
reset_all();
|
||||
CHECK((net_poll_events(NULL, 1, 0) == -1) && (net_poll_events(p, 0, 0) == -1),
|
||||
"TC-NETSEL-013 poll 参数非法 -> -1");
|
||||
"TC-NETSEL-013 invalid poll arguments -> -1");
|
||||
|
||||
reset_all();
|
||||
(void)memset(p, 0, sizeof(p));
|
||||
@@ -319,7 +319,7 @@ static void tc_poll(void)
|
||||
rc = net_poll_events(p, 2, 0);
|
||||
CHECK((rc == 2) && ((p[0].revents & NET_POLLNVAL) != 0) &&
|
||||
((p[1].revents & NET_POLLNVAL) != 0),
|
||||
"TC-NETSEL-014 负 fd 与越界 fd 都置 POLLNVAL 并计入 ready");
|
||||
"TC-NETSEL-014 negative and out-of-range fd both set POLLNVAL and count as ready");
|
||||
|
||||
reset_all();
|
||||
make_sock(0, NET_SOCK_STATE_CLOSED, 0u, 0u);
|
||||
@@ -328,7 +328,7 @@ static void tc_poll(void)
|
||||
p[0].events = NET_POLLIN;
|
||||
rc = net_poll_events(p, 1, 0);
|
||||
CHECK((rc == 1) && ((p[0].revents & NET_POLLHUP) != 0),
|
||||
"TC-NETSEL-015 CLOSED 置 POLLHUP");
|
||||
"TC-NETSEL-015 CLOSED sets POLLHUP");
|
||||
|
||||
reset_all();
|
||||
make_sock(0, NET_SOCK_STATE_ESTABLISHED, 1u, 0u);
|
||||
@@ -338,7 +338,7 @@ static void tc_poll(void)
|
||||
rc = net_poll_events(p, 1, 0);
|
||||
CHECK((rc == 1) && ((p[0].revents & NET_POLLIN) != 0) &&
|
||||
((p[0].revents & NET_POLLHUP) == 0),
|
||||
"TC-NETSEL-016 可读置 POLLIN 且不误置 HUP");
|
||||
"TC-NETSEL-016 readable sets POLLIN without falsely setting HUP");
|
||||
|
||||
reset_all();
|
||||
make_sock(0, NET_SOCK_STATE_ESTABLISHED, 1u, 1u);
|
||||
@@ -347,7 +347,7 @@ static void tc_poll(void)
|
||||
p[0].events = 0; /* 什么都不请求 */
|
||||
(void)net_poll_events(p, 1, 0);
|
||||
CHECK((p[0].revents & NET_POLLERR) == 0,
|
||||
"TC-NETSEL-017 现状:POLLERR 实际永不为真(表项为空已被 POLLNVAL 抢先)");
|
||||
"TC-NETSEL-017 current state: POLLERR is never true (an empty entry is caught earlier by POLLNVAL)");
|
||||
|
||||
reset_all();
|
||||
make_sock(0, NET_SOCK_STATE_ESTABLISHED, 0u, 0u);
|
||||
@@ -356,7 +356,7 @@ static void tc_poll(void)
|
||||
p[0].events = NET_POLLIN;
|
||||
rc = net_poll_events(p, 1, 0); /* timeout = 0 */
|
||||
CHECK((rc == 0) && (g_delay_calls == 0),
|
||||
"TC-NETSEL-018 poll 的 timeout=0 立即返回(与 select 那条形成对照)");
|
||||
"TC-NETSEL-018 poll timeout=0 returns at once (contrast with the matching select case)");
|
||||
|
||||
reset_all();
|
||||
make_sock(0, NET_SOCK_STATE_ESTABLISHED, 1u, 0u);
|
||||
@@ -365,7 +365,7 @@ static void tc_poll(void)
|
||||
p[0].events = NET_POLLIN;
|
||||
p[0].revents = NET_POLLOUT; /* 脏的上一轮残留 */
|
||||
(void)net_poll_events(p, 1, 0);
|
||||
CHECK((p[0].revents & NET_POLLOUT) == 0, "TC-NETSEL-019 每轮先清 revents");
|
||||
CHECK((p[0].revents & NET_POLLOUT) == 0, "TC-NETSEL-019 revents is cleared at the start of every round");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
@@ -0,0 +1,712 @@
|
||||
/*
|
||||
* 模块名称:NET 套接字核心(net_socket)主机侧单元测试
|
||||
* 模块功能:在 PC 上离线验证 Drivers/BSP/NET/net_socket.c 的**跨上下文决策面**。
|
||||
* 被测对象:真实源文件 #include 原样纳入(同 TU 内可直接驱动它的 static)。
|
||||
*
|
||||
* 分片(同一 TU 内按片组织;一个目录=一个模块名,所以只有一个 runner):
|
||||
* 分片 1 纯函数与 fd 表:inet_addr/inet_ntoa(E13 段序定案)、hton 与 ntoh 四件套、
|
||||
* alloc_socket/net_get_sock/net_sock_raw 的边界、net_socket() 的"新 fd 不得继承"、
|
||||
* net_bind 的动态端口与回绕、getsockname/getpeername、setblocking/set_event_cb/
|
||||
* sock_state、net_poll 未初始化短路。
|
||||
* 分片 2 每任务错误码槽(E-03)+ 队列句柄绑定(adopt 幂等):内核未跑→[0] 槽退化、
|
||||
* 不同任务不同槽位互不覆写、槽位用尽回退 [0]、临界区成对且不嵌套泄漏。
|
||||
* 分片 3 跨上下文消息面:八个 msg.type 全分派 + 结果经**任务通知值**回传 +
|
||||
* "登记挂起即不发即时回执"(do_notify=0)+ 失败时把 netTask 自己槽位的精确
|
||||
* 错误码搭通知值带回来 + 调用方在自己的槽位里恢复 errno。
|
||||
* 分片 4 状态机与闸门:监听门控 net_listener_armed 三支放行/其余挡下、net_can_read、
|
||||
* net_poll 的"全局位命中才读本 Socket SINT、永不读全局"(B3/F-NET-7)、
|
||||
* 事件计数、每轮全量刷新驱动缓存 + sock_reset 步进(I1/E-02)。
|
||||
* 分片 5 读写错误分支:send 分块上限/短写/挂起登记/末块不登记(NET_PENDING 拒收边界)、
|
||||
* recv 快路径与 EOF vs WOULDBLOCK、connect 命令被拒的回滚、accept 的
|
||||
* closed-with-readable 排空接受、close 的先结算再关芯片。
|
||||
*
|
||||
* ⚠ 断言只钉**真实契约**(AGENTS 模块约定 + docs/网络/网络自愈架构设计.md §4.10 +
|
||||
* 源码注释里的定案编号),不钉假件规则:凡"因为假件这么做所以断言这么做"的一律不写。
|
||||
*
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL。跑法见同目录 run_unit_test.py。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "net_socket.h"
|
||||
#include "net_types.h"
|
||||
#include "net_pending.h"
|
||||
#include "net_listener.h"
|
||||
#include "net_events.h"
|
||||
#include "ch395f.h"
|
||||
|
||||
/* dbg_log 桩的计数定义(桩头只 extern) */
|
||||
int g_ns_info_count;
|
||||
int g_ns_error_count;
|
||||
int g_ns_debug_count;
|
||||
|
||||
/* —— 假件实现与生产源进同一个 TU(本仓主机单测既有做法)—— */
|
||||
#include "ch395f_fake.c"
|
||||
#include "ns_rtos.c"
|
||||
#include "ns_peers.c"
|
||||
|
||||
/* —— 被测生产源 —— */
|
||||
#include "net_socket.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)
|
||||
|
||||
/* ===================== 槽位表只读小工具(断言用,不参与被测逻辑) ===================== */
|
||||
|
||||
static int ns_slot_owner_count(void)
|
||||
{
|
||||
int i;
|
||||
int n = 0;
|
||||
|
||||
for (i = 0; i < (int)NET_ERRNO_TASK_SLOTS; i++) {
|
||||
if (s_net_errno_slots[i].owner != NULL) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static int ns_slot_index_of(TaskHandle_t h)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < (int)NET_ERRNO_TASK_SLOTS; i++) {
|
||||
if (s_net_errno_slots[i].owner == (void *)h) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ===================== 场景复位 ===================== */
|
||||
/* 生产模块的 static 在场景之间会残留,每个场景开头必须显式清干净(漏一次红一片) */
|
||||
static void reset_all(void)
|
||||
{
|
||||
(void)memset(s_net_socks, 0, sizeof(s_net_socks));
|
||||
(void)memset(s_net_errno_slots, 0, sizeof(s_net_errno_slots));
|
||||
(void)memset(s_local_ip, 0, sizeof(s_local_ip));
|
||||
s_dynamic_port = 49152;
|
||||
s_net_generation = 0U;
|
||||
s_net_initialized = 0U;
|
||||
s_net_msg_queue = NULL;
|
||||
/* UDP 上下文守卫的判据也是生产 static:不清就会把上一场景的"执行任务"带进本场景
|
||||
* (表现为守卫判错上下文,且与任务句柄复用后的地址撞车) */
|
||||
s_net_owner_task = NULL;
|
||||
g_ns_info_count = 0;
|
||||
g_ns_error_count = 0;
|
||||
g_ns_debug_count = 0;
|
||||
ch395f_fake_reset();
|
||||
ns_rtos_reset();
|
||||
ns_peers_reset();
|
||||
}
|
||||
|
||||
/* 把网络层拉到"已初始化 + 队列已绑定"的常态(绝大多数场景的前置) */
|
||||
static void bring_up(void)
|
||||
{
|
||||
static int s_fake_queue; /* 句柄只是不透明标识,假内核不解引用 */
|
||||
|
||||
reset_all();
|
||||
(void)net_msg_queue_adopt((osMessageQueueId_t)&s_fake_queue);
|
||||
/* 装泵:调用方阻塞等待时以 netTask 身份跑一轮消息分派(= 跨上下文那条链) */
|
||||
g_ns_pump = net_process_messages;
|
||||
/* net_init 以 netTask 身份跑:生产里它就是 net_stack_bring_up() 在 netTask 内
|
||||
* 调用的(App/task/net_task.c:92),而本层执行任务句柄(UDP 上下文守卫的判据)
|
||||
* 正是在这里捕获——这里若不换身份,整个 UDP 守卫就只能测到"自己判自己" */
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_net;
|
||||
(void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL);
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
g_peers.pending_capture_calls = 0; /* bring_up 内部的登记动作不该算进场景 */
|
||||
g_ns_error_count = 0;
|
||||
g_ns_info_count = 0;
|
||||
}
|
||||
|
||||
/* UDP 两接口的场景前置:net_sendto / net_recvfrom 有 netTask 上下文守卫,
|
||||
* 合法用法只能在 netTask 里调 —— 这里把"换身份"收进一个入口,避免每个场景各写
|
||||
* 一遍换/还原而漏掉一次(漏了就是"守卫把正常路径判成拒绝"的假故障)。 */
|
||||
static void bring_up_netctx(void)
|
||||
{
|
||||
bring_up();
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_net;
|
||||
}
|
||||
|
||||
/* 直接造一个在用的控制块(多数场景不需要走完整的 socket()/bind()) */
|
||||
static int mk_sock(int type, net_sock_state_t state)
|
||||
{
|
||||
int fd = net_socket(NET_AF_INET, type);
|
||||
|
||||
if (fd < 0) {
|
||||
return fd;
|
||||
}
|
||||
s_net_socks[fd].state = state;
|
||||
return fd;
|
||||
}
|
||||
/* ===================== 分片:纯函数 / 错误码槽 / 消息面 / 状态机 / 读写分支 ===================== */
|
||||
/* 一个目录 = 一个模块名(check_test_coverage 的发现规则),所以只有一个 runner 与一个
|
||||
* TU;分片以文件维度拆开,是为了让 1251 行口径的最大块能逐片推进、逐片可查。 */
|
||||
#include "ns_s3_msg.c"
|
||||
#include "ns_s4_state.c"
|
||||
#include "ns_s5_io.c"
|
||||
#include "ns_s6_edges.c"
|
||||
|
||||
/* = 分片 1 纯函数面与 fd 表 ===================== */
|
||||
|
||||
/* TC-NETSOCK-100 点分十进制 ↔ 32 位:**正向断言**(字节序统一修复后)
|
||||
* 全仓唯一约定:s_addr 是网络字节序整数,&s_addr 的第 0 个内存字节 == 点分第一段;
|
||||
* 小端主机上 "192.168.1.100" 的数值 = 0x6401A8C0。本组断言的落点是「互逆」与
|
||||
* 「同族可比」,不是某个中间数值——数值只是这条约定的一个投影。 */
|
||||
static void tc_100_inet_helpers(void)
|
||||
{
|
||||
/* 往返不变量的样本集:两个全零/全一边界 + 三个非回文地址(非回文才能考出顺位) */
|
||||
static const char *const k_ips[5] = {
|
||||
"0.0.0.0", "255.255.255.255", "1.1.1.1", "192.168.1.100", "10.0.0.0"
|
||||
};
|
||||
char buf[16];
|
||||
int i;
|
||||
|
||||
reset_all();
|
||||
|
||||
CHECK(net_inet_addr(NULL) == 0U, "TC-NETSOCK-100a inet_addr(NULL) returns 0 (invalid)");
|
||||
CHECK(net_inet_addr("192.168.1.100") == 0x6401A8C0U,
|
||||
"TC-NETSOCK-100b inet_addr returns network-order int (LE value 0x6401A8C0 = bytes C0 A8 01 64)");
|
||||
CHECK(net_inet_ntoa(net_inet_addr("192.168.1.100"), buf) == buf &&
|
||||
strcmp(buf, "192.168.1.100") == 0,
|
||||
"TC-NETSOCK-100c ntoa(inet_addr(s)) == s: the two are exact inverses");
|
||||
CHECK(net_inet_ntoa(0x6401A8C0U, buf) == buf && strcmp(buf, "192.168.1.100") == 0,
|
||||
"TC-NETSOCK-100d ntoa segment order is MSB-first (fixed by E13/TRAP-NET-21, unchanged here)");
|
||||
CHECK(net_inet_ntoa(0U, NULL) == NULL, "TC-NETSOCK-100e ntoa(NULL buf) returns NULL without crashing");
|
||||
CHECK(net_inet_addr("10.0.0.1") == 0x0100000AU,
|
||||
"TC-NETSOCK-100f non-palindrome address: 10.0.0.1 -> 0x0100000A (first octet in low byte)");
|
||||
|
||||
/* 正向断言 1:字符串 → 整数 → 字符串 必须回到原串(含 0.0.0.0 与 255.255.255.255) */
|
||||
for (i = 0; i < 5; i++) {
|
||||
(void)memset(buf, 0, sizeof(buf));
|
||||
if (net_inet_ntoa(net_inet_addr(k_ips[i]), buf) == NULL || strcmp(buf, k_ips[i]) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(i == 5, "TC-NETSOCK-100g round trip ntoa(inet_addr(s)) == s over 5 addrs incl. 0.0.0.0 and 255.255.255.255");
|
||||
|
||||
/* 正向断言 2:整数 → 字符串 → 整数 必须回到原值(反方向也闭合) */
|
||||
for (i = 0; i < 5; i++) {
|
||||
uint32_t v = net_inet_addr(k_ips[i]);
|
||||
|
||||
(void)memset(buf, 0, sizeof(buf));
|
||||
(void)net_inet_ntoa(v, buf);
|
||||
if (net_inet_addr(buf) != v) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(i == 5, "TC-NETSOCK-100h round trip inet_addr(ntoa(v)) == v (inverse direction closes too)");
|
||||
|
||||
/* 边界语义别改坏:255.255.255.255 是**合法地址**,不得被当失败;
|
||||
* 失败只有"参数为 NULL"一种,返回值 0 同时是 0.0.0.0 —— 这是 BSD inet_addr
|
||||
* 的既有歧义,本轮刻意不改(改它要动全部调用者的判错方式,风险大于收益)。 */
|
||||
CHECK(net_inet_addr("255.255.255.255") == 0xFFFFFFFFU,
|
||||
"TC-NETSOCK-100i 255.255.255.255 parses to a valid value, never demoted to failure");
|
||||
CHECK(net_inet_addr("0.0.0.0") == 0U,
|
||||
"TC-NETSOCK-100j 0.0.0.0 -> 0 (BSD inet_addr ambiguity, not introduced by this fix)");
|
||||
/* 段值溢出:按低 8 位截断(旧实现会把溢出位溅进相邻段,得到不可预期的混合值) */
|
||||
CHECK(net_inet_addr("300.1.1.1") == net_inet_addr("44.1.1.1"),
|
||||
"TC-NETSOCK-100k segment 300 truncates to 44 (300 and 0xFF) with no spill into neighbours");
|
||||
}
|
||||
|
||||
static void tc_101_byteorder(void)
|
||||
{
|
||||
reset_all();
|
||||
|
||||
CHECK(net_htons(0x1234U) == 0x3412U, "TC-NETSOCK-101a htons byte swap");
|
||||
CHECK(net_ntohs(net_htons(80U)) == 80U, "TC-NETSOCK-101b htons/ntohs are inverses");
|
||||
CHECK(net_htonl(0x01020304U) == 0x04030201U, "TC-NETSOCK-101c htonl byte swap");
|
||||
CHECK(net_ntohl(net_htonl(0xDEADBEEFU)) == 0xDEADBEEFU, "TC-NETSOCK-101d htonl/ntohl are inverses");
|
||||
}
|
||||
|
||||
static void tc_102_parse_ip(void)
|
||||
{
|
||||
uint8_t a[4];
|
||||
|
||||
reset_all();
|
||||
|
||||
(void)memset(a, 0xEE, sizeof(a));
|
||||
parse_ip_string(NULL, a);
|
||||
CHECK(a[0] == 0xEEU, "TC-NETSOCK-102a parse_ip_string(NULL) returns at once, target buffer untouched");
|
||||
parse_ip_string("192.168.1.100", a);
|
||||
CHECK(a[0] == 192U && a[1] == 168U && a[2] == 1U && a[3] == 100U,
|
||||
"TC-NETSOCK-102b parse_ip_string puts segment 0 at [0] (the order chip set_ip_addr wants)");
|
||||
parse_ip_string("10.20.30.40", NULL); /* 只验不崩 */
|
||||
CHECK(1, "TC-NETSOCK-102c parse_ip_string with NULL target does not crash");
|
||||
}
|
||||
|
||||
static void tc_103_alloc_and_getsock(void)
|
||||
{
|
||||
int i;
|
||||
int fd;
|
||||
net_sock_t *p;
|
||||
|
||||
reset_all();
|
||||
|
||||
CHECK(net_get_sock(-1) == NULL && net_get_sock(NET_MAX_SOCKETS) == NULL,
|
||||
"TC-NETSOCK-103a net_get_sock out of range returns NULL (incl. negative)");
|
||||
CHECK(net_get_sock(0) == NULL, "TC-NETSOCK-103b unallocated slot returns NULL");
|
||||
CHECK(net_sock_raw(3) == &s_net_socks[3],
|
||||
"TC-NETSOCK-103c net_sock_raw skips the in_use check (reconciler must see CLOSED slots)");
|
||||
CHECK(net_sock_state(0) == -1, "TC-NETSOCK-103d unused sock_state returns -1 and leaves errno alone");
|
||||
|
||||
/* 逐个占满:alloc_socket 线性扫描首个空闲槽 */
|
||||
for (i = 0; i < NET_MAX_SOCKETS; i++) {
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
if (fd != i) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(i == NET_MAX_SOCKETS, "TC-NETSOCK-103e alloc_socket hands out 0..7 in order");
|
||||
p = net_get_sock(0);
|
||||
CHECK(p != NULL && p->in_use == 1U, "TC-NETSOCK-103f allocated slot has in_use=1");
|
||||
|
||||
reset_all();
|
||||
}
|
||||
|
||||
static void tc_104_socket_no_inherit(void)
|
||||
{
|
||||
int fd;
|
||||
net_sock_t *p;
|
||||
|
||||
reset_all();
|
||||
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
CHECK(fd == 0, "TC-NETSOCK-104a net_socket returns the first free fd");
|
||||
p = &s_net_socks[fd];
|
||||
CHECK(p->ch395_sock == (uint8_t)fd, "TC-NETSOCK-104b chip socket number maps one-to-one to the descriptor");
|
||||
CHECK(p->state == NET_SOCK_STATE_CREATED && p->type == NET_SOCK_STREAM,
|
||||
"TC-NETSOCK-104c initial state CREATED with type recorded");
|
||||
CHECK(p->send_ready == 1U && p->readable == 0U,
|
||||
"TC-NETSOCK-104d initial state: send buffer seen as free, nothing readable");
|
||||
/* 三条"新 fd 不得继承"的定案,逐条钉(都是真机上静默出错的形态) */
|
||||
CHECK(p->blocking == 1U, "TC-NETSOCK-104e blocking must default to explicitly 1 (BSD semantics)");
|
||||
CHECK(p->released_abnormal == 0U, "TC-NETSOCK-104f must not inherit the previous release reason (TRAP-NET-18)");
|
||||
CHECK(p->session_dead == 0U, "TC-NETSOCK-104g must not inherit the previous session-dead level (N-32 P1)");
|
||||
CHECK(g_peers.listener_clear_calls == 1 && g_peers.pending_clear_calls == 1 &&
|
||||
g_peers.send_clear_calls == 1,
|
||||
"TC-NETSOCK-104h new fd clears listener retry and both pending tables (once each)");
|
||||
|
||||
/* 脏槽位复用:把上一条会话的残留写进去,再看新建是否清干净 */
|
||||
p->blocking = 0U;
|
||||
p->released_abnormal = 1U;
|
||||
p->session_dead = 1U;
|
||||
p->readable = 1U;
|
||||
p->send_ready = 0U;
|
||||
p->event_cb = (net_event_cb_t)0x1234;
|
||||
/* 这里刻意用 netTask 直调版(net_close_sock)而不是公开 net_close:
|
||||
* 本场景没绑消息队列,公开入口会先撞上"队列未创建"的 NET_ERR,
|
||||
* 而那属于分片 3 的断言面(TC-NETSOCK-110h)。close 的清零主路径在 net_close_locked。 */
|
||||
CHECK(net_close_sock(&s_net_socks[fd]) == 0,
|
||||
"TC-NETSOCK-104i net_close_sock succeeds (its memset is the main reset path)");
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
p = &s_net_socks[fd];
|
||||
CHECK(fd == 0 && p->blocking == 1U && p->released_abnormal == 0U &&
|
||||
p->session_dead == 0U && p->readable == 0U && p->send_ready == 1U &&
|
||||
p->event_cb == NULL,
|
||||
"TC-NETSOCK-104j reusing the slot: no leftover field may carry over");
|
||||
/* 地址族不支持 → INVAL(在 alloc 之前,不占槽) */
|
||||
CHECK(net_socket(NET_AF_INET6, NET_SOCK_STREAM) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL && s_net_socks[1].in_use == 0U,
|
||||
"TC-NETSOCK-104k non-INET address family -> INVAL and no slot taken");
|
||||
|
||||
reset_all();
|
||||
}
|
||||
|
||||
static void tc_105_bind(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
reset_all();
|
||||
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
in.sin_family = NET_AF_INET;
|
||||
in.sin_port = net_htons(2404);
|
||||
in.sin_addr.s_addr = net_inet_addr("192.168.1.100");
|
||||
|
||||
CHECK(net_bind(-1, (struct net_sockaddr *)&in, sizeof(in)) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-105a out of range fd -> BADF");
|
||||
CHECK(net_bind(0, (struct net_sockaddr *)&in, sizeof(in)) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-105b bind before socket -> BADF");
|
||||
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
CHECK(net_bind(fd, NULL, sizeof(in)) == -1 && net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-105c addr NULL -> INVAL");
|
||||
CHECK(net_bind(fd, (struct net_sockaddr *)&in, 1) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-105d addrlen too small -> INVAL");
|
||||
CHECK(net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)) == 0,
|
||||
"TC-NETSOCK-105e TCP bind succeeds");
|
||||
CHECK(s_net_socks[fd].local_port == 2404U &&
|
||||
s_net_socks[fd].state == NET_SOCK_STATE_BOUND,
|
||||
"TC-NETSOCK-105f TCP bind only stores host-order port and goes BOUND, no chip touch");
|
||||
CHECK(g_chip.set_port_calls == 0 && g_chip.open_calls == 0,
|
||||
"TC-NETSOCK-105g bind must not set source port or open the chip socket (open belongs to listen)");
|
||||
|
||||
/* port=0 → 动态端口 49152 起,并推进 */
|
||||
reset_all();
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
in.sin_port = 0U;
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
rc = net_bind(fd, (struct net_sockaddr *)&in, sizeof(in));
|
||||
CHECK(rc == 0 && s_net_socks[fd].local_port == 49152U,
|
||||
"TC-NETSOCK-105h port=0 allocates 49152");
|
||||
CHECK(s_dynamic_port == 49153U,
|
||||
"TC-NETSOCK-105h2 counter already advanced to 49153 (advance on allocate, never reuse)");
|
||||
(void)net_socket(NET_AF_INET, NET_SOCK_STREAM); /* 只创建不 bind */
|
||||
CHECK(s_dynamic_port == 49153U,
|
||||
"TC-NETSOCK-105i3 creating a socket does NOT consume a dynamic port (only bind does)");
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
rc = net_bind(fd, (struct net_sockaddr *)&in, sizeof(in));
|
||||
CHECK(rc == 0 && s_net_socks[fd].local_port == 49153U && s_dynamic_port == 49154U,
|
||||
"TC-NETSOCK-105i second bind gets the next port (same port never issued twice)");
|
||||
|
||||
/* 回绕:把计数器压到 65535,分配后应回 49152(不是 0——0 会让芯片按"未绑定"处理) */
|
||||
s_dynamic_port = 65535U;
|
||||
reset_all();
|
||||
s_dynamic_port = 65535U;
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
rc = net_bind(fd, (struct net_sockaddr *)&in, sizeof(in));
|
||||
CHECK(rc == 0 && s_net_socks[fd].local_port == 65535U && s_dynamic_port == 49152U,
|
||||
"TC-NETSOCK-105j dynamic port wraps from 65535 back to 49152 (never to 0)");
|
||||
}
|
||||
|
||||
static void tc_106_bind_udp(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
int fd;
|
||||
|
||||
reset_all();
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
in.sin_family = NET_AF_INET;
|
||||
in.sin_port = net_htons(8888);
|
||||
|
||||
/* UDP:bind 就直接开芯片侧(无监听态),且必须配 DesIP=255.255.255.255 收任意源 */
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
CHECK(net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)) == 0, "TC-NETSOCK-106a UDP bind succeeds");
|
||||
CHECK(s_net_socks[fd].state == NET_SOCK_STATE_UDP_OPEN, "TC-NETSOCK-106b UDP goes straight to UDP_OPEN");
|
||||
CHECK(g_chip.set_proto_calls == 1 && g_chip.last_proto == CH395F_PROTO_TYPE_UDP,
|
||||
"TC-NETSOCK-106c protocol set to UDP (not TCP)");
|
||||
CHECK(g_chip.set_port_calls == 1 && g_chip.last_port == 8888U,
|
||||
"TC-NETSOCK-106d source port delivered with bind");
|
||||
CHECK(g_fx.des_ip[0][0] == 0xFFU && g_fx.des_ip[0][1] == 0xFFU &&
|
||||
g_fx.des_ip[0][2] == 0xFFU && g_fx.des_ip[0][3] == 0xFFU &&
|
||||
g_fx.des_port[0] == 0U,
|
||||
"TC-NETSOCK-106e UDP server mode: DesIP all FF and dest port 0 (else only one peer is received)");
|
||||
CHECK(g_chip.open_calls == 1, "TC-NETSOCK-106f UDP bind opens the chip socket once");
|
||||
|
||||
/* 槽位 >=4 必须补设收发缓冲(net_init 已配过,这里是兜底):布局 6 块/槽 */
|
||||
reset_all();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
CHECK(fd == 0, "TC-NETSOCK-106g take fd0 first (control case: no buffer reconfig)");
|
||||
CHECK(g_fxchip.set_recv_buf_calls == 0, "TC-NETSOCK-106h UDP bind on fd<4 does not reconfigure buffers");
|
||||
|
||||
reset_all();
|
||||
for (fd = 0; fd < 4; fd++) {
|
||||
(void)net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
}
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); /* fd4 */
|
||||
(void)net_bind(fd, (struct net_sockaddr *)&in, sizeof(in));
|
||||
CHECK(g_fx.recv_blk_start[4] == (uint8_t)(4 * 6) &&
|
||||
g_fx.recv_blk_cnt[4] == (uint8_t)CH395F_RECV_BLOCKS &&
|
||||
g_fx.send_blk_start[4] == (uint8_t)(4 * 6 + (int)CH395F_RECV_BLOCKS) &&
|
||||
g_fx.send_blk_cnt[4] == (uint8_t)CH395F_SEND_BLOCKS,
|
||||
"TC-NETSOCK-106i fd>=4 falls back to reconfiguring rx/tx buffers, same 6-block layout as net_init");
|
||||
|
||||
/* open 失败 → BUSY 且留在原状态(不得谎报 UDP_OPEN) */
|
||||
reset_all();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
g_fake.open_rc[fd] = CH395F_ERR_USE;
|
||||
CHECK(net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)) == -1 &&
|
||||
net_get_errno() == NET_ERR_BUSY &&
|
||||
s_net_socks[fd].state != NET_SOCK_STATE_UDP_OPEN &&
|
||||
g_ns_error_count == 1,
|
||||
"TC-NETSOCK-106j UDP open failure -> BUSY plus error log and state does not advance");
|
||||
}
|
||||
|
||||
static void tc_107_peer_and_localname(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
struct net_sockaddr *sa = (struct net_sockaddr *)∈
|
||||
int addrlen;
|
||||
int fd;
|
||||
|
||||
reset_all();
|
||||
(void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL);
|
||||
|
||||
CHECK(net_getsockname(-1, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-107a getsockname out of range -> BADF");
|
||||
CHECK(net_getsockname(0, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-107b unused CLOSED slot -> BADF");
|
||||
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
in.sin_port = net_htons(7003);
|
||||
CHECK(net_bind(fd, sa, (int)sizeof(in)) == 0, "TC-NETSOCK-107c bind precondition ok");
|
||||
addrlen = (int)sizeof(in);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
CHECK(net_getsockname(fd, sa, &addrlen) == 0, "TC-NETSOCK-107d getsockname succeeds");
|
||||
CHECK(in.sin_family == NET_AF_INET && net_ntohs(in.sin_port) == 7003U &&
|
||||
addrlen == (int)sizeof(in),
|
||||
"TC-NETSOCK-107e fills family, port (network order) and length");
|
||||
/* 钉**具体顺位**而不是二选一:getsockname 走的是「首段在最低字节」这一族
|
||||
* (与 net_inet_ntoa、net_events 的 remote_ip 同族),所以 192.168.1.100 =
|
||||
* 0x6401A8C0。net_inet_addr 返回的是另一族(首段在最高字节,0xC0A80164),
|
||||
* 两族不互逆——已作为疑似缺陷报出,这里刻意不写成「两个都行」。 */
|
||||
CHECK(in.sin_addr.s_addr == 0x6401A8C0U,
|
||||
"TC-NETSOCK-107f local IP byte order = first octet in low byte (ntoa family)");
|
||||
|
||||
addrlen = 1;
|
||||
CHECK(net_getsockname(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-107g length too small -> INVAL");
|
||||
CHECK(net_getsockname(fd, sa, NULL) == -1 && net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-107h addrlen NULL -> INVAL");
|
||||
|
||||
/* getpeername:没有 CONNECT 回读过对端 → NOTCONN(不发 SPI 是它的卖点) */
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
addrlen = (int)sizeof(in);
|
||||
CHECK(net_getpeername(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_NOTCONN,
|
||||
"TC-NETSOCK-107i no peer yet -> NOTCONN (judged by remote_port==0)");
|
||||
CHECK(g_chip.get_status_calls == 0 && g_fxchip.glob_calls == 0,
|
||||
"TC-NETSOCK-107j getpeername only reads the cache, issues no chip transactions");
|
||||
|
||||
s_net_socks[fd].remote_port = 51999U;
|
||||
s_net_socks[fd].remote_ip = 0x0B01A8C0U;
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
addrlen = (int)sizeof(in);
|
||||
CHECK(net_getpeername(fd, sa, &addrlen) == 0 &&
|
||||
net_ntohs(in.sin_port) == 51999U && in.sin_addr.s_addr == 0x0B01A8C0U &&
|
||||
in.sin_family == NET_AF_INET,
|
||||
"TC-NETSOCK-107k getpeername fills the cached peer IP and port");
|
||||
CHECK(net_getpeername(-1, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-107l getpeername out of range -> BADF");
|
||||
CHECK(net_getpeername(5, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-107m getpeername on unused fd -> BADF");
|
||||
addrlen = 2;
|
||||
CHECK(net_getpeername(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-107n getpeername length too small -> INVAL");
|
||||
|
||||
reset_all();
|
||||
}
|
||||
|
||||
static void tc_108_sockprops(void)
|
||||
{
|
||||
int fd;
|
||||
int arg = 7;
|
||||
|
||||
reset_all();
|
||||
CHECK(net_setblocking(-1, 1) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-108a setblocking out of range -> BADF");
|
||||
CHECK(net_setblocking(0, 1) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-108b not in use -> BADF");
|
||||
CHECK(net_set_event_cb(-1, NULL, NULL) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-108c set_event_cb out of range -> BADF");
|
||||
CHECK(net_set_event_cb(0, NULL, NULL) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-108d set_event_cb not in use -> BADF");
|
||||
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
CHECK(net_setblocking(fd, 0) == 0 && s_net_socks[fd].blocking == 0U,
|
||||
"TC-NETSOCK-108e setblocking(0) sets the attribute (mode lives in the control block, not per call)");
|
||||
CHECK(net_setblocking(fd, 5) == 0 && s_net_socks[fd].blocking == 1U,
|
||||
"TC-NETSOCK-108f any non-zero value counts as blocking");
|
||||
CHECK(net_set_event_cb(fd, (net_event_cb_t)0x1234, &arg) == 0 &&
|
||||
s_net_socks[fd].event_cb == (net_event_cb_t)0x1234 &&
|
||||
s_net_socks[fd].event_arg == &arg,
|
||||
"TC-NETSOCK-108g registers callback and user argument");
|
||||
CHECK(net_set_event_cb(fd, NULL, NULL) == 0 && s_net_socks[fd].event_cb == NULL,
|
||||
"TC-NETSOCK-108h cb=NULL unregisters");
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_ESTABLISHED;
|
||||
CHECK(net_sock_state(fd) == (int)NET_SOCK_STATE_ESTABLISHED,
|
||||
"TC-NETSOCK-108i sock_state returns the state snapshot");
|
||||
CHECK(net_sock_state(NET_MAX_SOCKETS) == -1, "TC-NETSOCK-108j sock_state out of range -> -1");
|
||||
s_net_socks[fd].in_use = 0U;
|
||||
CHECK(net_sock_state(fd) == -1, "TC-NETSOCK-108k sock_state on unused fd -> -1");
|
||||
|
||||
reset_all();
|
||||
}
|
||||
|
||||
static void tc_109_generation(void)
|
||||
{
|
||||
reset_all();
|
||||
CHECK(net_get_generation() == 0U, "TC-NETSOCK-109a initial value 0");
|
||||
net_generation_inc();
|
||||
net_generation_inc();
|
||||
CHECK(net_get_generation() == 2U, "TC-NETSOCK-109b each network rebuild adds 1 (services rebuild listeners from it)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
static void tc_110_queue_adopt(void)
|
||||
{
|
||||
static int q1;
|
||||
static int q2;
|
||||
|
||||
reset_all();
|
||||
CHECK(net_msg_queue_adopt(NULL) == -1, "TC-NETSOCK-110a NULL handle -> -1");
|
||||
CHECK(net_msg_queue_adopt((osMessageQueueId_t)&q1) == 0, "TC-NETSOCK-110b first binding succeeds");
|
||||
CHECK(net_msg_queue_adopt((osMessageQueueId_t)&q2) == -1,
|
||||
"TC-NETSOCK-110c rebinding must be refused (no silent handle swap)");
|
||||
CHECK(s_net_msg_queue == (osMessageQueueId_t)&q1,
|
||||
"TC-NETSOCK-110d rebinding keeps the FIRST handle - swapping would strand queued requests forever");
|
||||
/* 未绑定时所有公开 API 一律 NET_ERR,而不是崩在空句柄上 */
|
||||
reset_all();
|
||||
CHECK(net_listen(0) == -1 && net_get_errno() == NET_ERR,
|
||||
"TC-NETSOCK-110e without a bound queue: net_listen -> NET_ERR");
|
||||
CHECK(net_recv(0, NULL, 0, 0) == -1 && net_get_errno() == NET_ERR,
|
||||
"TC-NETSOCK-110f without a bound queue: net_recv -> NET_ERR (range check precedes queue check)");
|
||||
CHECK(net_send(0, "x", 1, 0) == -1 && net_get_errno() == NET_ERR,
|
||||
"TC-NETSOCK-110g without a bound queue: net_send -> NET_ERR");
|
||||
CHECK(net_close(0) == -1 && net_get_errno() == NET_ERR,
|
||||
"TC-NETSOCK-110h without a bound queue: net_close -> NET_ERR");
|
||||
CHECK(net_drop_conn(0) == -1 && net_get_errno() == NET_ERR,
|
||||
"TC-NETSOCK-110i without a bound queue: net_drop_conn -> NET_ERR");
|
||||
CHECK(net_recv_to(0, NULL, 0, 10U) == -1 && net_get_errno() == NET_ERR,
|
||||
"TC-NETSOCK-110j without a bound queue: net_recv_to -> NET_ERR");
|
||||
CHECK(net_accept(0, NULL, NULL) == -1 && net_get_errno() == NET_ERR,
|
||||
"TC-NETSOCK-110k without a bound queue: net_accept -> NET_ERR");
|
||||
CHECK(net_connect(0, NULL, 0) == -1 && net_get_errno() == NET_ERR,
|
||||
"TC-NETSOCK-110l without a bound queue: net_connect -> NET_ERR");
|
||||
/* 越界优先于"未初始化":调用方拿到的码要能区分"我传错了"和"网还没起来" */
|
||||
CHECK(net_send(NET_MAX_SOCKETS, "x", 1, 0) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-110m range check before queue check -> BADF");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* ===================== 分片 2 每任务错误码槽(E-03) ===================== */
|
||||
|
||||
static void tc_120_errno_slot_per_task(void)
|
||||
{
|
||||
reset_all();
|
||||
|
||||
/* 内核没跑起来:不做槽位分配,一律退到 [0] 槽(中断/内核前上下文) */
|
||||
g_ns_kernel_state = osKernelInactive;
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)net_set_errno(NET_ERR_BADF);
|
||||
CHECK(net_get_errno() == NET_ERR_BADF && s_net_errno_slots[0].owner == NULL,
|
||||
"TC-NETSOCK-120a kernel not running -> uses slot [0] and takes no slot (owner not written)");
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_net;
|
||||
CHECK(net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-120b with the kernel down all tasks share slot [0] (degraded semantics)");
|
||||
|
||||
/* 内核在跑:每个任务自己的槽位 */
|
||||
g_ns_kernel_state = osKernelRunning;
|
||||
reset_all();
|
||||
g_ns_kernel_state = osKernelRunning;
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)net_set_errno(NET_ERR_TIMEDOUT);
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_net;
|
||||
CHECK(net_get_errno() == NET_OK,
|
||||
"TC-NETSOCK-120c another task cannot read the caller code (the positive wording of E-03)");
|
||||
(void)net_set_errno(NET_ERR_WOULDBLOCK);
|
||||
CHECK(net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-120d a task reads its own code");
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
CHECK(net_get_errno() == NET_ERR_TIMEDOUT,
|
||||
"TC-NETSOCK-120e no cross overwrite: caller code survives the netTask write (old red line)");
|
||||
/* 临界区:两个任务各占一次槽 → 两次进入、两次退出,绝不泄漏嵌套计数 */
|
||||
CHECK(g_ns_crit_enter == 2 && g_ns_crit_exit == 2 && ns_os_crit_depth() == 0,
|
||||
"TC-NETSOCK-120f each first slot take enters the critical section once and leaves in pairs");
|
||||
|
||||
/* 快路径只读不写:已有槽位的任务反复读写都不该再进临界区、也不该再占槽 */
|
||||
{
|
||||
int e0 = g_ns_crit_enter;
|
||||
int owners0 = ns_slot_owner_count();
|
||||
int i_caller = ns_slot_index_of((TaskHandle_t)&g_ns_task_caller);
|
||||
int i_net = ns_slot_index_of((TaskHandle_t)&g_ns_task_net);
|
||||
|
||||
CHECK(i_caller >= 0 && i_net >= 0 && i_caller != i_net,
|
||||
"TC-NETSOCK-120g two tasks land in DIFFERENT slots (table is inspectable, not convention)");
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)net_set_errno(NET_ERR_BUSY);
|
||||
(void)net_get_errno();
|
||||
(void)net_set_errno(NET_ERR_CONNRESET);
|
||||
CHECK(g_ns_crit_enter == e0, "TC-NETSOCK-120h existing slot hit: zero critical sections (fast path is read-only)");
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_net;
|
||||
CHECK(net_get_errno() == NET_ERR_WOULDBLOCK &&
|
||||
ns_slot_owner_count() == owners0,
|
||||
"TC-NETSOCK-120i repeated reads by another task add no slot and never see the caller code");
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
CHECK(net_get_errno() == NET_ERR_CONNRESET, "TC-NETSOCK-120j caller reads its own latest code");
|
||||
}
|
||||
|
||||
/* 槽位用尽 → 回退 [0](刻意不重试,避免把竞态带回来) */
|
||||
reset_all();
|
||||
g_ns_kernel_state = osKernelRunning;
|
||||
{
|
||||
int i;
|
||||
static int filler[10];
|
||||
for (i = 0; i < (int)NET_ERRNO_TASK_SLOTS; i++) {
|
||||
g_ns_cur_task = (TaskHandle_t)&filler[i];
|
||||
(void)net_set_errno(NET_ERR_ISCONN);
|
||||
}
|
||||
g_ns_cur_task = (TaskHandle_t)&filler[0];
|
||||
CHECK(net_get_errno() == NET_ERR_ISCONN, "TC-NETSOCK-120k 8 slots, one owner each");
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; /* 第 9 个任务 */
|
||||
(void)net_set_errno(NET_ERR_NOMEM);
|
||||
CHECK(net_get_errno() == NET_ERR_NOMEM,
|
||||
"TC-NETSOCK-120l slots exhausted: a new task still reads its own code from slot [0]");
|
||||
g_ns_cur_task = (TaskHandle_t)&filler[0];
|
||||
CHECK(net_get_errno() == NET_ERR_NOMEM,
|
||||
"TC-NETSOCK-120m after exhaustion it degrades to the shared slot [0] (only hurts debuggability)");
|
||||
}
|
||||
reset_all();
|
||||
}
|
||||
|
||||
static void tc_121_errno_slot_isr(void)
|
||||
{
|
||||
reset_all();
|
||||
g_ns_kernel_state = osKernelRunning;
|
||||
g_ns_cur_task = NULL; /* 中断上下文/无句柄 */
|
||||
(void)net_set_errno(NET_ERR_CONNRESET);
|
||||
CHECK(net_get_errno() == NET_ERR_CONNRESET && s_net_errno_slots[0].owner == NULL,
|
||||
"TC-NETSOCK-121 NULL handle (ISR context) -> falls back to slot [0] and leaves the table alone");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* ===================== 主入口 ===================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== NET Socket Tests (net_socket.c host unit tests) ===\n");
|
||||
|
||||
printf("[shard1] pure functions and fd table\n");
|
||||
tc_100_inet_helpers();
|
||||
tc_101_byteorder();
|
||||
tc_102_parse_ip();
|
||||
tc_103_alloc_and_getsock();
|
||||
tc_104_socket_no_inherit();
|
||||
tc_105_bind();
|
||||
tc_106_bind_udp();
|
||||
tc_107_peer_and_localname();
|
||||
tc_108_sockprops();
|
||||
tc_109_generation();
|
||||
tc_110_queue_adopt();
|
||||
|
||||
printf("[shard2] per-task errno slots and queue binding\n");
|
||||
tc_120_errno_slot_per_task();
|
||||
tc_121_errno_slot_isr();
|
||||
|
||||
printf("[shard3] cross-context messages and notify-value return\n");
|
||||
ns_run_s3();
|
||||
|
||||
printf("[shard4] state machine and gates\n");
|
||||
ns_run_s4();
|
||||
|
||||
printf("[shard5] stream setup and read/write branches\n");
|
||||
ns_run_s5();
|
||||
|
||||
printf("[shard6] edge branches and remaining exits\n");
|
||||
ns_run_s6();
|
||||
|
||||
printf("=== NET Socket Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
* 模块名称:NET 层兄弟模块桩(net_socket 主机侧单测专用)
|
||||
* 模块功能:net_socket.c 依赖的三个兄弟模块(net_listener / net_pending / net_events)
|
||||
* 在这里**按边界契约打桩**,让本套单测只测 net_socket.c 自己那部分决策。
|
||||
*
|
||||
* ⚠ 仅供 test/host/net_socket/ 使用,不参与 MCU 构建。
|
||||
*
|
||||
* 为什么打桩而不是 #include 真实源(而不是"越真越好"):
|
||||
* 1. 那三个模块**已各有自己的主机单测**(net_listener 34 条、net_events 34 条、
|
||||
* net_pending 43 条),本套再跑一遍不增加任何对 net_socket.c 的判别力,
|
||||
* 只会把"谁的红"变成糊账;
|
||||
* 2. 本套真正要钉的是**边界契约**——net_socket.c 在什么条件下把什么 reason 登记给
|
||||
* 监听层、什么条件下把挂起请求登记给 pending 层、以及"登记了就不能再自己发回执"。
|
||||
* 桩把这些变成可断言的记录,比让真实 tick 在背后默默跑更有判别力;
|
||||
* 3. 原型一律取**真实头**(net_listener.h / net_pending.h / net_events.h),
|
||||
* 兄弟模块改签名 → 本套编译期就红。
|
||||
*
|
||||
* 桩的取值纪律(不自己发明语义,全部按真实头的函数说明):
|
||||
* - net_pending_register / net_send_pending_register 返回 NET_PENDING_REGISTERED,
|
||||
* 与真实实现一致(登记成功=哨兵,供分派跳过即时通知);可注入 -1 测登记被拒;
|
||||
* - net_pending_tick 支持"下一轮把脚本里的结果当作数据到达,通知登记时的 caller",
|
||||
* 复刻真实 tick 的"数据到达即通知"契约;
|
||||
* - 其余全为纯记录(次数 + 最近一次入参),供"不该登记/必须登记"的断言。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "net_listener.h"
|
||||
#include "net_pending.h"
|
||||
#include "net_events.h"
|
||||
#include "net_socket.h"
|
||||
#include "ns_peers.h"
|
||||
#include "ns_rtos.h"
|
||||
|
||||
ns_peers_obs_t g_peers;
|
||||
|
||||
/* 桩内部"完成/取消"要发通知:转给假内核,这样测试能断言通知次数与通知值 */
|
||||
void ns_peers_note_notify(TaskHandle_t to, uint32_t v)
|
||||
{
|
||||
ns_rtos_post_notify(to, v);
|
||||
}
|
||||
|
||||
/* 挂起登记簿(只记一条,够本套断言用) */
|
||||
static int s_pend_active[NET_MAX_SOCKETS];
|
||||
static void *s_pend_buf[NET_MAX_SOCKETS];
|
||||
static int s_pend_len[NET_MAX_SOCKETS];
|
||||
static TaskHandle_t s_pend_caller[NET_MAX_SOCKETS];
|
||||
static int s_snd_active[NET_MAX_SOCKETS];
|
||||
static TaskHandle_t s_snd_caller[NET_MAX_SOCKETS];
|
||||
|
||||
/* tick 完成脚本:fd -> 结果值(一次生效) */
|
||||
static int s_pend_done_armed[NET_MAX_SOCKETS];
|
||||
static int s_pend_done_val[NET_MAX_SOCKETS];
|
||||
static int s_snd_done_armed[NET_MAX_SOCKETS];
|
||||
static int s_snd_done_val[NET_MAX_SOCKETS];
|
||||
|
||||
void ns_peers_reset(void)
|
||||
{
|
||||
(void)memset(&g_peers, 0, sizeof(g_peers));
|
||||
(void)memset(s_pend_active, 0, sizeof(s_pend_active));
|
||||
(void)memset(s_pend_buf, 0, sizeof(s_pend_buf));
|
||||
(void)memset(s_pend_len, 0, sizeof(s_pend_len));
|
||||
(void)memset(s_pend_caller, 0, sizeof(s_pend_caller));
|
||||
(void)memset(s_snd_active, 0, sizeof(s_snd_active));
|
||||
(void)memset(s_snd_caller, 0, sizeof(s_snd_caller));
|
||||
(void)memset(s_pend_done_armed, 0, sizeof(s_pend_done_armed));
|
||||
(void)memset(s_pend_done_val, 0, sizeof(s_pend_done_val));
|
||||
(void)memset(s_snd_done_armed, 0, sizeof(s_snd_done_armed));
|
||||
(void)memset(s_snd_done_val, 0, sizeof(s_snd_done_val));
|
||||
g_peers.pend_register_rc = NET_PENDING_REGISTERED;
|
||||
g_peers.send_register_rc = NET_PENDING_REGISTERED;
|
||||
}
|
||||
|
||||
void ns_peers_arm_pending_done(int sockfd, int result)
|
||||
{
|
||||
if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) {
|
||||
s_pend_done_armed[sockfd] = 1;
|
||||
s_pend_done_val[sockfd] = result;
|
||||
}
|
||||
}
|
||||
|
||||
void ns_peers_arm_send_pending_done(int sockfd, int result)
|
||||
{
|
||||
if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) {
|
||||
s_snd_done_armed[sockfd] = 1;
|
||||
s_snd_done_val[sockfd] = result;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= net_listener.h ================= */
|
||||
|
||||
void net_listener_request(int sockfd, net_listener_req_t reason)
|
||||
{
|
||||
g_peers.listener_req_calls++;
|
||||
g_peers.last_req_fd = sockfd;
|
||||
g_peers.last_req_reason = (int)reason;
|
||||
if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) {
|
||||
g_peers.req_armed[sockfd] = 1;
|
||||
g_peers.req_reason[sockfd] = (int)reason;
|
||||
}
|
||||
}
|
||||
|
||||
void net_listener_clear(int sockfd)
|
||||
{
|
||||
g_peers.listener_clear_calls++;
|
||||
g_peers.last_clear_fd = sockfd;
|
||||
if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) {
|
||||
g_peers.req_armed[sockfd] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void net_listener_tick(void)
|
||||
{
|
||||
g_peers.listener_tick_calls++;
|
||||
}
|
||||
|
||||
void net_listener_settle(int sockfd, net_listener_settle_t level)
|
||||
{
|
||||
g_peers.listener_settle_calls++;
|
||||
g_peers.last_settle_fd = sockfd;
|
||||
g_peers.last_settle_level = (int)level;
|
||||
}
|
||||
|
||||
/* ================= net_pending.h(recv 侧) ================= */
|
||||
|
||||
void net_pending_capture_owner(void)
|
||||
{
|
||||
g_peers.pending_capture_calls++;
|
||||
}
|
||||
|
||||
int net_pending_register(int sockfd, void *buf, int len, uint32_t to_ms,
|
||||
TaskHandle_t caller)
|
||||
{
|
||||
g_peers.pending_reg_calls++;
|
||||
g_peers.last_pend_fd = sockfd;
|
||||
g_peers.last_pend_to = to_ms;
|
||||
g_peers.last_pend_caller = caller;
|
||||
if (g_peers.pend_register_rc != NET_PENDING_REGISTERED) {
|
||||
return g_peers.pend_register_rc; /* 测"登记被拒"分支 */
|
||||
}
|
||||
if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) {
|
||||
s_pend_active[sockfd] = 1;
|
||||
s_pend_buf[sockfd] = buf;
|
||||
s_pend_len[sockfd] = len;
|
||||
s_pend_caller[sockfd] = caller;
|
||||
g_peers.pend_active_now[sockfd] = 1;
|
||||
}
|
||||
return NET_PENDING_REGISTERED;
|
||||
}
|
||||
|
||||
void net_pending_tick(net_sock_t *p_socks)
|
||||
{
|
||||
int i;
|
||||
|
||||
g_peers.pending_tick_calls++;
|
||||
(void)p_socks;
|
||||
/* 复刻真实 tick 的主用例:数据到达 → 按登记时的 caller 通知 → 清槽 */
|
||||
for (i = 0; i < NET_MAX_SOCKETS; i++) {
|
||||
if ((s_pend_active[i] != 0) && (s_pend_done_armed[i] != 0)) {
|
||||
s_pend_done_armed[i] = 0;
|
||||
s_pend_active[i] = 0;
|
||||
g_peers.pend_active_now[i] = 0;
|
||||
g_peers.pending_notified++;
|
||||
ns_peers_note_notify(s_pend_caller[i], (uint32_t)s_pend_done_val[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void net_pending_clear(int sockfd)
|
||||
{
|
||||
g_peers.pending_clear_calls++;
|
||||
g_peers.last_pend_clear_fd = sockfd;
|
||||
if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) {
|
||||
s_pend_active[sockfd] = 0;
|
||||
g_peers.pend_active_now[sockfd] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void net_pending_cancel(int sockfd, int err)
|
||||
{
|
||||
g_peers.pending_cancel_calls++;
|
||||
g_peers.last_pend_cancel_err = err;
|
||||
if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) {
|
||||
s_pend_active[sockfd] = 0;
|
||||
g_peers.pend_active_now[sockfd] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void net_pending_cancel_all(int err)
|
||||
{
|
||||
int i;
|
||||
|
||||
g_peers.pending_cancel_all_calls++;
|
||||
g_peers.last_pend_cancel_err = err;
|
||||
for (i = 0; i < NET_MAX_SOCKETS; i++) {
|
||||
if (s_pend_active[i] != 0) {
|
||||
g_peers.pending_notified++;
|
||||
ns_peers_note_notify(s_pend_caller[i], (uint32_t)err);
|
||||
}
|
||||
s_pend_active[i] = 0;
|
||||
g_peers.pend_active_now[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void net_pending_settle_close(int sockfd, net_sock_t *p_sock)
|
||||
{
|
||||
g_peers.pending_settle_calls++;
|
||||
g_peers.last_pend_fd = sockfd;
|
||||
(void)p_sock;
|
||||
if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS) && (s_pend_active[sockfd] != 0)) {
|
||||
/* 真实实现:有数据先送达,无数据按 CONNRESET 取消——两者都**必通知** */
|
||||
s_pend_active[sockfd] = 0;
|
||||
g_peers.pend_active_now[sockfd] = 0;
|
||||
g_peers.pending_notified++;
|
||||
ns_peers_note_notify(s_pend_caller[sockfd], (uint32_t)NET_ERR_CONNRESET);
|
||||
}
|
||||
}
|
||||
|
||||
int net_pending_active(int sockfd)
|
||||
{
|
||||
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
|
||||
return 0;
|
||||
}
|
||||
return s_pend_active[sockfd];
|
||||
}
|
||||
|
||||
/* ================= net_pending.h(send 侧) ================= */
|
||||
|
||||
int net_send_pending_register(int sockfd, const void *buf, int len, int sent,
|
||||
TaskHandle_t caller)
|
||||
{
|
||||
g_peers.send_reg_calls++;
|
||||
g_peers.last_snd_fd = sockfd;
|
||||
g_peers.last_snd_sent = sent;
|
||||
g_peers.last_snd_len = len;
|
||||
(void)buf;
|
||||
if (g_peers.send_register_rc != NET_PENDING_REGISTERED) {
|
||||
return g_peers.send_register_rc;
|
||||
}
|
||||
/* 真实实现的硬前置:sent >= len 视为参数非法拒收(末块边界) */
|
||||
if (sent >= len) {
|
||||
return -1;
|
||||
}
|
||||
if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) {
|
||||
s_snd_active[sockfd] = 1;
|
||||
s_snd_caller[sockfd] = caller;
|
||||
}
|
||||
return NET_PENDING_REGISTERED;
|
||||
}
|
||||
|
||||
void net_send_pending_tick(net_sock_t *p_socks)
|
||||
{
|
||||
int i;
|
||||
|
||||
g_peers.send_tick_calls++;
|
||||
(void)p_socks;
|
||||
for (i = 0; i < NET_MAX_SOCKETS; i++) {
|
||||
if ((s_snd_active[i] != 0) && (s_snd_done_armed[i] != 0)) {
|
||||
s_snd_done_armed[i] = 0;
|
||||
s_snd_active[i] = 0;
|
||||
g_peers.send_notified++;
|
||||
ns_peers_note_notify(s_snd_caller[i], (uint32_t)s_snd_done_val[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void net_send_pending_clear(int sockfd)
|
||||
{
|
||||
g_peers.send_clear_calls++;
|
||||
if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) {
|
||||
s_snd_active[sockfd] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void net_send_pending_cancel(int sockfd, int err)
|
||||
{
|
||||
g_peers.send_cancel_calls++;
|
||||
g_peers.last_snd_cancel_err = err;
|
||||
if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS) && (s_snd_active[sockfd] != 0)) {
|
||||
s_snd_active[sockfd] = 0;
|
||||
g_peers.send_notified++;
|
||||
ns_peers_note_notify(s_snd_caller[sockfd], (uint32_t)err);
|
||||
}
|
||||
}
|
||||
|
||||
void net_send_pending_cancel_all(int err)
|
||||
{
|
||||
int i;
|
||||
|
||||
g_peers.send_cancel_all_calls++;
|
||||
g_peers.last_snd_cancel_err = err;
|
||||
for (i = 0; i < NET_MAX_SOCKETS; i++) {
|
||||
if (s_snd_active[i] != 0) {
|
||||
g_peers.send_notified++;
|
||||
ns_peers_note_notify(s_snd_caller[i], (uint32_t)err);
|
||||
}
|
||||
s_snd_active[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int net_send_pending_active(int sockfd)
|
||||
{
|
||||
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
|
||||
return 0;
|
||||
}
|
||||
return s_snd_active[sockfd];
|
||||
}
|
||||
|
||||
/* ================= net_events.h(事件面与对账) ================= */
|
||||
|
||||
/* 事件处理桩:按真实头注释的效果改控制块(CONNECT→ESTABLISHED + 记对端等),
|
||||
* 让 net_poll 的"中断 → 状态机"分派可断言;同时计数。 */
|
||||
void handle_connect_event(net_sock_t *p_sock)
|
||||
{
|
||||
g_peers.evt_connect_calls++;
|
||||
if (p_sock != NULL) {
|
||||
p_sock->state = NET_SOCK_STATE_ESTABLISHED;
|
||||
p_sock->readable = 0U;
|
||||
p_sock->released_abnormal = 0U;
|
||||
p_sock->session_dead = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
void handle_disconnect_event(net_sock_t *p_sock)
|
||||
{
|
||||
g_peers.evt_disconnect_calls++;
|
||||
if (p_sock != NULL) {
|
||||
p_sock->last_peer_port = p_sock->remote_port;
|
||||
p_sock->state = NET_SOCK_STATE_CLOSED;
|
||||
p_sock->session_dead = 1U;
|
||||
}
|
||||
}
|
||||
|
||||
void handle_recv_event(net_sock_t *p_sock)
|
||||
{
|
||||
g_peers.evt_recv_calls++;
|
||||
if (p_sock != NULL) {
|
||||
p_sock->readable = 1U;
|
||||
}
|
||||
}
|
||||
|
||||
void handle_send_event(net_sock_t *p_sock)
|
||||
{
|
||||
g_peers.evt_send_calls++;
|
||||
if (p_sock != NULL) {
|
||||
p_sock->send_ready = 1U;
|
||||
}
|
||||
}
|
||||
|
||||
void handle_timeout_event(net_sock_t *p_sock)
|
||||
{
|
||||
g_peers.evt_timeout_calls++;
|
||||
if (p_sock != NULL) {
|
||||
p_sock->last_peer_port = p_sock->remote_port;
|
||||
p_sock->state = NET_SOCK_STATE_CLOSED;
|
||||
p_sock->released_abnormal = 1U;
|
||||
p_sock->session_dead = 1U;
|
||||
}
|
||||
}
|
||||
|
||||
void net_reconcile_tick(void)
|
||||
{
|
||||
g_peers.reconcile_calls++;
|
||||
}
|
||||
|
||||
void net_lost_connect_probe(net_sock_t *p_sock)
|
||||
{
|
||||
g_peers.lost_probe_calls++;
|
||||
g_peers.last_probe_state = (p_sock != NULL) ? (int)p_sock->state : -1;
|
||||
if ((p_sock != NULL) && (g_peers.lost_probe_hit != 0)) {
|
||||
/* 脚本:探针命中 → 直接转 ESTABLISHED(复刻补 accept 的净效果) */
|
||||
p_sock->state = NET_SOCK_STATE_ESTABLISHED;
|
||||
p_sock->lost_conn_seen = 0U;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 模块名称:NET 层兄弟模块桩的观测面(net_socket 主机侧单测专用)
|
||||
* 模块功能:声明 ns_peers.c 的记录结构与复位入口。原型层面的约束(必须与真实头逐字
|
||||
* 一致)在 ns_peers.c 里由"include 真实头 + 直接实现同名函数"保证:
|
||||
* 签名漂移 = 本套编译期红。
|
||||
* ⚠ 仅供 test/host/net_socket/ 使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef NS_PEERS_H
|
||||
#define NS_PEERS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "net_types.h"
|
||||
|
||||
typedef struct {
|
||||
/* —— net_listener —— */
|
||||
int listener_req_calls;
|
||||
int last_req_fd;
|
||||
int last_req_reason;
|
||||
int listener_clear_calls;
|
||||
int last_clear_fd;
|
||||
int listener_tick_calls;
|
||||
int listener_settle_calls;
|
||||
int last_settle_fd;
|
||||
int last_settle_level;
|
||||
int req_armed[NET_MAX_SOCKETS]; /* 登记未清=1(clear/tick 后归 0) */
|
||||
int req_reason[NET_MAX_SOCKETS];
|
||||
|
||||
/* —— net_pending(recv) —— */
|
||||
int pending_capture_calls;
|
||||
int pending_reg_calls;
|
||||
int pend_register_rc; /* 注入登记结果(默认 NET_PENDING_REGISTERED) */
|
||||
int last_pend_fd;
|
||||
uint32_t last_pend_to;
|
||||
TaskHandle_t last_pend_caller;
|
||||
int pending_tick_calls;
|
||||
int pending_clear_calls;
|
||||
int last_pend_clear_fd;
|
||||
int pending_cancel_calls;
|
||||
int pending_cancel_all_calls;
|
||||
int last_pend_cancel_err;
|
||||
int pending_settle_calls;
|
||||
int pending_notified; /* 经由桩真实发出通知的次数(必通知契约) */
|
||||
int pend_active_now[NET_MAX_SOCKETS];
|
||||
|
||||
/* —— net_pending(send) —— */
|
||||
int send_reg_calls;
|
||||
int send_register_rc;
|
||||
int last_snd_fd;
|
||||
int last_snd_sent;
|
||||
int last_snd_len;
|
||||
int send_tick_calls;
|
||||
int send_clear_calls;
|
||||
int send_cancel_calls;
|
||||
int send_cancel_all_calls;
|
||||
int last_snd_cancel_err;
|
||||
int send_notified;
|
||||
|
||||
/* —— net_events —— */
|
||||
int evt_connect_calls;
|
||||
int evt_disconnect_calls;
|
||||
int evt_recv_calls;
|
||||
int evt_send_calls;
|
||||
int evt_timeout_calls;
|
||||
int reconcile_calls;
|
||||
int lost_probe_calls;
|
||||
int lost_probe_hit; /* 1=让探针命中(补 accept) */
|
||||
int last_probe_state;
|
||||
} ns_peers_obs_t;
|
||||
|
||||
extern ns_peers_obs_t g_peers;
|
||||
|
||||
void ns_peers_reset(void);
|
||||
void ns_peers_arm_pending_done(int sockfd, int result);
|
||||
void ns_peers_arm_send_pending_done(int sockfd, int result);
|
||||
|
||||
/* 由 ns_peers.c 调用,把"桩内部完成的通知"接到假内核的通知面上 */
|
||||
void ns_peers_note_notify(TaskHandle_t to, uint32_t v);
|
||||
|
||||
#endif /* NS_PEERS_H */
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* 模块名称:假内核实现(net_socket 主机侧单测专用)
|
||||
* 模块功能:实现 ns_rtos.h 声明的假队列/假通知/假时基。由测试单元 #include 使用。
|
||||
* ⚠ 仅供 test/host/net_socket/ 使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ns_rtos.h"
|
||||
#include "net_socket.h"
|
||||
|
||||
int g_ns_task_caller;
|
||||
int g_ns_task_net;
|
||||
|
||||
TaskHandle_t g_ns_cur_task;
|
||||
ns_pump_fn g_ns_pump;
|
||||
ns_poll_hook_fn g_ns_on_poll;
|
||||
int g_ns_pump_calls;
|
||||
int g_ns_block_waits;
|
||||
int g_ns_notify_calls;
|
||||
uint32_t g_ns_notify_last_val;
|
||||
TaskHandle_t g_ns_notify_last_to;
|
||||
int g_ns_put_calls;
|
||||
int g_ns_get_calls;
|
||||
int g_ns_crit_enter;
|
||||
int g_ns_crit_exit;
|
||||
uint32_t g_ns_tick;
|
||||
osKernelState_t g_ns_kernel_state = osKernelRunning;
|
||||
|
||||
/* 通知槽:按句柄区分(只服务上面两个假任务,超出的句柄一律现场分配不了→报错) */
|
||||
typedef struct {
|
||||
TaskHandle_t h;
|
||||
uint32_t val;
|
||||
int pending;
|
||||
} ns_notify_slot_t;
|
||||
|
||||
static ns_notify_slot_t s_slot[2];
|
||||
|
||||
/* 假队列:定长环形,存 net_msg_t 按值拷贝(与真队列语义一致) */
|
||||
static net_msg_t s_q[NS_Q_CAP];
|
||||
static int s_q_head;
|
||||
static int s_q_count;
|
||||
|
||||
void ns_rtos_reset(void)
|
||||
{
|
||||
(void)memset(s_slot, 0, sizeof(s_slot));
|
||||
s_slot[0].h = (TaskHandle_t)&g_ns_task_caller;
|
||||
s_slot[1].h = (TaskHandle_t)&g_ns_task_net;
|
||||
(void)memset(s_q, 0, sizeof(s_q));
|
||||
s_q_head = 0;
|
||||
s_q_count = 0;
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
g_ns_pump = NULL;
|
||||
g_ns_on_poll = NULL;
|
||||
g_ns_pump_calls = 0;
|
||||
g_ns_block_waits = 0;
|
||||
g_ns_notify_calls = 0;
|
||||
g_ns_notify_last_val = 0U;
|
||||
g_ns_notify_last_to = NULL;
|
||||
g_ns_put_calls = 0;
|
||||
g_ns_get_calls = 0;
|
||||
g_ns_crit_enter = 0;
|
||||
g_ns_crit_exit = 0;
|
||||
g_ns_tick = 0U;
|
||||
g_ns_kernel_state = osKernelRunning;
|
||||
g_ns_int_pin_set = 1;
|
||||
g_ns_poll_tick_inc = 1U;
|
||||
g_ns_poll_calls = 0U;
|
||||
}
|
||||
|
||||
void ns_rtos_q_flush(void)
|
||||
{
|
||||
s_q_head = 0;
|
||||
s_q_count = 0;
|
||||
}
|
||||
|
||||
int ns_rtos_q_count(void)
|
||||
{
|
||||
return s_q_count;
|
||||
}
|
||||
|
||||
void ns_rtos_q_fill(int n)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; (i < n) && ((uint32_t)s_q_count < NS_Q_CAP); i++) {
|
||||
(void)memset(&s_q[(s_q_head + s_q_count) % (int)NS_Q_CAP], 0, sizeof(net_msg_t));
|
||||
s_q_count++;
|
||||
}
|
||||
}
|
||||
|
||||
int ns_rtos_q_put_raw(const net_msg_t *msg)
|
||||
{
|
||||
if (msg == NULL || (uint32_t)s_q_count >= NS_Q_CAP) {
|
||||
return -1;
|
||||
}
|
||||
(void)memcpy(&s_q[(s_q_head + s_q_count) % (int)NS_Q_CAP], msg, sizeof(net_msg_t));
|
||||
s_q_count++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 假内核里的"另一次通知发出":与 xTaskNotify 共用同一套观测计数,
|
||||
* 这样"谁发了几次通知"在测试里只有一个口径(挂起完成/取消走的就是这条) */
|
||||
void ns_rtos_post_notify(TaskHandle_t to, uint32_t v)
|
||||
{
|
||||
int i;
|
||||
|
||||
g_ns_notify_calls++;
|
||||
g_ns_notify_last_val = v;
|
||||
g_ns_notify_last_to = to;
|
||||
for (i = 0; i < 2; i++) {
|
||||
if (s_slot[i].h == to) {
|
||||
s_slot[i].val = v;
|
||||
s_slot[i].pending = 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
printf("FATAL: ns_rtos_post_notify got an unknown task handle\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
static ns_notify_slot_t *slot_of(TaskHandle_t h)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 2; i++) {
|
||||
if (s_slot[i].h == h) {
|
||||
return &s_slot[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* —— CMSIS-RTOS2 侧(原型逐字取自真实 cmsis_os2.h:689/697/298/459)—— */
|
||||
|
||||
osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr,
|
||||
uint8_t msg_prio, uint32_t timeout)
|
||||
{
|
||||
(void)mq_id;
|
||||
(void)msg_prio;
|
||||
(void)timeout;
|
||||
|
||||
g_ns_put_calls++;
|
||||
if (msg_ptr == NULL) {
|
||||
return osErrorParameter;
|
||||
}
|
||||
if ((uint32_t)s_q_count >= NS_Q_CAP) {
|
||||
return osErrorResource; /* 队列满(真机是 osWaitForever 阻塞,测试里用满队列测 BUSY 分支) */
|
||||
}
|
||||
(void)memcpy(&s_q[(s_q_head + s_q_count) % (int)NS_Q_CAP], msg_ptr, sizeof(net_msg_t));
|
||||
s_q_count++;
|
||||
return osOK;
|
||||
}
|
||||
|
||||
osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr,
|
||||
uint8_t *msg_prio, uint32_t timeout)
|
||||
{
|
||||
(void)mq_id;
|
||||
(void)msg_prio;
|
||||
|
||||
g_ns_get_calls++;
|
||||
if (s_q_count == 0) {
|
||||
(void)timeout;
|
||||
return osErrorTimeout;
|
||||
}
|
||||
if (msg_ptr != NULL) {
|
||||
(void)memcpy(msg_ptr, &s_q[s_q_head], sizeof(net_msg_t));
|
||||
}
|
||||
s_q_head = (s_q_head + 1) % (int)NS_Q_CAP;
|
||||
s_q_count--;
|
||||
return osOK;
|
||||
}
|
||||
|
||||
osKernelState_t osKernelGetState(void)
|
||||
{
|
||||
return g_ns_kernel_state;
|
||||
}
|
||||
|
||||
osStatus_t osDelay(uint32_t ticks)
|
||||
{
|
||||
static int guard;
|
||||
|
||||
g_ns_tick += ticks;
|
||||
if (++guard > 20000) {
|
||||
printf("FATAL: osDelay runaway (%d calls) - the wait condition is never met\n", guard);
|
||||
exit(2);
|
||||
}
|
||||
if (guard > 0) {
|
||||
/* 每次 osDelay 也让 HAL_GetTick 往前走,600ms 复位上限才量得到 */
|
||||
g_ns_tick += 0U;
|
||||
}
|
||||
return osOK;
|
||||
}
|
||||
|
||||
/* —— FreeRTOS 侧(桩 task.h 声明)—— */
|
||||
|
||||
TaskHandle_t xTaskGetCurrentTaskHandle(void)
|
||||
{
|
||||
return g_ns_cur_task;
|
||||
}
|
||||
|
||||
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue,
|
||||
eNotifyAction eAction)
|
||||
{
|
||||
ns_notify_slot_t *p = slot_of(xTaskToNotify);
|
||||
|
||||
(void)eAction; /* 本测试只用 eSetValueWithOverwrite,语义相同 */
|
||||
g_ns_notify_calls++;
|
||||
g_ns_notify_last_val = ulValue;
|
||||
g_ns_notify_last_to = xTaskToNotify;
|
||||
if (p == NULL) {
|
||||
printf("FATAL: xTaskNotify got an unknown task handle\n");
|
||||
exit(2);
|
||||
}
|
||||
p->val = ulValue;
|
||||
p->pending = 1;
|
||||
return pdTRUE;
|
||||
}
|
||||
|
||||
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
|
||||
uint32_t ulBitsToClearOnExit,
|
||||
uint32_t *pulNotificationValue,
|
||||
TickType_t xTicksToWait)
|
||||
{
|
||||
ns_notify_slot_t *p = slot_of(g_ns_cur_task);
|
||||
|
||||
if (p == NULL) {
|
||||
printf("FATAL: xTaskNotifyWait current task handle unknown\n");
|
||||
exit(2);
|
||||
}
|
||||
if (pulNotificationValue != NULL) {
|
||||
*pulNotificationValue = p->val & (~ulBitsToClearOnEntry);
|
||||
}
|
||||
if (p->pending != 0) {
|
||||
if (pulNotificationValue != NULL) {
|
||||
*pulNotificationValue = p->val;
|
||||
}
|
||||
p->val = p->val & (~ulBitsToClearOnExit);
|
||||
p->pending = 0;
|
||||
return pdTRUE;
|
||||
}
|
||||
if (xTicksToWait == 0U) {
|
||||
return pdFALSE; /* 零等待拾取:没有就是没有 */
|
||||
}
|
||||
|
||||
/* 阻塞分支:这就是"睡到 netTask 回执"的调度点 → 驱动一轮 netTask 再拾取 */
|
||||
g_ns_block_waits++;
|
||||
if (g_ns_pump == NULL) {
|
||||
printf("FATAL: reached the blocking wait but the test installed no pump hook\n");
|
||||
exit(2);
|
||||
}
|
||||
{
|
||||
TaskHandle_t save = g_ns_cur_task;
|
||||
int rounds = 0;
|
||||
|
||||
while (rounds < NS_PUMP_MAX) {
|
||||
rounds++;
|
||||
g_ns_pump_calls++;
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; /* 以 netTask 身份跑 */
|
||||
g_ns_pump();
|
||||
g_ns_cur_task = save;
|
||||
if (p->pending != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (p->pending == 0) {
|
||||
printf("FATAL: blocking wait still got no receipt after %d rounds (a missing notify would hang real hardware)\n", rounds);
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
if (pulNotificationValue != NULL) {
|
||||
*pulNotificationValue = p->val;
|
||||
}
|
||||
p->val = p->val & (~ulBitsToClearOnExit);
|
||||
p->pending = 0;
|
||||
return pdTRUE;
|
||||
}
|
||||
|
||||
uint32_t xTaskGetTickCount(void)
|
||||
{
|
||||
return g_ns_tick;
|
||||
}
|
||||
|
||||
int ns_os_crit_depth(void)
|
||||
{
|
||||
return g_ns_crit_enter - g_ns_crit_exit;
|
||||
}
|
||||
|
||||
void ns_os_enter_critical(void)
|
||||
{
|
||||
g_ns_crit_enter++;
|
||||
}
|
||||
|
||||
void ns_os_exit_critical(void)
|
||||
{
|
||||
g_ns_crit_exit++;
|
||||
}
|
||||
|
||||
/* —— main.h 桩:时基 + INT# 引脚 —— */
|
||||
|
||||
uint32_t HAL_GetTick(void)
|
||||
{
|
||||
return g_ns_tick;
|
||||
}
|
||||
|
||||
/* INT# 电平可注入:默认 SET(无中断)——net_poll 默认走"引脚高即退出"的短路分支 */
|
||||
int g_ns_int_pin_set = 1;
|
||||
uint32_t g_ns_poll_tick_inc = 1U;
|
||||
uint32_t g_ns_poll_calls;
|
||||
|
||||
/* 读 INT# 就是"net_poll 又轮了一轮"的天然计数点:
|
||||
* - 时基自走 g_ns_poll_tick_inc 毫秒(默认 1ms),NET_SEND_SPIN_MS=2 这种
|
||||
* "有界自旋"预算才有可量测的时间轴;
|
||||
* - 跑飞保险:轮询次数超阈值直接 exit(2)。net_send_locked / net_recvfrom 的
|
||||
* 等待环里没有 osDelay,光靠 osDelay 的保险挡不住死循环(实测过一次挂门禁)。 */
|
||||
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
|
||||
{
|
||||
(void)GPIOx;
|
||||
(void)GPIO_Pin;
|
||||
g_ns_poll_calls++;
|
||||
g_ns_tick += g_ns_poll_tick_inc;
|
||||
if (g_ns_on_poll != NULL) {
|
||||
g_ns_on_poll(); /* 并发事件注入点(在时间推进之后,超时判定才量得到) */
|
||||
}
|
||||
if (g_ns_poll_calls > 400000U) {
|
||||
printf("FATAL: net_poll still not converged after %lu rounds (wait condition never met)\n",
|
||||
(unsigned long)g_ns_poll_calls);
|
||||
exit(2);
|
||||
}
|
||||
return (g_ns_int_pin_set != 0) ? GPIO_PIN_SET : GPIO_PIN_RESET;
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
/*
|
||||
* 分片 3:跨上下文消息面(net_process_messages 全分派 + 结果经任务通知值回传)
|
||||
* 前置:net_socket_test.c 已纳入生产源与假内核/假芯片/兄弟模块桩(同一 TU)。
|
||||
*
|
||||
* 这一片是整套里最值钱的部分,钉的是 AGENTS 模块约定的两条硬契约:
|
||||
* A.「所有 net_* socket 操作经 netMsgQueue 在 netTask 串行执行」
|
||||
* B.「net_* 公开 API 的错误码经任务通知值回传:net_msg_t 是按值拷入队列的,
|
||||
* netTask 改的是自己那份副本,调用方永远看不到 → 错误码搭 xTaskNotify 通知值回来;
|
||||
* 勿给 net_msg_t 加回传字段(加了只会让调用方读到栈垃圾)」
|
||||
* 断言形态因此刻意是:先看**通知值本身**(32 位整体即结果,负值=精确错误码),
|
||||
* 再看调用方在自己上下文恢复出的 errno——而不是去读队列里的 msg.result
|
||||
* (那正是旧实现的错觉,也是 B 条明文禁止的方向)。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
/* close 场景的增量快照基准(前面场景也会 clear,必须按增量断言) */
|
||||
static int s_close_base_clear;
|
||||
static int s_close_base_settle;
|
||||
|
||||
/* 造一条已建立的 TCP 会话(多数消息场景的前置) */
|
||||
static int mk_established(int type)
|
||||
{
|
||||
int fd = net_socket(NET_AF_INET, type);
|
||||
net_sock_t *p;
|
||||
|
||||
if (fd < 0) {
|
||||
return fd;
|
||||
}
|
||||
p = &s_net_socks[fd];
|
||||
p->state = NET_SOCK_STATE_ESTABLISHED;
|
||||
p->send_ready = 1U;
|
||||
p->readable = 0U;
|
||||
p->local_port = 2404U;
|
||||
p->remote_port = 51999U;
|
||||
p->remote_ip = 0x0B01A8C0U;
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-200 未知消息类型:枚举闭合外的值也必须给出**本条消息自身**的确定结果
|
||||
* (不许把上一条的回执残留当成这次的结果) */
|
||||
static void tc_200_unknown_type(void)
|
||||
{
|
||||
net_msg_t msg;
|
||||
|
||||
bring_up();
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = (net_msg_type_t)999;
|
||||
msg.sockfd = 0;
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_caller;
|
||||
msg.result = 12345; /* 脏值:分派必须覆写它 */
|
||||
CHECK(ns_rtos_q_put_raw(&msg) == 0, "TC-NETSOCK-200a raw message enqueued");
|
||||
CHECK(g_ns_notify_calls == 0, "TC-NETSOCK-200b enqueue itself produces no notification");
|
||||
|
||||
net_process_messages(); /* 以 netTask 身份跑一轮 */
|
||||
|
||||
CHECK(g_ns_notify_calls == 1, "TC-NETSOCK-200c unknown type must still send a receipt (else the caller hangs forever)");
|
||||
CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR,
|
||||
"TC-NETSOCK-200d notify value for unknown type = this message exact code NET_ERR, not a leftover");
|
||||
CHECK(ns_rtos_q_count() == 0, "TC-NETSOCK-200e queue drained within this round (non-blocking while semantics)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-201 队列为空:四个自愈 tick 必跑,且不产生任何通知 */
|
||||
static void tc_201_ticks_run_before_dispatch(void)
|
||||
{
|
||||
bring_up();
|
||||
|
||||
net_process_messages();
|
||||
|
||||
CHECK(g_peers.listener_tick_calls == 1 && g_peers.pending_tick_calls == 1 &&
|
||||
g_peers.send_tick_calls == 1 && g_peers.reconcile_calls == 1,
|
||||
"TC-NETSOCK-201a even with an empty queue all four self-heal ticks run (D10/pending x2/reconcile)");
|
||||
CHECK(g_ns_notify_calls == 0 && g_ns_get_calls == 1,
|
||||
"TC-NETSOCK-201b empty queue: one peek then exit, no notification");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-202 队列句柄未绑定:直接返回,连 tick 都不该跑 */
|
||||
static void tc_202_no_queue_bound(void)
|
||||
{
|
||||
reset_all();
|
||||
g_ns_pump = net_process_messages;
|
||||
|
||||
net_process_messages();
|
||||
|
||||
CHECK(g_peers.listener_tick_calls == 0 && g_ns_get_calls == 0,
|
||||
"TC-NETSOCK-202 net_process_messages without a bound queue returns at once (no chip, no ticks)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-210 SEND 快路径:端到端跑通"入队 → 分派 → 写芯片 → 通知值=字节数" */
|
||||
static void tc_210_send_end_to_end(void)
|
||||
{
|
||||
char buf[8];
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
(void)memset(buf, 'A', sizeof(buf));
|
||||
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
rc = net_send(fd, buf, (int)sizeof(buf), 0);
|
||||
|
||||
CHECK(rc == (int)sizeof(buf), "TC-NETSOCK-210a net_send returns the number of bytes actually written");
|
||||
CHECK(g_chip.write_calls == 1 && g_chip.last_read_len == (uint16_t)sizeof(buf),
|
||||
"TC-NETSOCK-210b one full write (a healthy path neither chunks nor goes pending)");
|
||||
CHECK(s_net_socks[fd].send_bytes == sizeof(buf), "TC-NETSOCK-210c sent byte statistic accumulates");
|
||||
CHECK(g_ns_notify_calls == 1 && g_ns_notify_last_val == (uint32_t)sizeof(buf),
|
||||
"TC-NETSOCK-210d result travels back on the notify value (the whole 32 bits are the result)");
|
||||
CHECK(g_ns_notify_last_to == (TaskHandle_t)&g_ns_task_caller,
|
||||
"TC-NETSOCK-210e notify goes to the REGISTERED caller only, not a broadcast");
|
||||
CHECK(g_ns_block_waits == 1,
|
||||
"TC-NETSOCK-210f zero-wait found nothing -> blocking branch (second stage of the two-step peek)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-211 大帧分块:超过 NET_SEND_CHUNK_MAX 必须按硬件发送缓冲上界切块
|
||||
* (TRAP-NET-16:单次写 >1KB 会写穿 1KB 发送 FIFO) */
|
||||
static void tc_211_send_chunking(void)
|
||||
{
|
||||
static char buf[3000];
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
/* 造"每写完一块芯片就报 SENBUF_FREE"的健康链路:让 net_poll 把 send_ready 置回来 */
|
||||
g_ns_int_pin_set = 0;
|
||||
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
|
||||
/* SENBUF_FREE 是**条件位**:发送缓冲空着就还会再报。用假件的电平型掩码而不是
|
||||
* 一次性位图,否则第二块起就再也等不到空闲 → 变成测假件的清零时机。 */
|
||||
ch395f_fake_set_sint_sticky((uint8_t)fd, CH395F_SINT_STAT_SENBUF_FREE);
|
||||
|
||||
rc = net_send(fd, buf, 2500, 0);
|
||||
|
||||
CHECK(rc == 2500, "TC-NETSOCK-211a all 2500 bytes delivered");
|
||||
CHECK(g_chip.write_calls == 3, "TC-NETSOCK-211b split into 3 chunks at the 1024 bound (1024/1024/452)");
|
||||
CHECK(g_chip.last_read_len == 452U, "TC-NETSOCK-211c last chunk writes only the remainder");
|
||||
CHECK(NET_SEND_CHUNK_MAX == 1024,
|
||||
"TC-NETSOCK-211d chunk size comes from the chip send buffer, not a fake-defined number");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-212 SEND 挂起登记:等发送缓冲超预算 → 登记挂起并**跳过即时回执** */
|
||||
static void tc_212_send_pending_defers_notify(void)
|
||||
{
|
||||
static char buf[64];
|
||||
net_msg_t msg;
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].send_ready = 0U; /* 芯片一直不腾缓冲 */
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_SEND;
|
||||
msg.sockfd = fd;
|
||||
msg.buf = buf;
|
||||
msg.len = (int)sizeof(buf);
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_caller;
|
||||
|
||||
CHECK(ns_rtos_q_put_raw(&msg) == 0, "TC-NETSOCK-212a SEND message enqueued");
|
||||
net_process_messages();
|
||||
|
||||
CHECK(g_peers.send_reg_calls == 1,
|
||||
"TC-NETSOCK-212b past the spin budget it registers a pending send instead of busy-waiting in netTask");
|
||||
CHECK(g_peers.last_snd_fd == fd && g_peers.last_snd_len == (int)sizeof(buf) &&
|
||||
g_peers.last_snd_sent == 0,
|
||||
"TC-NETSOCK-212c registered progress = bytes already written to the chip (0 here), length and caller travel too");
|
||||
CHECK(g_ns_notify_calls == 0,
|
||||
"TC-NETSOCK-212d a successful registration NEVER sends an immediate receipt (else it reads as the result)");
|
||||
CHECK(g_ns_notify_last_to == NULL,
|
||||
"TC-NETSOCK-212e not a single notification (the 0x70000000 sentinel can not leak into a notify value)");
|
||||
|
||||
/* 完成时刻:由挂起 send 的 tick 结束(本套由桩脚本模拟"缓冲腾出来了") */
|
||||
ns_peers_arm_send_pending_done(fd, 64);
|
||||
net_process_messages();
|
||||
CHECK(g_ns_notify_calls == 1 && g_ns_notify_last_val == 64U,
|
||||
"TC-NETSOCK-212f receipt only after the pending completes, notify value = bytes actually written");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-213 SEND 失败:未建立连接 → 通知值带**精确**错误码,不是笼统 -1 */
|
||||
static void tc_213_send_error_code_roundtrip(void)
|
||||
{
|
||||
char buf[4];
|
||||
net_msg_t msg;
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); /* CREATED,未连接 */
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_SEND;
|
||||
msg.sockfd = fd;
|
||||
msg.buf = buf;
|
||||
msg.len = (int)sizeof(buf);
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages();
|
||||
|
||||
CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_NOTCONN,
|
||||
"TC-NETSOCK-213a SEND on a not-connected socket: notify value = NET_ERR_NOTCONN (exact code for caller branches)");
|
||||
|
||||
/* 端到端:公开 net_send 的 BADF 形态(fd 越界由**分派侧**再判一次) */
|
||||
reset_all();
|
||||
bring_up();
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_SEND;
|
||||
msg.sockfd = 3; /* 合法范围内但未分配 */
|
||||
msg.buf = buf;
|
||||
msg.len = 4;
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages();
|
||||
CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF,
|
||||
"TC-NETSOCK-213b in range but unallocated fd: net_get_sock null -> BADF travels on the notify value");
|
||||
|
||||
/* 调用方上下文恢复 errno:拿到的必须是"本任务的"精确码 */
|
||||
reset_all();
|
||||
bring_up();
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
rc = net_send(6, buf, 4, 0); /* 6 未分配 → 分派侧 BADF → 通知值 -2 */
|
||||
CHECK(rc == -1, "TC-NETSOCK-213c BSD semantics kept for callers: any failure is -1");
|
||||
CHECK(net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-213d caller reads the exact code from its own errno slot (E-03 plus notify value together)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-220 RECV 快路径:芯片缓冲里已有数据 → 当场读出,不登记挂起 */
|
||||
static void tc_220_recv_fast_path(void)
|
||||
{
|
||||
char buf[16];
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].readable = 1U;
|
||||
ch395f_fake_push_recv_len((uint8_t)fd, 1400U);
|
||||
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
rc = net_recv(fd, buf, 10, NET_MSG_DONTWAIT);
|
||||
|
||||
CHECK(rc == 10, "TC-NETSOCK-220a chip has 1400B but the caller buffer is 10B -> reads 10 (a partial read is legal)");
|
||||
CHECK(g_chip.read_calls == 1 && g_chip.last_read_len == 10U,
|
||||
"TC-NETSOCK-220b reads only what the caller offered (never the chip available length)");
|
||||
CHECK(s_net_socks[fd].recv_bytes == 10U, "TC-NETSOCK-220c received byte statistic accumulates");
|
||||
CHECK(s_net_socks[fd].readable == 0U,
|
||||
"TC-NETSOCK-220d reading clears readable (the next RECV_OK interrupt sets it again)");
|
||||
CHECK(g_peers.pending_reg_calls == 0, "TC-NETSOCK-220e data is already in hand, no pending registration");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-221 RECV 非阻塞无数据 → WOULDBLOCK(且不许登记挂起) */
|
||||
static void tc_221_recv_wouldblock(void)
|
||||
{
|
||||
char buf[8];
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
rc = net_recv(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT);
|
||||
CHECK(rc == -1 && net_get_errno() == NET_ERR_WOULDBLOCK,
|
||||
"TC-NETSOCK-221a DONTWAIT with no data -> -1 plus WOULDBLOCK");
|
||||
CHECK(g_peers.pending_reg_calls == 0,
|
||||
"TC-NETSOCK-221b non-blocking never registers pending (else the caller thinks it waits while it is parked)");
|
||||
|
||||
/* 模式属性(net_setblocking(0))与单次标志同判据 */
|
||||
CHECK(net_setblocking(fd, 0) == 0, "TC-NETSOCK-221c switching to non-blocking mode succeeds");
|
||||
rc = net_recv(fd, buf, (int)sizeof(buf), 0);
|
||||
CHECK(rc == -1 && net_get_errno() == NET_ERR_WOULDBLOCK,
|
||||
"TC-NETSOCK-221d blocking attribute 0 behaves exactly like NET_MSG_DONTWAIT");
|
||||
CHECK(net_recv_nb(fd, buf, (int)sizeof(buf)) == -1 &&
|
||||
net_get_errno() == NET_ERR_WOULDBLOCK,
|
||||
"TC-NETSOCK-221e net_recv_nb named entry takes the same path");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-222 RECV EOF:会话已结束 + 无数据 → 返 0 并登记重建(不是 -1) */
|
||||
static void tc_222_recv_eof_relisten(void)
|
||||
{
|
||||
char buf[8];
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_CLOSED;
|
||||
s_net_socks[fd].auto_relisten = 1U;
|
||||
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
rc = net_recv(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT);
|
||||
|
||||
CHECK(rc == 0, "TC-NETSOCK-222a CLOSED with no data -> EOF(0), not an error");
|
||||
CHECK(g_peers.listener_req_calls == 1 &&
|
||||
g_peers.last_req_reason == (int)NET_LISTENER_REQ_EOF,
|
||||
"TC-NETSOCK-222b EOF registers REQ_EOF (the rebuild belongs to the listener layer, this layer never touches the chip)");
|
||||
CHECK(g_chip.open_calls == 0 && g_chip.listen_calls == 0 && g_chip.close_calls == 0,
|
||||
"TC-NETSOCK-222c the registration path issues NOT ONE chip transaction (single-owner invariant II)");
|
||||
|
||||
/* 抢先 relisten 后 state 已回 LISTENING,但会话电平仍是"已结束" */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_LISTENING;
|
||||
s_net_socks[fd].session_dead = 1U;
|
||||
s_net_socks[fd].auto_relisten = 1U;
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
rc = net_recv(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT);
|
||||
CHECK(rc == 0,
|
||||
"TC-NETSOCK-222d still EOF after relisten moved it back to LISTENING (N-32 P1 level, else EOF vanishes forever)");
|
||||
|
||||
/* auto_relisten=0(客户端 fd):EOF 不登记重建,交调用方决定重连 */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_CLOSED;
|
||||
s_net_socks[fd].session_dead = 1U;
|
||||
s_net_socks[fd].auto_relisten = 0U;
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
rc = net_recv(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT);
|
||||
CHECK(rc == 0 && g_peers.listener_req_calls == 0,
|
||||
"TC-NETSOCK-222e EOF on a non-listener fd must not rebuild behind the caller back");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-223 RECV 阻塞无数据 → 登记挂起 + 跳过即时回执;到点由 tick 结束 */
|
||||
static void tc_223_recv_pending_roundtrip(void)
|
||||
{
|
||||
char buf[16];
|
||||
net_msg_t msg;
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_RECV;
|
||||
msg.sockfd = fd;
|
||||
msg.buf = buf;
|
||||
msg.len = (int)sizeof(buf);
|
||||
msg.flags = 0;
|
||||
msg.timeout_ms = 250U; /* 显式预算必须原样带到登记处 */
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages();
|
||||
|
||||
CHECK(g_peers.pending_reg_calls == 1 && g_peers.last_pend_fd == fd,
|
||||
"TC-NETSOCK-223a blocking with no data -> registers a pending recv (netTask returns with zero occupancy)");
|
||||
CHECK(g_peers.last_pend_to == 250U,
|
||||
"TC-NETSOCK-223b the caller explicit budget travels with the registration (0 means infinite, here it must not be 0)");
|
||||
CHECK(g_peers.last_pend_caller == (TaskHandle_t)&g_ns_task_caller,
|
||||
"TC-NETSOCK-223c completion must know whom to notify");
|
||||
CHECK(g_ns_notify_calls == 0,
|
||||
"TC-NETSOCK-223d registering then immediately notifying would pass off registration as the result - pinned absent");
|
||||
|
||||
/* 数据到达:由挂起 recv 的 tick 结束(脚本模拟) */
|
||||
ns_peers_arm_pending_done(fd, 7);
|
||||
net_process_messages();
|
||||
CHECK(g_ns_notify_calls == 1 && g_ns_notify_last_val == 7U,
|
||||
"TC-NETSOCK-223e receipt only once data arrives, notify value = byte count");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-224 net_recv_to:分片超时用独立返回码 -2(不靠 errno,避免跨任务竞态) */
|
||||
static void tc_224_recv_to_timeout_code(void)
|
||||
{
|
||||
char buf[16];
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
|
||||
/* 先造一条"已登记"的挂起,再让 tick 判到点 */
|
||||
ns_peers_arm_pending_done(fd, NET_ERR_TIMEDOUT);
|
||||
rc = net_recv_to(fd, buf, (int)sizeof(buf), 30U);
|
||||
CHECK(rc == -2, "TC-NETSOCK-224a this slice timed out -> dedicated return code -2 (distinct from a real error -1)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-225 RECV 参数非法:不碰芯片、不登记 */
|
||||
static void tc_225_recv_bad_args(void)
|
||||
{
|
||||
char buf[4];
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
|
||||
CHECK(net_recv_to(0, buf, 4, 0U) == -1 && net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-225a net_recv_to(timeout=0) is refused up front - 0 means infinite wait on the netTask side,"
|
||||
"which contradicts this API whole reason for having a bounded occupancy");
|
||||
CHECK(g_ns_put_calls == 0, "TC-NETSOCK-225b the refusal happens before enqueue");
|
||||
|
||||
CHECK(net_recv(fd, NULL, 4, NET_MSG_DONTWAIT) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-225c buf NULL -> INVAL");
|
||||
CHECK(net_recv(fd, buf, 0, NET_MSG_DONTWAIT) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-225d len <= 0 -> INVAL");
|
||||
CHECK(net_send(fd, NULL, 4, 0) == -1 && net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-225e send with buf NULL -> INVAL");
|
||||
CHECK(net_send(fd, buf, -1, 0) == -1 && net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-225f send with len < 0 -> INVAL");
|
||||
CHECK(g_chip.write_calls == 0 && g_chip.read_calls == 0 &&
|
||||
g_peers.pending_reg_calls == 0 && g_peers.send_reg_calls == 0,
|
||||
"TC-NETSOCK-225g on the invalid-argument path: zero chip transactions and zero pending registrations");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-230 CLOSE:先结束挂起再关芯片,且 last_peer_port 跨释放保留 */
|
||||
static void tc_230_close_order_and_peer(void)
|
||||
{
|
||||
net_msg_t msg;
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].auto_relisten = 1U;
|
||||
/* 登记一条挂起 recv,再看 close 是否先把它结算掉(排空/取消都必通知) */
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_RECV;
|
||||
msg.sockfd = fd;
|
||||
msg.buf = (void *)"x";
|
||||
msg.len = 1;
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_net; /* 另一个任务在等这条流 */
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages(); /* 走登记路径(不发回执) */
|
||||
CHECK(g_peers.pending_reg_calls == 1 && g_ns_notify_calls == 0,
|
||||
"TC-NETSOCK-230a precondition: pending registered and no receipt yet");
|
||||
|
||||
/* 芯片侧此刻仍是 OPEN:close 才需要真去拆(已 CLOSED 的形态见 TC-NETSOCK-232) */
|
||||
g_fake.cached_open[fd] = CH395F_SOCKET_OPEN;
|
||||
g_ns_notify_calls = 0;
|
||||
{
|
||||
s_close_base_clear = g_peers.listener_clear_calls;
|
||||
s_close_base_settle = g_peers.pending_settle_calls;
|
||||
}
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_CLOSE;
|
||||
msg.sockfd = fd;
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages();
|
||||
|
||||
CHECK(g_peers.pending_settle_calls == s_close_base_settle + 1,
|
||||
"TC-NETSOCK-230b close must first settle this fd pending recv (else the waiter hangs forever)");
|
||||
CHECK(g_peers.pending_notified == 1,
|
||||
"TC-NETSOCK-230c settling always notifies (a cancel notifies too, not a silent slot clear)");
|
||||
CHECK(g_chip.close_calls >= 1 && g_fxchip.rst_begin_calls == 1,
|
||||
"TC-NETSOCK-230d chip still OPEN -> sock_reset deterministically drains and tears it down");
|
||||
CHECK(g_fxchip.glob_calls == 0,
|
||||
"TC-NETSOCK-230e close must never read the global interrupt register (B3: sole reader is net_poll)");
|
||||
CHECK(s_net_socks[fd].in_use == 0U && s_net_socks[fd].last_peer_port == 51999U,
|
||||
"TC-NETSOCK-230f control block cleared but last_peer_port kept (ghost discrimination is chip-socket level)");
|
||||
CHECK(g_peers.listener_clear_calls == s_close_base_clear + 1,
|
||||
"TC-NETSOCK-230g close idempotently clears the listener retry (closing cancels pending retries, D10)");
|
||||
CHECK(g_peers.pend_active_now[fd] == 0,
|
||||
"TC-NETSOCK-230h after close this fd has no pending recv (both tables zeroed)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-231 CLOSE 未在用:net_get_sock 判空 → BADF 通知值 */
|
||||
static void tc_231_close_badfd(void)
|
||||
{
|
||||
net_msg_t msg;
|
||||
|
||||
bring_up();
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_CLOSE;
|
||||
msg.sockfd = 5;
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages();
|
||||
CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF,
|
||||
"TC-NETSOCK-231a close on an unallocated fd -> BADF travels back on the notify value");
|
||||
CHECK(g_chip.close_calls == 0, "TC-NETSOCK-231b BADF does not touch the chip");
|
||||
|
||||
bring_up();
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_ACCEPT; /* 没有 in_use 的监听 */
|
||||
msg.sockfd = 5;
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages();
|
||||
CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_INVAL,
|
||||
"TC-NETSOCK-231c accept on an unused fd -> INVAL (control-block check, not fd range)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-240 DROP_CONN:四类前置各走各的分支(幂等语义是关键) */
|
||||
static void tc_240_drop_conn_branches(void)
|
||||
{
|
||||
net_msg_t msg;
|
||||
int fd;
|
||||
|
||||
/* 非在用 → BADF */
|
||||
bring_up();
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_DROP_CONN;
|
||||
msg.sockfd = 2;
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages();
|
||||
CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF, "TC-NETSOCK-240a unallocated -> BADF");
|
||||
|
||||
/* UDP → INVAL */
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_DROP_CONN;
|
||||
msg.sockfd = fd;
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages();
|
||||
CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_INVAL,
|
||||
"TC-NETSOCK-240b not a TCP stream -> INVAL (this API only tears down TCP sessions)");
|
||||
|
||||
/* 监听态但无活动会话 → 幂等 0,不动芯片 */
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_LISTENING;
|
||||
s_net_socks[fd].auto_relisten = 1U;
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_DROP_CONN;
|
||||
msg.sockfd = fd;
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages();
|
||||
CHECK(g_ns_notify_last_val == 0U, "TC-NETSOCK-240c no active session -> idempotent success 0");
|
||||
CHECK(g_chip.close_calls == 0 && g_fxchip.rst_begin_calls == 0,
|
||||
"TC-NETSOCK-240d the idempotent branch must not tear down the chip (it would destroy the live listener)");
|
||||
|
||||
/* ESTABLISHED + 监听型 → QUARANTINE 拆除 + 登记重建 */
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].auto_relisten = 1U;
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_DROP_CONN;
|
||||
msg.sockfd = fd;
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_caller;
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages();
|
||||
|
||||
CHECK(g_peers.pending_settle_calls == 1 && g_peers.send_cancel_calls == 1,
|
||||
"TC-NETSOCK-240e both pending tables are ended before dropping the session (with notification)");
|
||||
CHECK(g_fxchip.rst_begin_calls == 1,
|
||||
"TC-NETSOCK-240f teardown uses sock_reset deterministic draining, not close plus blind waiting");
|
||||
CHECK(g_peers.listener_req_calls == 1 &&
|
||||
g_peers.last_req_reason == (int)NET_LISTENER_REQ_DROP,
|
||||
"TC-NETSOCK-240g listener-type fd registers REQ_DROP after the drop (rebuild belongs to the listener layer)");
|
||||
CHECK(s_net_socks[fd].state == NET_SOCK_STATE_CLOSED &&
|
||||
s_net_socks[fd].in_use == 1U && s_net_socks[fd].local_port == 2404U &&
|
||||
s_net_socks[fd].auto_relisten == 1U,
|
||||
"TC-NETSOCK-240h only session bookkeeping is closed out: fd, listener attribute and port all survive");
|
||||
CHECK(g_fxchip.glob_calls == 0,
|
||||
"TC-NETSOCK-240i teardown only touches its own socket (F-NET-7: never reads the global register)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-250 入队失败(队列满)→ 调用方 BUSY,不留下"已提交"的假象 */
|
||||
static void tc_250_put_fail_busy(void)
|
||||
{
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
ns_rtos_q_fill((int)NS_Q_CAP); /* 把队列灌满 */
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
|
||||
rc = net_send(fd, "abc", 3, 0);
|
||||
CHECK(rc == -1 && net_get_errno() == NET_ERR_BUSY,
|
||||
"TC-NETSOCK-250a queue full -> net_send reports BUSY, it does not silently drop");
|
||||
CHECK(g_chip.write_calls == 0 && g_ns_notify_calls == 0,
|
||||
"TC-NETSOCK-250b on enqueue failure: zero chip transactions and zero notifications (caller will not wait in vain)");
|
||||
CHECK(net_listen(fd) == -1 && net_get_errno() == NET_ERR_BUSY,
|
||||
"TC-NETSOCK-250c the same shape holds for net_listen");
|
||||
CHECK(net_close(fd) == -1 && net_get_errno() == NET_ERR_BUSY,
|
||||
"TC-NETSOCK-250d the same shape holds for net_close");
|
||||
CHECK(net_drop_conn(fd) == -1 && net_get_errno() == NET_ERR_BUSY,
|
||||
"TC-NETSOCK-250e the same shape holds for net_drop_conn");
|
||||
CHECK(net_recv_to(fd, (void *)"x", 1, 5U) == -1 && net_get_errno() == NET_ERR_BUSY,
|
||||
"TC-NETSOCK-250f the same shape holds for net_recv_to");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
static void ns_run_s3(void)
|
||||
{
|
||||
tc_200_unknown_type();
|
||||
tc_201_ticks_run_before_dispatch();
|
||||
tc_202_no_queue_bound();
|
||||
tc_210_send_end_to_end();
|
||||
tc_211_send_chunking();
|
||||
tc_212_send_pending_defers_notify();
|
||||
tc_213_send_error_code_roundtrip();
|
||||
tc_220_recv_fast_path();
|
||||
tc_221_recv_wouldblock();
|
||||
tc_222_recv_eof_relisten();
|
||||
tc_223_recv_pending_roundtrip();
|
||||
tc_224_recv_to_timeout_code();
|
||||
tc_225_recv_bad_args();
|
||||
tc_230_close_order_and_peer();
|
||||
tc_231_close_badfd();
|
||||
tc_240_drop_conn_branches();
|
||||
tc_250_put_fail_busy();
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* 分片 4:net_poll 中断分派、监听/可读闸门、close 与 drop_conn 的拆除序列
|
||||
*
|
||||
* 这一片钉的是 AGENTS 模块约定里**没有文档就无法发现**的那条芯片约束:
|
||||
* 所有 CH395F 中断在 net_poll() 内经 GET_GLOB_INT_STATUS 集中读取并清除,不得在
|
||||
* ISR 或其他地方单独处理。全局寄存器整体读即清、芯片无按位清除手段,而 net_poll
|
||||
* 只在全局位为 1 时才读该 Socket 的 SINT → 顺手全量清一遍等于把别的在用 Socket 的
|
||||
* 门焊死(B3 实证,详见 docs/网络/网络自愈架构设计.md §4.10)。
|
||||
*
|
||||
* 断言手法(刻意不用「读了几次」这种弱口径):GET_SOCK_INT_STATUS 是**读即清**,
|
||||
* 所以给每个 Socket 预置互不相同的挂起位图,跑一轮 net_poll 后看哪些位图被清掉了
|
||||
* ——被清=被读过。这比数调用次数更硬:它直接照出「门有没有被焊死」。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
/* 预置:给每个 Socket 挂一个只有自己才有的 SINT 位图(只用低 6 位,避开保留位) */
|
||||
static void prime_sint_masks(void)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < NET_MAX_SOCKETS; i++) {
|
||||
g_fake.int_status[i] = (uint8_t)(0x01U << (i % 6));
|
||||
}
|
||||
g_chip.int_status_calls = 0;
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-300 net_poll 的前置闸门 */
|
||||
static void tc_300_poll_gates(void)
|
||||
{
|
||||
/* 1) 未初始化:直接 0,连引脚都不该读 */
|
||||
reset_all();
|
||||
g_ns_pump = net_process_messages;
|
||||
CHECK(net_poll() == 0, "TC-NETSOCK-300a uninitialized -> 0 events");
|
||||
CHECK(g_ns_poll_calls == 0U && g_fxchip.glob_calls == 0,
|
||||
"TC-NETSOCK-300b uninitialized must not read the INT# pin nor the global interrupt register (saves SPI)");
|
||||
|
||||
/* 2) 初始化了但 INT# 为高(无中断待处理):不读全局寄存器 */
|
||||
bring_up();
|
||||
g_ns_int_pin_set = 1;
|
||||
CHECK(net_poll() == 0, "TC-NETSOCK-300c INT# high -> 0 events");
|
||||
CHECK(g_fxchip.glob_calls == 0,
|
||||
"TC-NETSOCK-300d with INT# high no global interrupt read may be issued (pin polling exists to save that step)");
|
||||
|
||||
/* 3) INT# 低但全局状态为 0:读了全局一次,且不进 Socket 循环 */
|
||||
bring_up();
|
||||
g_ns_int_pin_set = 0;
|
||||
ch395f_fake_push_glob(0U);
|
||||
CHECK(net_poll() == 0, "TC-NETSOCK-300e all global bits clear -> 0 events");
|
||||
CHECK(g_fxchip.glob_calls == 1 && g_chip.int_status_calls == 0,
|
||||
"TC-NETSOCK-300f global register read exactly once and it stops on 0 (no per-socket sweep)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-301 门控精度:只读全局位置位的那个 Socket 的 SINT(B3 的正面表述) */
|
||||
static void tc_301_poll_only_gated_sockets(void)
|
||||
{
|
||||
int i;
|
||||
int a;
|
||||
int b;
|
||||
|
||||
bring_up();
|
||||
a = mk_established(NET_SOCK_STREAM);
|
||||
b = mk_established(NET_SOCK_STREAM);
|
||||
(void)mk_established(NET_SOCK_STREAM);
|
||||
CHECK(a == 0 && b == 1, "TC-NETSOCK-301a precondition: three fds in use");
|
||||
|
||||
prime_sint_masks();
|
||||
g_ns_int_pin_set = 0;
|
||||
ch395f_fake_push_glob((uint16_t)(1U << (1 + 4))); /* Socket n 的位 = bit(n+4) */
|
||||
|
||||
(void)net_poll();
|
||||
|
||||
CHECK(g_fake.int_status[0] == (uint8_t)(0x01U << 0) &&
|
||||
g_fake.int_status[2] == (uint8_t)(0x01U << 2),
|
||||
"TC-NETSOCK-301b pending bitmaps of unlit sockets 0/2 stay uncleared (read-clear means read, still there means not read)");
|
||||
CHECK(g_fake.int_status[1] == 0U, "TC-NETSOCK-301c the lit socket 1 got read (bitmap cleared)");
|
||||
CHECK(g_chip.int_status_calls == 1, "TC-NETSOCK-301d the whole round issued exactly one SINT read");
|
||||
/* 缓存刷新环是另一回事:它对所有在用 fd 都要刷(单一事实源 I1) */
|
||||
CHECK(g_fxchip.rst_poll_calls == 3,
|
||||
"TC-NETSOCK-301e every round steps sock_reset for all 3 in-use fds (IDLE slots cost zero SPI)");
|
||||
(void)net_poll();
|
||||
CHECK(g_fxchip.rst_poll_calls == 6,
|
||||
"TC-NETSOCK-301f another round adds 3 more: the refresh loop is not gated by global bits (events vs state split)");
|
||||
reset_all();
|
||||
|
||||
/* 高位字节:Socket 4~7 的全局位在 bit8~bit11 */
|
||||
bring_up();
|
||||
for (i = 0; i < 5; i++) {
|
||||
(void)mk_established(NET_SOCK_STREAM);
|
||||
}
|
||||
prime_sint_masks();
|
||||
g_ns_int_pin_set = 0;
|
||||
ch395f_fake_push_glob((uint16_t)(1U << (4 + 4)));
|
||||
(void)net_poll();
|
||||
CHECK(g_fake.int_status[4] == 0U && g_fake.int_status[3] == (uint8_t)(0x01U << 3),
|
||||
"TC-NETSOCK-301g sockets 4-7 are located in the high byte bit(n+4)=bit8..11 (a wrong shift never sees their events)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-302 六种 SINT 位各自的分派与事件计数 */
|
||||
static void tc_302_poll_event_dispatch(void)
|
||||
{
|
||||
static const uint8_t bits[6] = {
|
||||
CH395F_SINT_STAT_CONNECT, CH395F_SINT_STAT_DISCONNECT, CH395F_SINT_STAT_RECV_OK,
|
||||
CH395F_SINT_STAT_SEND_OK, CH395F_SINT_STAT_SOCK_TIMEOUT, CH395F_SINT_STAT_SENBUF_FREE
|
||||
};
|
||||
int k;
|
||||
int fd;
|
||||
|
||||
for (k = 0; k < 6; k++) {
|
||||
int expect = (bits[k] == CH395F_SINT_STAT_SENBUF_FREE) ? 0 : 1;
|
||||
int n;
|
||||
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].send_ready = 0U;
|
||||
g_ns_int_pin_set = 0;
|
||||
g_fake.int_status[fd] = bits[k];
|
||||
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
|
||||
|
||||
n = net_poll();
|
||||
CHECK(n == expect, "TC-NETSOCK-302a event counting distinguishes bit kinds (SENBUF_FREE is a state bit, not an event)");
|
||||
switch (bits[k]) {
|
||||
case CH395F_SINT_STAT_CONNECT:
|
||||
CHECK(g_peers.evt_connect_calls == 1, "TC-NETSOCK-302b CONNECT -> handle_connect_event");
|
||||
break;
|
||||
case CH395F_SINT_STAT_DISCONNECT:
|
||||
CHECK(g_peers.evt_disconnect_calls == 1, "TC-NETSOCK-302c DISCONNECT -> handle_disconnect_event");
|
||||
break;
|
||||
case CH395F_SINT_STAT_RECV_OK:
|
||||
CHECK(g_peers.evt_recv_calls == 1 && s_net_socks[fd].readable == 1U,
|
||||
"TC-NETSOCK-302d RECV_OK -> handle_recv_event sets the readable level");
|
||||
break;
|
||||
case CH395F_SINT_STAT_SEND_OK:
|
||||
CHECK(g_peers.evt_send_calls == 1, "TC-NETSOCK-302e SEND_OK -> handle_send_event");
|
||||
break;
|
||||
case CH395F_SINT_STAT_SOCK_TIMEOUT:
|
||||
CHECK(g_peers.evt_timeout_calls == 1, "TC-NETSOCK-302f TIMEOUT -> handle_timeout_event");
|
||||
break;
|
||||
default:
|
||||
CHECK(s_net_socks[fd].send_ready == 1U && g_peers.evt_send_calls == 0,
|
||||
"TC-NETSOCK-302g SENBUF_FREE only sets send_ready (no event, never mistaken for SEND_OK)");
|
||||
break;
|
||||
}
|
||||
CHECK(g_peers.evt_connect_calls + g_peers.evt_disconnect_calls +
|
||||
g_peers.evt_recv_calls + g_peers.evt_send_calls +
|
||||
g_peers.evt_timeout_calls <= 1,
|
||||
"TC-NETSOCK-302h one incoming bit drives exactly one handler (no cross-talk)");
|
||||
}
|
||||
|
||||
/* 同一次 SINT 里多个位 → 多个 handler 都被叫到,事件数叠加 */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
g_ns_int_pin_set = 0;
|
||||
g_fake.int_status[fd] = (uint8_t)(CH395F_SINT_STAT_RECV_OK | CH395F_SINT_STAT_DISCONNECT);
|
||||
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
|
||||
CHECK(net_poll() == 2, "TC-NETSOCK-302i multiple event bits in one batch: each counted (the later bit is not swallowed)");
|
||||
CHECK(g_peers.evt_recv_calls == 1 && g_peers.evt_disconnect_calls == 1,
|
||||
"TC-NETSOCK-302j two bits each reach their own handler");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-303 PHY 变更位经驱动 feed 落到链路缓存(F-DRV-13 接线) */
|
||||
static void tc_303_phy_feed(void)
|
||||
{
|
||||
bring_up();
|
||||
g_ns_int_pin_set = 0;
|
||||
g_fx.phy_status = CH395F_PHY_DISCONN; /* 下一次 feed 读到断开 */
|
||||
ch395f_fake_push_glob(CH395F_GINT_STAT_PHY_CHANGE);
|
||||
(void)net_poll();
|
||||
CHECK(g_fake.link_up == 0U && g_fake.link_change == 1U,
|
||||
"TC-NETSOCK-303 PHY_CHANGE -> driver re-reads PHY and counts one link change (cable pull is no longer silent)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-310 监听门控 net_listener_armed 的三支放行与全部挡下形态 */
|
||||
static void tc_310_listener_armed(void)
|
||||
{
|
||||
int fd;
|
||||
|
||||
reset_all();
|
||||
CHECK(net_listener_armed(-1) == 0 && net_listener_armed(NET_MAX_SOCKETS) == 0,
|
||||
"TC-NETSOCK-310a out of range fd -> 0 (pure RAM read, no RPC)");
|
||||
CHECK(net_listener_armed(0) == 0, "TC-NETSOCK-310b unallocated fd -> 0");
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
CHECK(net_listener_armed(fd) == 1,
|
||||
"TC-NETSOCK-310c ESTABLISHED (pending-accept level) must pass: i104 bench proved starvation without it");
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_LISTENING;
|
||||
CHECK(net_listener_armed(fd) == 1,
|
||||
"TC-NETSOCK-310d LISTENING must pass: the missed-accept probe lives inside net_accept_locked, blocking it never runs");
|
||||
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_CLOSED;
|
||||
s_net_socks[fd].auto_relisten = 1U;
|
||||
s_net_socks[fd].local_port = 7003U;
|
||||
s_net_socks[fd].readable = 1U;
|
||||
CHECK(net_listener_armed(fd) == 1,
|
||||
"TC-NETSOCK-310e CLOSED plus auto_relisten plus leftover data -> pass (keeps closed-with-readable accept alive)");
|
||||
s_net_socks[fd].readable = 0U;
|
||||
CHECK(net_listener_armed(fd) == 0,
|
||||
"TC-NETSOCK-310f CLOSED with no leftover must be blocked (else every round in that window sends an RPC and the watchdog starves)");
|
||||
s_net_socks[fd].readable = 1U;
|
||||
s_net_socks[fd].auto_relisten = 0U;
|
||||
CHECK(net_listener_armed(fd) == 0, "TC-NETSOCK-310g CLOSED on a non-listener fd does not pass");
|
||||
s_net_socks[fd].auto_relisten = 1U;
|
||||
s_net_socks[fd].local_port = 0U;
|
||||
CHECK(net_listener_armed(fd) == 0, "TC-NETSOCK-310h a half-built listener without a bound port does not pass");
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_CONNECTING;
|
||||
CHECK(net_listener_armed(fd) == 0, "TC-NETSOCK-310i CONNECTING is not a listening state -> 0");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-311 recv 门控 net_can_read:非 ESTABLISHED 一律放行 */
|
||||
static void tc_311_can_read(void)
|
||||
{
|
||||
int fd;
|
||||
|
||||
reset_all();
|
||||
CHECK(net_can_read(0) == 1,
|
||||
"TC-NETSOCK-311a dead descriptor -> 1: let recv report BADF instead of hiding a missing connection");
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].readable = 0U;
|
||||
CHECK(net_can_read(fd) == 0, "TC-NETSOCK-311b ESTABLISHED with nothing readable -> 0");
|
||||
s_net_socks[fd].readable = 1U;
|
||||
CHECK(net_can_read(fd) == 1, "TC-NETSOCK-311c readable set -> 1");
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_CLOSED;
|
||||
s_net_socks[fd].readable = 0U;
|
||||
CHECK(net_can_read(fd) == 1,
|
||||
"TC-NETSOCK-311d connection no longer ESTABLISHED -> recv must pass once to fetch EOF or the error");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-320 close 的拆除序列:快路径(芯片已 CLOSED)与慢路径(走复位机) */
|
||||
static void tc_320_close_two_paths(void)
|
||||
{
|
||||
int fd;
|
||||
|
||||
/* 快路径:芯片已 CLOSED → 只清本 Socket 残留事件,不发复位 */
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
g_fake.cached_open[fd] = CH395F_SOCKET_CLOSED;
|
||||
CHECK(net_close(fd) == 0, "TC-NETSOCK-320a close succeeds (fast path)");
|
||||
CHECK(g_fxchip.cmd_done_calls >= 1 && g_fxchip.rst_begin_calls == 0,
|
||||
"TC-NETSOCK-320b already CLOSED -> reset skipped (saves a pointless 100-600ms occupation)");
|
||||
CHECK(g_fxchip.sock_events_calls >= 1,
|
||||
"TC-NETSOCK-320c the fast path still clears this socket leftover events (else the next connection sees a false immediate drop)");
|
||||
CHECK(g_fxchip.glob_calls == 0, "TC-NETSOCK-320d zero global reads throughout (F-NET-7)");
|
||||
reset_all();
|
||||
|
||||
/* 慢路径:芯片还 OPEN → 走 sock_reset,且必须是 GRACEFUL */
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
g_fake.cached_open[fd] = CH395F_SOCKET_OPEN;
|
||||
CHECK(net_close(fd) == 0, "TC-NETSOCK-320e close succeeds (slow path)");
|
||||
CHECK(g_fxchip.rst_begin_calls == 1 && g_fx.rst_mode[fd] == 0U,
|
||||
"TC-NETSOCK-320f slow path uses GRACEFUL (100ms floor for a normal release), not QUARANTINE");
|
||||
CHECK(g_fx.rst_state[fd] == (uint8_t)CH395F_FAKE_RST_IDLE,
|
||||
"TC-NETSOCK-320g the reset machine ends back at IDLE (slot reusable, never stranded)");
|
||||
reset_all();
|
||||
|
||||
/* 复位启动被拒(槽位已在运行)→ close 仍放行,只留痕 */
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
g_fake.cached_open[fd] = CH395F_SOCKET_OPEN;
|
||||
g_fx.rst_state[fd] = (uint8_t)CH395F_FAKE_RST_DRAINED;
|
||||
CHECK(net_close(fd) == 0, "TC-NETSOCK-320h close still succeeds when starting the reset fails (do not block the caller)");
|
||||
CHECK(g_fxchip.rst_begin_reject == 1 && g_ns_error_count >= 1,
|
||||
"TC-NETSOCK-320i a failed reset must leave an [ERR] line for the reconciler/pump to take over");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-321 drop_conn 用 QUARANTINE(异常释放语义),且不动监听簿记 */
|
||||
static void tc_321_drop_conn_settle(void)
|
||||
{
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].auto_relisten = 1U;
|
||||
s_net_socks[fd].readable = 1U;
|
||||
s_net_socks[fd].lost_conn_seen = 3U;
|
||||
g_fake.cached_open[fd] = CH395F_SOCKET_OPEN;
|
||||
|
||||
CHECK(net_drop_conn(fd) == 0, "TC-NETSOCK-321a drop_conn succeeds");
|
||||
CHECK(g_fxchip.rst_begin_calls == 1 && g_fx.rst_mode[fd] == 1U,
|
||||
"TC-NETSOCK-321b session drop uses QUARANTINE with a 400ms floor (TRAP-NET-18 abnormal release, graded against close)");
|
||||
CHECK(s_net_socks[fd].lost_conn_seen == 0U,
|
||||
"TC-NETSOCK-321c new session start: the missed-event debounce counter is cleared (N-30)");
|
||||
CHECK(s_net_socks[fd].local_port == 2404U && s_net_socks[fd].auto_relisten == 1U &&
|
||||
s_net_socks[fd].in_use == 1U,
|
||||
"TC-NETSOCK-321d port, listener attribute and fd all survive: this is drop-the-connection-not-the-listener");
|
||||
CHECK(g_peers.listener_req_calls == 1 &&
|
||||
g_peers.last_req_reason == (int)NET_LISTENER_REQ_DROP,
|
||||
"TC-NETSOCK-321e registers REQ_DROP; the rebuild action lives only in net_listener_tick");
|
||||
CHECK(s_net_socks[fd].readable == 0U && s_net_socks[fd].state == NET_SOCK_STATE_CLOSED,
|
||||
"TC-NETSOCK-321f session bookkeeping closes out: clear readable and move to CLOSED");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-322 close 前必先把挂起结算掉,再动芯片(顺序不可换) */
|
||||
static void tc_322_close_before_chip(void)
|
||||
{
|
||||
int fd;
|
||||
int close_at;
|
||||
int settle_at;
|
||||
char rxbuf[4];
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
g_fake.cached_open[fd] = CH395F_SOCKET_OPEN;
|
||||
/* 先登记一条挂起 recv:走 netTask 直调版 recv(无数据 + 阻塞 → 登记挂起) */
|
||||
g_ns_pump = NULL; /* 本场景不靠泵,直接看登记结果 */
|
||||
CHECK(net_recv_sock(&s_net_socks[fd], rxbuf, (int)sizeof(rxbuf), 0)
|
||||
== NET_PENDING_REGISTERED,
|
||||
"TC-NETSOCK-322p precondition: blocking recv registered a pending request (returns the sentinel)");
|
||||
ch395f_fake_clear_log();
|
||||
settle_at = g_peers.pending_settle_calls;
|
||||
close_at = g_chip.close_calls;
|
||||
|
||||
CHECK(net_close_sock(&s_net_socks[fd]) == 0, "TC-NETSOCK-322a netTask-direct close succeeds");
|
||||
CHECK(g_peers.pending_settle_calls == settle_at + 1,
|
||||
"TC-NETSOCK-322b close must first settle this fd pending recv");
|
||||
CHECK(g_chip.close_calls > close_at || g_fxchip.rst_close_calls > 0,
|
||||
"TC-NETSOCK-322c the chip only sees CLOSE after the settlement");
|
||||
CHECK(g_peers.pending_notified == 1,
|
||||
"TC-NETSOCK-322d settlement always notifies (cancel notifies too, else the waiter hangs forever)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
static void ns_run_s4(void)
|
||||
{
|
||||
tc_300_poll_gates();
|
||||
tc_301_poll_only_gated_sockets();
|
||||
tc_302_poll_event_dispatch();
|
||||
tc_303_phy_feed();
|
||||
tc_310_listener_armed();
|
||||
tc_311_can_read();
|
||||
tc_320_close_two_paths();
|
||||
tc_321_drop_conn_settle();
|
||||
tc_322_close_before_chip();
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
/*
|
||||
* 分片 5:建流三件套(listen / accept / connect)的锁定版决策 + net_init 下发面
|
||||
* + UDP 数据报路径 + netTask 直调入口
|
||||
*
|
||||
* 为什么值得单独一片:这三条是「真机极难按需复现」的决策分支——open 被芯片拒
|
||||
* (USE)、LISTEN 命令失败、TCP_CONNECT 被命令级拒绝、CONNECT 事件未收到的补 accept、
|
||||
* 对端「发完就 FIN」的 closed-with-readable,在台架上要凑齐对端行为+时序+总线状态
|
||||
* 才见得到一次;而每一条的错误形态都是「静默毁掉监听」或「数据被吞」(TRAP-NET-09/
|
||||
* -12/-18/-20 与 E14 复核实验的原文结论)。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
/* 断线注入点:第二次轮询时把状态打回 CLOSED(模拟 net_poll 处理 DISCONNECT 之后的形态)。
|
||||
* 用假内核现成的「每轮 net_poll 回调」注入并发事件,而不是自己发明时间或事件 API。 */
|
||||
static int s_ns_poll_seen;
|
||||
static void ns_drop_conn_on_second_poll(void)
|
||||
{
|
||||
s_ns_poll_seen++;
|
||||
if (s_ns_poll_seen == 2) {
|
||||
s_net_socks[0].state = NET_SOCK_STATE_CLOSED;
|
||||
s_net_socks[0].session_dead = 1U;
|
||||
}
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-400 net_init 的下发面与门控(一次调用钉十条契约) */
|
||||
static void tc_400_net_init_downlink(void)
|
||||
{
|
||||
uint8_t mac_zero[6] = {0, 0, 0, 0, 0, 0};
|
||||
uint8_t mac_real[6] = {0x02, 0x11, 0x22, 0x33, 0x44, 0x55};
|
||||
int i;
|
||||
|
||||
/* 1) 芯片不在位:直接失败,且不得继续下发任何参数 */
|
||||
reset_all();
|
||||
g_fx.exist_rc = (int)CH395F_STATUS_NOT_DETECTED;
|
||||
CHECK(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL) == -1,
|
||||
"TC-NETSOCK-400a chip not present -> -1");
|
||||
CHECK(g_fxchip.reset_calls == 0 && g_fxchip.init_calls == 0,
|
||||
"TC-NETSOCK-400b when absent it must not reset or initialise (do not drag the error into the stack)");
|
||||
CHECK(g_ns_error_count == 1, "TC-NETSOCK-400c an absent chip must leave an [ERR] line (else only a dead port is left to observe)");
|
||||
|
||||
/* 2) 协议栈初始化失败 */
|
||||
reset_all();
|
||||
g_fx.init_rc = (int)CH395F_STATUS_ERROR;
|
||||
CHECK(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL) == -1,
|
||||
"TC-NETSOCK-400d ch395f_init failure -> -1");
|
||||
CHECK(g_fxchip.ping_calls == 0, "TC-NETSOCK-400e after an init failure PING must not be enabled (half-configured state)");
|
||||
|
||||
/* 3) 版本门控:低于 0x46 必须**拒置** bit3(SOCKET_CLOSE),退回芯片自动关断 */
|
||||
reset_all();
|
||||
g_fx.version = 0x45U;
|
||||
CHECK(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL) == 0,
|
||||
"TC-NETSOCK-400f a too-old version is not fatal: bring-up continues");
|
||||
CHECK(g_fx.fun_para == 0U && g_ns_error_count == 1,
|
||||
"TC-NETSOCK-400g ver<0x46 -> FUN_PARA actually sends 0 (bit3 refused) plus [ERR], never silently degraded");
|
||||
|
||||
/* 4) 版本够新:FUN_PARA=0x08;bit1(TCP_SERVER 多连接) 与 bit0(保留) 都必须为 0 */
|
||||
reset_all();
|
||||
CHECK(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL) == 0,
|
||||
"TC-NETSOCK-400h normal initialisation succeeds");
|
||||
/* bit3=SOCKET_CLOSE 要置;bit1=TCP_SERVER 多连接(本项目单连接模型)与
|
||||
* bit0=保留位都必须为 0——旧实现把 0x09 当「多 Socket 模式」下发,2026-10-11
|
||||
* 定案为 0x08(bit0 是保留位,证据链 TRAP-NET-15)。所以掩码要考 bit0|bit1,
|
||||
* 拿 0x09 考会把我们正想要的 bit3 一起算进去(第一版就这么写错了)。 */
|
||||
CHECK(g_fx.fun_para == CH395F_FUN_PARA_SOCKET_CLOSE &&
|
||||
(g_fx.fun_para & 0x03UL) == 0UL,
|
||||
"TC-NETSOCK-400i FUN_PARA sets bit3 only, bit0/bit1 are 0 (0x09 was a historical mistake, TRAP-NET-15)");
|
||||
CHECK(g_fx.mss == (uint16_t)CH395F_TCP_MSS && g_fx.retrans_cnt == 8U &&
|
||||
g_fx.retrans_period == 200U,
|
||||
"TC-NETSOCK-400j MSS and retransmit parameters come from net_config.h truth (8 tries / 200ms, P0-a)");
|
||||
CHECK(g_fx.ka_idle == 60000U && g_fx.ka_intvl == 5000U && g_fx.ka_cnt == 3U,
|
||||
"TC-NETSOCK-400k KeepAlive 60s/5s/3 and it must be issued before ch395f_init");
|
||||
CHECK(g_fx.mss != 0U || g_fx.ka_idle != 0U ? 1 : 0, "TC-NETSOCK-400k2 parameters really were issued");
|
||||
CHECK(g_chip.log_n == 0 ||
|
||||
ch395f_fake_log_find('n') > ch395f_fake_log_find('Q'),
|
||||
"TC-NETSOCK-400l SET-class commands (MSS/KeepAlive) come before INIT_CH395 (manual hard requirement)");
|
||||
|
||||
/* 5) IP/掩码/网关三样都按点分顺位下发,并缓存本机 IP 供 getsockname 用 */
|
||||
CHECK(g_fx.ip[0] == 192U && g_fx.ip[1] == 168U && g_fx.ip[2] == 1U &&
|
||||
g_fx.ip[3] == 100U && g_fx.gw[3] == 1U && g_fx.mask[0] == 255U,
|
||||
"TC-NETSOCK-400m IP, gateway and mask follow the parse_ip_string order (segment 0 lands at [0])");
|
||||
CHECK(g_fxchip.set_dhcp_calls == 0 && g_fx.dhcp_enable == 0U,
|
||||
"TC-NETSOCK-400n with a static IP given, DHCP is never enabled");
|
||||
|
||||
/* 6) ip=NULL → 走 DHCP,不下发静态三样 */
|
||||
reset_all();
|
||||
(void)net_init(NULL, NULL, NULL, NULL);
|
||||
CHECK(g_fxchip.set_dhcp_calls == 1 && g_fx.dhcp_enable == 1U,
|
||||
"TC-NETSOCK-400o ip=NULL -> enables DHCP");
|
||||
CHECK(g_fxchip.set_ip_calls == 0 && g_fxchip.set_gw_calls == 0 &&
|
||||
g_fxchip.set_mask_calls == 0, "TC-NETSOCK-400p the DHCP form issues no static address at all");
|
||||
|
||||
/* 7) MAC:全零=用芯片内置,不下发;非零必须下发(写 EEPROM,须在 INIT 之前) */
|
||||
reset_all();
|
||||
(void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", mac_zero);
|
||||
CHECK(g_fxchip.set_mac_calls == 0, "TC-NETSOCK-400q all-zero MAC -> keep the chip built-in one, do not issue it");
|
||||
reset_all();
|
||||
(void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", mac_real);
|
||||
CHECK(g_fxchip.set_mac_calls == 1 && g_fx.mac[0] == 0x02U && g_fx.mac[5] == 0x55U,
|
||||
"TC-NETSOCK-400r a non-zero MAC is issued (device online identity stays consistent with the UI)");
|
||||
CHECK(ch395f_fake_log_find('M') < ch395f_fake_log_find('n') ||
|
||||
ch395f_fake_log_find('M') < 0,
|
||||
"TC-NETSOCK-400s SET_MAC comes BEFORE INIT_CH395 (TRAP-NET-19: reversed order does not take effect this power cycle)");
|
||||
|
||||
/* 8) 收发缓冲块布局:6 块/Socket,先配数据 Socket 再配监听 Socket 0(TRAP-NET-01) */
|
||||
reset_all();
|
||||
(void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL);
|
||||
CHECK(g_fxchip.set_recv_buf_calls == NET_MAX_SOCKETS &&
|
||||
g_fxchip.set_send_buf_calls == NET_MAX_SOCKETS,
|
||||
"TC-NETSOCK-400t all 8 sockets are explicitly reallocated (power-on defaults only cover 0-3)");
|
||||
for (i = 0; i < NET_MAX_SOCKETS; i++) {
|
||||
if (g_fx.recv_blk_start[i] != (uint8_t)(i * 6) ||
|
||||
g_fx.recv_blk_cnt[i] != (uint8_t)CH395F_RECV_BLOCKS ||
|
||||
g_fx.send_blk_start[i] != (uint8_t)(i * 6 + (int)CH395F_RECV_BLOCKS) ||
|
||||
g_fx.send_blk_cnt[i] != (uint8_t)CH395F_SEND_BLOCKS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(i == NET_MAX_SOCKETS,
|
||||
"TC-NETSOCK-400u layout recv=i*6 / send=recv+4, socket 7 uses exactly blocks 0..47 (manual 5.45)");
|
||||
CHECK(g_fx.ping_enable == 1U && g_fxchip.ping_calls == 1, "TC-NETSOCK-400v PING response enabled");
|
||||
|
||||
/* 9) net_init 的第一件事是取消挂起并通知调用方(顺序错了 = 调用方永久挂死) */
|
||||
reset_all();
|
||||
(void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL);
|
||||
CHECK(g_peers.pending_cancel_all_calls == 1 &&
|
||||
g_peers.send_cancel_all_calls == 1,
|
||||
"TC-NETSOCK-400w both pending tables are cancelled before a rebuild (recv and send each one, missing one strands a task)");
|
||||
CHECK(g_peers.last_pend_cancel_err == NET_ERR_CONNRESET &&
|
||||
g_peers.last_snd_cancel_err == NET_ERR_CONNRESET,
|
||||
"TC-NETSOCK-400x cancellation reason = CONNRESET");
|
||||
CHECK(g_peers.pending_capture_calls == 1,
|
||||
"TC-NETSOCK-400y registers the executing task (the pending self-wait guard judges who is executing from it)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-410 net_listen_locked:open 重试、清事件时序、失败对称关断 */
|
||||
static void tc_410_listen_locked(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
int fd;
|
||||
|
||||
/* 非 TCP 流 → INVAL */
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
CHECK(net_listen_locked(fd) == -1 && net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-410a UDP cannot listen");
|
||||
|
||||
/* 状态不对(还没 bind 也没到 CREATED)→ INVAL */
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
CHECK(net_listen_locked(fd) == -1 && net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-410b listening again on an established fd -> INVAL (never demote a data channel to a listener)");
|
||||
|
||||
/* 成功路径:BOUND → LISTENING,并启用 auto_relisten */
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
in.sin_port = net_htons(2404);
|
||||
CHECK(net_bind(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0, "TC-NETSOCK-410c bind precondition");
|
||||
ch395f_fake_clear_log();
|
||||
CHECK(net_listen_locked(fd) == 0, "TC-NETSOCK-410d listen succeeds");
|
||||
CHECK(s_net_socks[fd].state == NET_SOCK_STATE_LISTENING &&
|
||||
s_net_socks[fd].auto_relisten == 1U && s_net_socks[fd].lost_conn_seen == 0U,
|
||||
"TC-NETSOCK-410e enters LISTENING with auto_relisten=1 (BSD listener stays resident) and debounce cleared");
|
||||
CHECK(g_chip.set_port_calls == 1 && g_chip.last_port == 2404U,
|
||||
"TC-NETSOCK-410f source port comes from the control block local_port (the port must not drift)");
|
||||
CHECK(ch395f_fake_log_find('i') >= 0 &&
|
||||
ch395f_fake_log_find('i') < ch395f_fake_log_find('l'),
|
||||
"TC-NETSOCK-410g clearing this socket events comes BEFORE TCP_LISTEN (E14: reversed swallows the new CONNECT)");
|
||||
CHECK(g_fxchip.glob_calls == 0,
|
||||
"TC-NETSOCK-410h listen must never read the global interrupt register (exactly what the B3 audit removed)");
|
||||
|
||||
/* open 一直失败:重试 3 次,每次都走确定性排渣,最后 BUSY */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
g_fake.open_rc[fd] = CH395F_ERR_USE;
|
||||
CHECK(net_listen_locked(fd) == -1 && net_get_errno() == NET_ERR_BUSY,
|
||||
"TC-NETSOCK-410i open still failing after 3 tries -> BUSY");
|
||||
CHECK(g_chip.open_calls == 3 && g_fxchip.rst_begin_calls == 3,
|
||||
"TC-NETSOCK-410j three opens plus three sock_reset (TRAP-NET-09 retry semantics kept, not a blind 500ms wait)");
|
||||
CHECK(g_chip.listen_calls == 0, "TC-NETSOCK-410k with no successful open, never issue the LISTEN command");
|
||||
|
||||
/* LISTEN 命令失败:open 已成的 Socket 必须关回去(U12 对称,否则卡 OPEN 无监听) */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
g_fake.listen_rc[fd] = CH395F_ERR_VAL;
|
||||
CHECK(net_listen_locked(fd) == -1 && net_get_errno() == NET_ERR_BUSY,
|
||||
"TC-NETSOCK-410l TCP_LISTEN failure -> BUSY");
|
||||
CHECK(g_chip.close_calls == 1 && g_chip.open_calls == 1,
|
||||
"TC-NETSOCK-410m a failed LISTEN must close the opened socket (else every later open returns USE)");
|
||||
CHECK(s_net_socks[fd].state != NET_SOCK_STATE_LISTENING,
|
||||
"TC-NETSOCK-410n failure must not falsely report LISTENING");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-411 公开 net_listen:越界 / 队列 / 结果恢复 */
|
||||
static void tc_411_listen_public(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
CHECK(net_listen(-1) == -1 && net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-411a out of range -> BADF");
|
||||
CHECK(net_listen(NET_MAX_SOCKETS) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-411b upper bound out of range -> BADF");
|
||||
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
in.sin_port = net_htons(7003);
|
||||
CHECK(net_bind(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0, "TC-NETSOCK-411c bind");
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
CHECK(net_listen(fd) == 0, "TC-NETSOCK-411d end-to-end listen succeeds (enqueue, dispatch, notify value 0)");
|
||||
CHECK(s_net_socks[fd].state == NET_SOCK_STATE_LISTENING,
|
||||
"TC-NETSOCK-411e the public entry really drove the chip-side listener");
|
||||
CHECK(g_ns_notify_last_val == 0U,
|
||||
"TC-NETSOCK-411f a successful listen notify value is 0 (the 0x70000000 sentinel must not leak)");
|
||||
|
||||
/* 已监听的 fd 再 listen:netTask 里 INVAL,经通知值回传 */
|
||||
CHECK(net_listen(fd) == -1 && net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-411g listening twice -> -1 with errno INVAL (exact code, not a generic -1)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-420 accept:单连接模型的四种形态 */
|
||||
static void tc_420_accept_shapes(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
struct net_sockaddr *sa = (struct net_sockaddr *)∈
|
||||
int addrlen;
|
||||
int fd;
|
||||
|
||||
/* 1) 已有可接受连接:返回的就是监听 fd 自身(单连接),并回填对端 */
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
addrlen = (int)sizeof(in);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
CHECK(net_accept(fd, sa, &addrlen) == fd, "TC-NETSOCK-420a single connection: accept returns the listening fd itself");
|
||||
CHECK(in.sin_family == NET_AF_INET && net_ntohs(in.sin_port) == 51999U &&
|
||||
addrlen == (int)sizeof(in),
|
||||
"TC-NETSOCK-420b fills the peer address and length (lftpd prints who connected from this)");
|
||||
CHECK(g_peers.lost_probe_calls == 0,
|
||||
"TC-NETSOCK-420c non-LISTENING never calls the missed-accept probe (the probe is listener-only self-rescue)");
|
||||
|
||||
/* 2) LISTENING 且探针命中 → 接受(CONNECT 事件未收到的自愈路径) */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_LISTENING;
|
||||
s_net_socks[fd].local_port = 2404U;
|
||||
s_net_socks[fd].remote_port = 55555U;
|
||||
g_peers.lost_probe_hit = 1;
|
||||
addrlen = 0;
|
||||
CHECK(net_accept(fd, NULL, NULL) == fd,
|
||||
"TC-NETSOCK-420d the probe moved the listener to ESTABLISHED -> accept succeeds at once (N-30 root fix)");
|
||||
CHECK(g_peers.lost_probe_calls == 1,
|
||||
"TC-NETSOCK-420e only LISTENING calls the probe (once per accept round, with debounce)");
|
||||
|
||||
/* 3) LISTENING 且无连接 → WOULDBLOCK,且不动监听重建 */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_LISTENING;
|
||||
addrlen = (int)sizeof(in);
|
||||
CHECK(net_accept(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_WOULDBLOCK,
|
||||
"TC-NETSOCK-420f no connection -> WOULDBLOCK (never blocks netTask)");
|
||||
CHECK(g_peers.listener_req_calls == 0,
|
||||
"TC-NETSOCK-420g while the listener is alive, never register a rebuild (that is the U12 accident shape)");
|
||||
|
||||
/* 4) CLOSED + auto_relisten + RX 有残留 → 接受"已关但可读",先排空再 EOF */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_CLOSED;
|
||||
s_net_socks[fd].auto_relisten = 1U;
|
||||
s_net_socks[fd].local_port = 21U;
|
||||
s_net_socks[fd].remote_port = 44444U;
|
||||
ch395f_fake_push_recv_len((uint8_t)fd, 1027U);
|
||||
addrlen = (int)sizeof(in);
|
||||
CHECK(net_accept(fd, sa, &addrlen) == fd,
|
||||
"TC-NETSOCK-420h closed-with-readable: accept this connection to drain it (N-30, reversed yields 425)");
|
||||
CHECK(g_peers.listener_req_calls == 0,
|
||||
"TC-NETSOCK-420i with leftover data present, do NOT register a rebuild (relisting first destroys arrived data)");
|
||||
CHECK(g_chip.get_status_calls == 0,
|
||||
"TC-NETSOCK-420j the drain decision only reads GET_RECV_LEN, no status query");
|
||||
|
||||
/* 5) CLOSED + auto_relisten + RX 空 → 登记 REQ_ACCEPT 并 WOULDBLOCK */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_CLOSED;
|
||||
s_net_socks[fd].auto_relisten = 1U;
|
||||
s_net_socks[fd].local_port = 21U;
|
||||
CHECK(net_accept(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_WOULDBLOCK,
|
||||
"TC-NETSOCK-420k CLOSED with no leftover -> WOULDBLOCK");
|
||||
CHECK(g_peers.listener_req_calls == 1 &&
|
||||
g_peers.last_req_reason == (int)NET_LISTENER_REQ_ACCEPT &&
|
||||
g_peers.last_req_fd == fd,
|
||||
"TC-NETSOCK-420l only a consumer hitting empty registers an immediate rebuild (N-32 P2: removing it widens the CLOSED window)");
|
||||
CHECK(g_chip.open_calls == 0 && g_chip.listen_calls == 0,
|
||||
"TC-NETSOCK-420m the registration path costs zero chip transactions (rebuild lives only in net_listener_tick)");
|
||||
|
||||
/* 6) 非监听型 CLOSED(客户端)→ 不擅自登记 */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
s_net_socks[fd].state = NET_SOCK_STATE_CLOSED;
|
||||
s_net_socks[fd].auto_relisten = 0U;
|
||||
CHECK(net_accept(fd, sa, &addrlen) == -1 && g_peers.listener_req_calls == 0,
|
||||
"TC-NETSOCK-420n CLOSED on a client fd registers no listener rebuild");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-421 accept 的公开入口边界 */
|
||||
static void tc_421_accept_public(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
struct net_sockaddr *sa = (struct net_sockaddr *)∈
|
||||
int addrlen;
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
CHECK(net_accept(-1, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-421a out of range -> BADF");
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
addrlen = 0;
|
||||
CHECK(net_accept(fd, sa, NULL) == fd,
|
||||
"TC-NETSOCK-421b accept still works with addrlen NULL (address fill skipped, no crash)");
|
||||
addrlen = (int)sizeof(in);
|
||||
CHECK(net_accept(fd, NULL, &addrlen) == fd,
|
||||
"TC-NETSOCK-421c addr NULL: returns just the fd and fills no address");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-430 connect:地址簿记、open 重试、命令被拒回滚 */
|
||||
static void tc_430_connect(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
CHECK(net_connect(-1, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-430a out of range -> BADF");
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
CHECK(net_connect(fd, NULL, (int)sizeof(in)) == -1 && net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-430b addr=NULL -> INVAL (arguments blocked before enqueue)");
|
||||
CHECK(net_connect(fd, (struct net_sockaddr *)&in, 1) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-430c addrlen too small -> INVAL");
|
||||
CHECK(ns_rtos_q_count() == 0, "TC-NETSOCK-430d invalid arguments create no message (netTask is not occupied)");
|
||||
|
||||
/* 成功:地址簿记 + 大端顺位 + 状态 CONNECTING */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
in.sin_family = NET_AF_INET;
|
||||
in.sin_port = net_htons(8888);
|
||||
in.sin_addr.s_addr = net_inet_addr("192.168.1.200");
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0,
|
||||
"TC-NETSOCK-430e connect command issued (asynchronous, the interrupt decides success)");
|
||||
CHECK(s_net_socks[fd].state == NET_SOCK_STATE_CONNECTING,
|
||||
"TC-NETSOCK-430f local state moves to CONNECTING");
|
||||
CHECK(s_net_socks[fd].remote_port == 8888U,
|
||||
"TC-NETSOCK-430g peer port stored in bookkeeping as host order");
|
||||
CHECK(g_fx.des_ip[fd][0] == 192U && g_fx.des_ip[fd][1] == 168U &&
|
||||
g_fx.des_ip[fd][2] == 1U && g_fx.des_ip[fd][3] == 200U,
|
||||
"TC-NETSOCK-430h destination IP issued as a big-endian array (the order set_des_ip needs, reversed means no link)");
|
||||
CHECK(g_chip.set_port_calls == 1 && g_fx.des_port[fd] == 8888U,
|
||||
"TC-NETSOCK-430i source port issued once (SET_SOUR_PORT), the peer port goes via SET_DES_PORT=8888");
|
||||
CHECK(g_fxchip.sock_events_calls == 1 && g_fxchip.glob_calls == 0,
|
||||
"TC-NETSOCK-430j clearing stale interrupts only clears this socket (F-NET-7: the old extra global read welded others shut)");
|
||||
CHECK(ch395f_fake_log_find('v') < ch395f_fake_log_find('T') ||
|
||||
ch395f_fake_log_find('v') < 0,
|
||||
"TC-NETSOCK-430k clearing events precedes TCP_CONNECT (the two-stage pre-clear of TRAP-NET-12/-18)");
|
||||
|
||||
/* 命令级被拒(非 BUSY):必须回滚,不能停在 CONNECTING 干等 */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
g_fx.connect_rc[fd] = CH395F_ERR_VAL;
|
||||
CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-430l chip refused the command -> -1");
|
||||
CHECK(g_chip.close_calls == 1,
|
||||
"TC-NETSOCK-430m after a refusal the manual requires reopening from OPEN, so the socket is closed");
|
||||
CHECK(s_net_socks[fd].session_dead == 1U &&
|
||||
s_net_socks[fd].state == NET_SOCK_STATE_CLOSED,
|
||||
"TC-NETSOCK-430n a command-level failure also ends the session: sets session_dead and CLOSED (B2, else pending recv waits forever)");
|
||||
CHECK(g_ns_error_count >= 1, "TC-NETSOCK-430o a refusal must leave an [ERR] line");
|
||||
|
||||
/* BUSY 只是命令仍在执行,不得判失败 */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
g_fx.connect_rc[fd] = CH395F_ERR_BUSY;
|
||||
CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0,
|
||||
"TC-NETSOCK-430p BUSY means the chip is still executing -> keep the old behaviour and let the interrupt decide (do not kill a slow chip)");
|
||||
CHECK(s_net_socks[fd].state == NET_SOCK_STATE_CONNECTING && g_chip.close_calls == 0,
|
||||
"TC-NETSOCK-430q BUSY neither rolls back nor closes the socket");
|
||||
|
||||
/* open 三次失败 → BUSY,且不发 CONNECT */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
g_fake.open_rc[fd] = CH395F_ERR_USE;
|
||||
CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 &&
|
||||
net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-430r open failing 3 times -> BUSY");
|
||||
CHECK(g_chip.open_calls == 3 && g_fxchip.connect_calls == 0,
|
||||
"TC-NETSOCK-430s with no successful open, never issue TCP_CONNECT");
|
||||
|
||||
/* 非 TCP 流 / 未在用 → INVAL(netTask 侧再判一次) */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
CHECK(net_connect_locked(fd, (const struct net_sockaddr *)&in, (int)sizeof(in)) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-430t a UDP slot never takes the TCP connect path");
|
||||
CHECK(net_connect_locked(7, (const struct net_sockaddr *)&in, (int)sizeof(in)) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-430u in range but not in use -> INVAL (in_use is checked with the control block; BADF is only for range)");
|
||||
CHECK(net_connect_locked(NET_MAX_SOCKETS, (const struct net_sockaddr *)&in,
|
||||
(int)sizeof(in)) == -1, "TC-NETSOCK-430v out of range -> BADF");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-440 UDP 数据报:sendto 分块 / recvfrom 剥 8 字节报文头 */
|
||||
static void tc_440_udp_datagram(void)
|
||||
{
|
||||
static char big[2100];
|
||||
char buf[64];
|
||||
struct net_sockaddr_in in;
|
||||
struct net_sockaddr *sa = (struct net_sockaddr *)∈
|
||||
int addrlen;
|
||||
int fd;
|
||||
|
||||
bring_up_netctx();
|
||||
CHECK(net_sendto(-1, "x", 1, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-440a sendto out of range -> BADF");
|
||||
CHECK(net_sendto(0, "x", 1, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-440b not in use -> BADF");
|
||||
CHECK(net_recvfrom(0, buf, sizeof(buf), 0, NULL, NULL) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-440c recvfrom on an unused fd -> BADF");
|
||||
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
CHECK(net_sendto(fd, NULL, 5, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-440d sendto invalid arguments -> INVAL");
|
||||
CHECK(net_recvfrom(fd, NULL, 5, NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-440e recvfrom with a NULL buf -> INVAL");
|
||||
|
||||
/* 带目的地址:先设 DesIP/DesPort 再写数据(不带上限的自旋实现,健康链路一次写完) */
|
||||
reset_all();
|
||||
bring_up_netctx();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
in.sin_port = net_htons(8888);
|
||||
in.sin_addr.s_addr = net_inet_addr("192.168.1.9");
|
||||
CHECK(net_sendto(fd, "hello", 5, 0, sa, (int)sizeof(in)) == 5,
|
||||
"TC-NETSOCK-440f sendto returns the number of bytes sent");
|
||||
CHECK(g_fx.des_ip[fd][3] == 9U && g_fx.des_port[fd] == 8888U,
|
||||
"TC-NETSOCK-440g the destination address travels with this send (UDP is connectionless, address is per packet)");
|
||||
CHECK(g_chip.write_calls == 1 && s_net_socks[fd].send_bytes == 5U,
|
||||
"TC-NETSOCK-440h small packet written in one go plus statistics");
|
||||
|
||||
/* 大报分块(仍受 NET_SEND_CHUNK_MAX 上界约束) */
|
||||
reset_all();
|
||||
bring_up_netctx();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
g_ns_int_pin_set = 0;
|
||||
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
|
||||
ch395f_fake_set_sint_sticky((uint8_t)fd, CH395F_SINT_STAT_SENBUF_FREE);
|
||||
CHECK(net_sendto(fd, big, 2100, 0, NULL, 0) == 2100, "TC-NETSOCK-440i all 2100 bytes delivered");
|
||||
CHECK(g_chip.write_calls == 3, "TC-NETSOCK-440j UDP also chunks at the 1024 bound (the FIFO overrun constraint matches TCP)");
|
||||
|
||||
/* recvfrom:前 8 字节是芯片 UDP 头(长度/srcPort/srcIP),要剥掉再交出去 */
|
||||
reset_all();
|
||||
bring_up_netctx();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
ch395f_fake_push_recv_len((uint8_t)fd, 8U + 20U);
|
||||
addrlen = (int)sizeof(in);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen) == 20,
|
||||
"TC-NETSOCK-440k returns 20: the 8-byte report header was stripped, the header is not handed over as data");
|
||||
CHECK(addrlen == (int)sizeof(in) && in.sin_family == NET_AF_INET,
|
||||
"TC-NETSOCK-440l fills the source address structure and length");
|
||||
|
||||
/* 无数据 + 非阻塞 → WOULDBLOCK(阻塞形态是自旋无预算,本套刻意不测,见交付说明) */
|
||||
reset_all();
|
||||
bring_up_netctx();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
|
||||
net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440m non-blocking with no data -> WOULDBLOCK");
|
||||
s_net_socks[fd].blocking = 0U;
|
||||
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == -1 &&
|
||||
net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440n the non-blocking attribute follows the same decision");
|
||||
/* 短于 8 字节的残包:不剥头,整包交出去(钉当前实现的边界,不猜"应该怎么算") */
|
||||
reset_all();
|
||||
bring_up_netctx();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
ch395f_fake_push_recv_len((uint8_t)fd, 5U);
|
||||
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == 5,
|
||||
"TC-NETSOCK-440o with recv_len<8 no header is stripped, the whole packet is returned (current implementation)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-450 netTask 直调入口的 NULL 守卫与委托 */
|
||||
static void tc_450_direct_entries(void)
|
||||
{
|
||||
bring_up();
|
||||
CHECK(net_send_sock(NULL, "x", 1) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-450a net_send_sock(NULL) -> BADF");
|
||||
CHECK(net_recv_sock(NULL, (void *)"x", 1, 0) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-450b net_recv_sock(NULL) -> BADF");
|
||||
CHECK(net_close_sock(NULL) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-450c net_close_sock(NULL) -> BADF");
|
||||
|
||||
/* netTask 内直调 send:挂起登记会被"执行者自等待"守卫拒 → 返回 -1(BUSY) */
|
||||
reset_all();
|
||||
bring_up();
|
||||
{
|
||||
int fd = mk_established(NET_SOCK_STREAM);
|
||||
|
||||
s_net_socks[fd].send_ready = 0U;
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_net;
|
||||
g_peers.send_register_rc = -1; /* 复刻守卫:netTask 自己发的必被拒 */
|
||||
CHECK(net_send_sock(&s_net_socks[fd], "hello", 5) == -1,
|
||||
"TC-NETSOCK-450d inside netTask a stalled buffer never registers pending (nobody can complete it) -> -1");
|
||||
CHECK(g_peers.send_reg_calls == 1,
|
||||
"TC-NETSOCK-450e the registration call really happened (the refusal came from the register side, not from us not trying)");
|
||||
}
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-460 复位收口超时:芯片永远关不掉时上层必须有界退出(不得无限占用 netTask) */
|
||||
static void tc_460_reset_sync_timeout_bound(void)
|
||||
{
|
||||
uint32_t t_before;
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
g_fake.cached_open[fd] = CH395F_SOCKET_OPEN;
|
||||
g_fx.rst_keep_open[fd] = 1U; /* 复位机怎么努力芯片都不落回 CLOSED */
|
||||
t_before = g_ns_tick;
|
||||
|
||||
CHECK(net_close(fd) == 0,
|
||||
"TC-NETSOCK-460a even when the chip cannot be closed, close proceeds (fd-side bookkeeping must still close out)");
|
||||
CHECK((g_ns_tick - t_before) <= 700U,
|
||||
"TC-NETSOCK-460b the reset wait has a hard 600ms ceiling (without one a bad chip drags netTask down)");
|
||||
CHECK(g_ns_error_count >= 1,
|
||||
"TC-NETSOCK-460c a reset timeout must leave [ERR]: that line is the only way to ever notice it");
|
||||
CHECK(s_net_socks[fd].in_use == 0U,
|
||||
"TC-NETSOCK-460d the slot is released even on timeout (a chip state must not jam the local fd table)");
|
||||
reset_all();
|
||||
|
||||
/* drop_conn 同一条超时线 */
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].auto_relisten = 1U;
|
||||
g_fake.cached_open[fd] = CH395F_SOCKET_OPEN;
|
||||
g_fx.rst_keep_open[fd] = 1U;
|
||||
t_before = g_ns_tick;
|
||||
CHECK(net_drop_conn(fd) == 0 && (g_ns_tick - t_before) <= 700U &&
|
||||
g_ns_error_count >= 1,
|
||||
"TC-NETSOCK-460e drop_conn takes the same bounded close-out and logs it too");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-470 发送途中的并发事件:断线要按短写交出已发量,不能假装全成功 */
|
||||
static void tc_470_send_disconnect_midway(void)
|
||||
{
|
||||
static char big[2100];
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
/* 第一块写得进去,第二块等待期间对端断线(net_poll 处理 DISCONNECT 把状态打回 CLOSED) */
|
||||
g_ns_int_pin_set = 0;
|
||||
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
|
||||
g_fake.int_status[fd] = CH395F_SINT_STAT_DISCONNECT;
|
||||
g_ns_on_poll = ns_drop_conn_on_second_poll;
|
||||
s_ns_poll_seen = 0;
|
||||
|
||||
rc = net_send(fd, big, 2100, 0);
|
||||
|
||||
CHECK(rc >= 0 && rc < 2100,
|
||||
"TC-NETSOCK-470a a mid-send disconnect only truncates: returns bytes already sent (a short write), not -1 and not a false 2100");
|
||||
CHECK((int)s_net_socks[fd].send_bytes == rc,
|
||||
"TC-NETSOCK-470b byte statistic matches what was actually delivered (what the peer got cannot be clawed back)");
|
||||
CHECK(g_ns_error_count >= 1,
|
||||
"TC-NETSOCK-470c a short write must log [ERR] (a silent truncation once made the peer see zero bytes from a UI push)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
static void ns_run_s5(void)
|
||||
{
|
||||
tc_400_net_init_downlink();
|
||||
tc_410_listen_locked();
|
||||
tc_411_listen_public();
|
||||
tc_420_accept_shapes();
|
||||
tc_421_accept_public();
|
||||
tc_430_connect();
|
||||
tc_440_udp_datagram();
|
||||
tc_450_direct_entries();
|
||||
tc_460_reset_sync_timeout_bound();
|
||||
tc_470_send_disconnect_midway();
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
/*
|
||||
* 分片 6:边界残支(公开入口的越界/队满、直调入口的参数支、等待环里的其余出口)
|
||||
*
|
||||
* 这一片不为「覆盖率好看」而写:每条都对应一条**能被改坏**的分支——越界检查挪到
|
||||
* 入队之后,调用方就会在 netTask 里等到一个没人处理的 BADF;短写留痕被删掉,
|
||||
* 上层就会像 2026-09-21 那次 UI 推流一样「对端永久等待剩余字节」而日志全白。
|
||||
* 唯一一条刻意**不是**真实时序的场景是 TC-NETSOCK-530(一次轮询之间被压了 1s),
|
||||
* 它在真机上的对应形态是 netTask 被更高优先级任务压住——用大步进时基表达,
|
||||
* 并在注释里写明它考的是「停滞超时先于自旋预算生效」这条判定顺序。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
/* TC-NETSOCK-500 六个公开入口的越界形态:BADF 必须在**入队之前**由调用方侧挡下 */
|
||||
static void tc_500_public_bounds(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
char buf[8];
|
||||
int addrlen;
|
||||
|
||||
bring_up();
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
addrlen = (int)sizeof(in);
|
||||
|
||||
CHECK(net_send(NET_MAX_SOCKETS, buf, 4, 0) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-500a net_send out of range -> BADF");
|
||||
CHECK(net_recv(NET_MAX_SOCKETS, buf, 4, NET_MSG_DONTWAIT) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500b net_recv out of range -> BADF");
|
||||
CHECK(net_recv_to(NET_MAX_SOCKETS, buf, 4, 10U) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500c net_recv_to out of range -> BADF");
|
||||
CHECK(net_close(NET_MAX_SOCKETS) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-500d net_close out of range -> BADF");
|
||||
CHECK(net_drop_conn(NET_MAX_SOCKETS) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-500e net_drop_conn out of range -> BADF");
|
||||
CHECK(net_accept(NET_MAX_SOCKETS, (struct net_sockaddr *)&in, &addrlen) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500f net_accept out of range -> BADF");
|
||||
CHECK(net_connect(NET_MAX_SOCKETS, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500g net_connect out of range -> BADF");
|
||||
CHECK(net_sendto(NET_MAX_SOCKETS, buf, 4, 0, NULL, 0) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500h net_sendto out of range -> BADF");
|
||||
CHECK(net_recvfrom(NET_MAX_SOCKETS, buf, 4, NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500i net_recvfrom out of range -> BADF");
|
||||
CHECK(g_ns_put_calls == 0,
|
||||
"TC-NETSOCK-500j every out-of-range shape enqueues nothing (blocking after the enqueue hands netTask an ownerless request)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-501 队满时每个公开入口都要报 BUSY(不能有的报、有的静默) */
|
||||
static void tc_501_all_entries_busy_on_full_queue(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
|
||||
ns_rtos_q_fill((int)NS_Q_CAP);
|
||||
CHECK(net_accept(fd, NULL, NULL) == -1 && net_get_errno() == NET_ERR_BUSY,
|
||||
"TC-NETSOCK-501a net_accept on a full queue -> BUSY");
|
||||
ns_rtos_q_fill((int)NS_Q_CAP);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 &&
|
||||
net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-501b net_connect on a full queue -> BUSY");
|
||||
ns_rtos_q_fill((int)NS_Q_CAP);
|
||||
CHECK(net_recv_to(fd, (void *)"x", 1, 20U) == -1 && net_get_errno() == NET_ERR_BUSY,
|
||||
"TC-NETSOCK-501c net_recv_to on a full queue -> BUSY");
|
||||
ns_rtos_q_fill((int)NS_Q_CAP);
|
||||
CHECK(net_recv(fd, (void *)"x", 1, NET_MSG_DONTWAIT) == -1 &&
|
||||
net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-501d net_recv on a full queue -> BUSY");
|
||||
ns_rtos_q_fill((int)NS_Q_CAP);
|
||||
CHECK(net_send(fd, "x", 1, 0) == -1 && net_get_errno() == NET_ERR_BUSY,
|
||||
"TC-NETSOCK-501e net_send on a full queue -> BUSY");
|
||||
CHECK(g_ns_notify_calls == 0, "TC-NETSOCK-501f the full-queue path sends no notification (the caller leaves with the return value)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-502 fd 表用尽:NOMEM 且不影响已建立的会话 */
|
||||
static void tc_502_socket_table_full(void)
|
||||
{
|
||||
int i;
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
for (i = 0; i < NET_MAX_SOCKETS; i++) {
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
if (fd < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(i == NET_MAX_SOCKETS, "TC-NETSOCK-502a all 8 slots are allocatable");
|
||||
CHECK(net_socket(NET_AF_INET, NET_SOCK_STREAM) == -1 &&
|
||||
net_get_errno() == NET_ERR_NOMEM, "TC-NETSOCK-502b the ninth -> NOMEM (no out-of-range table write)");
|
||||
s_net_socks[3].state = NET_SOCK_STATE_ESTABLISHED;
|
||||
CHECK(net_get_sock(3) != NULL && net_listener_armed(3) == 1,
|
||||
"TC-NETSOCK-502c a full table does not disturb live sessions (alloc failure only returns NOMEM)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-503 net_wait_result 的两段式拾取:先零等待,已有通知就不必再睡 */
|
||||
static void tc_503_zero_wait_pickup(void)
|
||||
{
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
/* 预置一条**已挂起**的通知(真机上=上一条消息的回执还没被读走),
|
||||
* 公开入口应先零等待拾取它,而不是再睡一次(睡过头就是 实测踩过的坑) */
|
||||
ns_rtos_post_notify((TaskHandle_t)&g_ns_task_caller, (uint32_t)(int)4);
|
||||
g_ns_block_waits = 0;
|
||||
|
||||
rc = net_send(fd, "abcd", 4, 0);
|
||||
|
||||
CHECK(rc == 4, "TC-NETSOCK-503a the zero-wait peek takes the notify value -> returns the result immediately");
|
||||
CHECK(g_ns_block_waits == 0, "TC-NETSOCK-503b with a notification already pending it never enters the blocking branch");
|
||||
CHECK(g_peers.send_reg_calls == 0 && g_chip.write_calls == 0,
|
||||
"TC-NETSOCK-503c this call never touched the chip (the value peeked was the previous receipt, a shape we already hit in practice)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-504 直调入口的参数支(这些分支从公开入口不可达,只能直接钉) */
|
||||
static void tc_504_locked_entry_branches(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
in.sin_port = net_htons(80);
|
||||
in.sin_addr.s_addr = net_inet_addr("10.1.1.1");
|
||||
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
CHECK(net_connect_locked(fd, NULL, (int)sizeof(in)) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504a connect_locked with addr=NULL -> INVAL");
|
||||
CHECK(net_connect_locked(fd, (const struct net_sockaddr *)&in, 1) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504b connect_locked with a short addrlen -> INVAL");
|
||||
s_net_socks[fd].in_use = 1U;
|
||||
CHECK(net_send_locked(&s_net_socks[fd], "x", 1, NULL) == -1 &&
|
||||
net_get_errno() == NET_ERR_NOTCONN, "TC-NETSOCK-504c direct send on a not-established block -> NOTCONN");
|
||||
CHECK(net_recv_locked(&s_net_socks[fd], (void *)"x", 1, NET_MSG_DONTWAIT, 0U, NULL) == -1 &&
|
||||
net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-504d direct recv, no data, non-blocking -> WOULDBLOCK");
|
||||
CHECK(net_recv_locked(&s_net_socks[fd], NULL, 1, NET_MSG_DONTWAIT, 0U, NULL) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504e direct recv with a NULL buf -> INVAL");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-505 发送途中缓冲再也不腾出来:内层等待也要转挂起,并且末块不许登记 */
|
||||
static void tc_505_inner_wait_and_last_chunk(void)
|
||||
{
|
||||
static char big[2100];
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
/* 第一块写得进、之后芯片不再报空闲 → 第二块的**内层**等待转挂起 */
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
g_ns_int_pin_set = 0;
|
||||
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
|
||||
g_fake.int_status[fd] = CH395F_SINT_STAT_SENBUF_FREE; /* 只给一次空闲 */
|
||||
rc = net_send_locked(&s_net_socks[fd], big, 2100,
|
||||
(TaskHandle_t)&g_ns_task_caller);
|
||||
CHECK(rc == NET_PENDING_REGISTERED,
|
||||
"TC-NETSOCK-505a the inner wait (after writing one chunk) also converts to pending past budget, no in-place busy wait");
|
||||
CHECK(g_chip.write_calls == 2 && g_peers.last_snd_sent == 2048,
|
||||
"TC-NETSOCK-505b registered progress = bytes written into the chip (1024+1024=2048), delivered bytes are never resent");
|
||||
reset_all();
|
||||
|
||||
/* 末块:全部写完但芯片不回执 → 按完成返回,**不许**登记挂起
|
||||
* (2026-10-14 台架回归 TC-NET-902 正是登记了却无人完成 → 永久挂死) */
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
rc = net_send_locked(&s_net_socks[fd], big, 1024,
|
||||
(TaskHandle_t)&g_ns_task_caller);
|
||||
CHECK(rc == 1024, "TC-NETSOCK-505c a single chunk finishing exactly -> returns 1024 (complete)");
|
||||
CHECK(g_peers.send_reg_calls == 0,
|
||||
"TC-NETSOCK-505d the last-chunk wait must not register pending (the sent>=len guard has to hold)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-506 UDP sendto:等待腾出与短写留痕(与 TCP 同一套语义的另一份实现) */
|
||||
static void tc_506_sendto_stall_and_short(void)
|
||||
{
|
||||
static char big[2100];
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
bring_up_netctx();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
s_net_socks[fd].send_ready = 0U;
|
||||
g_ns_int_pin_set = 0;
|
||||
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
|
||||
/* 每轮只走 1ms:先命中 NET_SEND_TIMEOUT_MS(1s) 之前不会有别出口 → 短写 */
|
||||
g_ns_poll_tick_inc = 500U;
|
||||
rc = net_sendto(fd, big, 2100, 0, NULL, 0);
|
||||
CHECK(rc < 2100 && rc >= 0,
|
||||
"TC-NETSOCK-506a sendto stalled returns only what went out (short-write semantics match TCP)");
|
||||
CHECK(g_ns_error_count >= 2,
|
||||
"TC-NETSOCK-506b both the stall [ERR] and the short-write [ERR] must be there (missing one degrades into a silent truncation)");
|
||||
reset_all();
|
||||
|
||||
/* 写完一块后等发完的超时出口 */
|
||||
bring_up_netctx();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
g_ns_int_pin_set = 0;
|
||||
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
|
||||
g_fake.int_status[fd] = CH395F_SINT_STAT_SENBUF_FREE; /* 只给一次 */
|
||||
g_ns_poll_tick_inc = 500U;
|
||||
rc = net_sendto(fd, big, 2100, 0, NULL, 0);
|
||||
CHECK(rc >= 1024 && rc < 2100,
|
||||
"TC-NETSOCK-506c first chunk delivered and the second chunk wait timed out -> a value between 1024 and 2099");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-530 停滞超时优先于自旋预算:考判定顺序,不考真实时长 */
|
||||
static void tc_530_stall_timeout_before_spin(void)
|
||||
{
|
||||
static char buf[64];
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
/* 一次轮询之间被压了 2s(真机形态:netTask 被更高优先级任务压住),
|
||||
* 停滞超时(NET_SEND_TIMEOUT_MS=1s)必须**先于**自旋预算(2ms)命中:
|
||||
* 生产代码里两条判定的先后顺序就是这条断言的内容。 */
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].send_ready = 0U;
|
||||
g_ns_poll_tick_inc = 2000U;
|
||||
|
||||
rc = net_send_locked(&s_net_socks[fd], buf, (int)sizeof(buf),
|
||||
(TaskHandle_t)&g_ns_task_caller);
|
||||
CHECK(rc == 0,
|
||||
"TC-NETSOCK-530a the stall timeout returns the bytes sent so far, 0 (a short write, not -1)");
|
||||
CHECK(g_peers.send_reg_calls == 0,
|
||||
"TC-NETSOCK-530b the stall timeout wins over the spin budget: no pending registration (a swapped order would register)");
|
||||
CHECK(net_get_errno() == NET_ERR_TIMEDOUT,
|
||||
"TC-NETSOCK-530c errno = TIMEDOUT (lets the caller tell a stall from a disconnect)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-535 已经写出去一块之后的超时收口(内层等待的另一条出口) */
|
||||
static void tc_535_inner_timeout_after_chunk(void)
|
||||
{
|
||||
static char big[2100];
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
/* 第一块写得进(send_ready 初值 1),第二块的**内层**等待直接撞上停滞超时
|
||||
* (判定顺序:先超时后自旋,所以这里收口在超时支而不是登记挂起) */
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
g_ns_poll_tick_inc = 2000U;
|
||||
|
||||
rc = net_send_locked(&s_net_socks[fd], big, 2100,
|
||||
(TaskHandle_t)&g_ns_task_caller);
|
||||
|
||||
CHECK(rc == 1024, "TC-NETSOCK-535a the already delivered first chunk still counts (1024), the rest is a short write");
|
||||
CHECK(g_peers.send_reg_calls == 0,
|
||||
"TC-NETSOCK-535b the stall timeout closes out BEFORE any pending registration (swapping the order parks a pending that never completes)");
|
||||
CHECK(g_chip.write_calls == 1, "TC-NETSOCK-535c the chip was written exactly once: a delivered chunk must never be resent");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-536 net_recv_to 的三条回传分支(-2 / -1+errno / >=0) */
|
||||
static void tc_536_recv_to_three_exits(void)
|
||||
{
|
||||
char buf[16];
|
||||
int fd;
|
||||
|
||||
/* >=0:正常拿到数据 */
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
s_net_socks[fd].readable = 1U;
|
||||
ch395f_fake_push_recv_len((uint8_t)fd, 12U);
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
CHECK(net_recv_to(fd, buf, (int)sizeof(buf), 100U) == 12,
|
||||
"TC-NETSOCK-536a data available -> returns the actual byte count (the >=0 branch)");
|
||||
|
||||
/* <0 且非 TIMEDOUT:分派侧 BADF → 恢复 errno 后返回 -1 */
|
||||
reset_all();
|
||||
bring_up();
|
||||
(void)net_socket(NET_AF_INET, NET_SOCK_STREAM); /* 占掉 fd0,用未分配的 fd1 */
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
CHECK(net_recv_to(1, buf, (int)sizeof(buf), 100U) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-536b a real error -> -1 with errno restored from the notify value (strictly distinct from -2)");
|
||||
|
||||
/* ==TIMEDOUT:已在 TC-NETSOCK-224 钉过 -2;这里补一条「-2 不改写 errno」 */
|
||||
reset_all();
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
(void)net_set_errno(NET_OK);
|
||||
ns_peers_arm_pending_done(fd, NET_ERR_TIMEDOUT);
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
CHECK(net_recv_to(fd, buf, (int)sizeof(buf), 30U) == -2 &&
|
||||
net_get_errno() == NET_OK,
|
||||
"TC-NETSOCK-536c timeout is judged by the return code only and does NOT rewrite the errno slot (this branch sets none)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-537 fire_event:注册了回调才会被叫到(本层唯一的回调触发点在 SEND_OK) */
|
||||
static void tc_537_event_callback_path(void)
|
||||
{
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
fd = mk_established(NET_SOCK_STREAM);
|
||||
/* handle_send_event 是兄弟模块(net_events)的职责,本层只负责把中断交给它;
|
||||
* 回调是否触发属 net_events 的断言面(其单测 TC-NETEV 已覆盖),此处只钉
|
||||
* 「net_poll 不因回调未注册而跳过事件」。 */
|
||||
s_net_socks[fd].event_cb = NULL;
|
||||
g_ns_int_pin_set = 0;
|
||||
ch395f_fake_push_glob((uint16_t)(1U << (fd + 4)));
|
||||
g_fake.int_status[fd] = CH395F_SINT_STAT_SEND_OK;
|
||||
CHECK(net_poll() == 1, "TC-NETSOCK-537 events are counted even with no callback registered (a null pointer never skips dispatch)");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-540 close / drop_conn 的失败结果也要经通知值恢复精确 errno */
|
||||
static void tc_540_negative_results(void)
|
||||
{
|
||||
net_msg_t msg;
|
||||
int fd;
|
||||
|
||||
/* close 的负结果支:net_close_locked 自身恒返 0(复位没成也放行,见 TC-NETSOCK-320h),
|
||||
* 所以 CLOSE 唯一的负值来自**分派侧**的 net_get_sock 判空 → 通知值 -2 →
|
||||
* 调用方在自己的槽位里恢复成 BADF。范围合法但未分配的 fd 正好走这条。 */
|
||||
bring_up();
|
||||
fd = 3; /* 范围内、未分配 */
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller;
|
||||
CHECK(net_close(fd) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-540a close on an unallocated fd -> -1 with errno restored to BADF from the notify value, not a generic NET_ERR");
|
||||
CHECK(net_drop_conn(fd) == -1 && net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-540a2 drop_conn takes the same negative-value restoration branch");
|
||||
reset_all();
|
||||
|
||||
/* drop_conn 同理:用未分配 fd 走分派侧 BADF 后再由调用方恢复 */
|
||||
bring_up();
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_DROP_CONN;
|
||||
msg.sockfd = 1;
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_net;
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages();
|
||||
CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF,
|
||||
"TC-NETSOCK-540b drop_conn BADF from the dispatch side travels back on the notify value");
|
||||
|
||||
/* RECV 的 BADF 支(分派侧 net_get_sock 判空) */
|
||||
reset_all();
|
||||
bring_up();
|
||||
(void)memset(&msg, 0, sizeof(msg));
|
||||
msg.type = NET_MSG_RECV;
|
||||
msg.sockfd = 4;
|
||||
msg.buf = (void *)"x";
|
||||
msg.len = 1;
|
||||
msg.caller = (TaskHandle_t)&g_ns_task_net;
|
||||
(void)ns_rtos_q_put_raw(&msg);
|
||||
net_process_messages();
|
||||
CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF,
|
||||
"TC-NETSOCK-540c recv BADF from the dispatch side likewise travels back on the notify value");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-545 未绑队列时的 sendto/recvfrom/getpeername 等纯本地入口 */
|
||||
static void tc_545_local_entries_without_queue(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
int addrlen = (int)sizeof(in);
|
||||
|
||||
reset_all();
|
||||
CHECK(net_getsockname(0, (struct net_sockaddr *)&in, &addrlen) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-545a even uninitialised it safely reports BADF (no crash)");
|
||||
CHECK(net_getpeername(0, (struct net_sockaddr *)&in, &addrlen) == -1,
|
||||
"TC-NETSOCK-545b getpeername behaves the same");
|
||||
CHECK(net_inet_addr("1.2.3.4") != 0U, "TC-NETSOCK-545c pure helpers do not depend on initialisation");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
|
||||
/* ============= TC-NETSOCK-550 s_addr 同族一致性(字节序统一后的收口断言) =============
|
||||
*
|
||||
* 这次字节序统一的实质不是「给 inet_addr 换个数值」,而是「全仓只剩一套 s_addr 约定」。
|
||||
* 所以这组断言全部写在**跨函数的接缝**上:解析→存储→芯片下发→回读→打印,
|
||||
* 每一环都拿同一句点分十进制做基准。任何一处又引入第二套顺位,这里必红。
|
||||
*/
|
||||
|
||||
/* TC-NETSOCK-550 解析 → 存储 → 打印 闭合(getsockname / getpeername 同族可比对) */
|
||||
static void tc_550_family_roundtrip(void)
|
||||
{
|
||||
struct net_sockaddr_in in;
|
||||
struct net_sockaddr *sa = (struct net_sockaddr *)∈
|
||||
char txt[16];
|
||||
int addrlen;
|
||||
int fd;
|
||||
|
||||
bring_up();
|
||||
|
||||
/* 1) getsockname 的 s_addr 与 inet_addr 解析结果**直接相等**(旧实现差一次换端) */
|
||||
(void)mk_established(NET_SOCK_STREAM); /* getsockname 要 in_use 的描述符 */
|
||||
addrlen = (int)sizeof(in);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
CHECK(net_getsockname(0, sa, &addrlen) == 0,
|
||||
"TC-NETSOCK-550a getsockname precondition ok");
|
||||
CHECK(in.sin_addr.s_addr == net_inet_addr("192.168.1.100"),
|
||||
"TC-NETSOCK-550b getsockname s_addr is the same family as inet_addr (direct equality)");
|
||||
CHECK(strcmp(net_inet_ntoa(in.sin_addr.s_addr, txt), "192.168.1.100") == 0,
|
||||
"TC-NETSOCK-550c local address closes parse-store-print (old code printed 100.1.168.192)");
|
||||
|
||||
/* 2) connect 的对端簿记同样闭合(lftpd:864 与 104 显示走的就是这条链) */
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
in.sin_family = NET_AF_INET;
|
||||
in.sin_port = net_htons(2404U);
|
||||
in.sin_addr.s_addr = net_inet_addr("192.168.1.200");
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_net;
|
||||
CHECK(net_connect_locked(fd, sa, (int)sizeof(in)) == 0,
|
||||
"TC-NETSOCK-550d connect precondition ok");
|
||||
addrlen = (int)sizeof(in);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
CHECK(net_getpeername(fd, sa, &addrlen) == 0,
|
||||
"TC-NETSOCK-550e getpeername ok (reads event cache, zero SPI)");
|
||||
CHECK(in.sin_addr.s_addr == net_inet_addr("192.168.1.200"),
|
||||
"TC-NETSOCK-550f getpeername peer equals the s_addr fed into net_connect");
|
||||
CHECK(strcmp(net_inet_ntoa(in.sin_addr.s_addr, txt), "192.168.1.200") == 0,
|
||||
"TC-NETSOCK-550g peer address prints right (lftpd connection-received log depends on it)");
|
||||
CHECK(s_net_socks[fd].remote_ip_arr[0] == 192U &&
|
||||
s_net_socks[fd].remote_ip_arr[3] == 200U,
|
||||
"TC-NETSOCK-550h chip des_ip array stays wire order [192,168,1,200]: on-wire behaviour unchanged");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
/* TC-NETSOCK-555 UDP 报文头里的源地址拆装(同族的第四个生产者:net_recvfrom) */
|
||||
static void tc_555_udp_src_addr(void)
|
||||
{
|
||||
/* CH395F UDP 头(§9.2.4):[0-1] 总长(LE) [2-3] 源端口(LE) [4-7] 源 IP(线序) */
|
||||
static const uint8_t hdr[8] = {
|
||||
11U, 0U, /* 总长 = 8 头 + 3 数据 */
|
||||
0x1FU, 0xCBU, /* 源端口 51999(小端 1F CB) */
|
||||
239U, 1U, 2U, 3U /* 源 IP 线序:239.1.2.3 */
|
||||
};
|
||||
struct net_sockaddr_in in;
|
||||
struct net_sockaddr *sa = (struct net_sockaddr *)∈
|
||||
char buf[16];
|
||||
char txt[16];
|
||||
int addrlen;
|
||||
int fd;
|
||||
|
||||
bring_up_netctx();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
ch395f_fake_push_recv_len((uint8_t)fd, (uint16_t)(8U + 3U));
|
||||
ch395f_fake_set_recv_bytes(hdr, (uint16_t)sizeof(hdr));
|
||||
|
||||
addrlen = (int)sizeof(in);
|
||||
(void)memset(&in, 0, sizeof(in));
|
||||
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, sa, &addrlen) == 3,
|
||||
"TC-NETSOCK-555a strips the 8-byte chip header and delivers only the 3-byte payload");
|
||||
CHECK(in.sin_addr.s_addr == net_inet_addr("239.1.2.3"),
|
||||
"TC-NETSOCK-555b source IP unpack matches the inet_addr family");
|
||||
CHECK(in.sin_port == net_htons(51999U),
|
||||
"TC-NETSOCK-555c source port read chip little-endian then stored in network order");
|
||||
CHECK(addrlen == (int)sizeof(in) && net_ntohs(in.sin_port) == 51999U,
|
||||
"TC-NETSOCK-555d addrlen filled and port round-trips to 51999");
|
||||
CHECK(strcmp(net_inet_ntoa(in.sin_addr.s_addr, txt), "239.1.2.3") == 0,
|
||||
"TC-NETSOCK-555e multicast source address prints correctly");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
|
||||
/* TC-NETSOCK-560 UDP 上下文守卫:非执行任务不得直调芯片
|
||||
* 三条一起才成立,缺一条都不算把危险面关掉:
|
||||
* ① 执行任务内照常成功(守卫不是"把 UDP 判死")
|
||||
* ② 非执行任务被拒且**一次芯片事务都没有**(拒了却已经抢过 SPI = 白拒)
|
||||
* ③ 正常错误路径不被守卫吞掉,且**走消息队列的接口完全不受影响**
|
||||
* (那些接口生来就是给非 netTask 用的,误伤它们才是大事故)
|
||||
*/
|
||||
static void tc_560_udp_context_guard(void)
|
||||
{
|
||||
char buf[8];
|
||||
int fd;
|
||||
|
||||
/* ① 执行任务上下文:与修复前逐字相同 */
|
||||
bring_up_netctx();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == 3,
|
||||
"TC-NETSOCK-560a net_sendto unchanged inside the executing task");
|
||||
reset_all();
|
||||
|
||||
/* ② 非执行任务:拒绝 + 零芯片事务(bring_up 里 net_init 是以 netTask 身份跑的,
|
||||
* 此刻 g_ns_cur_task 已还原成 caller,正是"别的任务") */
|
||||
bring_up();
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
g_chip.write_calls = 0;
|
||||
g_chip.read_calls = 0;
|
||||
g_fxchip.set_des_ip_calls = 0;
|
||||
g_fxchip.glob_calls = 0;
|
||||
g_ns_poll_calls = 0;
|
||||
g_ns_error_count = 0;
|
||||
|
||||
CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == -1 &&
|
||||
net_get_errno() == NET_ERR_NOT_IN_NETTASK,
|
||||
"TC-NETSOCK-560b non-executing task gets NET_ERR_NOT_IN_NETTASK (exact code, not -1)");
|
||||
CHECK(g_chip.write_calls == 0 && g_fxchip.set_des_ip_calls == 0 &&
|
||||
g_ns_poll_calls == 0,
|
||||
"TC-NETSOCK-560c clean refusal: zero writes, zero set_des_ip, zero net_poll rounds");
|
||||
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
|
||||
net_get_errno() == NET_ERR_NOT_IN_NETTASK,
|
||||
"TC-NETSOCK-560d net_recvfrom guarded the same way");
|
||||
CHECK(g_chip.read_calls == 0 && g_fxchip.glob_calls == 0 && g_ns_poll_calls == 0,
|
||||
"TC-NETSOCK-560e recvfrom touched no chip and no global interrupt register either");
|
||||
CHECK(g_ns_error_count == 2,
|
||||
"TC-NETSOCK-560f each refusal logs one ERR line (silence hides the wrong task)");
|
||||
|
||||
/* ③ 正常错误路径优先:守卫排在参数校验之后 */
|
||||
CHECK(net_sendto(-1, "abc", 3, 0, NULL, 0) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-560g out-of-range fd still reports BADF first");
|
||||
CHECK(net_sendto(fd, NULL, 3, 0, NULL, 0) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL,
|
||||
"TC-NETSOCK-560h NULL buf still reports INVAL, the guard does not swallow it");
|
||||
CHECK(net_recvfrom(fd, NULL, 3, NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
|
||||
net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-560i recvfrom keeps the same precedence");
|
||||
|
||||
/* ④ 走消息队列的接口不受守卫影响:同一个非 netTask 上下文照常能把活干完 */
|
||||
CHECK(net_close(fd) == 0,
|
||||
"TC-NETSOCK-560j net_close from a non-executing task still works (queue path untouched)");
|
||||
/* 执行完的证据用"收到回执 + 槽位释放",不看 close_calls:芯片此刻已是
|
||||
* CLOSED,走的是快路径(不再补发 CLOSE,见 TC-NETSOCK-320a/b),拿它当证据
|
||||
* 会把一条正确的快路径判成失败 */
|
||||
CHECK(s_net_socks[fd].in_use == 0U && g_ns_notify_calls >= 1,
|
||||
"TC-NETSOCK-560k queue path really completed (receipt seen and slot released)");
|
||||
reset_all();
|
||||
|
||||
/* ⑤ 未捕获执行任务(net_init 没跑):守卫不抢先,仍由参数/状态检查给确定错误 */
|
||||
reset_all();
|
||||
g_ns_pump = net_process_messages;
|
||||
CHECK(net_sendto(0, "abc", 3, 0, NULL, 0) == -1 &&
|
||||
net_get_errno() == NET_ERR_BADF,
|
||||
"TC-NETSOCK-560l before net_init sendto reports BADF, no invented context error");
|
||||
|
||||
/* 未捕获执行任务、但 socket 确实在用时,守卫**选择放行**:宁可不做判定,也不给
|
||||
* 一个无从判定的场景编造错误码。放行后走的是完全正常的执行路径(这里 DONTWAIT
|
||||
* 无数据 → WOULDBLOCK),证明守卫不是"没初始化就一律拒绝"的粗暴实现。
|
||||
* 刻意用 recvfrom 而不是 sendto:未初始化时 net_poll 直接短路返回、时间基准
|
||||
* 不前进,sendto 的等待环永远等不到超时(真机上不可达:net_socket 之后还有
|
||||
* NET_READY 屏障,但这条差异值得记一笔,见交付报告)。 */
|
||||
reset_all();
|
||||
g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; /* net_init 没跑:判据恒 NULL */
|
||||
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
g_fxchip.glob_calls = 0;
|
||||
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 &&
|
||||
net_get_errno() == NET_ERR_WOULDBLOCK,
|
||||
"TC-NETSOCK-560m guard stays open when the executing task was never captured");
|
||||
CHECK(net_get_errno() != NET_ERR_NOT_IN_NETTASK && g_fxchip.glob_calls == 0,
|
||||
"TC-NETSOCK-560n no context error invented and no global register read");
|
||||
reset_all();
|
||||
}
|
||||
|
||||
static void ns_run_s6(void)
|
||||
{
|
||||
tc_500_public_bounds();
|
||||
tc_501_all_entries_busy_on_full_queue();
|
||||
tc_502_socket_table_full();
|
||||
tc_503_zero_wait_pickup();
|
||||
tc_504_locked_entry_branches();
|
||||
tc_505_inner_wait_and_last_chunk();
|
||||
tc_506_sendto_stall_and_short();
|
||||
tc_530_stall_timeout_before_spin();
|
||||
tc_535_inner_timeout_after_chunk();
|
||||
tc_536_recv_to_three_exits();
|
||||
tc_537_event_callback_path();
|
||||
tc_540_negative_results();
|
||||
tc_545_local_entries_without_queue();
|
||||
tc_550_family_roundtrip();
|
||||
tc_555_udp_src_addr();
|
||||
tc_560_udp_context_guard();
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""NET 套接字核心(net_socket)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/net_socket/net_socket_test.c(以 #include 纳入生产源
|
||||
Drivers/BSP/NET/net_socket.c + 共享假芯片 test/host/common/ch395f_fake.c +
|
||||
本目录的假内核 ns_rtos.c 与兄弟模块桩 ns_peers.c),在 PC 上离线验证:
|
||||
跨上下文消息全分派、结果经任务通知值回传、每任务错误码槽(E-03)、
|
||||
监听门控与 closed-with-readable、net_poll 的中断分派与"只读自己 SINT",
|
||||
以及读写路径的短写 / 挂起登记 / EOF / 命令被拒回滚。
|
||||
工具链与判定委托 test/host/common/unit_test_harness.py。
|
||||
|
||||
先跑常量闸(本 runner 独有的第二道检查,见 check_fake_chip_constants 注释),
|
||||
不干净直接不编译——编译错误会把"假件值漂移"这件事埋得很深。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
net_socket.c 是本产品**唯一的 SPI 串行化点**:所有 net_* 公开 API 把请求按值拷进
|
||||
消息队列,由 netTask 串行执行,结果只能经 xTaskNotify 的 32 位通知值回来
|
||||
(net_msg_t 是按值入队的,netTask 改自己那份副本,调用方永远看不到——AGENTS
|
||||
模块约定明写"勿给 net_msg_t 加回传字段")。这条链的正确性取决于两个任务的
|
||||
交错时序,真机上"通知丢了"表现为永久挂死并被看门狗复位,日志里只剩一条
|
||||
[ERR];而错误码本身还按任务隔离(E-03),跨任务覆写只会让排障读到别人的码。
|
||||
把队列/通知/时基做成显式量后,"登记了挂起却仍发即时回执"这种缺陷在 PC 上
|
||||
一次就能钉死。
|
||||
|
||||
桩的边界(每一层为什么这么切):
|
||||
- 真实头:net_socket.h / net_types.h / net_config.h / net_pending.h /
|
||||
net_listener.h / net_events.h / cmsis_os2.h(只依赖 stdint+stddef,宿主可编)
|
||||
→ 字段名、枚举取值、队列原型一改即编译期报错;
|
||||
- 假芯片:test/host/common/ch395f_fake.h/.c(NET 域共享件,本批只做**纯新增**扩展:
|
||||
net_socket.c 这一层才会调的初始化下发类 + 全局中断 0x19 + sock_reset 复位机 +
|
||||
cmd_done 判据;既有宏/字段/函数一字未动);
|
||||
- 假内核:本目录 ns_rtos.c(osMessageQueue 环形队列 + 任务通知 + 时基 +
|
||||
临界区计数);**阻塞等待 = 调度点**:xTaskNotifyWait(portMAX_DELAY) 里以
|
||||
netTask 身份跑一轮 net_process_messages(),于是单次公开 API 调用能跑完整条
|
||||
入队→分派→通知值回传→调用方恢复 errno 的链;
|
||||
- 兄弟模块桩:本目录 ns_peers.c(net_listener/net_pending/net_events 的边界契约
|
||||
+ 调用记录)——这三个模块各有自己的主机单测(34/34/43 条),本套只测
|
||||
net_socket.c 自己的决策,并把"该不该登记、登记哪个 reason"变成可断言面;
|
||||
- main.h / FreeRTOS.h / task.h / dbg_log.h / test_config.h / net_latency_test.h
|
||||
是本目录桩;刻意不定义 TEST_LATENCY_PROBE 与 NET_BENCH_CMD_LOG。
|
||||
- 不需要 #undef s_addr:本套不 include ff.h(唯一会经 Windows 支路拉进 winsock.h
|
||||
并把 s_addr 定义成 S_un.S_addr 的来源),真实 net_types.h 用的是自有的
|
||||
struct net_in_addr 镜像类型,不受污染。
|
||||
|
||||
用法(仓库根目录):
|
||||
python test/host/net_socket/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败(含常量闸不过)/ 2=环境错误。
|
||||
|
||||
设计依据:docs/网络/网络自愈架构设计.md §4.10、docs/测试说明/主机单测覆盖率口径.md §9
|
||||
创建日期:2026-10-24
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
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 / FreeRTOS.h / task.h / main.h / dbg_log.h);
|
||||
# CMSIS_RTOS_V2 提供**真实** cmsis_os2.h;common 提供共享假芯片;NET 目录给真实
|
||||
# net_socket.h / net_types.h / net_pending.h / net_listener.h / net_events.h;HERE 放本模块头。
|
||||
BUILD = TestBuild(
|
||||
name="NET Socket Tests",
|
||||
src=os.path.join(HERE, "net_socket_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "Middlewares", "Third_Party", "FreeRTOS", "Source",
|
||||
"CMSIS_RTOS_V2"),
|
||||
os.path.join(ROOT, "test", "host", "common"),
|
||||
os.path.join(ROOT, "Drivers", "BSP", "NET"),
|
||||
HERE,
|
||||
],
|
||||
# /wd4244:生产源里 ch395f_set_*(sockfd, ...) 一律传 int fd 给 uint8_t 形参,
|
||||
# ARMCC 不报、MSVC /W4 每条都报一遍(20+ 条)——那是宿主与原编译器的诊断差异,
|
||||
# 不是本批要盯的问题,屏蔽以保持门禁输出可读(不改生产代码去"消警")。
|
||||
extra_flags=["/wd4244", "/wd4100"],
|
||||
summary_marker="=== NET Socket Tests:",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 常量闸:真头与假件的**同名** CH395F_* 常量数值必须逐一对齐
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# 为什么必须有这根闸(2026-10-24 实测漂移):假件头自称"值抄自真实头,勿顺手改",
|
||||
# 实际 CH395F_SOCKET_OPEN 写成 0x01(真值 0x05)、CH395F_TCP_ESTABLISHED 写成 0x05
|
||||
# (真值 0x04)。host 编译里生产码看到的也是假宏,宏对宏自洽 → 三套全绿也照不出来。
|
||||
# 谁一旦写**数字**断言或从假件抄常量,就拿到了与真芯片不同的值。
|
||||
#
|
||||
# 为什么只能文本核对:两个头在 host 编译里是互斥的(stub/ch395f.h 转发到假件),
|
||||
# 跨头 _Static_assert 不可能同时看到两份定义 → 编译期闸不存在。
|
||||
#
|
||||
# 只做同名常量的数值比对,且**只比对两边都是纯十六进制/十进制字面量的**:
|
||||
# 宏链(CH395F_SEND_BUF_SIZE_BYTES 那种引用其它宏的)跳过——它们由结构而不是值约束。
|
||||
_DEFINE_RE = re.compile(r"^\s*#\s*define\s+(CH395F_[A-Z0-9_]+)\s+([^\s/]+)")
|
||||
_NUM_RE = re.compile(r"^(0[xX][0-9a-fA-F]+|[0-9]+)(U|UL|ULL)?$")
|
||||
|
||||
|
||||
def parse_consts(path):
|
||||
"""{CH395F_xxx: int},只收字面量取值的宏;文件不存在返回 None。"""
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
out = {}
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
for line in f:
|
||||
m = _DEFINE_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
name, val = m.group(1), m.group(2)
|
||||
if not _NUM_RE.match(val):
|
||||
continue
|
||||
digits = re.sub(r"[uUlL]+$", "", val)
|
||||
try:
|
||||
num = int(digits, 0)
|
||||
except ValueError:
|
||||
continue
|
||||
out.setdefault(name, (num, line.rstrip("\n")))
|
||||
except OSError:
|
||||
return None
|
||||
return out
|
||||
|
||||
|
||||
def check_fake_chip_constants(verbose=True):
|
||||
"""返回不一致的 (名字, 真头值, 假件值) 列表;两侧任一头读不到按环境问题报告。"""
|
||||
real = parse_consts(os.path.join(ROOT, "Drivers", "BSP", "CH395F", "ch395f.h"))
|
||||
fake = parse_consts(os.path.join(HERE, os.pardir, "common", "ch395f_fake.h"))
|
||||
if real is None:
|
||||
return None
|
||||
if fake is None:
|
||||
return None
|
||||
drift = []
|
||||
for name, (val, _line) in sorted(real.items()):
|
||||
if name in fake and fake[name][0] != val:
|
||||
drift.append((name, val, fake[name][0]))
|
||||
if verbose and drift:
|
||||
print("[DRIFT] 假件常量与真实 ch395f.h 不一致(host 编译里宏对宏自洽,照不出来):",
|
||||
file=sys.stderr)
|
||||
for name, rv, fv in drift:
|
||||
print(" {} 真=0x{:02X} 假=0x{:02X}".format(name, rv, fv), file=sys.stderr)
|
||||
return drift
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
drift = check_fake_chip_constants()
|
||||
if drift is None:
|
||||
print("[ENV] 常量闸无法读取真头或假件头(路径不对?)", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if drift:
|
||||
print("[FAIL] 常量闸拦下 {} 处漂移:修 test/host/common/ch395f_fake.h 到真值"
|
||||
"(出处见 Drivers/BSP/CH395F/ch395f.h 与 docs/芯片手册/CH395F.md)"
|
||||
.format(len(drift)), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print("[OK] 常量闸:假件 CH395F_* 常量与真实 ch395f.h 同名项全对齐")
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - FreeRTOS.h(net_socket 模块专用)
|
||||
* 模块功能:给宿主一份与 FreeRTOS 语义等价的类型定义。net_socket.c 用到 TaskHandle_t /
|
||||
* TickType_t / BaseType_t / eNotifyAction / pdTRUE / portMAX_DELAY,真实那份
|
||||
* 经 portmacro.h 拉编译器与芯片寄存器,PC 上编不动。
|
||||
* ⚠ 枚举成员与取值照抄 Middlewares/.../include/task.h:92-97 与 projdefs.h:44
|
||||
* (eSetBits=0 起、pdTRUE=1、pdFALSE=0),改了就是换分母。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef FREERTOS_H
|
||||
#define FREERTOS_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef void *TaskHandle_t;
|
||||
typedef long BaseType_t;
|
||||
typedef unsigned long UBaseType_t;
|
||||
typedef uint32_t TickType_t;
|
||||
|
||||
#define pdFALSE ((BaseType_t)0)
|
||||
#define pdTRUE ((BaseType_t)1)
|
||||
#define pdPASS pdTRUE
|
||||
#define pdFAIL pdFALSE
|
||||
|
||||
#ifndef portMAX_DELAY
|
||||
#define portMAX_DELAY ((TickType_t)0xFFFFFFFFU)
|
||||
#endif
|
||||
|
||||
/* 与真实 task.h 同序(eSetBits..eSetValueWithoutOverwrite),取值不可重排 */
|
||||
typedef enum {
|
||||
eNoAction = 0,
|
||||
eSetBits,
|
||||
eIncrement,
|
||||
eSetValueWithOverwrite,
|
||||
eSetValueWithoutOverwrite
|
||||
} eNotifyAction;
|
||||
|
||||
#endif /* FREERTOS_H */
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - ch395f.h(net_socket 模块入口)
|
||||
* 模块功能:只做转发,真身在 test/host/common/ch395f_fake.h/.c(NET 域共享假芯片)。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef CH395F_H
|
||||
#define CH395F_H
|
||||
#include "ch395f_fake.h"
|
||||
#endif /* CH395F_H */
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - cmsis_os.h(net_socket 模块专用)
|
||||
* 模块功能:net_socket.c include 的是 CMSIS-RTOS **v1** 头,但它实际用的 API
|
||||
* (osDelay / osMessageQueueGet/Put / osKernelGetState / osOK / osWaitForever)
|
||||
* 全在 v2 头里。真实 v1 头会拉 FreeRTOS.h + task.h + cmsis_os2.h,
|
||||
* 前两个由本目录桩接管,cmsis_os2.h 用**真实头**(只依赖 stdint/stddef,
|
||||
* 宿主可编)——所以本桩就是一次转发,把 v2 的真实类型/枚举/原型接进来。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef CMSIS_OS_H
|
||||
#define CMSIS_OS_H
|
||||
#include "cmsis_os2.h"
|
||||
#endif /* CMSIS_OS_H */
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - dbg_log.h(net_socket 模块专用)
|
||||
* 模块功能:DBG_* 换成计数,风格与本目录既有 NET 域桩一致(net_listener / net_events
|
||||
* 同名文件)。net_socket.c 有 11 条 DBG_ERROR + 6 条 DBG_INFO,其中
|
||||
* 「短写必留痕」「版本门控拒置 bit3 必 [ERR]」「open 失败必 [ERR]」
|
||||
* 「close 复位没成也放行仍要留痕」这四条本身就是可断言的契约。
|
||||
* 本模块在 include 本头之前把 DBG_LINE_BUF_MAX 设为 128U(AGENTS 深调用链纪律),
|
||||
* 桩不关心缓冲大小,只数条数。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef DBG_LOG_H
|
||||
#define DBG_LOG_H
|
||||
|
||||
extern int g_ns_info_count;
|
||||
extern int g_ns_error_count;
|
||||
extern int g_ns_debug_count;
|
||||
|
||||
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_ns_info_count++; } while (0)
|
||||
#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_ns_debug_count++; } while (0)
|
||||
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_ns_error_count++; } while (0)
|
||||
|
||||
#endif /* DBG_LOG_H */
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - main.h(net_socket 模块专用)
|
||||
* 模块功能:net_socket.c 从真实 Inc/main.h 只要三样:HAL_GetTick(自旋/超时/挂起预算的
|
||||
* 唯一致度源)、HAL_GPIO_ReadPin(轮询 CH395F INT#,低电平有效)、
|
||||
* CH395_INT_GPIO_Port / CH395_INT_Pin(Inc/main.h:60-61,PE5)。
|
||||
* 引脚名/号按真实头转抄(真值改了就编译期/断言期报错),GPIO_TypeDef 用不透明
|
||||
* void*(本层不碰寄存器,只需类型能编)。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef MAIN_H
|
||||
#define MAIN_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef void GPIO_TypeDef;
|
||||
|
||||
typedef enum {
|
||||
GPIO_PIN_RESET = 0,
|
||||
GPIO_PIN_SET
|
||||
} GPIO_PinState;
|
||||
|
||||
/* 转抄 Inc/main.h:60-61 */
|
||||
#define CH395_INT_Pin 0x0020U /* GPIO_PIN_5 */
|
||||
#define CH395_INT_GPIO_Port ((GPIO_TypeDef *)0)
|
||||
|
||||
uint32_t HAL_GetTick(void);
|
||||
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
|
||||
|
||||
/* 每轮 net_poll 时基自走多少毫秒(默认 1ms:轮询本身就要花时间,自走才量得到
|
||||
* NET_SEND_SPIN_MS 这类"有界自旋"预算;测试可置 0 造"时钟冻结"形态) */
|
||||
extern uint32_t g_ns_poll_tick_inc;
|
||||
extern uint32_t g_ns_poll_calls; /* net_poll 经 GPIO 读 INT# 的次数(也是跑飞保险计数) */
|
||||
|
||||
#endif /* MAIN_H */
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - net_latency_test.h(net_socket 模块专用)
|
||||
* 模块功能:空壳,配合 TEST_LATENCY_PROBE 不定义即把 lat_* 调用全部条件编译掉。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef NET_LATENCY_TEST_H
|
||||
#define NET_LATENCY_TEST_H
|
||||
#endif /* NET_LATENCY_TEST_H */
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 模块名称:假内核(net_socket 主机侧单测专用)
|
||||
* 模块功能:给 net_socket.c 一份可在 PC 上跑的 FreeRTOS/CMSIS-RTOS2 薄壳——
|
||||
* 消息队列(osMessageQueuePut/Get,真实 cmsis_os2.h 的原型)、
|
||||
* 任务通知(xTaskNotify / xTaskNotifyWait)、当前任务句柄、时基、临界区计数。
|
||||
*
|
||||
* ⚠ 仅供 test/host/net_socket/ 使用,不参与 MCU 构建。
|
||||
*
|
||||
* 关键设计:**阻塞等待即调度点**。net_wait_result 的第二段
|
||||
* xTaskNotifyWait(..., portMAX_DELAY) 在真机上是"睡到 netTask 回执";这里把这一次
|
||||
* 等待实现成"调用测试装泵的 pump 钩子(= 以 netTask 身份跑一轮
|
||||
* net_process_messages()),再拾取通知值"。于是单次 net_send()/net_recv() 调用就能
|
||||
* 把"入队 → 分派 → 通知值回传 → 调用方恢复 errno"整条跨上下文链真实跑完,
|
||||
* 而不是把中间环节假掉。
|
||||
*
|
||||
* 泵保险:pump 钩子最多跑 NS_PUMP_MAX 轮,超了直接 exit(2)——否则一条"该通知却没通知"
|
||||
* 的缺陷会让门禁挂死而不是报错(本仓假 osDelay 的跑飞保险同一条纪律)。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef NS_RTOS_H
|
||||
#define NS_RTOS_H
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "cmsis_os2.h"
|
||||
#include "main.h" /* 桩:HAL_GetTick / HAL_GPIO_ReadPin / CH395_INT_* */
|
||||
|
||||
#define NS_Q_CAP 8u
|
||||
#define NS_PUMP_MAX 50
|
||||
|
||||
/* 两个假任务:调用方任务 + netTask(句柄取地址即唯一) */
|
||||
extern int g_ns_task_caller;
|
||||
extern int g_ns_task_net;
|
||||
|
||||
/* 当前任务句柄(测试直接读写:切换"我是调用方"还是"我是 netTask") */
|
||||
extern TaskHandle_t g_ns_cur_task;
|
||||
|
||||
/* 阻塞等待时跑的泵(测试装:通常 = 跑一轮 net_process_messages) */
|
||||
typedef void (*ns_pump_fn)(void);
|
||||
extern ns_pump_fn g_ns_pump;
|
||||
extern int g_ns_pump_calls; /* 泵被驱动的次数 */
|
||||
extern int g_ns_block_waits; /* 真正走到"阻塞分支"的次数 */
|
||||
|
||||
/* 通知/队列观测面 */
|
||||
extern int g_ns_notify_calls; /* xTaskNotify 总次数 */
|
||||
extern uint32_t g_ns_notify_last_val;
|
||||
extern TaskHandle_t g_ns_notify_last_to;
|
||||
extern int g_ns_put_calls;
|
||||
extern int g_ns_get_calls;
|
||||
extern int g_ns_crit_enter;
|
||||
extern int g_ns_crit_exit;
|
||||
extern uint32_t g_ns_tick;
|
||||
|
||||
void ns_rtos_reset(void);
|
||||
void ns_rtos_q_flush(void);
|
||||
int ns_rtos_q_count(void);
|
||||
void ns_rtos_q_fill(int n); /* 预灌 n 条占位消息(把队列灌到满,测 BUSY 分支) */
|
||||
|
||||
/* 测试用的显式通知投递(不经队列,直接给某任务置通知值) */
|
||||
void ns_rtos_post_notify(TaskHandle_t to, uint32_t v);
|
||||
|
||||
/* 手工投一条原始消息(绕过公开包装器:造未知 type、造 netTask 自调等异常形态) */
|
||||
int ns_rtos_q_put_raw(const net_msg_t *msg);
|
||||
|
||||
/* —— 下面是给桩头对接的实现(原型与真实头一致) —— */
|
||||
osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr,
|
||||
uint8_t msg_prio, uint32_t timeout);
|
||||
osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr,
|
||||
uint8_t *msg_prio, uint32_t timeout);
|
||||
osKernelState_t osKernelGetState(void);
|
||||
osStatus_t osDelay(uint32_t ticks);
|
||||
|
||||
/* 每轮 net_poll(= 一次 INT# 读取)都调一下测试回调:用来在"等待发送缓冲/等待数据"
|
||||
* 的循环途中投递并发事件(对端断线、芯片突然可读),而不必自己发明时间 API */
|
||||
typedef void (*ns_poll_hook_fn)(void);
|
||||
extern ns_poll_hook_fn g_ns_on_poll;
|
||||
|
||||
/* CH395F INT# 电平(1=SET 无中断,net_poll 走短路分支) */
|
||||
extern int g_ns_int_pin_set;
|
||||
|
||||
/* 内核状态可注入(net_errno_slot 靠它判"内核没跑起来"的退化分支) */
|
||||
extern osKernelState_t g_ns_kernel_state;
|
||||
|
||||
#endif /* NS_RTOS_H */
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - task.h(net_socket 模块专用)
|
||||
* 模块功能:声明 net_socket.c 用到的三件套 + 临界区宏。原型与真实
|
||||
* Middlewares/.../include/task.h 逐字一致(返回值/参数顺序都不许改),
|
||||
* 实现在 ns_rtos.c(假内核:任务通知值 + 当前任务句柄可切换)。
|
||||
* taskENTER_CRITICAL/EXIT_CRITICAL 记计数——临界区只存在于本层槽位分配,
|
||||
* 计数能断言"只在占槽时进临界区、且成对退出"。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef TASK_H
|
||||
#define TASK_H
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
|
||||
TaskHandle_t xTaskGetCurrentTaskHandle(void);
|
||||
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue,
|
||||
eNotifyAction eAction);
|
||||
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
|
||||
uint32_t ulBitsToClearOnExit,
|
||||
uint32_t *pulNotificationValue,
|
||||
TickType_t xTicksToWait);
|
||||
uint32_t xTaskGetTickCount(void);
|
||||
|
||||
int ns_os_crit_depth(void);
|
||||
void ns_os_enter_critical(void);
|
||||
void ns_os_exit_critical(void);
|
||||
|
||||
#define taskENTER_CRITICAL() ns_os_enter_critical()
|
||||
#define taskEXIT_CRITICAL() ns_os_exit_critical()
|
||||
|
||||
#endif /* TASK_H */
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - test_config.h(net_socket 模块专用)
|
||||
* 模块功能:空壳。net_socket.c 只在 TEST_LATENCY_PROBE 打开时才用延迟探针,
|
||||
* 主机单测刻意**不开**(探针要写真实时间/真实周期计数,不是被测对象);
|
||||
* 也不定义 NET_BENCH_CMD_LOG(压测命令日志同理,见 net_events 同名说明)。
|
||||
* ⚠ 仅供主机侧单测使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
#ifndef TEST_CONFIG_H
|
||||
#define TEST_CONFIG_H
|
||||
#endif /* TEST_CONFIG_H */
|
||||
@@ -217,14 +217,14 @@ static void tc_load_triage(void)
|
||||
backend_reset();
|
||||
g_read_rc = -1;
|
||||
rc = prot_set_store_load();
|
||||
CHECK(rc == 1, "TC-PROTSTOR-001 读失败归 1(按无档处理)");
|
||||
CHECK(rc == 1, "TC-PROTSTOR-001 read failure maps to 1 (treat as no file)");
|
||||
|
||||
backend_reset();
|
||||
len = make_blob(blob, sizeof(blob));
|
||||
memcpy(g_img, blob, len);
|
||||
stage_blob(len);
|
||||
rc = prot_set_store_load();
|
||||
CHECK(rc == 0, "TC-PROTSTOR-002 有效 blob 装载成功");
|
||||
CHECK(rc == 0, "TC-PROTSTOR-002 valid blob loads successfully");
|
||||
|
||||
/* ver 换代:magic 完好、ver 与当前固件不同 → 预期升级路径,归 2 */
|
||||
backend_reset();
|
||||
@@ -233,9 +233,9 @@ static void tc_load_triage(void)
|
||||
g_img[4] = (uint8_t)(PROT_SET_BLOB_VER + 1u);
|
||||
stage_blob(len);
|
||||
rc = prot_set_store_load();
|
||||
CHECK(rc == 2, "TC-PROTSTOR-003 ver 换代归 2(不是 -1)");
|
||||
CHECK(rc == 2, "TC-PROTSTOR-003 ver mismatch maps to 2 (not -1)");
|
||||
CHECK((g_pstore_info_count == 1) && (g_pstore_error_count == 0),
|
||||
"TC-PROTSTOR-004 换代只打 INFO,不误报 ERROR");
|
||||
"TC-PROTSTOR-004 version mismatch logs INFO only, no false ERROR");
|
||||
|
||||
/* cnt 换代:B8 定值表 32→40 项就是这个形态 */
|
||||
backend_reset();
|
||||
@@ -244,7 +244,7 @@ static void tc_load_triage(void)
|
||||
g_img[6] = (uint8_t)((uint32_t)PS_ID_COUNT + 1u);
|
||||
stage_blob(len);
|
||||
rc = prot_set_store_load();
|
||||
CHECK(rc == 2, "TC-PROTSTOR-005 cnt 换代归 2");
|
||||
CHECK(rc == 2, "TC-PROTSTOR-005 cnt mismatch maps to 2");
|
||||
|
||||
/* magic 坏 = 真损坏 */
|
||||
backend_reset();
|
||||
@@ -253,7 +253,7 @@ static void tc_load_triage(void)
|
||||
g_img[0] = (uint8_t)(g_img[0] ^ 0xFFu);
|
||||
stage_blob(len);
|
||||
rc = prot_set_store_load();
|
||||
CHECK((rc == -1) && (g_pstore_error_count == 0), "TC-PROTSTOR-006 magic 坏归 -1");
|
||||
CHECK((rc == -1) && (g_pstore_error_count == 0), "TC-PROTSTOR-006 broken magic maps to -1");
|
||||
|
||||
/* magic/ver/cnt 全好,但数据被改 → 校验不过 = 损坏 */
|
||||
backend_reset();
|
||||
@@ -262,7 +262,7 @@ static void tc_load_triage(void)
|
||||
g_img[PROT_SET_BLOB_HDR_SZ] = (uint8_t)(g_img[PROT_SET_BLOB_HDR_SZ] ^ 0x5Au);
|
||||
stage_blob(len);
|
||||
rc = prot_set_store_load();
|
||||
CHECK(rc == -1, "TC-PROTSTOR-007 数据被改(校验失败)归 -1");
|
||||
CHECK(rc == -1, "TC-PROTSTOR-007 data tampered (checksum fail) maps to -1");
|
||||
|
||||
/* 截断到不足头部:不得误判为换代 */
|
||||
backend_reset();
|
||||
@@ -270,7 +270,7 @@ static void tc_load_triage(void)
|
||||
memcpy(g_img, blob, len);
|
||||
stage_blob(PROT_SET_BLOB_HDR_SZ - 1u);
|
||||
rc = prot_set_store_load();
|
||||
CHECK(rc == -1, "TC-PROTSTOR-008 长度不足头部归 -1 且不误判换代");
|
||||
CHECK(rc == -1, "TC-PROTSTOR-008 length below header maps to -1 and is not misjudged as version mismatch");
|
||||
|
||||
backend_reset();
|
||||
len = make_blob(blob, sizeof(blob));
|
||||
@@ -278,7 +278,7 @@ static void tc_load_triage(void)
|
||||
stage_blob(len);
|
||||
rc = prot_set_store_load();
|
||||
CHECK((rc == 0) && (g_pstore_info_count == 0),
|
||||
"TC-PROTSTOR-009 成功路径不打 INFO(version change 措辞不许乱出现)");
|
||||
"TC-PROTSTOR-009 success path logs no INFO (version change wording must not appear)");
|
||||
}
|
||||
|
||||
/* ===================== 后端调用契约 ===================== */
|
||||
@@ -295,9 +295,9 @@ static void tc_backend_contract(void)
|
||||
stage_blob(len);
|
||||
(void)prot_set_store_load();
|
||||
CHECK(strcmp(g_read_path, "/prot_set.bin") == 0,
|
||||
"TC-PROTSTOR-010 路径 = /prot_set.bin(8.3 合规)");
|
||||
"TC-PROTSTOR-010 path = /prot_set.bin (8.3 compliant)");
|
||||
CHECK(g_read_cap == (size_t)PROT_SET_BLOB_SZ,
|
||||
"TC-PROTSTOR-011 读取容量 = PROT_SET_BLOB_SZ(静态缓冲,不压栈)");
|
||||
"TC-PROTSTOR-011 read capacity = PROT_SET_BLOB_SZ (static buffer, not on stack)");
|
||||
}
|
||||
|
||||
/* ===================== save 与脏标志 ===================== */
|
||||
@@ -311,30 +311,30 @@ static void tc_save_dirty(void)
|
||||
backend_reset();
|
||||
dirty_clear();
|
||||
rc = prot_set_store_save();
|
||||
CHECK((rc == 0) && (g_write_calls == 0), "TC-PROTSTOR-012 无脏不写盘");
|
||||
CHECK((rc == 0) && (g_write_calls == 0), "TC-PROTSTOR-012 not dirty -> no flash write");
|
||||
|
||||
prot_set_dirty_mark();
|
||||
g_write_rc = 0;
|
||||
rc = prot_set_store_save();
|
||||
CHECK((rc == 0) && (g_write_calls == 1) &&
|
||||
(strcmp(g_write_path, "/prot_set.bin") == 0) && (g_write_len > 0u),
|
||||
"TC-PROTSTOR-013 有脏且写成功:路径与长度正确");
|
||||
"TC-PROTSTOR-013 dirty + write ok: path and length correct");
|
||||
|
||||
rc = prot_set_store_save();
|
||||
CHECK((rc == 0) && (g_write_calls == 1),
|
||||
"TC-PROTSTOR-014 脏已消费,紧接着再调不再刷 Flash");
|
||||
"TC-PROTSTOR-014 dirty consumed, immediate second call does not flush flash again");
|
||||
|
||||
prot_set_dirty_mark();
|
||||
g_write_rc = -1;
|
||||
rc = prot_set_store_save();
|
||||
CHECK((rc == -1) && (g_write_calls == 2), "TC-PROTSTOR-015 写失败返回 -1");
|
||||
CHECK((rc == -1) && (g_write_calls == 2), "TC-PROTSTOR-015 write failure returns -1");
|
||||
|
||||
/* 关键:失败必须把脏标志还回去,否则这一轮定值永久丢失 */
|
||||
g_write_rc = 0;
|
||||
g_write_calls = 0;
|
||||
rc = prot_set_store_save();
|
||||
CHECK((rc == 0) && (g_write_calls == 1),
|
||||
"TC-PROTSTOR-016 上一轮写失败后下一轮仍真的再写(脏已回置)");
|
||||
"TC-PROTSTOR-016 after a failed round the next round really writes again (dirty re-armed)");
|
||||
|
||||
dirty_clear();
|
||||
}
|
||||
|
||||
@@ -73,15 +73,15 @@ static void tc_vectors(void)
|
||||
|
||||
sha256_calc("", 0u, d);
|
||||
CHECK(hex_eq_vector(d, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
|
||||
"TC-SHA256-001 空串向量");
|
||||
"TC-SHA256-001 empty-string vector");
|
||||
|
||||
sha256_calc("abc", 3u, d);
|
||||
CHECK(hex_eq_vector(d, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"),
|
||||
"TC-SHA256-002 \"abc\" 向量");
|
||||
"TC-SHA256-002 \"abc\" vector");
|
||||
|
||||
sha256_calc("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56u, d);
|
||||
CHECK(hex_eq_vector(d, "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"),
|
||||
"TC-SHA256-003 56 字节向量(补位跨分组)");
|
||||
"TC-SHA256-003 56-byte vector (padding crosses block)");
|
||||
|
||||
{
|
||||
sha256_ctx_t ctx;
|
||||
@@ -93,7 +93,7 @@ static void tc_vectors(void)
|
||||
sha256_final(&ctx, d);
|
||||
}
|
||||
CHECK(hex_eq_vector(d, "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"),
|
||||
"TC-SHA256-004 1e6 × 'a' 向量(流式)");
|
||||
"TC-SHA256-004 1e6 x 'a' vector (streaming)");
|
||||
}
|
||||
|
||||
/* ===================== TC-SHA256-005:流式等价 ===================== */
|
||||
@@ -111,7 +111,7 @@ static void tc_stream_equiv(void)
|
||||
sha256_update(&ctx, &msg[i], 1u);
|
||||
}
|
||||
sha256_final(&ctx, d2);
|
||||
CHECK(memcmp(d1, d2, sizeof(d1)) == 0, "TC-SHA256-005 逐字节喂入与一次性等价");
|
||||
CHECK(memcmp(d1, d2, sizeof(d1)) == 0, "TC-SHA256-005 byte-by-byte feed equals one-shot");
|
||||
}
|
||||
|
||||
/* ===================== TC-SHA256-006..007:hex_eq ===================== */
|
||||
@@ -131,11 +131,11 @@ static void tc_hex_eq(void)
|
||||
}
|
||||
upper[64] = '\0';
|
||||
|
||||
CHECK(sha256_hex_eq(d, lower) == 1, "TC-SHA256-006a 小写 hex 匹配");
|
||||
CHECK(sha256_hex_eq(d, upper) == 1, "TC-SHA256-006b 大写 hex 匹配(大小写不敏感)");
|
||||
CHECK(sha256_hex_eq(d, lower) == 1, "TC-SHA256-006a lowercase hex matches");
|
||||
CHECK(sha256_hex_eq(d, upper) == 1, "TC-SHA256-006b uppercase hex matches (case-insensitive)");
|
||||
|
||||
lower[63] = (char)((lower[63] == '0') ? '1' : '0');
|
||||
CHECK(sha256_hex_eq(d, lower) == 0, "TC-SHA256-007a 内容不等 → 0");
|
||||
CHECK(sha256_hex_eq(d, lower) == 0, "TC-SHA256-007a content mismatch -> 0");
|
||||
lower[63] = (char)((lower[63] == '0') ? '1' : '0'); /* 还原 */
|
||||
|
||||
{
|
||||
@@ -144,19 +144,19 @@ static void tc_hex_eq(void)
|
||||
memcpy(bad, lower, 64);
|
||||
bad[64] = '\0';
|
||||
bad[10] = 'g';
|
||||
CHECK(sha256_hex_eq(d, bad) == 0, "TC-SHA256-007b 非法 hex 字符 → 0");
|
||||
CHECK(sha256_hex_eq(d, bad) == 0, "TC-SHA256-007b invalid hex char -> 0");
|
||||
}
|
||||
|
||||
memcpy(shortv, lower, 63);
|
||||
shortv[63] = '\0';
|
||||
CHECK(sha256_hex_eq(d, shortv) == 0, "TC-SHA256-007c 长度 63 → 0");
|
||||
CHECK(sha256_hex_eq(d, shortv) == 0, "TC-SHA256-007c length 63 -> 0");
|
||||
|
||||
memcpy(longv, lower, 64);
|
||||
longv[64] = 'a';
|
||||
longv[65] = '\0';
|
||||
CHECK(sha256_hex_eq(d, longv) == 0, "TC-SHA256-007d 长度 65 → 0");
|
||||
CHECK(sha256_hex_eq(d, longv) == 0, "TC-SHA256-007d length 65 -> 0");
|
||||
|
||||
CHECK(sha256_hex_eq(d, NULL) == 0, "TC-SHA256-007e NULL → 0");
|
||||
CHECK(sha256_hex_eq(d, NULL) == 0, "TC-SHA256-007e NULL -> 0");
|
||||
}
|
||||
|
||||
/* ===================== TC-SHA256-008:默认口令哈希钉值 ===================== */
|
||||
@@ -168,7 +168,7 @@ static void tc_default_hash(void)
|
||||
/* 与 ftp_cfg.h FTP_CFG_DEF_HASH 同源钉值(两处若漂移本测试立刻红) */
|
||||
sha256_calc("adminadmin", 10u, d);
|
||||
CHECK(sha256_hex_eq(d, "d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892") == 1,
|
||||
"TC-SHA256-008 SHA256(\"adminadmin\") = 出厂默认 pwd_hash");
|
||||
"TC-SHA256-008 SHA256(\"adminadmin\") = factory default pwd_hash");
|
||||
}
|
||||
|
||||
/* ===================== main ===================== */
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""SOE 落盘胶水层(soe_store)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/soe_store/soe_store_test.c(以 #include 纳入**生产源**
|
||||
App/log/soe_store.c 与 App/log/soe_rec.c,并纳入共享假卷
|
||||
test/host/common/fatfs_fake.c),在 PC 上离线跑 post 侧零 I/O 契约、环/队列分流与
|
||||
丢弃计数、drain 侧的卷锁 try / FA_WRITE|FA_OPEN_APPEND / 写回字节数校验 / 容量满
|
||||
重建 / 故障留痕去重,失败即非 0 退出。工具链与判定委托
|
||||
test/host/common/unit_test_harness.py。
|
||||
|
||||
⚠ 与 test/host/soe/ 是两码事:那套测的是 soe_rec.c 的**编解码**(纯逻辑,零 FS);
|
||||
本套测 soe_store.c 的**落盘胶水**。目录、模块名、TC 号族都分开
|
||||
(TC 模块码用 SOESTORE,SOE 已被存量占用)。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
1. post 侧"一条 DBG 都不许打"是 protectTask 的**栈预算硬约束**(函数头原话:
|
||||
该任务静态最坏链 = 经 s_log_active_edge 的 console 尾链 872 B,post 里加一条
|
||||
日志就 +320 B → stack_report FAIL)。真机上忘这条不会崩,只会某次最坏路径
|
||||
把栈打穿;只有把 DBG_* 换成计数才能机械断言"这条路径 0 日志"。
|
||||
2. 环写失败不推进 tail(不丢)而队列写失败丢已出队那一条——**两条通道不对称**,
|
||||
台架要恰好撞上"FTP 正占卷"才看得到一次。
|
||||
3. 短写(FR_OK + 写回不足)、容量满重建、故障留痕去重都要故障注入才成立。
|
||||
4. 陷阱二(名字/参数类错误码严禁升级成卷操作)只有假卷能把 FR_INVALID_NAME 稳定
|
||||
喂进自愈路径,并断言"邻居文件与 /log 目录一根毛都没动"。
|
||||
|
||||
桩的边界:ff.h / sys_clock.h / vol_lock.h / soe_rec.h / soe_store.h 用**真实头**
|
||||
(FRESULT、FA_*、sys_clock_get 签名一改即编译期报错);main.h 是桩,只为截断
|
||||
sys_clock.h -> sd2506.h -> main.h -> stm32f4xx_hal.h 这条链(真实 main.h 第一行
|
||||
就拉整棵 HAL);cmsis_os.h / dbg_log.h 是本目录桩。FatFs 由
|
||||
test/host/common/fatfs_fake.c 给 RAM 实现(**显式纳入,不用目录通配**,
|
||||
避免被卷进别人的构建)。sys_clock_* / vol_lock_* / 消息队列强符号桩在本测试单元里。
|
||||
|
||||
用法(仓库根目录):
|
||||
& '.venv\Scripts\python.exe' test/host/soe_store/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
|
||||
|
||||
设计依据:docs/架构设计/STM32F4-Base存储架构说明.md §10.1 第 10/11 条、
|
||||
docs/算法/继电保护功能复现规划.md §3.4(L4-4/L4-5,B9)、
|
||||
docs/测试说明/主机单测覆盖率口径.md §9 P2 第 2 项
|
||||
创建日期:2026-10-24
|
||||
"""
|
||||
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 必须最先(覆盖 cmsis_os.h / dbg_log.h / main.h),
|
||||
# 其余全是真实头目录;假卷 fatfs_fake.c 由测试单元 #include 纳入(同 ch395f_fake.c 惯例)。
|
||||
BUILD = TestBuild(
|
||||
name="SOE Store Tests",
|
||||
src=os.path.join(HERE, "soe_store_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "test", "host", "common"),
|
||||
os.path.join(ROOT, "App", "log"),
|
||||
os.path.join(ROOT, "App", "util"),
|
||||
os.path.join(ROOT, "App", "fs"),
|
||||
os.path.join(ROOT, "Drivers", "BSP"), # bsp_err.h(sd2506.h 依赖)
|
||||
os.path.join(ROOT, "Drivers", "BSP", "SD2506"),
|
||||
os.path.join(ROOT, "Lib", "FatFs"), # 真实 ff.h / ffconf.h
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== SOE Store Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
@@ -0,0 +1,742 @@
|
||||
/*
|
||||
* 模块名称:SOE 落盘胶水层(soe_store)主机侧单元测试(L0.5)
|
||||
* 模块功能:在 PC 上离线验证 App/log/soe_store.c 的四条对外契约与它自己的
|
||||
* "照 sys_log_mcu 的台架实证纪律"注释:
|
||||
* - post 侧**零文件系统、零日志**(protectTask 栈预算 + DoD 硬约束);
|
||||
* - 环 / 队列两条待发通道的分流与"丢弃只计数";
|
||||
* - drain 侧的 vol_lock try、FA_WRITE|FA_OPEN_APPEND、写回字节数校验、
|
||||
* 容量满 unlink 重建、故障留痕去重;
|
||||
* - **名字/参数类错误码绝不升级成卷操作**(存储架构 §10.1 第 10 条)。
|
||||
*
|
||||
* 被测对象:真实源文件(#include 原样纳入,不改生产代码):
|
||||
* App/log/soe_store.c —— 本模块主体
|
||||
* App/log/soe_rec.c —— 64B 记录构建(纯逻辑,零 OS,直接用真件;
|
||||
* test/host/soe 已单独覆盖其编解码,本套不重测)
|
||||
* 假件与桩:
|
||||
* test/host/common/fatfs_fake.{h,c} —— 假 FatFs 内存卷(真实 ff.h 声明)
|
||||
* stub/dbg_log.h / stub/cmsis_os.h / stub/main.h —— 本目录桩
|
||||
* sys_clock / vol_lock 的可控强符号实现写在本文件里
|
||||
*
|
||||
* 为什么要主机侧单测(而不是只靠台架):
|
||||
* 1. **post 侧禁 DBG 是栈预算硬约束**(soe_store.c 函数头原话:protectTask 静态最
|
||||
* 坏链 = 经 s_log_active_edge 的 console 尾链 872 B,post 内任何日志调用点都会
|
||||
* 再垫高一帧,stack_report 实测 +320 B → FAIL)。真机上"忘了这条"不会崩,只会
|
||||
* 在某次最坏路径上把栈打穿 → 只有把 DBG_* 换成计数才能机械断言"这条路径 0 日志"。
|
||||
* 2. **环 / 队列的丢弃语义不对称**:环写失败**不推进 tail**(下轮重试、条目不丢),
|
||||
* 队列条目已出队、卷忙就丢那一条。真机上要恰好撞上"FTP 正在占卷"才看得到,
|
||||
* 而"到底丢不丢"是运维读 SOE 时能不能信任序号连续的前提。
|
||||
* 3. **短写 = 失败**(FR_OK + 写回字节数不足,簇分配没有错误码)与**满则重建**
|
||||
* 这两条只在介质坏块 / 容量见底时发作;假卷可以把"只写回 63 字节"直接喂进来。
|
||||
* 4. 写故障留痕的去重(s_fault_reported)决定了长跑时 /log 与串口会不会被同一条
|
||||
* 故障刷屏——真机要复现"失败→恢复→再失败"这个序列极慢。
|
||||
*
|
||||
* 覆盖点(TC 号族 = SOESTORE(三位序号);SOE 已被存量 test/host/soe 占用):
|
||||
* —— post 侧(零 I/O 契约)——
|
||||
* TC-SOESTORE-001 post 3 条只写环:零 FatFs 调用、零卷锁调用、零日志
|
||||
* TC-SOESTORE-002 墙钟未建立(UINT32_MAX)→ flags 带 IV 且 epoch 字段落 0(不造假时标)
|
||||
* TC-SOESTORE-003 seq 上电内单调 +0123(丢弃也占号:号不跳 = 假象)
|
||||
* TC-SOESTORE-004 环满(8 条)→ 第 9 条丢弃 + 计数,不阻塞、不崩、**不打日志**
|
||||
* TC-SOESTORE-005 内核运行 + 队列已建 → 走队列,环不再写入
|
||||
* TC-SOESTORE-006 队列满 → 丢弃 + 计数(与环满同一条可观测出口)
|
||||
* TC-SOESTORE-007 丢弃计数饱和:UINT32_MAX 不再回绕
|
||||
* —— drain 侧:落盘与打开模式 ——
|
||||
* TC-SOESTORE-010 首次 drain 惰性建队列;1 条落盘 = 64 字节且可被 soe_rec_parse 解回
|
||||
* TC-SOESTORE-011 打开模式必须是 FA_WRITE|FA_OPEN_APPEND;对照保真:只给
|
||||
* FA_OPEN_APPEND 的句柄 f_write 恒返 FR_DENIED(陷阱一,本仓实证根因)
|
||||
* TC-SOESTORE-012 多条落盘:一条一次 open/write/close(本模块**没有**批量合并——
|
||||
* 与 sys_log 的 N-24 不同),FIFO 顺序、seq 递增
|
||||
* TC-SOESTORE-013 open 与 close 严格配对(脏句柄不跨调用残留)
|
||||
* —— drain 侧:故障注入 ——
|
||||
* TC-SOESTORE-014 短写 63B → 判失败 → unlink 重建 → 重试成功 → 最终恰好 64 字节
|
||||
* TC-SOESTORE-015 连续两次短写 → 返回 -1;留痕恰好一次(去重);恢复后再失败才再留痕
|
||||
* TC-SOESTORE-016 卷忙(vol_lock_wait(0)=0)→ 返回 -2;**不碰 FatFs、不 release**
|
||||
* TC-SOESTORE-017 环条目在写失败时不推进 tail → 下轮重试不丢、且不重复落盘
|
||||
* TC-SOESTORE-018 队列条目已出队:卷忙只丢当前一条,剩余留在队列(不对称现状钉住)
|
||||
* TC-SOESTORE-019 容量满(1024 条)→ unlink 重建 + INFO 留痕,新文件恰好 64 字节
|
||||
* TC-SOESTORE-020 丢弃留痕按差值上报一次,不刷屏
|
||||
* TC-SOESTORE-021 队列创建失败(返回 NULL)→ 条目滞留环、下轮再试,不崩
|
||||
* —— 陷阱二:名字/参数类错误码不得升级成卷操作 ——
|
||||
* TC-SOESTORE-022 f_open 失败(FR_INVALID_NAME)-> 只留痕返回 -1:**不 unlink**、
|
||||
* 不碰 /log 目录、已有 3 条历史一条不少、条目留在待发环
|
||||
* TC-SOESTORE-022b 逐码对齐(8 个码含介质类):打开失败**任何码都不 unlink**——
|
||||
* SOE 侧比 run.log 侧更保守,是刻意差异(事件序列不可销毁),
|
||||
* 与 test/host/sys_log_mcu/ 的 TC-SYSLOG-101/102 同一张表
|
||||
* TC-SOESTORE-023 写路径反复失败时 f_unlink 次数有界(每轮 <=1),目录与邻居文件不动
|
||||
* TC-SOESTORE-024 假卷严格 8.3 校验的保真自检:".fschk" -> 名字/盘符类拒绝码,
|
||||
* 而生产路径 SOE_PATH 必过(钉住"改名即静默写失败"的约束)
|
||||
*
|
||||
* 覆盖率台账(--module soe_store 自量):App/log/soe_store.c 78/79 行 = 98.7%、
|
||||
* 函数 4/4。唯一未覆盖行 soe_store.c:202 = drain 里 for (;;) 的收尾大括号,
|
||||
* 该循环只能靠 break 退出,不存在"走到行尾再回跳"的机器码,属工具口径缺口。
|
||||
*
|
||||
* 本轮(第 1 轮缺陷修复批次 task-4)已修,本套相应断言已从"钉现状"翻成正向:
|
||||
* D 项:soe_store.c 函数头 ④ 旧写"打开/写入结构性错误都 unlink 重试一次(同
|
||||
* run.log)",与实现不符且方向错误——实现里 f_open 失败只 return -1、不 unlink。
|
||||
* 定案是**实现不动、改注释**:SOE 是事件序列,打开失败就删文件等于销毁已发生的
|
||||
* 事件;run.log 是滚动运行日志,坏了重建损失有限。现在 TC-SOESTORE-022 正向钉
|
||||
* "打开失败不得 unlink、历史一条不少",TC-SOESTORE-022b 对 8 个码逐个钉同一件事
|
||||
* (含介质类码——SOE 侧比 run.log 侧更保守,两处注释同源,改动必须同步)。
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
*
|
||||
* 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2)
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ff.h" /* 真实 Lib/FatFs/ff.h:FRESULT / FA_* / FIL 原汁 */
|
||||
#include "fatfs_fake.h" /* 假卷:类型与常量都取自上面那份真头 */
|
||||
#include "cmsis_os.h" /* 桩:消息队列 + 内核状态(签名照 cmsis_os2.h) */
|
||||
#include "dbg_log.h" /* 桩:DBG_* -> 计数 */
|
||||
#include "sys_clock.h" /* 真实头(App/util):签名一改即编译期报错 */
|
||||
#include "vol_lock.h" /* 真实头(App/fs) */
|
||||
#include "soe_rec.h"
|
||||
#include "soe_store.h"
|
||||
|
||||
/* ===================== 断言框架 ===================== */
|
||||
|
||||
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)
|
||||
|
||||
/* ===================== 假件与生产源码纳入 ===================== */
|
||||
|
||||
#include "fatfs_fake.c" /* 假 FatFs 实现 */
|
||||
#include "soe_rec.c" /* 真实 64B 记录构建(纯逻辑) */
|
||||
#include "soe_store.c" /* 被测主体 */
|
||||
|
||||
/* ===================== dbg_log 桩计数(stub/dbg_log.h 声明) ===================== */
|
||||
|
||||
int g_soe_err_count;
|
||||
int g_soe_info_count;
|
||||
int g_soe_debug_count;
|
||||
|
||||
/* ===================== 假墙钟 ===================== */
|
||||
|
||||
static uint32_t s_clock_now = 1760234567u; /* 2025-10-12 前后,CP56 年域内 */
|
||||
static int s_clock_valid = 1;
|
||||
|
||||
uint32_t sys_clock_get(void)
|
||||
{
|
||||
return (s_clock_valid != 0) ? s_clock_now : UINT32_MAX;
|
||||
}
|
||||
|
||||
/* sys_clock.h 里声明、本模块不调用的接口:给出空实现,纯为可链接 */
|
||||
int sys_clock_init(void) { return 0; }
|
||||
int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *p_time)
|
||||
{
|
||||
(void)unix_secs; (void)p_time; return 0;
|
||||
}
|
||||
int sys_clock_set_unix(uint32_t unix_secs) { (void)unix_secs; return 0; }
|
||||
int sys_clock_get_time(sd2506_time_t *p_time) { (void)p_time; return -1; }
|
||||
uint32_t HAL_GetTick(void) { return 0u; }
|
||||
|
||||
char *sys_clock_get_str(char *buf, size_t buf_size)
|
||||
{
|
||||
(void)buf; (void)buf_size;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ===================== 假卷锁(try 语义 + 配对检查) ===================== */
|
||||
|
||||
static int s_lock_busy_left; /* >0:接下来 N 次 vol_lock_wait 返回"忙"(try 失败)
|
||||
* 用计数器而不是按序号取值的脚本数组——脚本数组的下标
|
||||
* 会被场景里前几次 drain 消耗掉,注入看着设了其实没生效 */
|
||||
static int s_lock_waits;
|
||||
static int s_lock_releases;
|
||||
static int s_lock_held;
|
||||
static int s_lock_seq_err; /* 未持锁却 release = 锁纪律被写反 */
|
||||
|
||||
static void lock_busy_next(int n)
|
||||
{
|
||||
s_lock_busy_left = n;
|
||||
}
|
||||
|
||||
static void locks_clear(void)
|
||||
{
|
||||
s_lock_busy_left = 0;
|
||||
s_lock_waits = 0;
|
||||
s_lock_releases = 0;
|
||||
s_lock_held = 0;
|
||||
s_lock_seq_err = 0;
|
||||
}
|
||||
|
||||
int vol_lock_init(void) { return 0; }
|
||||
|
||||
int vol_lock_wait(uint32_t timeout_ms)
|
||||
{
|
||||
int r;
|
||||
|
||||
(void)timeout_ms;
|
||||
/* 跑飞保险:调用数越界直接判死,不让死循环拖挂门禁 */
|
||||
if (s_lock_waits > 20000) {
|
||||
(void)fprintf(stderr, "[FATAL] vol_lock_wait call guard tripped\n");
|
||||
exit(2);
|
||||
}
|
||||
r = (s_lock_busy_left > 0) ? 0 : 1;
|
||||
if (s_lock_busy_left > 0) {
|
||||
s_lock_busy_left--;
|
||||
}
|
||||
s_lock_waits++;
|
||||
if (r != 0) {
|
||||
s_lock_held = 1;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
void vol_lock_release(void)
|
||||
{
|
||||
s_lock_releases++;
|
||||
if (s_lock_held == 0) {
|
||||
s_lock_seq_err++; /* 没拿到锁就释放:fail-open 语义被写反的证据 */
|
||||
}
|
||||
s_lock_held = 0;
|
||||
}
|
||||
|
||||
/* ===================== 假消息队列(真 FIFO + 满/空语义) ===================== */
|
||||
|
||||
#define MQ_SLOTS 16u
|
||||
#define MQ_MAXMSG 64u /* SOE 记录 64B;syslog 侧条目 12B,同一池够用 */
|
||||
|
||||
typedef struct {
|
||||
int used; /* 槽位有无消息 */
|
||||
uint8_t data[MQ_MAXMSG];
|
||||
} mq_slot_t;
|
||||
|
||||
static mq_slot_t s_q_pool[MQ_SLOTS];
|
||||
static uint32_t s_q_head; /* 出队下标 */
|
||||
static uint32_t s_q_cnt;
|
||||
static uint32_t s_q_cap; /* osMessageQueueNew 给的 msg_count */
|
||||
static uint32_t s_q_msgsz;
|
||||
static int s_q_new_calls;
|
||||
static int s_q_new_fail; /* 1 = osMessageQueueNew 返 NULL */
|
||||
static int s_q_get_calls;
|
||||
static int s_q_put_calls;
|
||||
static int s_q_create_fail_left; /* 预留:连续失败注入(当前用 0/1) */
|
||||
|
||||
static void queue_clear(void)
|
||||
{
|
||||
(void)memset(s_q_pool, 0, sizeof(s_q_pool));
|
||||
s_q_head = 0u;
|
||||
s_q_cnt = 0u;
|
||||
s_q_cap = SOE_QUEUE_MAX;
|
||||
s_q_msgsz = SOE_REC_SIZE;
|
||||
s_q_new_calls = 0;
|
||||
s_q_new_fail = 0;
|
||||
s_q_create_fail_left = 0;
|
||||
s_q_get_calls = 0;
|
||||
s_q_put_calls = 0;
|
||||
}
|
||||
|
||||
osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size,
|
||||
const osMessageQueueAttr_t *attr)
|
||||
{
|
||||
(void)attr;
|
||||
s_q_new_calls++;
|
||||
if ((s_q_new_fail != 0) || (s_q_create_fail_left > 0)) {
|
||||
if (s_q_create_fail_left > 0) {
|
||||
s_q_create_fail_left--;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
s_q_cap = msg_count;
|
||||
s_q_msgsz = msg_size;
|
||||
s_q_head = 0u;
|
||||
s_q_cnt = 0u;
|
||||
/* 句柄本身没有意义:返回一个稳定非空指针即可 */
|
||||
return (osMessageQueueId_t)&s_q_pool[0];
|
||||
}
|
||||
|
||||
osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr,
|
||||
uint8_t msg_prio, uint32_t timeout)
|
||||
{
|
||||
uint32_t tail;
|
||||
|
||||
(void)mq_id; (void)msg_prio; (void)timeout;
|
||||
if (s_q_put_calls++ > 20000) {
|
||||
(void)fprintf(stderr, "[FATAL] osMessageQueuePut guard tripped\n");
|
||||
exit(2);
|
||||
}
|
||||
if ((msg_ptr == NULL) || (s_q_cnt >= s_q_cap) || (s_q_msgsz > MQ_MAXMSG)) {
|
||||
return osErrorResource; /* 队列满:真实 CMSIS 在 timeout=0 时的返回 */
|
||||
}
|
||||
tail = (s_q_head + s_q_cnt) % MQ_SLOTS;
|
||||
(void)memcpy(s_q_pool[tail].data, msg_ptr, s_q_msgsz);
|
||||
s_q_pool[tail].used = 1;
|
||||
s_q_cnt++;
|
||||
return osOK;
|
||||
}
|
||||
|
||||
osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr,
|
||||
uint8_t *msg_prio, uint32_t timeout)
|
||||
{
|
||||
(void)mq_id; (void)msg_prio; (void)timeout;
|
||||
if (s_q_get_calls++ > 20000) {
|
||||
(void)fprintf(stderr, "[FATAL] osMessageQueueGet guard tripped\n");
|
||||
exit(2);
|
||||
}
|
||||
if ((msg_ptr == NULL) || (s_q_cnt == 0u)) {
|
||||
return osErrorResource; /* 队列空 */
|
||||
}
|
||||
(void)memcpy(msg_ptr, s_q_pool[s_q_head].data, s_q_msgsz);
|
||||
s_q_pool[s_q_head].used = 0;
|
||||
s_q_head = (s_q_head + 1u) % MQ_SLOTS;
|
||||
s_q_cnt--;
|
||||
return osOK;
|
||||
}
|
||||
|
||||
uint32_t osMessageQueueGetCount(osMessageQueueId_t mq_id)
|
||||
{
|
||||
(void)mq_id;
|
||||
return s_q_cnt;
|
||||
}
|
||||
|
||||
osStatus_t osMessageQueueReset(osMessageQueueId_t mq_id)
|
||||
{
|
||||
(void)mq_id;
|
||||
s_q_head = 0u;
|
||||
s_q_cnt = 0u;
|
||||
return osOK;
|
||||
}
|
||||
|
||||
/* ===================== 内核状态开关 ===================== */
|
||||
|
||||
static osKernelState_t s_kernel_state = osKernelInactive;
|
||||
|
||||
osKernelState_t osKernelGetState(void)
|
||||
{
|
||||
return s_kernel_state;
|
||||
}
|
||||
|
||||
/* ===================== 复位与辅助 ===================== */
|
||||
|
||||
static void reset_all(void)
|
||||
{
|
||||
ff_fake_reset();
|
||||
locks_clear();
|
||||
queue_clear();
|
||||
s_clock_now = 1760234567u;
|
||||
s_clock_valid = 1;
|
||||
s_kernel_state = osKernelInactive;
|
||||
g_soe_err_count = 0;
|
||||
g_soe_info_count = 0;
|
||||
g_soe_debug_count = 0;
|
||||
/* 生产模块的 static 在场景之间会残留:逐位显式清,漏一次会红一片
|
||||
*(test/host/lftpd_io 与 nand_port 同一教训) */
|
||||
(void)memset(s_ring, 0, sizeof(s_ring));
|
||||
s_ring_head = 0u;
|
||||
s_ring_tail = 0u;
|
||||
s_queue = NULL;
|
||||
s_seq = 0u;
|
||||
s_drop_cnt = 0u;
|
||||
s_drop_logged = 0u;
|
||||
s_fault_reported = 0u;
|
||||
/* 生产语义前置:/log 由 defaultTask 挂载时建好,本模块假定它在(soe_store.h:
|
||||
* "由 syslog_drain 在卷就绪门之后挂接") */
|
||||
(void)ff_fake_mkdir_direct("/log");
|
||||
}
|
||||
|
||||
/* 用假卷 + 真 soe_rec_parse 校验落盘内容:路径常量取自生产宏 SOE_PATH */
|
||||
static int read_rec(uint32_t idx, soe_rec_t *p_out)
|
||||
{
|
||||
uint8_t raw[SOE_REC_SIZE];
|
||||
|
||||
if (ff_fake_fetch(SOE_PATH, raw, idx * SOE_REC_SIZE, SOE_REC_SIZE) != SOE_REC_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
return (soe_rec_parse(raw, p_out) == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ===================== TC-SOESTORE-001..007:post 侧 ===================== */
|
||||
|
||||
static void tc_post_side(void)
|
||||
{
|
||||
soe_rec_t out;
|
||||
uint32_t i;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-SOESTORE-001..007 post side (zero-IO contract) --\n");
|
||||
|
||||
/* 001:post 3 条只写环,绝不碰盘、绝不碰卷锁、绝不打日志 */
|
||||
reset_all();
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 0x1111u);
|
||||
soe_store_post((uint8_t)SOE_T_ELM_RETURN, 2u, 0x2222u);
|
||||
soe_store_post((uint8_t)SOE_T_PLATE_IN, SOE_ELM_NONE, 1u);
|
||||
ok = (s_ring_head == 3u) && (s_ring_tail == 0u) && (s_drop_cnt == 0u);
|
||||
for (i = 0u; i < (uint32_t)FF_FAKE_OP_N; i++) {
|
||||
if (g_ff_obs.calls[i] != 0) {
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
CHECK(ok && (s_lock_waits == 0) && (g_soe_err_count == 0) && (g_soe_info_count == 0),
|
||||
"TC-SOESTORE-001 post does zero FatFs / zero volume lock / zero logging (protectTask stack budget)");
|
||||
|
||||
/* 002:墙钟未建立 -> IV + epoch 字段 0(不造假时标) */
|
||||
reset_all();
|
||||
s_clock_valid = 0;
|
||||
soe_store_post((uint8_t)SOE_T_SAMPLE_BLOCK, 1u, 0xF0u);
|
||||
soe_store_drain(); /* 环 -> 卷:不 drain 就还在 RAM 里 */
|
||||
ok = read_rec(0u, &out);
|
||||
CHECK(ok && (out.flags & SOE_FLG_IV) != 0u && (out.epoch_s == 0u)
|
||||
&& (out.cp56[2] & 0x80u) != 0u && (out.value == 0xF0u),
|
||||
"TC-SOESTORE-002 no wall clock -> flags.IV + epoch=0 + CP56 IV bit (B9 rule)");
|
||||
|
||||
/* 003:seq 单调;丢弃的那条也占号(真机丢号 = 主机看到的号不连续是**如实**) */
|
||||
reset_all();
|
||||
for (i = 0u; i < 9u; i++) {
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i);
|
||||
}
|
||||
ok = (s_seq == 9u) && (s_drop_cnt == 1u);
|
||||
CHECK(ok, "TC-SOESTORE-003 seq increases monotonically, dropped entries still consume a number");
|
||||
|
||||
/* 004:环满丢弃不阻塞、不崩、**不打日志**(留痕归 drain 侧) */
|
||||
reset_all();
|
||||
for (i = 0u; i < 9u; i++) {
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i);
|
||||
}
|
||||
CHECK((s_ring_head == 8u) && (s_drop_cnt == 1u)
|
||||
&& (g_soe_err_count == 0) && (g_soe_info_count == 0),
|
||||
"TC-SOESTORE-004 ring-full drop: count only, zero DBG on post side (lesson: +320B tail chain)");
|
||||
|
||||
/* 005:内核运行 + 队列已建 -> 走队列,环不再写 */
|
||||
reset_all();
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); /* 先落一条,drain 会建队列 */
|
||||
s_kernel_state = osKernelRunning;
|
||||
soe_store_drain(); /* 建队列 + 落第 1 条 */
|
||||
s_ring_head = 0u; /* 观察用:清环位 */
|
||||
s_ring_tail = 0u;
|
||||
soe_store_post((uint8_t)SOE_T_ELM_RETURN, 1u, 2u);
|
||||
CHECK((s_ring_head == 0u) && (s_q_cnt == 1u) && (s_q_msgsz == SOE_REC_SIZE),
|
||||
"TC-SOESTORE-005 queue created + kernel running -> entries go to the queue, ring not written");
|
||||
|
||||
/* 006:队列满 -> 丢弃计数(与环满同一条可观测出口) */
|
||||
reset_all();
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||||
s_kernel_state = osKernelRunning;
|
||||
soe_store_drain(); /* 建队列 */
|
||||
(void)osMessageQueueReset(s_queue);
|
||||
for (i = 0u; i < SOE_QUEUE_MAX + 2u; i++) {
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i);
|
||||
}
|
||||
CHECK((s_q_cnt == SOE_QUEUE_MAX) && (s_drop_cnt == 2u),
|
||||
"TC-SOESTORE-006 queue full -> drop and count through the same soe_store_dropped exit");
|
||||
|
||||
/* 007:计数饱和不回绕 */
|
||||
reset_all();
|
||||
s_drop_cnt = UINT32_MAX;
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||||
CHECK((s_drop_cnt == UINT32_MAX) && (soe_store_dropped() == UINT32_MAX),
|
||||
"TC-SOESTORE-007 drop counter saturates at UINT32_MAX, never wraps");
|
||||
}
|
||||
|
||||
/* ===================== TC-SOESTORE-010..021:drain 落盘与故障注入 ===================== */
|
||||
|
||||
static void tc_drain_paths(void)
|
||||
{
|
||||
soe_rec_t out;
|
||||
uint32_t i;
|
||||
int ok;
|
||||
int opens_before;
|
||||
int writes_before;
|
||||
|
||||
printf("-- TC-SOESTORE-010..021 drain to disk / fault injection --\n");
|
||||
|
||||
/* 010:首次 drain 惰性建队列 + 1 条 64B 落盘且能解析回来 */
|
||||
reset_all();
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 3u, 0xABCDu);
|
||||
soe_store_drain();
|
||||
ok = (s_q_new_calls == 1) && (s_q_msgsz == SOE_REC_SIZE)
|
||||
&& (ff_fake_size(SOE_PATH) == SOE_REC_SIZE);
|
||||
CHECK(ok && read_rec(0u, &out) && (out.type == (uint8_t)SOE_T_ELM_ASSERT)
|
||||
&& (out.elm_id == 3u) && (out.value == 0xABCDu) && (out.seq == 0u),
|
||||
"TC-SOESTORE-010 lazy queue creation + fixed 64B record on disk + record decodes back");
|
||||
|
||||
/* 011:打开模式必须带 FA_WRITE —— 陷阱一 */
|
||||
reset_all();
|
||||
soe_store_post((uint8_t)SOE_T_PLATE_OUT, SOE_ELM_NONE, 0u);
|
||||
soe_store_drain();
|
||||
ok = (g_ff_obs.last_open_mode == (BYTE)(FA_WRITE | FA_OPEN_APPEND));
|
||||
/* 同一断言的"为什么必须带写位"保真对照:只给 FA_OPEN_APPEND 的句柄 f_write
|
||||
* 必须拿 FR_DENIED——假件如果哪天放行,这条会红,整套陷阱一断言随之失效 */
|
||||
{
|
||||
static FIL probe;
|
||||
static uint8_t payload[SOE_REC_SIZE];
|
||||
UINT bw = 0u;
|
||||
FRESULT fr2;
|
||||
|
||||
fr2 = f_open(&probe, SOE_PATH, FA_OPEN_APPEND);
|
||||
{
|
||||
FRESULT fr3 = f_write(&probe, payload, SOE_REC_SIZE, &bw);
|
||||
CHECK(ok && (fr2 == FR_OK) && (fr3 == FR_DENIED) && (bw == 0u),
|
||||
"TC-SOESTORE-011 mode must be FA_WRITE|FA_OPEN_APPEND; without the write bit f_write is FR_DENIED");
|
||||
}
|
||||
(void)f_close(&probe);
|
||||
}
|
||||
|
||||
/* 012:多条 = 一条一次 open/write/close(本模块刻意没有批量合并)+ FIFO */
|
||||
reset_all();
|
||||
for (i = 0u; i < 4u; i++) {
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)(i + 1u), 100u + i);
|
||||
}
|
||||
soe_store_drain();
|
||||
ok = (ff_fake_size(SOE_PATH) == 4u * SOE_REC_SIZE)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 4)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 4);
|
||||
if (ok) {
|
||||
for (i = 0u; i < 4u; i++) {
|
||||
if (!read_rec(i, &out) || (out.seq != i) || (out.value != 100u + i)) {
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
CHECK(ok, "TC-SOESTORE-012 per-entry open/write/close (no batch merge) + correct FIFO order");
|
||||
|
||||
/* 013:open/close 严格配对 */
|
||||
reset_all();
|
||||
soe_store_post((uint8_t)SOE_T_SAMPLE_RESUME, SOE_ELM_NONE, 0u);
|
||||
soe_store_drain();
|
||||
CHECK(g_ff_obs.calls[FF_FAKE_OP_CLOSE] == g_ff_obs.calls[FF_FAKE_OP_OPEN],
|
||||
"TC-SOESTORE-013 open/close stay paired (no stale handle left across calls)");
|
||||
|
||||
/* 014:短写判失败 -> unlink 重建 -> 重试成功 -> 恰好 64 字节 */
|
||||
reset_all();
|
||||
ff_fake_inject_write_short(SOE_PATH, SOE_REC_SIZE - 1u, 1);
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 7u, 0x555Au);
|
||||
soe_store_drain();
|
||||
CHECK((ff_fake_size(SOE_PATH) == SOE_REC_SIZE)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] >= 1)
|
||||
&& (g_ff_obs.last_write_bw == SOE_REC_SIZE)
|
||||
&& (g_soe_err_count == 0),
|
||||
"TC-SOESTORE-014 FR_OK plus short write counts as failure (63B would pass silently without check)");
|
||||
|
||||
/* 015:连续**两轮** drain 都失败 -> 留痕只许 1 次(一轮 = 首写 + 自愈重试
|
||||
* = 2 次 f_write,所以注入 4 次零字节短写才够撑两轮) */
|
||||
reset_all();
|
||||
ff_fake_inject_write_short(SOE_PATH, 0u, 4);
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||||
soe_store_drain(); /* 第 1 轮失败:留痕 1 次,环不推进 */
|
||||
ok = (g_soe_err_count == 1) && (s_ring_tail == 0u) && (s_fault_reported == 1u);
|
||||
soe_store_drain(); /* 第 2 轮仍失败:不得再留痕 */
|
||||
CHECK(ok && (g_soe_err_count == 1) && (s_ring_tail == 0u),
|
||||
"TC-SOESTORE-015 write-fault trace is de-duplicated (one fault must not flood UART and selftest)");
|
||||
|
||||
/* 015b:成功一次复位去重位,之后再失败才会重新留痕 */
|
||||
/* 015b:失败(留痕1) -> 恢复(去重位复位) -> 再失败(必须留痕2)。
|
||||
* 少了"成功复位"这一步,一次真机坏块就会让后续所有写故障永久静默。 */
|
||||
reset_all();
|
||||
ff_fake_inject_write_short(SOE_PATH, 0u, 2);
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||||
soe_store_drain(); /* 失败 -> 留痕 1 */
|
||||
soe_store_drain(); /* 注入耗尽 -> 成功 -> 复位 */
|
||||
ok = (g_soe_err_count == 1) && (s_fault_reported == 0u) && (s_ring_tail == 1u);
|
||||
ff_fake_inject_write_short(SOE_PATH, 0u, 2);
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 2u);
|
||||
soe_store_drain(); /* 又失败 -> 留痕 2 */
|
||||
CHECK(ok && (g_soe_err_count == 2) && (s_fault_reported == 1u),
|
||||
"TC-SOESTORE-015b a failure after recovery must trace again (missing reset = permanent silence)");
|
||||
|
||||
/* 016:卷忙 -> -2:不碰 FatFs、不 release、环不推进 */
|
||||
reset_all();
|
||||
lock_busy_next(1);
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||||
soe_store_drain();
|
||||
ok = (s_lock_waits == 1) && (s_lock_releases == 0) && (s_lock_seq_err == 0);
|
||||
CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0)
|
||||
&& (s_ring_tail == 0u) && (g_soe_err_count == 0),
|
||||
"TC-SOESTORE-016 volume busy means defer: zero FatFs, zero release, no fault counted (feed duty)");
|
||||
|
||||
/* 017:写失败时环不推进 tail -> 下轮重试、既不丢也不重复 */
|
||||
reset_all();
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 5u, 0x77u);
|
||||
ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1);
|
||||
soe_store_drain(); /* open 失败 -> -1,tail 不动 */
|
||||
ok = (s_ring_tail == 0u) && (ff_fake_size(SOE_PATH) == 0u ||
|
||||
ff_fake_exists(SOE_PATH) == 0);
|
||||
soe_store_drain(); /* 下轮成功 */
|
||||
CHECK(ok && (s_ring_tail == 1u) && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE)
|
||||
&& read_rec(0u, &out) && (out.elm_id == 5u),
|
||||
"TC-SOESTORE-017 ring write failure does not advance tail: nothing lost, no duplicate next round");
|
||||
|
||||
/* 018:队列条目已出队 -> 卷忙只丢当前一条,剩余留在队列(现状不对称) */
|
||||
reset_all();
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||||
s_kernel_state = osKernelRunning;
|
||||
soe_store_drain(); /* 建队列 */
|
||||
(void)osMessageQueueReset(s_queue);
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 10u, 10u);
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 11u, 11u);
|
||||
opens_before = g_ff_obs.calls[FF_FAKE_OP_OPEN]; /* 前面那次 drain 已开过盘 */
|
||||
writes_before = g_ff_obs.calls[FF_FAKE_OP_WRITE];
|
||||
lock_busy_next(1);
|
||||
soe_store_drain();
|
||||
CHECK((s_q_cnt == 1u)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_before)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_WRITE] == writes_before)
|
||||
&& (s_lock_waits >= 1) && (s_lock_seq_err == 0) && (s_q_get_calls >= 2),
|
||||
"TC-SOESTORE-018 queue path with busy volume: only the dequeued entry is lost, zero FatFs now");
|
||||
|
||||
/* 019:容量满 -> unlink 重建 + INFO 留痕,新文件恰好 64 字节 */
|
||||
reset_all();
|
||||
CHECK(ff_fake_mkfile_size(SOE_PATH, (uint32_t)SOE_STORE_MAX * SOE_REC_SIZE) == 0,
|
||||
"TC-SOESTORE-019 setup: lay down 1024 fake records (64KB)");
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 9u, 0x99u);
|
||||
soe_store_drain();
|
||||
CHECK((g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||||
&& (ff_fake_size(SOE_PATH) == SOE_REC_SIZE)
|
||||
&& (g_soe_info_count == 1) && read_rec(0u, &out) && (out.elm_id == 9u),
|
||||
"TC-SOESTORE-019 full file is deleted and recreated: the new file holds only this entry");
|
||||
|
||||
/* 020:丢弃留痕按差值上报一次,不刷屏 */
|
||||
reset_all();
|
||||
for (i = 0u; i < SOE_RING_MAX + 3u; i++) {
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i); /* 环 8 + 丢 3 */
|
||||
}
|
||||
soe_store_drain(); /* 差值上报 1 条 */
|
||||
soe_store_drain(); /* 无新丢弃 -> 不再报 */
|
||||
CHECK((s_drop_cnt == 3u) && (s_drop_logged == 3u) && (g_soe_err_count == 1),
|
||||
"TC-SOESTORE-020 drop trace reports the delta once (only the defaultTask side is log-safe)");
|
||||
|
||||
/* 021:队列创建失败 -> 条目滞留环、下轮再试、不崩 */
|
||||
/* 021:osMessageQueueNew 返回 NULL —— 生产代码里环在队列**之前**排空,
|
||||
* 所以条目不丢;留痕一次,内核已运行时后续 post 仍走环(s_queue 为空),
|
||||
* 下一轮再建队列。真机上 heap_4 见底就是这种形态。 */
|
||||
reset_all();
|
||||
s_kernel_state = osKernelRunning;
|
||||
s_q_new_fail = 1;
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 4u, 4u);
|
||||
soe_store_drain();
|
||||
ok = (s_queue == NULL) && (g_soe_err_count == 1)
|
||||
&& (s_ring_tail == 1u) && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE);
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 5u, 5u); /* 队列没建成 -> 仍进环 */
|
||||
CHECK(ok && (s_q_cnt == 0u) && (s_ring_head == 2u),
|
||||
"TC-SOESTORE-021 queue creation failure: ring path still writes to disk, loses nothing + one trace");
|
||||
s_q_new_fail = 0;
|
||||
soe_store_drain();
|
||||
CHECK((s_queue != NULL) && (s_ring_tail == 2u)
|
||||
&& (ff_fake_size(SOE_PATH) == 2u * SOE_REC_SIZE) && (g_soe_err_count == 1),
|
||||
"TC-SOESTORE-021b once the heap recovers the queue is created and all pending entries reach disk");
|
||||
}
|
||||
|
||||
/* ===================== TC-SOESTORE-022..024:陷阱二 —— 名字/参数类错误码 ===================== */
|
||||
|
||||
static void tc_name_error_not_volume(void)
|
||||
{
|
||||
uint32_t i;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-SOESTORE-022..024 name/parameter errors must never escalate into volume operations --\n");
|
||||
|
||||
/* 022【D 项已修 2026-10-24:正向断言】f_open 失败(名字/参数类码)
|
||||
* -> 只留痕返回 -1:**绝不 unlink**、不动 /log 目录、已有记录一条不少。
|
||||
* 生产侧的定案是"打开失败不重试也不删",理由写在 s_soe_append 函数头:
|
||||
* SOE 是事件序列,删文件 = 销毁已发生的事件。 */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(SOE_PATH, NULL, 3u * SOE_REC_SIZE); /* 已有 3 条历史 */
|
||||
(void)ff_fake_mkfile("/log/run.log", NULL, 0u); /* 邻居文件(同卷) */
|
||||
ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, FR_INVALID_NAME, 2);
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||||
soe_store_drain(); /* open 失败 */
|
||||
ok = (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0)
|
||||
&& (ff_fake_exists("/log") == 2)
|
||||
&& (ff_fake_size(SOE_PATH) == 3u * SOE_REC_SIZE) /* 历史原封不动 */
|
||||
&& (ff_fake_exists("/log/run.log") == 1)
|
||||
&& (s_ring_tail == 0u) && (g_soe_err_count == 1);
|
||||
CHECK(ok, "TC-SOESTORE-022 open failure keeps history: no unlink, entry stays queued");
|
||||
|
||||
/* 022b 逐码对齐(与 test/host/sys_log_mcu 的 TC-SYSLOG-101/102 同一张表):
|
||||
* SOE 侧对**所有**打开失败码都不 unlink,是 run.log 侧触发集的保守子集
|
||||
* ——介质类码在 run.log 侧会 unlink 重建,在 SOE 侧不会,这是刻意的差异
|
||||
* (事件序列不可销毁),两处注释同源、改动必须同步。 */
|
||||
{
|
||||
static const FRESULT codes[] = { FR_INVALID_NAME, FR_NO_PATH, FR_NO_FILE,
|
||||
FR_INVALID_OBJECT, FR_DISK_ERR, FR_INT_ERR,
|
||||
FR_NO_FILESYSTEM, FR_DENIED };
|
||||
uint32_t c;
|
||||
int all_ok = 1;
|
||||
for (c = 0u; c < (uint32_t)(sizeof(codes) / sizeof(codes[0])); c++) {
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile(SOE_PATH, NULL, SOE_REC_SIZE);
|
||||
ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, codes[c], 1);
|
||||
soe_store_post((uint8_t)SOE_T_ELM_RETURN, (uint8_t)c, c);
|
||||
soe_store_drain();
|
||||
if ((g_ff_obs.calls[FF_FAKE_OP_UNLINK] != 0)
|
||||
|| (ff_fake_size(SOE_PATH) != SOE_REC_SIZE)
|
||||
|| (s_ring_tail != 0u)) {
|
||||
all_ok = 0;
|
||||
}
|
||||
}
|
||||
CHECK(all_ok, "TC-SOESTORE-022b no open-failure code ever unlinks soe.log (8 codes)");
|
||||
}
|
||||
|
||||
/* 023:反复失败的自愈上限:每轮最多 unlink 一次,条目与卷都不被吞 */
|
||||
reset_all();
|
||||
(void)ff_fake_mkfile("/log/run.log", NULL, 0u);
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 2u);
|
||||
for (i = 0u; i < 5u; i++) {
|
||||
ff_fake_inject_write_short(SOE_PATH, 0u, 2); /* 每轮两次短写都失败 */
|
||||
soe_store_drain();
|
||||
}
|
||||
ok = (g_ff_obs.calls[FF_FAKE_OP_UNLINK] <= (int)(5 * 1))
|
||||
&& (ff_fake_exists("/log") == 2) && (ff_fake_exists("/log/run.log") == 1)
|
||||
&& (s_ring_tail == 0u);
|
||||
ff_fake_inject_write_short(SOE_PATH, 0u, 0); /* 清掉不再需要的注入 */
|
||||
(void)ok;
|
||||
CHECK(ok, "TC-SOESTORE-023 repeated failures: unlink stays bounded (<=1 per round), never a volume action");
|
||||
|
||||
/* 024:假卷严格 8.3 校验的保真自检 —— 生产路径必过、点开头必被拒 */
|
||||
reset_all();
|
||||
{
|
||||
static FIL probe;
|
||||
FRESULT fr_ok;
|
||||
FRESULT fr_dot;
|
||||
FRESULT fr_colon;
|
||||
|
||||
fr_ok = f_open(&probe, SOE_PATH, FA_WRITE | FA_OPEN_APPEND);
|
||||
(void)f_close(&probe);
|
||||
fr_dot = f_open(&probe, "/log/.fschk", FA_WRITE | FA_CREATE_ALWAYS);
|
||||
if (fr_dot == FR_OK) {
|
||||
(void)f_close(&probe);
|
||||
}
|
||||
fr_colon = f_open(&probe, "1:/log/soe.log", FA_WRITE | FA_OPEN_APPEND);
|
||||
if (fr_colon == FR_OK) {
|
||||
(void)f_close(&probe);
|
||||
}
|
||||
CHECK((fr_ok == FR_OK)
|
||||
&& ((fr_dot == FR_INVALID_NAME) || (fr_dot == FR_INVALID_DRIVE))
|
||||
&& (fr_colon == FR_INVALID_DRIVE),
|
||||
"TC-SOESTORE-024 fake fidelity: /log/soe.log passes 8.3, .fschk and a bad drive prefix are rejected");
|
||||
}
|
||||
/* 生产路径本身合规的正向断言:改名成非 8.3 会让上面那条保真自检变红 */
|
||||
reset_all();
|
||||
soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u);
|
||||
soe_store_drain();
|
||||
CHECK((strcmp(g_ff_obs.last_open_path, "/LOG/SOE.LOG") == 0)
|
||||
&& (g_ff_obs.last_open_rc == (int)FR_OK),
|
||||
"TC-SOESTORE-024b SOE_PATH really opens on a strict 8.3 volume (guard against silent write failure)");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== SOE Store Tests ===\n");
|
||||
|
||||
tc_post_side();
|
||||
tc_drain_paths();
|
||||
tc_name_error_not_volume();
|
||||
|
||||
printf("\n=== SOE Store Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - cmsis_os.h(soe_store 套件专用)
|
||||
* 模块功能:给 App/log/soe_store.c 一个主机可编译的 CMSIS-RTOS V2 **消息队列 +
|
||||
* 内核状态**最小子集。
|
||||
*
|
||||
* ⚠ 仅供 test/host/soe_store/ 使用,不参与 MCU 构建。
|
||||
* 真实 Middlewares/.../CMSIS_RTOS_V2/cmsis_os.h 第 126/127/155 行会拉
|
||||
* FreeRTOS.h + task.h + cmsis_os2.h(整棵内核 + portmacro),PC 上编不动。
|
||||
*
|
||||
* 保真点(逐条对照真实头,签名一字不改;改动真实头即编译期报错):
|
||||
* - osMessageQueueId_t = void * (cmsis_os2.h:207)
|
||||
* - osMessageQueueAttr_t 六字段同序同型 (cmsis_os2.h:272~279)
|
||||
* ——soe_store.c 用 .name 指定成员初始化,字段名也必须一致
|
||||
* - osStatus_t osMessageQueuePut(id, const void *, uint8_t, uint32_t) (:689)
|
||||
* - osStatus_t osMessageQueueGet (id, void *, uint8_t *, uint32_t) (:697)
|
||||
* - osMessageQueueId_t osMessageQueueNew(uint32_t, uint32_t, const attr *)(:676)
|
||||
* - osKernelState_t osKernelGetState(void) (cmsis_os2.h:298)
|
||||
* - osOK = 0 / osErrorResource = -3 (cmsis_os2.h:177/180)
|
||||
* - osKernelRunning = 2 (cmsis_os2.h:63)
|
||||
* 本模块只用到内核状态的两个值,其余按真实枚举原样列出(少列会让"顺手改了
|
||||
* 状态判定"这类漂移编不出来)。
|
||||
*
|
||||
* 队列行为由测试单元给**真 FIFO + 满/空语义**的强符号实现(不是恒返 osOK 的空壳):
|
||||
* soe_store_post 的"队列满就丢弃并计数"、drain 的"取空即停"全靠这两条才测得出。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
#ifndef CMSIS_OS_H
|
||||
#define CMSIS_OS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 不透明句柄(真实 cmsis_os2.h:195~207 全是 void *) */
|
||||
typedef void *osMessageQueueId_t;
|
||||
typedef void *osThreadId_t;
|
||||
typedef void *osTimerId_t;
|
||||
typedef void *osEventFlagsId_t;
|
||||
typedef void *osMutexId_t;
|
||||
typedef void *osSemaphoreId_t;
|
||||
|
||||
/* 状态码(cmsis_os2.h:176~186) */
|
||||
typedef enum {
|
||||
osOK = 0,
|
||||
osError = -1,
|
||||
osErrorTimeout = -2,
|
||||
osErrorResource = -3,
|
||||
osErrorParameter = -4,
|
||||
osErrorNoMemory = -5,
|
||||
osErrorISR = -6
|
||||
} osStatus_t;
|
||||
|
||||
/* 内核状态(cmsis_os2.h:60~68) */
|
||||
typedef enum {
|
||||
osKernelInactive = 0,
|
||||
osKernelReady = 1,
|
||||
osKernelRunning = 2,
|
||||
osKernelLocked = 3,
|
||||
osKernelSuspended = 4,
|
||||
osKernelError = -1
|
||||
} osKernelState_t;
|
||||
|
||||
/* 消息队列属性(cmsis_os2.h:272~279,字段名与顺序原样) */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint32_t attr_bits;
|
||||
void *cb_mem;
|
||||
uint32_t cb_size;
|
||||
void *mq_mem;
|
||||
uint32_t mq_size;
|
||||
} osMessageQueueAttr_t;
|
||||
|
||||
osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size,
|
||||
const osMessageQueueAttr_t *attr);
|
||||
osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr,
|
||||
uint8_t msg_prio, uint32_t timeout);
|
||||
osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr,
|
||||
uint8_t *msg_prio, uint32_t timeout);
|
||||
uint32_t osMessageQueueGetCount(osMessageQueueId_t mq_id);
|
||||
osStatus_t osMessageQueueReset(osMessageQueueId_t mq_id);
|
||||
osKernelState_t osKernelGetState(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* CMSIS_OS_H */
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - dbg_log.h(soe_store 套件专用)
|
||||
* 模块功能:DBG_* 换成计数,让"post 侧一条日志都不许打、drain 侧按差值只打一次"
|
||||
* 成为可断言的行为。
|
||||
*
|
||||
* ⚠ 仅供 test/host/soe_store/ 使用,不参与 MCU 构建。
|
||||
*
|
||||
* 为什么这条计数在本模块**尤其**重要(App/log/soe_store.c 的函数头原话):
|
||||
* soe_store_post() 由 protectTask 调用,该任务的静态最坏链 = 经 s_log_active_edge
|
||||
* 的 console 尾链(872 B,B5 实测),post 里任何日志调用点都会再垫高一帧
|
||||
* (stack_report 实测 +320 B → FAIL)。所以"post 零 DBG"不是风格问题,
|
||||
* 是栈预算硬约束;真实宏展开成 snprintf + 192 B 行缓冲,只有把宏换成计数
|
||||
* 才能在主机上把这条钉住。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
#ifndef DBG_LOG_H
|
||||
#define DBG_LOG_H
|
||||
|
||||
/* 计数定义在 soe_store_test.c */
|
||||
extern int g_soe_err_count;
|
||||
extern int g_soe_info_count;
|
||||
extern int g_soe_debug_count;
|
||||
|
||||
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_soe_info_count++; } while (0)
|
||||
#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_soe_debug_count++; } while (0)
|
||||
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_soe_err_count++; } while (0)
|
||||
|
||||
#endif /* DBG_LOG_H */
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - main.h(soe_store 套件专用)
|
||||
* 模块功能:截断 App/util/sys_clock.h -> Drivers/BSP/SD2506/sd2506.h -> main.h
|
||||
* -> Inc/main.h -> stm32f4xx_hal.h 这条 include 链。
|
||||
*
|
||||
* ⚠ 仅供 test/host/soe_store/ 使用,不参与 MCU 构建。
|
||||
* 真实 Inc/main.h 第一行就 #include "stm32f4xx_hal.h"(整棵 HAL + CMSIS),
|
||||
* PC 上编不动;本桩与 test/host/sys_clock/stub/main.h 同一招(该套件已验证
|
||||
* "真实 sys_clock.h + 真实 sd2506.h + 桩 main.h"这条组合可行),好处是
|
||||
* **sys_clock.h / sd2506.h 保持真实**:sys_clock_get() 的签名一改即编译期报错,
|
||||
* 不必像 test/host/time_sync 那样连 sys_clock.h 一起桩掉。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
#ifndef __MAIN_H
|
||||
#define __MAIN_H
|
||||
|
||||
#include <stddef.h> /* size_t:真实 sys_clock.h 的 get_str 原型要用 */
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* HAL 毫秒时基(MCU 端 = TIM7 中断累加)。本模块不读它,声明在此只为让
|
||||
* 真实 main.h 的消费者(sd2506.h / sys_clock.h)在 PC 上也拿得到同一符号。 */
|
||||
uint32_t HAL_GetTick(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __MAIN_H */
|
||||
@@ -189,19 +189,19 @@ static void tc_000_uninitialized(void) {
|
||||
sd2506_time_t t;
|
||||
char buf[32];
|
||||
|
||||
printf("TC-SYSCLK-000 未初始化语义\n");
|
||||
printf("TC-SYSCLK-000 uninitialised semantics\n");
|
||||
/* 注意:本用例不做 sys_clock_init / sys_clock_set */
|
||||
CHECK_EQ_U32(sys_clock_get(), UINT32_MAX, "未初始化 get 返回 UINT32_MAX");
|
||||
CHECK(sys_clock_get_time(&t) == -1, "未初始化 get_time 返回 -1");
|
||||
CHECK_EQ_U32(sys_clock_get(), UINT32_MAX, "get before init returns UINT32_MAX");
|
||||
CHECK(sys_clock_get_time(&t) == -1, "get_time before init returns -1");
|
||||
|
||||
memset(buf, 0, sizeof(buf));
|
||||
CHECK(sys_clock_get_str(buf, sizeof(buf)) == buf, "get_str 返回 buf 指针");
|
||||
CHECK(strcmp(buf, "----/--/-- --:--:--") == 0, "get_str 未初始化占位串");
|
||||
CHECK(sys_clock_get_str(buf, sizeof(buf)) == buf, "get_str returns the buf pointer");
|
||||
CHECK(strcmp(buf, "----/--/-- --:--:--") == 0, "get_str placeholder string before init");
|
||||
}
|
||||
|
||||
static void tc_001_init_ok(void) {
|
||||
int rc;
|
||||
printf("TC-SYSCLK-001 init 正常(RTC 合法)\n");
|
||||
printf("TC-SYSCLK-001 init normal (valid RTC)\n");
|
||||
rtc_set(2026, 8, 24, 0, 0, 0);
|
||||
s_rtc_read_rc = BSP_OK;
|
||||
s_rtc_write_rc = BSP_OK;
|
||||
@@ -209,60 +209,60 @@ static void tc_001_init_ok(void) {
|
||||
s_tick = 12345u;
|
||||
|
||||
rc = sys_clock_init();
|
||||
CHECK(rc == 0, "RTC 合法 → init 返回 0");
|
||||
CHECK(rc == 0, "valid RTC -> init returns 0");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 8, 24, 0, 0, 0),
|
||||
"基准 = 2026-08-24 00:00:00 的 Unix 秒");
|
||||
CHECK(s_rtc_set_calls == 0, "合法 RTC 不被写回(无校时)");
|
||||
"base = Unix seconds of 2026-08-24 00:00:00");
|
||||
CHECK(s_rtc_set_calls == 0, "valid RTC is never written back (no time correction)");
|
||||
}
|
||||
|
||||
static void tc_002_init_leap(void) {
|
||||
printf("TC-SYSCLK-002 init 闰年换算\n");
|
||||
printf("TC-SYSCLK-002 init leap-year conversion\n");
|
||||
/* 2024 是闰年:2/29 存在 */
|
||||
rtc_set(2024, 2, 29, 12, 0, 0);
|
||||
s_rtc_read_rc = BSP_OK;
|
||||
CHECK(sys_clock_init() == 0, "2024-02-29 合法");
|
||||
CHECK(sys_clock_init() == 0, "2024-02-29 accepted");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(2024, 2, 29, 12, 0, 0),
|
||||
"2024-02-29 12:00:00 换算正确");
|
||||
"2024-02-29 12:00:00 converts correctly");
|
||||
|
||||
/* 2000 是世纪闰年:2/29 存在 */
|
||||
rtc_set(2000, 2, 29, 0, 0, 0);
|
||||
CHECK(sys_clock_init() == 0, "2000-02-29 合法(世纪闰年)");
|
||||
CHECK(sys_clock_init() == 0, "2000-02-29 accepted (century leap year)");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(2000, 2, 29, 0, 0, 0),
|
||||
"2000-02-29 换算正确");
|
||||
"2000-02-29 converts correctly");
|
||||
|
||||
/* 3 月 1 日:验证闰年 2 月多 1 天被正确累计 */
|
||||
rtc_set(2024, 3, 1, 0, 0, 0);
|
||||
CHECK(sys_clock_init() == 0, "2024-03-01 合法");
|
||||
CHECK(sys_clock_init() == 0, "2024-03-01 accepted");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(2024, 3, 1, 0, 0, 0),
|
||||
"2024-03-01 与 2024-02-29 相差恰 1 天");
|
||||
"2024-03-01 is exactly 1 day after 2024-02-29");
|
||||
CHECK_EQ_U32(ref_unix(2024, 3, 1, 0, 0, 0) - ref_unix(2024, 2, 29, 0, 0, 0),
|
||||
86400u, "闰日间隔 = 86400 s");
|
||||
86400u, "leap-day gap = 86400 s");
|
||||
}
|
||||
|
||||
static void tc_003_init_month_end(void) {
|
||||
printf("TC-SYSCLK-003 init 月末边界\n");
|
||||
printf("TC-SYSCLK-003 init end-of-month boundaries\n");
|
||||
rtc_set(2026, 1, 31, 0, 0, 0); s_rtc_read_rc = BSP_OK;
|
||||
CHECK(sys_clock_init() == 0, "1/31 合法");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 1, 31, 0, 0, 0), "1/31 换算");
|
||||
CHECK(sys_clock_init() == 0, "1/31 accepted");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 1, 31, 0, 0, 0), "1/31 conversion");
|
||||
|
||||
rtc_set(2026, 2, 28, 23, 59, 59); s_rtc_read_rc = BSP_OK;
|
||||
CHECK(sys_clock_init() == 0, "2/28 合法(平年)");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 2, 28, 23, 59, 59), "2/28 末端换算");
|
||||
CHECK(sys_clock_init() == 0, "2/28 accepted (common year)");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 2, 28, 23, 59, 59), "2/28 end-of-day conversion");
|
||||
|
||||
rtc_set(2026, 4, 30, 0, 0, 0); s_rtc_read_rc = BSP_OK;
|
||||
CHECK(sys_clock_init() == 0, "4/30 合法(30 天月)");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 4, 30, 0, 0, 0), "4/30 换算");
|
||||
CHECK(sys_clock_init() == 0, "4/30 accepted (30-day month)");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 4, 30, 0, 0, 0), "4/30 conversion");
|
||||
|
||||
/* 2/28 → 3/1 间隔恰 1 天(平年) */
|
||||
rtc_set(2026, 2, 28, 0, 0, 0); sys_clock_init();
|
||||
uint32_t a = sys_clock_get();
|
||||
rtc_set(2026, 3, 1, 0, 0, 0); sys_clock_init();
|
||||
uint32_t b = sys_clock_get();
|
||||
CHECK_EQ_U32(b - a, 86400u, "平年 2/28 → 3/1 间隔 = 86400 s");
|
||||
CHECK_EQ_U32(b - a, 86400u, "common year 2/28 -> 3/1 gap = 86400 s");
|
||||
}
|
||||
|
||||
static void tc_004_init_epoch_bounds(void) {
|
||||
printf("TC-SYSCLK-004 init 世纪边界(Unix 下限 / 2038 上限)\n");
|
||||
printf("TC-SYSCLK-004 init century boundaries (Unix lower bound / 2038 upper bound)\n");
|
||||
/*
|
||||
* 注意:init 接受的 RTC 年份范围是 2000~2099(越界即判故障并自愈),
|
||||
* 故"Unix 0(1970-01-01)"**不可能经 init 建立**——1970 会被判越界。
|
||||
@@ -270,46 +270,46 @@ static void tc_004_init_epoch_bounds(void) {
|
||||
* 这里验证 init 可接受的最早年月,以及 uint32 上限。
|
||||
*/
|
||||
rtc_set(2000, 1, 1, 0, 0, 0); s_rtc_read_rc = BSP_OK;
|
||||
CHECK(sys_clock_init() == 0, "2000-01-01 合法(可接受最早年)");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(2000, 1, 1, 0, 0, 0), "2000-01-01 换算");
|
||||
CHECK(sys_clock_init() == 0, "2000-01-01 accepted (earliest acceptable year)");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(2000, 1, 1, 0, 0, 0), "2000-01-01 conversion");
|
||||
|
||||
/* 2038 上限:2038-01-19 03:14:07 = 0x7FFFFFFF */
|
||||
rtc_set(2038, 1, 19, 3, 14, 7); s_rtc_read_rc = BSP_OK;
|
||||
CHECK(sys_clock_init() == 0, "2038-01-19 合法(uint32 上限内)");
|
||||
CHECK_EQ_U32(sys_clock_get(), 0x7FFFFFFFu, "2038-01-19 03:14:07 → 0x7FFFFFFF");
|
||||
CHECK(sys_clock_init() == 0, "2038-01-19 accepted (within uint32 limit)");
|
||||
CHECK_EQ_U32(sys_clock_get(), 0x7FFFFFFFu, "2038-01-19 03:14:07 -> 0x7FFFFFFF");
|
||||
|
||||
/* Unix 0 经 set 路径显式可达(与 init 的范围限制无关) */
|
||||
clear_obs();
|
||||
CHECK(sys_clock_set(0u, NULL) == 0, "set(0) 合法(Unix 零点)");
|
||||
CHECK_EQ_U32(sys_clock_get(), 0u, "set(0) 后 get == 0(Unix 零点)");
|
||||
CHECK(sys_clock_set(0u, NULL) == 0, "set(0) accepted (Unix epoch)");
|
||||
CHECK_EQ_U32(sys_clock_get(), 0u, "after set(0) get == 0 (Unix epoch)");
|
||||
{
|
||||
sd2506_time_t t;
|
||||
CHECK(sys_clock_get_time(&t) == 0
|
||||
&& t.year == 1970 && t.month == 1 && t.day == 1,
|
||||
"Unix 0 → 1970-01-01(unix_to_sd2506 反向换算)");
|
||||
"Unix 0 -> 1970-01-01 (unix_to_sd2506 reverse conversion)");
|
||||
}
|
||||
}
|
||||
|
||||
static void tc_005_init_rtc_read_fail(void) {
|
||||
printf("TC-SYSCLK-005 init RTC 读失败 → 降级默认时间并返回 -1\n");
|
||||
printf("TC-SYSCLK-005 init RTC read failure -> degrade to default time and return -1\n");
|
||||
s_rtc_read_rc = BSP_ERR_IO; /* 注入读故障 */
|
||||
s_rtc_write_rc = BSP_OK;
|
||||
clear_obs();
|
||||
s_tick = 777u;
|
||||
|
||||
int rc = sys_clock_init();
|
||||
CHECK(rc == -1, "RTC 读失败 → init 返回 -1(报告芯片故障)");
|
||||
CHECK(rc == -1, "RTC read failure -> init returns -1 (reports the chip fault)");
|
||||
/* 降级基准 = 编译期默认 RTC_DEFAULT_*(2026-08-24 00:00:00) */
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(RTC_DEFAULT_YEAR, RTC_DEFAULT_MONTH,
|
||||
RTC_DEFAULT_DAY, RTC_DEFAULT_HOUR,
|
||||
RTC_DEFAULT_MINUTE, RTC_DEFAULT_SECOND),
|
||||
"降级基准 = 编译期默认时间(墙钟仍可用)");
|
||||
CHECK(g_sclk_err_count >= 1, "打了 DBG_ERROR(RTC 异常)");
|
||||
"degraded base = compile-time default time (wall clock still usable)");
|
||||
CHECK(g_sclk_err_count >= 1, "DBG_ERROR emitted (RTC abnormal)");
|
||||
s_rtc_read_rc = BSP_OK; /* 复位注入 */
|
||||
}
|
||||
|
||||
static void tc_006_init_out_of_range_selfheal(void) {
|
||||
printf("TC-SYSCLK-006 init RTC 越界 → 自愈写回默认时间(返回 0)\n");
|
||||
printf("TC-SYSCLK-006 init RTC out of range -> self-heal by writing back the default time (returns 0)\n");
|
||||
/*
|
||||
* RTC_DEFAULT_INIT_ENABLE=1 且 FORCE=0 时:越界值触发"写回默认时间",
|
||||
* 写成功且复读合法 → rtc_ok 恢复为 1 → **返回 0**(自愈,非降级)。
|
||||
@@ -319,23 +319,23 @@ static void tc_006_init_out_of_range_selfheal(void) {
|
||||
s_rtc_read_rc = BSP_OK;
|
||||
s_rtc_write_rc = BSP_OK;
|
||||
clear_obs();
|
||||
CHECK(sys_clock_init() == 0, "year=1999 → 自愈写回成功 → 返回 0");
|
||||
CHECK(s_rtc_set_calls == 1, "越界触发一次写回");
|
||||
CHECK(s_rtc.year == RTC_DEFAULT_YEAR, "阴影 RTC 已修正为默认年");
|
||||
CHECK(sys_clock_init() == 0, "year=1999 -> self-heal write-back ok -> returns 0");
|
||||
CHECK(s_rtc_set_calls == 1, "out-of-range triggers exactly one write-back");
|
||||
CHECK(s_rtc.year == RTC_DEFAULT_YEAR, "shadow RTC corrected to the default year");
|
||||
|
||||
rtc_set(2026, 13, 5, 0, 0, 0); /* month > 12 → 越界 */
|
||||
clear_obs();
|
||||
CHECK(sys_clock_init() == 0, "month=13 → 自愈 → 返回 0");
|
||||
CHECK(s_rtc_set_calls == 1, "越界触发一次写回");
|
||||
CHECK(sys_clock_init() == 0, "month=13 -> self-heal -> returns 0");
|
||||
CHECK(s_rtc_set_calls == 1, "out-of-range triggers exactly one write-back");
|
||||
|
||||
rtc_set(2026, 5, 0, 0, 0, 0); /* day < 1 → 越界 */
|
||||
clear_obs();
|
||||
CHECK(sys_clock_init() == 0, "day=0 → 自愈 → 返回 0");
|
||||
CHECK(s_rtc_set_calls == 1, "越界触发一次写回");
|
||||
CHECK(sys_clock_init() == 0, "day=0 -> self-heal -> returns 0");
|
||||
CHECK(s_rtc_set_calls == 1, "out-of-range triggers exactly one write-back");
|
||||
}
|
||||
|
||||
static void tc_020_init_selfheal_fail_degrades(void) {
|
||||
printf("TC-SYSCLK-020 init 越界且写回失败 → 降级默认时间并返回 -1\n");
|
||||
printf("TC-SYSCLK-020 init out of range and write-back fails -> degrade to default time and return -1\n");
|
||||
/*
|
||||
* 组合故障:读成功但值越界(触发写回),写回失败 → 复读仍越界 →
|
||||
* rtc_ok 清 0 → 走降级分支(启用默认值时建基准 + 返回 -1)。
|
||||
@@ -346,81 +346,81 @@ static void tc_020_init_selfheal_fail_degrades(void) {
|
||||
clear_obs();
|
||||
s_tick = 42u;
|
||||
int rc = sys_clock_init();
|
||||
CHECK(rc == -1, "越界 + 写回失败 → init 返回 -1");
|
||||
CHECK(s_rtc_set_calls == 1, "尝试写回一次(失败)");
|
||||
CHECK(g_sclk_err_count >= 1, "打了 DBG_ERROR");
|
||||
CHECK(rc == -1, "out of range + write-back failure -> init returns -1");
|
||||
CHECK(s_rtc_set_calls == 1, "write-back attempted once (failed)");
|
||||
CHECK(g_sclk_err_count >= 1, "DBG_ERROR emitted");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(RTC_DEFAULT_YEAR, RTC_DEFAULT_MONTH,
|
||||
RTC_DEFAULT_DAY, 0, 0, 0),
|
||||
"降级基准 = 编译期默认时间(墙钟仍可用)");
|
||||
"degraded base = compile-time default time (wall clock still usable)");
|
||||
s_rtc_write_rc = BSP_OK;
|
||||
}
|
||||
|
||||
static void tc_007_init_corrects_rtc(void) {
|
||||
printf("TC-SYSCLK-007 init 越界 → 自动写回默认时间并复验\n");
|
||||
printf("TC-SYSCLK-007 init out of range -> auto write-back of default time and re-verify\n");
|
||||
rtc_set(1999, 1, 1, 0, 0, 0); /* 越界,触发校时写回 */
|
||||
s_rtc_read_rc = BSP_OK;
|
||||
s_rtc_write_rc = BSP_OK;
|
||||
clear_obs();
|
||||
int rc = sys_clock_init();
|
||||
CHECK(s_rtc_set_calls == 1, "越界 → 写回 RTC 一次");
|
||||
CHECK(rc == 0, "写回成功 + 复读合法 → 返回 0(自愈成功,非降级)");
|
||||
CHECK(s_rtc_set_calls == 1, "out of range -> RTC written back once");
|
||||
CHECK(rc == 0, "write-back ok + re-read valid -> returns 0 (self-heal succeeded, not a degrade)");
|
||||
CHECK(s_rtc.year == RTC_DEFAULT_YEAR && s_rtc.month == RTC_DEFAULT_MONTH
|
||||
&& s_rtc.day == RTC_DEFAULT_DAY, "阴影 RTC 已被写入默认时间");
|
||||
&& s_rtc.day == RTC_DEFAULT_DAY, "shadow RTC was written with the default time");
|
||||
CHECK_EQ_U32(sys_clock_get(), ref_unix(RTC_DEFAULT_YEAR, RTC_DEFAULT_MONTH,
|
||||
RTC_DEFAULT_DAY, 0, 0, 0),
|
||||
"基准 = 修正后的默认时间");
|
||||
"base = the corrected default time");
|
||||
}
|
||||
|
||||
static void tc_008_get_advance(void) {
|
||||
printf("TC-SYSCLK-008 get 正常推进(含亚秒截断)\n");
|
||||
printf("TC-SYSCLK-008 get advances normally (sub-second truncation included)\n");
|
||||
base_at(1000000u, 5000u);
|
||||
CHECK_EQ_U32(sys_clock_get(), 1000000u, "基准时刻 = 基准值");
|
||||
CHECK_EQ_U32(sys_clock_get(), 1000000u, "at the base instant the value equals the base");
|
||||
tick_advance(999u);
|
||||
CHECK_EQ_U32(sys_clock_get(), 1000000u, "999ms 未满 1s → 不变(截断)");
|
||||
CHECK_EQ_U32(sys_clock_get(), 1000000u, "999ms is under 1 s -> unchanged (truncated)");
|
||||
tick_advance(1u);
|
||||
CHECK_EQ_U32(sys_clock_get(), 1000001u, "累计 1000ms → +1s");
|
||||
CHECK_EQ_U32(sys_clock_get(), 1000001u, "accumulated 1000ms -> +1 s");
|
||||
tick_advance(2500u);
|
||||
CHECK_EQ_U32(sys_clock_get(), 1000003u, "再 +2500ms → 共 +3s(余 500ms 截断)");
|
||||
CHECK_EQ_U32(sys_clock_get(), 1000003u, "another +2500ms -> +3 s total (remaining 500 ms truncated)");
|
||||
}
|
||||
|
||||
static void tc_009_get_no_rebase_below_threshold(void) {
|
||||
printf("TC-SYSCLK-009 未达 30 天阈值不触发再基准化\n");
|
||||
printf("TC-SYSCLK-009 below the 30-day threshold no re-baselining\n");
|
||||
base_at(2000000u, 0u);
|
||||
/* 29 天 < 30 天阈值:线性推进,基准不动 */
|
||||
tick_advance(29u * 24u * 3600u * 1000u);
|
||||
CHECK_EQ_U32(sys_clock_get(), 2000000u + 29u * 24u * 3600u,
|
||||
"29 天线性推进正确(未再基准化)");
|
||||
"29 days advance linearly and correctly (no re-baselining)");
|
||||
}
|
||||
|
||||
static void tc_010_get_rebase_at_threshold(void) {
|
||||
printf("TC-SYSCLK-010 30 天惰性再基准化(窗口清零、值无损、余数结转)\n");
|
||||
printf("TC-SYSCLK-010 30-day lazy re-baselining (window cleared, value lossless, remainder carried over)\n");
|
||||
base_at(3000000u, 0u);
|
||||
|
||||
/* 推进恰好 30 天 + 1500ms(余数 500ms 应结转到新窗) */
|
||||
tick_advance(30u * 24u * 3600u * 1000u + 1500u);
|
||||
uint32_t expect = 3000000u + 30u * 24u * 3600u + 1u; /* 1500ms → +1s,余 500ms */
|
||||
CHECK_EQ_U32(sys_clock_get(), expect, "再基准化后墙钟值无损(+30 天 1 秒)");
|
||||
CHECK_EQ_U32(sys_clock_get(), expect, "after re-baselining the wall clock value is lossless (+30 days 1 second)");
|
||||
|
||||
/* 再推 500ms:结转的余数正好补足,再多 1 秒 */
|
||||
tick_advance(500u);
|
||||
CHECK_EQ_U32(sys_clock_get(), expect + 1u, "亚秒余数已结转(+500ms 补足 1 秒)");
|
||||
CHECK_EQ_U32(sys_clock_get(), expect + 1u, "sub-second remainder carried over (+500 ms completes 1 second)");
|
||||
|
||||
/* 再推一整天:仍线性 */
|
||||
tick_advance(24u * 3600u * 1000u);
|
||||
CHECK_EQ_U32(sys_clock_get(), expect + 1u + 86400u, "再基准化后继续线性推进");
|
||||
CHECK_EQ_U32(sys_clock_get(), expect + 1u + 86400u, "keeps advancing linearly after re-baselining");
|
||||
}
|
||||
|
||||
static void tc_011_get_tick_wrap(void) {
|
||||
printf("TC-SYSCLK-011 tick 回绕(无符号差)安全\n");
|
||||
printf("TC-SYSCLK-011 tick wraparound (unsigned diff) is safe\n");
|
||||
/* 基准设在接近 2^32 处 */
|
||||
base_at(4000000u, 0xFFFFF000u);
|
||||
/* 推进 0x2000 ms(跨越 2^32 回绕):差 = 0x2000 = 8192ms → +8s */
|
||||
tick_advance(0x2000u);
|
||||
CHECK_EQ_U32(sys_clock_get(), 4000000u + 8u, "跨 tick 回绕后差值正确");
|
||||
CHECK_EQ_U32(sys_clock_get(), 4000000u + 8u, "diff is correct across the tick wraparound");
|
||||
}
|
||||
|
||||
static void tc_012_get_long_run_no_drift(void) {
|
||||
printf("TC-SYSCLK-012 多轮再基准化长期不漂移\n");
|
||||
printf("TC-SYSCLK-012 many re-baselining rounds do not drift over time\n");
|
||||
base_at(5000000u, 0u);
|
||||
/*
|
||||
* 惰性再基准化的**正确用法**:周期性调用 get() 让差值窗口始终 < 30 天。
|
||||
@@ -434,44 +434,44 @@ static void tc_012_get_long_run_no_drift(void) {
|
||||
for (i = 1u; i <= 9u; i++) {
|
||||
tick_advance(10u * 86400u * 1000u); /* 每次 +10 天(< 30 天阈值) */
|
||||
CHECK_EQ_U32(sys_clock_get(), base + i * 10u * 86400u,
|
||||
"10 天步进(含 3 轮再基准)线性无漂移");
|
||||
"10-day steps (with 3 re-baseline rounds) stay linear with no drift");
|
||||
}
|
||||
}
|
||||
|
||||
static void tc_013_get_str(void) {
|
||||
printf("TC-SYSCLK-013 get_str 格式化\n");
|
||||
printf("TC-SYSCLK-013 get_str formatting\n");
|
||||
char buf[32];
|
||||
base_at(ref_unix(2026, 10, 2, 13, 4, 5), 0u);
|
||||
memset(buf, 0, sizeof(buf));
|
||||
sys_clock_get_str(buf, sizeof(buf));
|
||||
CHECK(strcmp(buf, "2026-10-02 13:04:05") == 0, "格式化 YYYY-MM-DD HH:MM:SS");
|
||||
CHECK(strcmp(buf, "2026-10-02 13:04:05") == 0, "formatted as YYYY-MM-DD HH:MM:SS");
|
||||
|
||||
base_at(ref_unix(2000, 1, 1, 0, 0, 0), 0u);
|
||||
memset(buf, 0, sizeof(buf));
|
||||
sys_clock_get_str(buf, sizeof(buf));
|
||||
CHECK(strcmp(buf, "2000-01-01 00:00:00") == 0, "零点补零格式正确");
|
||||
CHECK(strcmp(buf, "2000-01-01 00:00:00") == 0, "zero-padded midnight format is correct");
|
||||
}
|
||||
|
||||
static void tc_014_get_time_fields(void) {
|
||||
printf("TC-SYSCLK-014 get_time 日历字段\n");
|
||||
printf("TC-SYSCLK-014 get_time calendar fields\n");
|
||||
sd2506_time_t t;
|
||||
/* 2026-10-02 是周五(已知) */
|
||||
base_at(ref_unix(2026, 10, 2, 8, 9, 10), 0u);
|
||||
CHECK(sys_clock_get_time(&t) == 0, "get_time 返回 0");
|
||||
CHECK(sys_clock_get_time(&t) == 0, "get_time returns 0");
|
||||
CHECK(t.year == 2026 && t.month == 10 && t.day == 2,
|
||||
"年月日正确");
|
||||
CHECK(t.hour == 8 && t.minute == 9 && t.second == 10, "时分秒正确");
|
||||
CHECK(t.week == 5, "2026-10-02 星期五(week==5,0=周日)");
|
||||
"year/month/day correct");
|
||||
CHECK(t.hour == 8 && t.minute == 9 && t.second == 10, "hour/minute/second correct");
|
||||
CHECK(t.week == 5, "2026-10-02 is Friday (week==5, 0=Sunday)");
|
||||
}
|
||||
|
||||
static void tc_015_get_time_null(void) {
|
||||
printf("TC-SYSCLK-015 get_time NULL 参数被拒\n");
|
||||
printf("TC-SYSCLK-015 get_time rejects a NULL argument\n");
|
||||
base_at(6000000u, 0u);
|
||||
CHECK(sys_clock_get_time(NULL) == -1, "p_time == NULL → 返回 -1");
|
||||
CHECK(sys_clock_get_time(NULL) == -1, "p_time == NULL -> returns -1");
|
||||
}
|
||||
|
||||
static void tc_016_set_writes_rtc(void) {
|
||||
printf("TC-SYSCLK-016 set 更新基准 + 写回 RTC\n");
|
||||
printf("TC-SYSCLK-016 set updates the base + writes back RTC\n");
|
||||
sd2506_time_t sd;
|
||||
memset(&sd, 0, sizeof(sd));
|
||||
sd.year = 2027; sd.month = 3; sd.day = 15;
|
||||
@@ -480,48 +480,48 @@ static void tc_016_set_writes_rtc(void) {
|
||||
s_rtc_write_rc = BSP_OK;
|
||||
clear_obs();
|
||||
s_tick = 100u;
|
||||
CHECK(sys_clock_set(7000000u, &sd) == 0, "set 返回 0");
|
||||
CHECK_EQ_U32(sys_clock_get(), 7000000u, "基准已更新");
|
||||
CHECK(s_rtc_set_calls == 1, "sd_time != NULL → 写回 RTC 一次");
|
||||
CHECK(sys_clock_set(7000000u, &sd) == 0, "set returns 0");
|
||||
CHECK_EQ_U32(sys_clock_get(), 7000000u, "base has been updated");
|
||||
CHECK(s_rtc_set_calls == 1, "sd_time != NULL -> RTC written back once");
|
||||
CHECK(s_rtc.year == 2027 && s_rtc.month == 3 && s_rtc.day == 15
|
||||
&& s_rtc.hour == 6 && s_rtc.minute == 7 && s_rtc.second == 8,
|
||||
"RTC 写入内容与传入一致");
|
||||
"RTC written content equals what was passed in");
|
||||
|
||||
/* 写失败:基准仍更新,但打错误日志 */
|
||||
clear_obs();
|
||||
s_rtc_write_rc = BSP_ERR_IO;
|
||||
sys_clock_set(7000001u, &sd);
|
||||
CHECK_EQ_U32(sys_clock_get(), 7000001u, "RTC 写失败仍更新基准");
|
||||
CHECK(g_sclk_err_count >= 1, "RTC 写失败打 DBG_ERROR");
|
||||
CHECK_EQ_U32(sys_clock_get(), 7000001u, "base is still updated when the RTC write fails");
|
||||
CHECK(g_sclk_err_count >= 1, "DBG_ERROR emitted when the RTC write fails");
|
||||
s_rtc_write_rc = BSP_OK;
|
||||
}
|
||||
|
||||
static void tc_017_set_no_rtc(void) {
|
||||
printf("TC-SYSCLK-017 set sd_time=NULL 不碰 RTC\n");
|
||||
printf("TC-SYSCLK-017 set sd_time=NULL never touches the RTC\n");
|
||||
clear_obs();
|
||||
CHECK(sys_clock_set(8000000u, NULL) == 0, "set(NULL) 返回 0");
|
||||
CHECK(s_rtc_set_calls == 0, "sd_time == NULL → 不写 RTC");
|
||||
CHECK_EQ_U32(sys_clock_get(), 8000000u, "基准更新");
|
||||
CHECK(sys_clock_set(8000000u, NULL) == 0, "set(NULL) returns 0");
|
||||
CHECK(s_rtc_set_calls == 0, "sd_time == NULL -> RTC not written");
|
||||
CHECK_EQ_U32(sys_clock_get(), 8000000u, "base updated");
|
||||
}
|
||||
|
||||
static void tc_018_set_unix_fields(void) {
|
||||
printf("TC-SYSCLK-018 set_unix 写回 RTC 且字段正确\n");
|
||||
printf("TC-SYSCLK-018 set_unix writes back the RTC with correct fields\n");
|
||||
sd2506_time_t expect_week;
|
||||
uint32_t u = ref_unix(2026, 10, 2, 15, 30, 45);
|
||||
clear_obs();
|
||||
s_rtc_write_rc = BSP_OK;
|
||||
s_tick = 0u;
|
||||
CHECK(sys_clock_set_unix(u) == 0, "set_unix 返回 0");
|
||||
CHECK(s_rtc_set_calls == 1, "写回 RTC 一次");
|
||||
CHECK(sys_clock_set_unix(u) == 0, "set_unix returns 0");
|
||||
CHECK(s_rtc_set_calls == 1, "RTC written back once");
|
||||
CHECK(s_rtc.year == 2026 && s_rtc.month == 10 && s_rtc.day == 2
|
||||
&& s_rtc.hour == 15 && s_rtc.minute == 30 && s_rtc.second == 45,
|
||||
"RTC 日历字段与 Unix 秒一致");
|
||||
CHECK(s_rtc.week == 5, "RTC 星期字段 = 5(2026-10-02 周五)");
|
||||
"RTC calendar fields match the Unix seconds");
|
||||
CHECK(s_rtc.week == 5, "RTC weekday field = 5 (2026-10-02 is Friday)");
|
||||
(void)expect_week;
|
||||
}
|
||||
|
||||
static void tc_019_week_known_dates(void) {
|
||||
printf("TC-SYSCLK-019 星期(Zeller)已知日期穷举\n");
|
||||
printf("TC-SYSCLK-019 weekday (Zeller) exhaustive check against known dates\n");
|
||||
/* 期望值经 Python datetime 独立核对(0=周日);
|
||||
* 覆盖 1/2 月(Zeller 需上调为上年 13/14 月的分支)与 3~12 月(2026-10-02
|
||||
* 修正前全部错误),以及世纪闰年、非闰世纪、范围首尾。*/
|
||||
@@ -554,14 +554,14 @@ static void tc_019_week_known_dates(void) {
|
||||
cases[i].y, cases[i].m, cases[i].d, cases[i].w);
|
||||
CHECK(t.week == cases[i].w, nm);
|
||||
} else {
|
||||
CHECK(0, "get_time 失败(用例数据问题)");
|
||||
CHECK(0, "get_time failed (bad test data)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== System Clock 主机侧单元测试 ===\n");
|
||||
printf("被测:App/util/sys_clock.c(时间换算 + 墙钟基准)\n\n");
|
||||
printf("=== System Clock host-side unit tests ===\n");
|
||||
printf("Target: App/util/sys_clock.c (time conversion + wall-clock base)\n\n");
|
||||
|
||||
/* tc_000 必须最先(依赖"刚上电"的静态态) */
|
||||
tc_000_uninitialized();
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""运行/自检记录落盘后端(sys_log_mcu)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/sys_log_mcu/sys_log_mcu_test.c(以 #include 纳入**生产源**
|
||||
App/log/sys_log_mcu.c,并纳入共享假卷 test/host/common/fatfs_fake.c),在 PC 上离线
|
||||
跑 64B 定长记录格式、单条/批量两条写路径、syslog_drain 的 preboot+队列排空与
|
||||
"失败回投不丢条目"、N-24 的 8 条滑动读窗口 + 写者代数失效、挂载幂等与
|
||||
上电/重启分类、sys_log_evidence 的 wdev.log 取证通道,失败即非 0 退出。
|
||||
工具链与判定委托 test/host/common/unit_test_harness.py。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
1. **FA_OPEN_APPEND 不含 FA_WRITE**(存储架构 §10.1 第 11 条):漏写位时 f_open
|
||||
会成功、文件甚至被创建出来,只有 f_write 恒返 FR_DENIED —— 真机表现是
|
||||
"/log 写失败 x5/开机",2026-09-20 才定位到根因。假卷按真实 ff.c 判 fp->flag
|
||||
就能把这条做成必然失败的断言。
|
||||
2. **f_write 簇分配失败 = FR_OK + 短写**(同一页第 11 条后半):只判 fr != FR_OK
|
||||
等于把丢记录当写成功;短写注入只有假卷给得出。
|
||||
3. **挂载幂等**:syslog_mcu_mounted 曾在建 /log 失败后永久 no-op(运行期日志与自检
|
||||
全部滞留),改成幂等 + 重试后又必须防"每 2s 刷一条同样错误" → 要注入 mkdir
|
||||
失败才测得到,且必须能数出"到底留痕几次"。
|
||||
4. **读窗口 try 语义 / N-24 读侧**:窗口"命中即零 I/O"与"写者一落地就失效"是一枚硬币两面,
|
||||
只做前者会把陈旧读当正确、只做后者会退回逐条 open/seek/close 的旧实现。
|
||||
|
||||
桩的边界:ff.h / sys_log.h / soe_store.h / sys_clock.h / vol_lock.h 用**真实头**;
|
||||
main.h 是桩,只为截断 sys_clock.h -> sd2506.h -> main.h -> stm32f4xx_hal.h;
|
||||
cmsis_os.h / dbg_log.h 本目录桩。soe_store_drain() 用**计数桩**(本套只断言
|
||||
"每次 drain 各调一次"这个 B9 挂接点,SOE 落盘本身归 test/host/soe_store/)。
|
||||
FatFs 由 test/host/common/fatfs_fake.c 给 RAM 实现(**显式纳入,不用目录通配**)。
|
||||
sys_clock_* / vol_lock_* / osMessageQueue* 强符号桩写在本测试单元里。
|
||||
|
||||
用法(仓库根目录):
|
||||
& '.venv\\Scripts\\python.exe' test/host/sys_log_mcu/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
|
||||
|
||||
设计依据:docs/子系统设计/运行与自检记录设计说明.md §4.4/§4.5/§5.3、
|
||||
docs/架构设计/STM32F4-Base存储架构说明.md §10.1 第 10/11 条、
|
||||
docs/测试说明/主机单测覆盖率口径.md §9 P2 第 2 项
|
||||
创建日期:2026-10-24
|
||||
"""
|
||||
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 必须最先(覆盖 cmsis_os.h / dbg_log.h / main.h),其余全用真实头。
|
||||
# 假卷 fatfs_fake.c 由测试单元 #include 纳入(同 ch395f_fake.c 惯例,显式列文件、不通配)。
|
||||
BUILD = TestBuild(
|
||||
name="Sys Log Mcu Tests",
|
||||
src=os.path.join(HERE, "sys_log_mcu_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "test", "host", "common"),
|
||||
os.path.join(ROOT, "App", "log"),
|
||||
os.path.join(ROOT, "App", "util"),
|
||||
os.path.join(ROOT, "App", "fs"),
|
||||
os.path.join(ROOT, "Drivers", "BSP"),
|
||||
os.path.join(ROOT, "Drivers", "BSP", "SD2506"),
|
||||
os.path.join(ROOT, "Lib", "FatFs"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== Sys Log Mcu Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - cmsis_os.h(sys_log_mcu 套件专用)
|
||||
* 模块功能:给 App/log/sys_log_mcu.c(与 soe_store.c)一个/g主机可编译的 CMSIS-RTOS V2 **消息队列 +
|
||||
* 内核状态**最小子集。
|
||||
*
|
||||
* ⚠ 仅供 test/host/sys_log_mcu/ 使用,不参与 MCU 构建。
|
||||
* 真实 Middlewares/.../CMSIS_RTOS_V2/cmsis_os.h 第 126/127/155 行会拉
|
||||
* FreeRTOS.h + task.h + cmsis_os2.h(整棵内核 + portmacro),PC 上编不动。
|
||||
*
|
||||
* 保真点(逐条对照真实头,签名一字不改;改动真实头即编译期报错):
|
||||
* - osMessageQueueId_t = void * (cmsis_os2.h:207)
|
||||
* - osMessageQueueAttr_t 六字段同序同型 (cmsis_os2.h:272~279)
|
||||
* ——sys_log_mcu.c 用 .name 指定成员初始化,字段名也必须一致
|
||||
* - osStatus_t osMessageQueuePut(id, const void *, uint8_t, uint32_t) (:689)
|
||||
* - osStatus_t osMessageQueueGet (id, void *, uint8_t *, uint32_t) (:697)
|
||||
* - osMessageQueueId_t osMessageQueueNew(uint32_t, uint32_t, const attr *)(:676)
|
||||
* - osKernelState_t osKernelGetState(void) (cmsis_os2.h:298)
|
||||
* - osOK = 0 / osErrorResource = -3 (cmsis_os2.h:177/180)
|
||||
* - osKernelRunning = 2 (cmsis_os2.h:63)
|
||||
* 本模块只用到内核状态的两个值,其余按真实枚举原样列出(少列会让"顺手改了
|
||||
* 状态判定"这类漂移编不出来)。
|
||||
*
|
||||
* 队列行为由测试单元给**真 FIFO + 满/空语义**的强符号实现(不是恒返 osOK 的空壳):
|
||||
* soe_store_post 的"队列满就丢弃并计数"、drain 的"取空即停"全靠这两条才测得出。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
#ifndef CMSIS_OS_H
|
||||
#define CMSIS_OS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 不透明句柄(真实 cmsis_os2.h:195~207 全是 void *) */
|
||||
typedef void *osMessageQueueId_t;
|
||||
typedef void *osThreadId_t;
|
||||
typedef void *osTimerId_t;
|
||||
typedef void *osEventFlagsId_t;
|
||||
typedef void *osMutexId_t;
|
||||
typedef void *osSemaphoreId_t;
|
||||
|
||||
/* 状态码(cmsis_os2.h:176~186) */
|
||||
typedef enum {
|
||||
osOK = 0,
|
||||
osError = -1,
|
||||
osErrorTimeout = -2,
|
||||
osErrorResource = -3,
|
||||
osErrorParameter = -4,
|
||||
osErrorNoMemory = -5,
|
||||
osErrorISR = -6
|
||||
} osStatus_t;
|
||||
|
||||
/* 内核状态(cmsis_os2.h:60~68) */
|
||||
typedef enum {
|
||||
osKernelInactive = 0,
|
||||
osKernelReady = 1,
|
||||
osKernelRunning = 2,
|
||||
osKernelLocked = 3,
|
||||
osKernelSuspended = 4,
|
||||
osKernelError = -1
|
||||
} osKernelState_t;
|
||||
|
||||
/* 消息队列属性(cmsis_os2.h:272~279,字段名与顺序原样) */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint32_t attr_bits;
|
||||
void *cb_mem;
|
||||
uint32_t cb_size;
|
||||
void *mq_mem;
|
||||
uint32_t mq_size;
|
||||
} osMessageQueueAttr_t;
|
||||
|
||||
osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size,
|
||||
const osMessageQueueAttr_t *attr);
|
||||
osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr,
|
||||
uint8_t msg_prio, uint32_t timeout);
|
||||
osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr,
|
||||
uint8_t *msg_prio, uint32_t timeout);
|
||||
uint32_t osMessageQueueGetCount(osMessageQueueId_t mq_id);
|
||||
osStatus_t osMessageQueueReset(osMessageQueueId_t mq_id);
|
||||
osKernelState_t osKernelGetState(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* CMSIS_OS_H */
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - dbg_log.h(sys_log_mcu 套件专用)
|
||||
* 模块功能:DBG_* 换成计数,让"写故障只报第一次(L2 去重)"与"挂载失败重试不刷屏"
|
||||
* 成为可断言的行为。
|
||||
*
|
||||
* ⚠ 仅供 test/host/sys_log_mcu/ 使用,不参与 MCU 构建。
|
||||
*
|
||||
* 为什么本模块尤其需要计数而不是直通打印:sys_log_mcu.c 的 s_append_rec() 里
|
||||
* 有一条刻意设计的静默规则(源码注释)——
|
||||
* "L2 错误去重:持续故障只报第一次,恢复前静默(消除每次开机同一错误刷屏)"
|
||||
* 直通 printf 只能让人眼看,"到底报了几次"必须可数才能钉住;而另一头,
|
||||
* 挂载幂等里的"建 /log 失败日志去重(成功复位)"同样是次断言。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
#ifndef DBG_LOG_H
|
||||
#define DBG_LOG_H
|
||||
|
||||
/* 计数定义在 sys_log_mcu_test.c */
|
||||
extern int g_sys_err_count;
|
||||
extern int g_sys_info_count;
|
||||
extern int g_sys_debug_count;
|
||||
|
||||
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_sys_info_count++; } while (0)
|
||||
#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_sys_debug_count++; } while (0)
|
||||
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_sys_err_count++; } while (0)
|
||||
|
||||
#endif /* DBG_LOG_H */
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - main.h(sys_log_mcu 套件专用)
|
||||
* 模块功能:截断 App/util/sys_clock.h -> Drivers/BSP/SD2506/sd2506.h -> main.h
|
||||
* -> Inc/main.h -> stm32f4xx_hal.h 这条 include 链。
|
||||
*
|
||||
* ⚠ 仅供 test/host/sys_log_mcu/ 使用,不参与 MCU 构建。
|
||||
* 真实 Inc/main.h 第一行就 #include "stm32f4xx_hal.h"(整棵 HAL + CMSIS),
|
||||
* PC 上编不动;本桩与 test/host/sys_clock/stub/main.h 同一招(该套件已验证
|
||||
* "真实 sys_clock.h + 真实 sd2506.h + 桩 main.h"这条组合可行),好处是
|
||||
* **sys_clock.h / sd2506.h 保持真实**:sys_clock_get() 的签名一改即编译期报错,
|
||||
* 不必像 test/host/time_sync 那样连 sys_clock.h 一起桩掉。
|
||||
*
|
||||
* 创建日期:2026-10-24
|
||||
*/
|
||||
|
||||
#ifndef __MAIN_H
|
||||
#define __MAIN_H
|
||||
|
||||
#include <stddef.h> /* size_t:真实 sys_clock.h 的 get_str 原型要用 */
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* HAL 毫秒时基(MCU 端 = TIM7 中断累加)。本模块不读它,声明在此只为让
|
||||
* 真实 main.h 的消费者(sd2506.h / sys_clock.h)在 PC 上也拿得到同一符号。 */
|
||||
uint32_t HAL_GetTick(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __MAIN_H */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -178,26 +178,26 @@ static void tc_sample_gate(void)
|
||||
CHECK((s_prof[TASK_PROF_UI_PERIOD].count == 1u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].last == 500u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].max == 500u),
|
||||
"TC-TASKPROF-001 有效样本记 count/last/max");
|
||||
"TC-TASKPROF-001 valid sample records count/last/max");
|
||||
|
||||
task_prof_sample(TASK_PROF_UI_PERIOD, 300u);
|
||||
CHECK((s_prof[TASK_PROF_UI_PERIOD].max == 500u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].last == 300u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].count == 2u),
|
||||
"TC-TASKPROF-002 更小样本不改 max");
|
||||
"TC-TASKPROF-002 smaller sample does not change max");
|
||||
|
||||
reset_all();
|
||||
task_prof_sample(TASK_PROF_UI_PERIOD, 0u);
|
||||
CHECK((s_prof[TASK_PROF_UI_PERIOD].glitch == 1u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].count == 0u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].max == 0u),
|
||||
"TC-TASKPROF-003 cyc=0 只累加 glitch(DWT 复位防线)");
|
||||
"TC-TASKPROF-003 cyc=0 only accumulates glitch (DWT reset defence)");
|
||||
|
||||
reset_all();
|
||||
task_prof_sample(TASK_PROF_UI_PERIOD, SystemCoreClock * 10u);
|
||||
CHECK((s_prof[TASK_PROF_UI_PERIOD].glitch == 1u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].count == 0u),
|
||||
"TC-TASKPROF-004 超上界样本只累加 glitch");
|
||||
"TC-TASKPROF-004 over-upper-bound sample only accumulates glitch");
|
||||
|
||||
reset_all();
|
||||
task_prof_sample((task_prof_id_t)TASK_PROF_ID_COUNT, 100u);
|
||||
@@ -205,7 +205,7 @@ static void tc_sample_gate(void)
|
||||
task_prof_enter((task_prof_id_t)TASK_PROF_ID_COUNT);
|
||||
task_prof_exit((task_prof_id_t)TASK_PROF_ID_COUNT);
|
||||
CHECK((s_prof[0].count == 0u) && (s_prof[TASK_PROF_ID_COUNT - 1].count == 0u),
|
||||
"TC-TASKPROF-005 id 越界安全返回,不碰任何槽");
|
||||
"TC-TASKPROF-005 out-of-range id returns safely, touches no slot");
|
||||
}
|
||||
|
||||
/* ===================== 时基与 enter/exit 配对 ===================== */
|
||||
@@ -219,27 +219,27 @@ static void tc_enter_exit(void)
|
||||
CHECK(((g_coredebug.DEMCR & CoreDebug_DEMCR_TRCENA_Msk) != 0u) &&
|
||||
((g_dwt.CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0u) &&
|
||||
(s_dwt_ok == 1u),
|
||||
"TC-TASKPROF-006 首次取时基即惰性使能 DWT");
|
||||
"TC-TASKPROF-006 first timebase read lazily enables DWT");
|
||||
|
||||
reset_all();
|
||||
set_cyc(2000u);
|
||||
task_prof_enter(TASK_PROF_UI_WORK);
|
||||
CHECK((s_prof[TASK_PROF_UI_WORK].in_seg == 1u) &&
|
||||
(s_prof[TASK_PROF_UI_WORK].t_start == 2000u),
|
||||
"TC-TASKPROF-007 enter 置 in_seg 并记起点");
|
||||
"TC-TASKPROF-007 enter sets in_seg and records the start point");
|
||||
|
||||
set_cyc(2640u);
|
||||
task_prof_exit(TASK_PROF_UI_WORK);
|
||||
CHECK((s_prof[TASK_PROF_UI_WORK].in_seg == 0u) &&
|
||||
(s_prof[TASK_PROF_UI_WORK].last == 640u) &&
|
||||
(s_prof[TASK_PROF_UI_WORK].count == 1u),
|
||||
"TC-TASKPROF-008 exit 结算 Δ 并清 in_seg");
|
||||
"TC-TASKPROF-008 exit settles the delta and clears in_seg");
|
||||
|
||||
reset_all();
|
||||
task_prof_exit(TASK_PROF_ADC_BEAT);
|
||||
CHECK((s_prof[TASK_PROF_ADC_BEAT].count == 0u) &&
|
||||
(s_prof[TASK_PROF_ADC_BEAT].glitch == 0u),
|
||||
"TC-TASKPROF-009 未 enter 直接 exit 不记账(防 0 起点假超大样本)");
|
||||
"TC-TASKPROF-009 exit without enter records nothing (guards fake huge sample from 0 start)");
|
||||
|
||||
reset_all();
|
||||
set_cyc(100u);
|
||||
@@ -247,7 +247,7 @@ static void tc_enter_exit(void)
|
||||
set_cyc(150u);
|
||||
task_prof_exit(TASK_PROF_NET_POLL);
|
||||
task_prof_exit(TASK_PROF_NET_POLL);
|
||||
CHECK(s_prof[TASK_PROF_NET_POLL].count == 1u, "TC-TASKPROF-010 重复 exit 不重复记账");
|
||||
CHECK(s_prof[TASK_PROF_NET_POLL].count == 1u, "TC-TASKPROF-010 repeated exit does not double count");
|
||||
}
|
||||
|
||||
/* ===================== 上报节流与通道 ===================== */
|
||||
@@ -258,26 +258,26 @@ static void tc_report_throttle(void)
|
||||
reset_all();
|
||||
g_tick = 4999u;
|
||||
task_prof_report();
|
||||
CHECK((g_line_calls == 0) && (g_raw_calls == 0), "TC-TASKPROF-011 首 5 s 前不发");
|
||||
CHECK((g_line_calls == 0) && (g_raw_calls == 0), "TC-TASKPROF-011 nothing sent before the first 5 s");
|
||||
|
||||
g_tick = 5000u;
|
||||
task_prof_report();
|
||||
CHECK((g_line_calls == 2) && (g_raw_calls == 0),
|
||||
"TC-TASKPROF-012 到点发两行走环形缓冲");
|
||||
"TC-TASKPROF-012 on schedule: sends two lines via the ring buffer");
|
||||
|
||||
g_tick = 6999u;
|
||||
task_prof_report();
|
||||
CHECK(g_line_calls == 2, "TC-TASKPROF-013 不足 2 s 不发");
|
||||
CHECK(g_line_calls == 2, "TC-TASKPROF-013 less than 2 s apart sends nothing");
|
||||
|
||||
g_tick = 7000u;
|
||||
task_prof_report();
|
||||
CHECK(g_line_calls == 4, "TC-TASKPROF-014 满 2 s 再发两行");
|
||||
CHECK(g_line_calls == 4, "TC-TASKPROF-014 after a full 2 s sends two lines again");
|
||||
|
||||
g_line_calls = 0;
|
||||
g_raw_calls = 0;
|
||||
task_prof_report_raw();
|
||||
CHECK((g_raw_calls == 2) && (g_line_calls == 0),
|
||||
"TC-TASKPROF-015 report_raw 无视节流且走裸发通道");
|
||||
"TC-TASKPROF-015 report_raw ignores throttling and uses the raw channel");
|
||||
}
|
||||
|
||||
/* ===================== 行内容与换算 ===================== */
|
||||
@@ -293,11 +293,11 @@ static void tc_line_format(void)
|
||||
CHECK((g_line_calls == 2) &&
|
||||
(strncmp(g_line[0], "[PROF] ", 7) == 0) &&
|
||||
(strncmp(g_line[1], "[PROF2] ", 8) == 0),
|
||||
"TC-TASKPROF-016 两行行首分别是 [PROF] / [PROF2]");
|
||||
"TC-TASKPROF-016 the two lines start with [PROF] / [PROF2] respectively");
|
||||
CHECK((strstr(g_line[0], "\r\n") != NULL) &&
|
||||
(strstr(g_line[1], "\r\n") != NULL),
|
||||
"TC-TASKPROF-016b 两行都以 CRLF 结尾");
|
||||
CHECK(field_of(0, "uip=") == 0L, "TC-TASKPROF-016c 未采样的槽打印 0");
|
||||
"TC-TASKPROF-016b both lines end with CRLF");
|
||||
CHECK(field_of(0, "uip=") == 0L, "TC-TASKPROF-016c an unsampled slot prints 0");
|
||||
|
||||
/* 017:打印的是**微秒**,分母是 SystemCoreClock —— 断言落在打印字段上 */
|
||||
reset_all();
|
||||
@@ -306,14 +306,14 @@ static void tc_line_format(void)
|
||||
g_tick = 5000u;
|
||||
task_prof_report();
|
||||
CHECK(field_of(0, "uip=") == 10L,
|
||||
"TC-TASKPROF-017 168 MHz:1680 周期的 max 打印成 uip=10");
|
||||
"TC-TASKPROF-017 168 MHz: max of 1680 cycles prints as uip=10");
|
||||
CHECK(field_of(0, "adcm=") == 20L,
|
||||
"TC-TASKPROF-017b 同频:3360 周期的 max 打印成 adcm=20");
|
||||
"TC-TASKPROF-017b same clock: max of 3360 cycles prints as adcm=20");
|
||||
SystemCoreClock = 84000000u; /* 改时钟树而忘了同步 → 整表偏移,这里必须显式跟着变 */
|
||||
g_tick = 7000u;
|
||||
task_prof_report();
|
||||
CHECK(field_of(2, "uip=") == 20L,
|
||||
"TC-TASKPROF-017c 84 MHz 下同一批周期数打印成 20");
|
||||
"TC-TASKPROF-017c at 84 MHz the same cycle counts print as 20");
|
||||
SystemCoreClock = 168000000u;
|
||||
|
||||
/* 018:gl= 是**所有槽**的 glitch 汇总,不是当前槽 */
|
||||
@@ -323,7 +323,7 @@ static void tc_line_format(void)
|
||||
task_prof_sample(TASK_PROF_NET_POLL, 0u);
|
||||
g_tick = 5000u;
|
||||
task_prof_report();
|
||||
CHECK(field_of(0, "gl=") == 3L, "TC-TASKPROF-018 gl= 汇总所有槽的 glitch");
|
||||
CHECK(field_of(0, "gl=") == 3L, "TC-TASKPROF-018 gl= sums the glitch of all slots");
|
||||
|
||||
/* 019:段未结算时,"此刻已在段内多久"要看得见(uipn / netn 两个字段) */
|
||||
/* 字段口径 = 上报时刻的 CYCCNT 减去各自的进段时刻(**不是**样本值):
|
||||
@@ -338,13 +338,13 @@ static void tc_line_format(void)
|
||||
g_tick = 5000u;
|
||||
task_prof_report();
|
||||
CHECK(field_of(0, "uipn=") == 301L,
|
||||
"TC-TASKPROF-019 UI_PERIOD 段内:uipn= 上报此刻已停留的 us");
|
||||
"TC-TASKPROF-019 inside UI_PERIOD segment: uip= reports the us elapsed so far");
|
||||
CHECK(field_of(1, "netn=") == 248L,
|
||||
"TC-TASKPROF-019b NET_POLL 段内:[PROF2] 的 netn= 同步可见");
|
||||
"TC-TASKPROF-019b inside NET_POLL segment: netn= of [PROF2] is visible too");
|
||||
CHECK(field_of(0, "adcn=") == 0L,
|
||||
"TC-TASKPROF-019c 未进段的 ADC_BEAT 打印 0(不串用别的槽)");
|
||||
"TC-TASKPROF-019c ADC_BEAT not in segment prints 0 (no cross-slot leakage)");
|
||||
CHECK(s_prof[TASK_PROF_UI_PERIOD].in_seg == 1u,
|
||||
"TC-TASKPROF-019d 上报不消费段状态(只观测)");
|
||||
"TC-TASKPROF-019d reporting does not consume segment state (observe only)");
|
||||
|
||||
/* 020:count 是累计量,两轮上报之间不能清零 */
|
||||
reset_all();
|
||||
@@ -355,7 +355,7 @@ static void tc_line_format(void)
|
||||
task_prof_report();
|
||||
CHECK((s_prof[TASK_PROF_UI_WORK].count == 1u) && (field_of(2, "uic=") == 0L) &&
|
||||
(field_of(0, "uic=") == 0L),
|
||||
"TC-TASKPROF-020 上报不清零(UI_WORK 不在 uic= 字段,仍为累计 1)");
|
||||
"TC-TASKPROF-020 reporting does not clear counters (UI_WORK not in uic= field, still cumulative 1)");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -418,31 +418,31 @@ static int delays_all(uint32_t v, int n) {
|
||||
static void tc_delay_fed(void) {
|
||||
fakes_reset();
|
||||
time_sync_delay_fed(0U);
|
||||
CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "TC-TIMESYNC-001 delay_fed(0) 不睡眠不喂狗");
|
||||
CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "TC-TIMESYNC-001 delay_fed(0) neither sleeps nor beats");
|
||||
|
||||
fakes_reset();
|
||||
time_sync_delay_fed(50U);
|
||||
CHECK((s_beat_calls == 1) && (s_delay_calls == 1) && (s_delays[0] == 50U),
|
||||
"TC-TIMESYNC-002 delay_fed(50) = 1 片 50ms + 1 次心跳");
|
||||
"TC-TIMESYNC-002 delay_fed(50) = one 50 ms slice + 1 beat");
|
||||
|
||||
fakes_reset();
|
||||
time_sync_delay_fed(120U);
|
||||
CHECK((s_beat_calls == 3) && (s_delay_calls == 3), "TC-TIMESYNC-003 delay_fed(120) 切成 3 片");
|
||||
CHECK((s_beat_calls == 3) && (s_delay_calls == 3), "TC-TIMESYNC-003 delay_fed(120) splits into 3 slices");
|
||||
CHECK((s_delays[0] == 50U) && (s_delays[1] == 50U) && (s_delays[2] == 20U),
|
||||
"TC-TIMESYNC-003b 片长 50/50/20(末片为余数)");
|
||||
"TC-TIMESYNC-003b slice lengths 50/50/20 (last slice is the remainder)");
|
||||
|
||||
fakes_reset();
|
||||
time_sync_delay_fed(2000U);
|
||||
CHECK((s_delay_calls == 40) && (s_beat_calls == 40) && delays_all(50U, 40),
|
||||
"TC-TIMESYNC-004 delay_fed(2000) = 40 片 × 50ms,每片喂一次心跳");
|
||||
"TC-TIMESYNC-004 delay_fed(2000) = 40 slices x 50 ms, one beat per slice");
|
||||
|
||||
fakes_reset();
|
||||
time_sync_delay_fed(49U);
|
||||
CHECK((s_beat_calls == 1) && (s_delays[0] == 49U), "TC-TIMESYNC-005 不足一片(49ms)单片处理");
|
||||
CHECK((s_beat_calls == 1) && (s_delays[0] == 49U), "TC-TIMESYNC-005 less than one slice (49 ms) handled as a single slice");
|
||||
|
||||
fakes_reset();
|
||||
time_sync_delay_fed(30000U);
|
||||
CHECK(s_delay_calls == 600, "TC-TIMESYNC-006 总预算 30s = 600 片(片长上限恒 50ms)");
|
||||
CHECK(s_delay_calls == 600, "TC-TIMESYNC-006 total budget 30 s = 600 slices (slice cap is always 50 ms)");
|
||||
}
|
||||
|
||||
/* ===================== TC-TIMESYNC-010..015:open_listener ===================== */
|
||||
@@ -451,13 +451,13 @@ static void tc_open_listener(void) {
|
||||
fakes_reset();
|
||||
{
|
||||
int s = time_sync_open_listener();
|
||||
CHECK(s == 5, "TC-TIMESYNC-010 open_listener 成功返回 socket 描述符");
|
||||
CHECK(s == 5, "TC-TIMESYNC-010 open_listener success returns the socket descriptor");
|
||||
CHECK((s_socket_calls == 1) && (s_bind_calls == 1) && (s_listen_calls == 1),
|
||||
"TC-TIMESYNC-010b socket / bind / listen 各一次");
|
||||
CHECK(s_htons_last == (uint16_t)TIME_SYNC_PORT, "TC-TIMESYNC-010c 端口经 net_htons(8888)");
|
||||
CHECK(s_bind_port == net_htons((uint16_t)TIME_SYNC_PORT), "TC-TIMESYNC-010d sin_port 为网络序");
|
||||
"TC-TIMESYNC-010b socket / bind / listen called once each");
|
||||
CHECK(s_htons_last == (uint16_t)TIME_SYNC_PORT, "TC-TIMESYNC-010c port goes through net_htons(8888)");
|
||||
CHECK(s_bind_port == net_htons((uint16_t)TIME_SYNC_PORT), "TC-TIMESYNC-010d sin_port is in network byte order");
|
||||
CHECK(s_bind_family == (uint16_t)NET_AF_INET, "TC-TIMESYNC-010e sin_family = NET_AF_INET");
|
||||
CHECK(s_delay_calls == 0, "TC-TIMESYNC-010f 成功路径无退避延时");
|
||||
CHECK(s_delay_calls == 0, "TC-TIMESYNC-010f no backoff delay on the success path");
|
||||
}
|
||||
|
||||
fakes_reset();
|
||||
@@ -465,10 +465,10 @@ static void tc_open_listener(void) {
|
||||
s_bind_seq_n = 1;
|
||||
{
|
||||
int s = time_sync_open_listener();
|
||||
CHECK(s == 5, "TC-TIMESYNC-011 bind 失败一次后重试成功");
|
||||
CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-011b bind 失败关闭该 socket");
|
||||
CHECK(s_delay_calls == 40, "TC-TIMESYNC-011c bind 失败退避 2000ms(40 片)");
|
||||
CHECK(g_tstest_err_count == 1, "TC-TIMESYNC-011d bind 失败留痕 DBG_ERROR");
|
||||
CHECK(s == 5, "TC-TIMESYNC-011 bind succeeds on retry after one failure");
|
||||
CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-011b bind failure closes that socket");
|
||||
CHECK(s_delay_calls == 40, "TC-TIMESYNC-011c bind failure backs off 2000 ms (40 slices)");
|
||||
CHECK(g_tstest_err_count == 1, "TC-TIMESYNC-011d bind failure leaves a DBG_ERROR trace");
|
||||
}
|
||||
|
||||
fakes_reset();
|
||||
@@ -476,19 +476,19 @@ static void tc_open_listener(void) {
|
||||
s_listen_seq_n = 1;
|
||||
{
|
||||
int s = time_sync_open_listener();
|
||||
CHECK(s == 5, "TC-TIMESYNC-012 listen 失败一次后重试成功");
|
||||
CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-012b listen 失败关闭该 socket");
|
||||
CHECK(s_delay_calls == 40, "TC-TIMESYNC-012c listen 失败退避 2000ms");
|
||||
CHECK(s == 5, "TC-TIMESYNC-012 listen succeeds on retry after one failure");
|
||||
CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-012b listen failure closes that socket");
|
||||
CHECK(s_delay_calls == 40, "TC-TIMESYNC-012c listen failure backs off 2000 ms");
|
||||
}
|
||||
|
||||
fakes_reset();
|
||||
s_socket_default = -1;
|
||||
{
|
||||
int s = time_sync_open_listener();
|
||||
CHECK(s == -1, "TC-TIMESYNC-013 socket 连续失败 -> 返回 -1");
|
||||
CHECK(s_socket_calls == 5, "TC-TIMESYNC-013b 重试上限 5 次");
|
||||
CHECK(g_tstest_err_count == 5, "TC-TIMESYNC-013c 每次失败各留痕一条(5 条 DBG_ERROR)");
|
||||
CHECK(s_delay_calls == 50, "TC-TIMESYNC-013d 每次失败退避 500ms(5 × 10 片)");
|
||||
CHECK(s == -1, "TC-TIMESYNC-013 socket keeps failing -> returns -1");
|
||||
CHECK(s_socket_calls == 5, "TC-TIMESYNC-013b retry cap is 5 attempts");
|
||||
CHECK(g_tstest_err_count == 5, "TC-TIMESYNC-013c every failure leaves its own trace (5 DBG_ERROR lines)");
|
||||
CHECK(s_delay_calls == 50, "TC-TIMESYNC-013d every failure backs off 500 ms (5 x 10 slices)");
|
||||
}
|
||||
|
||||
fakes_reset();
|
||||
@@ -498,16 +498,16 @@ static void tc_open_listener(void) {
|
||||
s_socket_seq_n = 3;
|
||||
{
|
||||
int s = time_sync_open_listener();
|
||||
CHECK(s == 9, "TC-TIMESYNC-014 第 3 次重试成功并返回新描述符");
|
||||
CHECK(s_socket_calls == 3, "TC-TIMESYNC-014b 恰好尝试 3 次");
|
||||
CHECK(s_delay_calls == 20, "TC-TIMESYNC-014c 两次失败退避 500ms × 2");
|
||||
CHECK(s == 9, "TC-TIMESYNC-014 3rd retry succeeds and returns the new descriptor");
|
||||
CHECK(s_socket_calls == 3, "TC-TIMESYNC-014b exactly 3 attempts");
|
||||
CHECK(s_delay_calls == 20, "TC-TIMESYNC-014c two failures back off 500 ms x 2");
|
||||
}
|
||||
|
||||
fakes_reset();
|
||||
s_socket_default = -1;
|
||||
(void)time_sync_open_listener();
|
||||
CHECK(s_beat_calls == 55, "TC-TIMESYNC-015 每轮重试(5) + 每片延时(50) 各喂一次心跳");
|
||||
CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-015b 心跳位恒为 TASK_WD_TIMESYNC");
|
||||
CHECK(s_beat_calls == 55, "TC-TIMESYNC-015 every retry round (5) + every delay slice (50) beats once");
|
||||
CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-015b the beat bit is always TASK_WD_TIMESYNC");
|
||||
}
|
||||
|
||||
/* ===================== TC-TIMESYNC-020..029:handle_conn =====================
|
||||
@@ -526,13 +526,13 @@ static void tc_handle_conn(void) {
|
||||
s_recv_seq_n = 1;
|
||||
s_tick = 100U;
|
||||
time_sync_handle_conn(7);
|
||||
CHECK(s_set_unix_calls == 1, "TC-TIMESYNC-020 TIME 包一次收齐 -> 更新墙钟一次");
|
||||
CHECK(s_set_unix_last == 0x12345678u, "TC-TIMESYNC-020b epoch 按大端解析");
|
||||
CHECK((s_recv_calls == 1) && (s_recv_len_log[0] == 16), "TC-TIMESYNC-020c 首次请求 16 字节");
|
||||
CHECK(s_recv_to_log[0] == 50U, "TC-TIMESYNC-020d 单次等待片长 = 50ms");
|
||||
CHECK(s_close_calls == 0, "TC-TIMESYNC-020e 处理完**不**关闭连接(close 交外层 conn!=ls 分支)");
|
||||
CHECK(s_drop_calls == 0, "TC-TIMESYNC-020g 成功会话不拆(drop 只用于放弃路径)");
|
||||
CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-020f 留痕 wall clock updated");
|
||||
CHECK(s_set_unix_calls == 1, "TC-TIMESYNC-020 TIME packet in one shot -> wall clock updated once");
|
||||
CHECK(s_set_unix_last == 0x12345678u, "TC-TIMESYNC-020b epoch parsed big-endian");
|
||||
CHECK((s_recv_calls == 1) && (s_recv_len_log[0] == 16), "TC-TIMESYNC-020c the first request asks for 16 bytes");
|
||||
CHECK(s_recv_to_log[0] == 50U, "TC-TIMESYNC-020d single wait slice length = 50 ms");
|
||||
CHECK(s_close_calls == 0, "TC-TIMESYNC-020e after handling, the connection is **not** closed (close is the outer conn!=ls branch job)");
|
||||
CHECK(s_drop_calls == 0, "TC-TIMESYNC-020g a successful session is not torn down (drop is only for give-up paths)");
|
||||
CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-020f leaves the wall clock updated trace");
|
||||
|
||||
/* 2) 分段送达:4 + 4 */
|
||||
fakes_reset();
|
||||
@@ -541,11 +541,11 @@ static void tc_handle_conn(void) {
|
||||
recv_set(1, 4, pkt + 4, 4, 10U);
|
||||
s_recv_seq_n = 2;
|
||||
time_sync_handle_conn(3);
|
||||
CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 10u), "TC-TIMESYNC-021 分段送达累积后解析正确");
|
||||
CHECK(s_recv_calls == 2, "TC-TIMESYNC-021b 读了两次");
|
||||
CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 10u), "TC-TIMESYNC-021 pieces accumulate and then parse correctly");
|
||||
CHECK(s_recv_calls == 2, "TC-TIMESYNC-021b read twice");
|
||||
CHECK((s_recv_len_log[0] == 16) && (s_recv_len_log[1] == 12),
|
||||
"TC-TIMESYNC-021c 第二次只请求剩余 12 字节");
|
||||
CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), "TC-TIMESYNC-021d 每片 50ms(预算充裕)");
|
||||
"TC-TIMESYNC-021c the second request asks only for the remaining 12 bytes");
|
||||
CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), "TC-TIMESYNC-021d 50 ms per slice (budget is ample)");
|
||||
|
||||
/* 3) 16 字节一次到达(只取前 8) */
|
||||
fakes_reset();
|
||||
@@ -553,8 +553,8 @@ static void tc_handle_conn(void) {
|
||||
recv_set(0, 16, pkt, 16, 0U);
|
||||
s_recv_seq_n = 1;
|
||||
time_sync_handle_conn(4);
|
||||
CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 5u), "TC-TIMESYNC-022 超长包只取前 8 字节");
|
||||
CHECK(s_recv_calls == 1, "TC-TIMESYNC-022b 一次读够即停");
|
||||
CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 5u), "TC-TIMESYNC-022 an over-long packet only takes the first 8 bytes");
|
||||
CHECK(s_recv_calls == 1, "TC-TIMESYNC-022b stops as soon as one read is enough");
|
||||
|
||||
/* 4) 前缀不是 TIME */
|
||||
fakes_reset();
|
||||
@@ -569,23 +569,23 @@ static void tc_handle_conn(void) {
|
||||
recv_set(0, 8, pkt, 8, 0U);
|
||||
s_recv_seq_n = 1;
|
||||
time_sync_handle_conn(4);
|
||||
CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-023 非 TIME 前缀不更新墙钟");
|
||||
CHECK(s_close_calls == 0, "TC-TIMESYNC-023b 非 TIME 前缀也不关闭连接");
|
||||
CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-023 a non-TIME prefix does not update the wall clock");
|
||||
CHECK(s_close_calls == 0, "TC-TIMESYNC-023b a non-TIME prefix also does not close the connection");
|
||||
|
||||
/* 5) 对端关闭(EOF) */
|
||||
fakes_reset();
|
||||
recv_set(0, 0, NULL, 0, 0U);
|
||||
s_recv_seq_n = 1;
|
||||
time_sync_handle_conn(6);
|
||||
CHECK((s_set_unix_calls == 0) && (s_recv_calls == 1), "TC-TIMESYNC-024 对端关闭(EOF)立即退出不解析");
|
||||
CHECK(s_close_calls == 0, "TC-TIMESYNC-024b EOF 也不关闭连接(net 层 auto_relisten 收口监听)");
|
||||
CHECK((s_set_unix_calls == 0) && (s_recv_calls == 1), "TC-TIMESYNC-024 peer close (EOF) exits immediately without parsing");
|
||||
CHECK(s_close_calls == 0, "TC-TIMESYNC-024b EOF also does not close the connection (net-layer auto_relisten re-arms the listener)");
|
||||
|
||||
/* 6) 真错误 */
|
||||
fakes_reset();
|
||||
recv_set(0, -1, NULL, 0, 0U);
|
||||
s_recv_seq_n = 1;
|
||||
time_sync_handle_conn(6);
|
||||
CHECK((s_recv_calls == 1) && (s_set_unix_calls == 0), "TC-TIMESYNC-025 真错误立即退出不解析");
|
||||
CHECK((s_recv_calls == 1) && (s_set_unix_calls == 0), "TC-TIMESYNC-025 a real error exits immediately without parsing");
|
||||
|
||||
/* 7) 4 字节后 EOF */
|
||||
fakes_reset();
|
||||
@@ -594,27 +594,27 @@ static void tc_handle_conn(void) {
|
||||
recv_set(1, 0, NULL, 0, 0U);
|
||||
s_recv_seq_n = 2;
|
||||
time_sync_handle_conn(6);
|
||||
CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-026 不足 8 字节不解析");
|
||||
CHECK(s_recv_calls == 2, "TC-TIMESYNC-026b 继续等待后续字节");
|
||||
CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-026 fewer than 8 bytes are not parsed");
|
||||
CHECK(s_recv_calls == 2, "TC-TIMESYNC-026b keeps waiting for the remaining bytes");
|
||||
|
||||
/* 8) 30s 预算耗尽 */
|
||||
fakes_reset();
|
||||
recv_set(0, -2, NULL, 0, 30000U);
|
||||
s_recv_seq_n = 1;
|
||||
time_sync_handle_conn(6);
|
||||
CHECK(s_recv_calls == 1, "TC-TIMESYNC-027 预算耗尽后不再读(只读 1 次)");
|
||||
CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-027b 留痕 no TIME packet in 30s, drop conn");
|
||||
CHECK(s_close_calls == 0, "TC-TIMESYNC-027c 预算耗尽也不关闭连接");
|
||||
CHECK(s_drop_calls == 1, "TC-TIMESYNC-027d 预算耗尽主动拆会话(E-02 收口:net_drop_conn 拆连接保监听)");
|
||||
CHECK(s_recv_calls == 1, "TC-TIMESYNC-027 no more reads once the budget is exhausted (only 1 read)");
|
||||
CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-027b leaves the no TIME packet in 30s, drop conn trace");
|
||||
CHECK(s_close_calls == 0, "TC-TIMESYNC-027c budget exhausted still does not close the connection");
|
||||
CHECK(s_drop_calls == 1, "TC-TIMESYNC-027d budget exhausted drops the session on purpose (E-02 wrap-up: net_drop_conn kills the connection, keeps the listener)");
|
||||
|
||||
/* 9) 剩余预算不足一片 */
|
||||
fakes_reset();
|
||||
recv_set(0, -2, NULL, 0, 29980U);
|
||||
s_recv_seq_n = 1;
|
||||
time_sync_handle_conn(6);
|
||||
CHECK(s_recv_calls == 2, "TC-TIMESYNC-028 两次等待后结束");
|
||||
CHECK(s_recv_calls == 2, "TC-TIMESYNC-028 ends after two waits");
|
||||
CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 20U),
|
||||
"TC-TIMESYNC-028b 第二次片长 = 剩余 20ms");
|
||||
"TC-TIMESYNC-028b second slice length = the remaining 20 ms");
|
||||
|
||||
/* 10) 32 位 tick 回绕 */
|
||||
fakes_reset();
|
||||
@@ -623,7 +623,7 @@ static void tc_handle_conn(void) {
|
||||
s_recv_seq_n = 1;
|
||||
time_sync_handle_conn(6);
|
||||
CHECK((s_recv_calls == 2) && (s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U),
|
||||
"TC-TIMESYNC-029 32 位 tick 回绕后预算计算仍正确");
|
||||
"TC-TIMESYNC-029 after a 32-bit tick wraparound the budget math is still correct");
|
||||
}
|
||||
|
||||
/* ===================== TC-TIMESYNC-040..044:time_sync_task ===================== */
|
||||
@@ -646,15 +646,15 @@ static void tc_task(void) {
|
||||
time_sync_task(NULL);
|
||||
}
|
||||
CHECK((s_wd_bind_calls == 1) && (s_wd_bind_id_bad == 0),
|
||||
"TC-TIMESYNC-040 任务入口登记心跳位(task_wd_bind 一次)");
|
||||
CHECK(s_evt_calls == 0, "TC-TIMESYNC-040b sys_evtHandle==NULL 时跳过就绪屏障");
|
||||
CHECK(s_accept_calls == 3, "TC-TIMESYNC-040c accept 轮询 3 次(2 次无连接 + 1 次成功)");
|
||||
CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 9u), "TC-TIMESYNC-040d 收到时间包并更新墙钟");
|
||||
CHECK(s_close_calls == 1, "TC-TIMESYNC-040e 连接被关闭一次");
|
||||
CHECK(s_delay_calls == 9, "TC-TIMESYNC-040f 延时片数 = 4+4+1(200+200+50ms)");
|
||||
CHECK(delays_all(50U, 9), "TC-TIMESYNC-040g 所有延时均按 50ms 分片");
|
||||
CHECK(s_beat_calls == 13, "TC-TIMESYNC-040h 心跳次数 = 主循环1 + 退避8 + 收连接1 + 收尾1 + 次轮1");
|
||||
CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-040i 所有心跳均打 TIMESYNC 位");
|
||||
"TC-TIMESYNC-040 task entry registers the watchdog beat bit (task_wd_bind once)");
|
||||
CHECK(s_evt_calls == 0, "TC-TIMESYNC-040b skips the readiness barrier when sys_evtHandle==NULL");
|
||||
CHECK(s_accept_calls == 3, "TC-TIMESYNC-040c accept polled 3 times (2 with no connection + 1 success)");
|
||||
CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 9u), "TC-TIMESYNC-040d the time packet arrived and the wall clock was updated");
|
||||
CHECK(s_close_calls == 1, "TC-TIMESYNC-040e the connection is closed once");
|
||||
CHECK(s_delay_calls == 9, "TC-TIMESYNC-040f delay slice count = 4+4+1 (200+200+50 ms)");
|
||||
CHECK(delays_all(50U, 9), "TC-TIMESYNC-040g every delay is sliced at 50 ms");
|
||||
CHECK(s_beat_calls == 13, "TC-TIMESYNC-040h beat count = main loop 1 + backoff 8 + accepted connection 1 + wrap-up 1 + next round 1");
|
||||
CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-040i every beat sets the TIMESYNC bit");
|
||||
|
||||
/* B) 就绪屏障:2 次未就绪 + 1 次就绪 */
|
||||
fakes_reset();
|
||||
@@ -667,12 +667,12 @@ static void tc_task(void) {
|
||||
if (setjmp(s_jmp) == 0) {
|
||||
time_sync_task(NULL);
|
||||
}
|
||||
CHECK(s_evt_calls == 3, "TC-TIMESYNC-041 就绪屏障轮询 3 次(2 次未就绪 + 1 次就绪)");
|
||||
CHECK(s_evt_last_timeout == 100U, "TC-TIMESYNC-041b 屏障等待按 100ms 分片");
|
||||
CHECK(s_evt_last_flags == SYS_EVT_NET_READY, "TC-TIMESYNC-041c 等待 NET_READY 位");
|
||||
CHECK(s_evt_last_options == (osFlagsWaitAny | osFlagsNoClear), "TC-TIMESYNC-041d 就绪位不被消费");
|
||||
CHECK(s_beat_calls == 3, "TC-TIMESYNC-041e 未就绪的两次各喂一次心跳(+主循环 1 次)");
|
||||
CHECK(s_wd_bind_calls == 1, "TC-TIMESYNC-041f 仍先登记心跳路由");
|
||||
CHECK(s_evt_calls == 3, "TC-TIMESYNC-041 readiness barrier polled 3 times (2 not ready + 1 ready)");
|
||||
CHECK(s_evt_last_timeout == 100U, "TC-TIMESYNC-041b barrier waits are sliced at 100 ms");
|
||||
CHECK(s_evt_last_flags == SYS_EVT_NET_READY, "TC-TIMESYNC-041c waits for the NET_READY bit");
|
||||
CHECK(s_evt_last_options == (osFlagsWaitAny | osFlagsNoClear), "TC-TIMESYNC-041d the ready bit is not consumed");
|
||||
CHECK(s_beat_calls == 3, "TC-TIMESYNC-041e the two not-ready rounds each beat once (+ 1 in the main loop)");
|
||||
CHECK(s_wd_bind_calls == 1, "TC-TIMESYNC-041f still registers the beat routing first");
|
||||
|
||||
/* C) 屏障期间持续喂心跳(就绪位长期不置位) */
|
||||
fakes_reset();
|
||||
@@ -682,8 +682,8 @@ static void tc_task(void) {
|
||||
if (setjmp(s_jmp) == 0) {
|
||||
time_sync_task(NULL);
|
||||
}
|
||||
CHECK(s_evt_calls == 4, "TC-TIMESYNC-042 屏障等待 4 次均未就绪(循环未挂死)");
|
||||
CHECK(s_beat_calls == 3, "TC-TIMESYNC-042b 每次未就绪后各喂一次心跳(第 4 次跳出前)");
|
||||
CHECK(s_evt_calls == 4, "TC-TIMESYNC-042 barrier waited 4 times, none ready (loop did not hang)");
|
||||
CHECK(s_beat_calls == 3, "TC-TIMESYNC-042b beats once after each not-ready round (before the 4th breaks out)");
|
||||
|
||||
/* D) 网络代次变化 -> 关闭旧监听并重监听 */
|
||||
fakes_reset();
|
||||
@@ -695,12 +695,12 @@ static void tc_task(void) {
|
||||
if (setjmp(s_jmp) == 0) {
|
||||
time_sync_task(NULL);
|
||||
}
|
||||
CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-043 代次变化时关闭旧监听");
|
||||
CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-043 closes the old listener when the network generation changes");
|
||||
/* INFO 三条:两次 open_listener 成功各一条 "TCP listen on" + 一条 "network re-init, relisten" */
|
||||
CHECK(g_tstest_info_count == 3, "TC-TIMESYNC-043b 留痕 network re-init, relisten(另含 2 条 listen 成功)");
|
||||
CHECK(s_accept_calls == 2, "TC-TIMESYNC-043c 每次循环只轮询一次 accept 即放弃");
|
||||
CHECK(g_tstest_info_count == 3, "TC-TIMESYNC-043b leaves the network re-init, relisten trace (plus 2 listen-success lines)");
|
||||
CHECK(s_accept_calls == 2, "TC-TIMESYNC-043c polls accept only once per loop then gives up");
|
||||
CHECK((s_socket_calls == 2) && (s_bind_calls == 2) && (s_listen_calls == 2),
|
||||
"TC-TIMESYNC-043d 第二次循环重建了监听");
|
||||
"TC-TIMESYNC-043d the second loop rebuilt the listener");
|
||||
|
||||
/* E) 监听建立失败 -> 5 次重试后放弃并退避 2000ms */
|
||||
fakes_reset();
|
||||
@@ -709,11 +709,11 @@ static void tc_task(void) {
|
||||
if (setjmp(s_jmp) == 0) {
|
||||
time_sync_task(NULL);
|
||||
}
|
||||
CHECK(s_socket_calls == 5, "TC-TIMESYNC-044 open_listener 内部重试 5 次后放弃");
|
||||
CHECK(g_tstest_err_count == 6, "TC-TIMESYNC-044b 5 条 net_socket FAIL + 1 条 retry later");
|
||||
CHECK(s_delay_calls == 90, "TC-TIMESYNC-044c 退避 5×500ms + 2000ms = 90 片");
|
||||
CHECK(s_beat_calls == 97, "TC-TIMESYNC-044d 心跳次数 = 2 + 5 + 50 + 40");
|
||||
CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-044e 所有心跳均打 TIMESYNC 位");
|
||||
CHECK(s_socket_calls == 5, "TC-TIMESYNC-044 open_listener gives up after 5 internal retries");
|
||||
CHECK(g_tstest_err_count == 6, "TC-TIMESYNC-044b 5 net_socket FAIL lines + 1 retry later line");
|
||||
CHECK(s_delay_calls == 90, "TC-TIMESYNC-044c backoff 5x500 ms + 2000 ms = 90 slices");
|
||||
CHECK(s_beat_calls == 97, "TC-TIMESYNC-044d beat count = 2 + 5 + 50 + 40");
|
||||
CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-044e every beat sets the TIMESYNC bit");
|
||||
}
|
||||
|
||||
/* ===================== main ===================== */
|
||||
|
||||
@@ -114,21 +114,21 @@ static void fakes_reset(void) {
|
||||
|
||||
static void tc_init(void) {
|
||||
fakes_reset();
|
||||
CHECK(vol_lock_init() == 0, "TC-VOLLOCK-001 init 首次调用返回 0");
|
||||
CHECK(s_new_calls == 1, "TC-VOLLOCK-001b init 创建互斥量一次(osMutexNew)");
|
||||
CHECK(s_lock != NULL, "TC-VOLLOCK-001c 模块静态态已持有句柄");
|
||||
CHECK(vol_lock_init() == 0, "TC-VOLLOCK-001 first init call returns 0");
|
||||
CHECK(s_new_calls == 1, "TC-VOLLOCK-001b init creates the mutex exactly once (osMutexNew)");
|
||||
CHECK(s_lock != NULL, "TC-VOLLOCK-001c module static state holds the handle");
|
||||
|
||||
CHECK(vol_lock_init() == 0, "TC-VOLLOCK-002 init 重复调用仍返回 0(幂等)");
|
||||
CHECK(s_new_calls == 1, "TC-VOLLOCK-002b 幂等:不再创建第二个互斥量");
|
||||
CHECK(vol_lock_init() == 0, "TC-VOLLOCK-002 repeated init still returns 0 (idempotent)");
|
||||
CHECK(s_new_calls == 1, "TC-VOLLOCK-002b idempotent: no second mutex created");
|
||||
|
||||
fakes_reset();
|
||||
s_new_fail = 1;
|
||||
CHECK(vol_lock_init() == -1, "TC-VOLLOCK-003 osMutexNew 失败 -> init 返回 -1");
|
||||
CHECK(s_lock == NULL, "TC-VOLLOCK-003b 失败时不留下半初始化句柄");
|
||||
CHECK(vol_lock_init() == -1, "TC-VOLLOCK-003 osMutexNew failure -> init returns -1");
|
||||
CHECK(s_lock == NULL, "TC-VOLLOCK-003b failure leaves no half-initialized handle");
|
||||
|
||||
s_new_fail = 0;
|
||||
CHECK(vol_lock_init() == 0, "TC-VOLLOCK-004 失败后重试可成功(返回 0)");
|
||||
CHECK(s_new_calls == 2, "TC-VOLLOCK-004b 重试确实再次调用 osMutexNew");
|
||||
CHECK(vol_lock_init() == 0, "TC-VOLLOCK-004 retry after failure can succeed (returns 0)");
|
||||
CHECK(s_new_calls == 2, "TC-VOLLOCK-004b retry really calls osMutexNew again");
|
||||
}
|
||||
|
||||
/* ===================== TC-VOLLOCK-005..010:vol_lock_wait ===================== */
|
||||
@@ -137,46 +137,46 @@ static void tc_wait(void) {
|
||||
/* --- 已创建:超时值原样透传 --- */
|
||||
fakes_reset();
|
||||
(void)vol_lock_init();
|
||||
CHECK(vol_lock_wait(100U) == 1, "TC-VOLLOCK-005 wait 获取成功返回 1");
|
||||
CHECK(s_wait_calls == 1, "TC-VOLLOCK-005b osMutexWait 被调用一次");
|
||||
CHECK(s_last_timeout == 100U, "TC-VOLLOCK-005c timeout 原样透传给 osMutexWait");
|
||||
CHECK(vol_lock_wait(100U) == 1, "TC-VOLLOCK-005 wait returns 1 on acquire success");
|
||||
CHECK(s_wait_calls == 1, "TC-VOLLOCK-005b osMutexWait called once");
|
||||
CHECK(s_last_timeout == 100U, "TC-VOLLOCK-005c timeout passed through to osMutexWait unchanged");
|
||||
|
||||
fakes_reset();
|
||||
(void)vol_lock_init();
|
||||
CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-014 wait(0) 获取成功返回 1(try 语义)");
|
||||
CHECK(s_last_timeout == 0U, "TC-VOLLOCK-014b wait(0) 的 timeout 透传为 0");
|
||||
CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-014 wait(0) returns 1 on acquire (try semantics)");
|
||||
CHECK(s_last_timeout == 0U, "TC-VOLLOCK-014b wait(0) timeout passes through as 0");
|
||||
|
||||
/* --- 超时:osMutexWait != osOK -> 返回 0 --- */
|
||||
fakes_reset();
|
||||
(void)vol_lock_init();
|
||||
s_wait_ok = 0;
|
||||
CHECK(vol_lock_wait(50U) == 0, "TC-VOLLOCK-006 osMutexWait 非 osOK -> wait 返回 0");
|
||||
CHECK(s_wait_calls == 1, "TC-VOLLOCK-006b 超时路径确实尝试过等待");
|
||||
CHECK(vol_lock_wait(50U) == 0, "TC-VOLLOCK-006 osMutexWait not osOK -> wait returns 0");
|
||||
CHECK(s_wait_calls == 1, "TC-VOLLOCK-006b timeout path really attempted the wait");
|
||||
|
||||
/* --- 锁未创建:自动 init 后正常获取 --- */
|
||||
fakes_reset();
|
||||
CHECK(vol_lock_wait(10U) == 1, "TC-VOLLOCK-007 锁未创建时 wait 自动 init 并获取");
|
||||
CHECK(s_new_calls == 1, "TC-VOLLOCK-007b 自动 init 创建了互斥量");
|
||||
CHECK(s_wait_calls == 1, "TC-VOLLOCK-007c 随后执行了一次等待");
|
||||
CHECK(vol_lock_wait(10U) == 1, "TC-VOLLOCK-007 wait auto-inits and acquires when no lock exists");
|
||||
CHECK(s_new_calls == 1, "TC-VOLLOCK-007b auto init created the mutex");
|
||||
CHECK(s_wait_calls == 1, "TC-VOLLOCK-007c then performed one wait");
|
||||
|
||||
/* --- ⭐ fail-open:创建失败必须返回 1 且绝不阻塞 --- */
|
||||
fakes_reset();
|
||||
s_new_fail = 1;
|
||||
CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-008 fail-open:锁创建失败仍返回 1(不阻塞喂狗点)");
|
||||
CHECK(s_wait_calls == 0, "TC-VOLLOCK-008b fail-open 时**不调用** osMutexWait(零阻塞)");
|
||||
CHECK(s_lock == NULL, "TC-VOLLOCK-008c fail-open 后仍无句柄");
|
||||
CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-008 fail-open: returns 1 even if lock creation fails (never blocks a feed point)");
|
||||
CHECK(s_wait_calls == 0, "TC-VOLLOCK-008b fail-open **does not call** osMutexWait (zero blocking)");
|
||||
CHECK(s_lock == NULL, "TC-VOLLOCK-008c still no handle after fail-open");
|
||||
|
||||
/* --- fail-open 之后 release 无害 --- */
|
||||
vol_lock_release();
|
||||
CHECK(s_release_calls == 0, "TC-VOLLOCK-009 fail-open 后 release 不调用 osMutexRelease(判空静默)");
|
||||
CHECK(s_release_calls == 0, "TC-VOLLOCK-009 after fail-open release does not call osMutexRelease (NULL guard, silent)");
|
||||
|
||||
/* --- 已创建时 wait 不再创建 --- */
|
||||
fakes_reset();
|
||||
(void)vol_lock_init();
|
||||
(void)vol_lock_wait(0U);
|
||||
(void)vol_lock_wait(0U);
|
||||
CHECK(s_new_calls == 1, "TC-VOLLOCK-010 多次 wait 不重复创建互斥量");
|
||||
CHECK(s_wait_calls == 2, "TC-VOLLOCK-010b 每次 wait 各等待一次");
|
||||
CHECK(s_new_calls == 1, "TC-VOLLOCK-010 repeated wait does not recreate the mutex");
|
||||
CHECK(s_wait_calls == 2, "TC-VOLLOCK-010b each wait performs one wait call");
|
||||
}
|
||||
|
||||
/* ===================== TC-VOLLOCK-011..013:vol_lock_release ===================== */
|
||||
@@ -187,23 +187,23 @@ static void tc_release(void) {
|
||||
(void)vol_lock_init();
|
||||
(void)vol_lock_wait(0U);
|
||||
vol_lock_release();
|
||||
CHECK(s_release_calls == 1, "TC-VOLLOCK-011 已持锁时 release 调用 osMutexRelease 一次");
|
||||
CHECK(s_release_calls == 1, "TC-VOLLOCK-011 release while holding the lock calls osMutexRelease once");
|
||||
|
||||
/* --- 锁未创建:静默返回 --- */
|
||||
fakes_reset();
|
||||
vol_lock_release();
|
||||
CHECK(s_release_calls == 0, "TC-VOLLOCK-012 锁未创建时 release 静默返回(不调用、不崩溃)");
|
||||
CHECK(s_release_calls == 0, "TC-VOLLOCK-012 release with no lock returns silently (no call, no crash)");
|
||||
|
||||
/* --- 完整周期 --- */
|
||||
fakes_reset();
|
||||
CHECK(vol_lock_init() == 0, "TC-VOLLOCK-013 周期:init -> 0");
|
||||
CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-013b 周期:wait -> 1");
|
||||
CHECK(vol_lock_init() == 0, "TC-VOLLOCK-013 cycle: init -> 0");
|
||||
CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-013b cycle: wait -> 1");
|
||||
vol_lock_release();
|
||||
CHECK(vol_lock_wait(100U) == 1, "TC-VOLLOCK-013c 周期:再 wait -> 1(可重复获取)");
|
||||
CHECK(vol_lock_wait(100U) == 1, "TC-VOLLOCK-013c cycle: wait again -> 1 (re-acquirable)");
|
||||
vol_lock_release();
|
||||
CHECK(s_new_calls == 1, "TC-VOLLOCK-013d 周期:全程只创建一次互斥量");
|
||||
CHECK(s_release_calls == 2, "TC-VOLLOCK-013e 周期:释放两次");
|
||||
CHECK(s_last_timeout == 100U, "TC-VOLLOCK-013f 周期:第二次 wait 的 timeout 透传正确");
|
||||
CHECK(s_new_calls == 1, "TC-VOLLOCK-013d cycle: only one mutex created overall");
|
||||
CHECK(s_release_calls == 2, "TC-VOLLOCK-013e cycle: released twice");
|
||||
CHECK(s_last_timeout == 100U, "TC-VOLLOCK-013f cycle: second wait timeout passes through correctly");
|
||||
}
|
||||
|
||||
/* ===================== main ===================== */
|
||||
|
||||
@@ -131,44 +131,44 @@ static void beat_all(void) {
|
||||
/* ===================== 用例 ===================== */
|
||||
|
||||
static void tc_001_not_armed(void) {
|
||||
printf("TC-WATCHDOG-001 未启动时 poll 不做判定\n");
|
||||
printf("TC-WATCHDOG-001 poll makes no decision before start\n");
|
||||
beat_all();
|
||||
poll_advance(1000);
|
||||
CHECK(s_feed_count == 0, "未启动时 poll 不喂狗");
|
||||
CHECK(task_wd_is_armed() == 0, "is_armed == 0(未启动)");
|
||||
CHECK(s_feed_count == 0, "poll does not feed before start");
|
||||
CHECK(task_wd_is_armed() == 0, "is_armed == 0 (not started)");
|
||||
}
|
||||
|
||||
static void tc_002_throttle(void) {
|
||||
printf("TC-WATCHDOG-002 节流:< 100ms 直接返回\n");
|
||||
printf("TC-WATCHDOG-002 throttle: < 100ms returns directly\n");
|
||||
start_and_clear();
|
||||
beat_all();
|
||||
poll_advance(50); /* 50ms < 100ms → 不抽样 */
|
||||
CHECK(s_feed_count == 0, "50ms 内 poll 不喂狗");
|
||||
CHECK(s_feed_count == 0, "poll within 50ms does not feed");
|
||||
poll_advance(50); /* 累计 100ms → 抽样 */
|
||||
CHECK(s_feed_count == 1, "累计 100ms 后喂狗 1 次");
|
||||
CHECK(s_feed_count == 1, "after 100ms accumulated, fed exactly once");
|
||||
}
|
||||
|
||||
static void tc_003_all_beat_feeds(void) {
|
||||
printf("TC-WATCHDOG-003 全员到位 → 喂狗一次\n");
|
||||
printf("TC-WATCHDOG-003 all tasks present -> feed once\n");
|
||||
start_and_clear();
|
||||
beat_all();
|
||||
poll_advance(100);
|
||||
CHECK(s_feed_count == 1, "全员到位喂狗 1 次");
|
||||
CHECK(task_wd_last_mask() == 0x1F7u, "last_mask == 0x1F7(bit0|1|2|4|5|6|7|8)");
|
||||
CHECK(s_feed_count == 1, "all present feeds exactly once");
|
||||
CHECK(task_wd_last_mask() == 0x1F7u, "last_mask == 0x1F7 (bit0|1|2|4|5|6|7|8)");
|
||||
}
|
||||
|
||||
static void tc_004_take_and_clear(void) {
|
||||
printf("TC-WATCHDOG-004 取走清零:旧位不残留\n");
|
||||
printf("TC-WATCHDOG-004 take-and-clear: no stale bits left\n");
|
||||
start_and_clear();
|
||||
beat_all();
|
||||
poll_advance(100); /* 消费位图并喂狗 */
|
||||
CHECK(s_feed_count == 1, "第一轮喂狗");
|
||||
CHECK(s_feed_count == 1, "first round fed");
|
||||
poll_advance(100); /* 不重新置位 */
|
||||
CHECK(s_feed_count == 1, "未重新置位 → 第二轮不喂狗(位图已取走清零)");
|
||||
CHECK(s_feed_count == 1, "no re-set -> second round does not feed (bitmap already taken and cleared)");
|
||||
}
|
||||
|
||||
static void tc_005_mask_required(void) {
|
||||
printf("TC-WATCHDOG-005 掩码判定:缺任一位不喂\n");
|
||||
printf("TC-WATCHDOG-005 mask check: missing any bit -> no feed\n");
|
||||
start_and_clear();
|
||||
/* 故意缺 UI(bit4) */
|
||||
task_wd_beat(TASK_WD_DEFAULT);
|
||||
@@ -179,31 +179,31 @@ static void tc_005_mask_required(void) {
|
||||
task_wd_beat(TASK_WD_IEC104);
|
||||
task_wd_beat(TASK_WD_PROTECT);
|
||||
poll_advance(100);
|
||||
CHECK(s_feed_count == 0, "缺 UI 位 → 不喂狗");
|
||||
CHECK(s_feed_count == 0, "UI bit missing -> no feed");
|
||||
CHECK(task_wd_last_mask() == (0x1F7u & ~(1u << TASK_WD_UI)),
|
||||
"last_mask 精确记录缺失位");
|
||||
"last_mask records the missing bit exactly");
|
||||
}
|
||||
|
||||
static void tc_006_stall_8(void) {
|
||||
printf("TC-WATCHDOG-006 连续 8 次缺席 → 打 stall 并停喂\n");
|
||||
printf("TC-WATCHDOG-006 8 consecutive misses -> print stall and stop feeding\n");
|
||||
start_and_clear();
|
||||
beat_all(); poll_advance(100); /* streak 清零 */
|
||||
int err0 = g_wdtest_err_count;
|
||||
for (int i = 1; i <= 7; i++) {
|
||||
poll_advance(100);
|
||||
CHECK(g_wdtest_err_count == err0, "第 1..7 次缺席不报 stall");
|
||||
CHECK(g_wdtest_err_count == err0, "misses 1..7 do not report stall");
|
||||
}
|
||||
poll_advance(100); /* 第 8 次 */
|
||||
CHECK(g_wdtest_err_count == err0 + 1, "第 8 次缺席打 [WD] task stall");
|
||||
CHECK(s_feed_count == 1, "全程仅在开始时喂过 1 次(缺席期间未喂)");
|
||||
CHECK(g_wdtest_err_count == err0 + 1, "8th miss prints [WD] task stall");
|
||||
CHECK(s_feed_count == 1, "fed exactly once at the start only (nothing fed during the missing period)");
|
||||
}
|
||||
|
||||
static void tc_007_stall_repeats_until_reset(void) {
|
||||
printf("TC-WATCHDOG-007 停喂后 stall 日志持续复报(当前设计:每 100ms 一条)\n");
|
||||
printf("TC-WATCHDOG-007 after feeding stops the stall log keeps repeating (current design: one per 100ms)\n");
|
||||
start_and_clear();
|
||||
for (int i = 0; i < 8; i++) poll_advance(100); /* 第 8 次触发 */
|
||||
int err8 = g_wdtest_err_count;
|
||||
CHECK(err8 == 1, "第 8 次缺席后已报 1 条 stall");
|
||||
CHECK(err8 == 1, "after the 8th miss exactly 1 stall line was reported");
|
||||
poll_advance(100); /* 第 9 次 */
|
||||
poll_advance(100); /* 第 10 次 */
|
||||
/*
|
||||
@@ -213,89 +213,89 @@ static void tc_007_stall_repeats_until_reset(void) {
|
||||
* 代价是日志冗余(约 9 条/停顿周期)。若改为"每停顿周期仅报一次",
|
||||
* 需加一个 s_stall_reported 标志,见文档"已知可优化点"。
|
||||
*/
|
||||
CHECK(g_wdtest_err_count == err8 + 2, "停喂后每次抽样都复报 stall(现状)");
|
||||
CHECK(g_wdtest_err_count == err8 + 2, "after feeding stops every sample re-reports stall (current behaviour)");
|
||||
}
|
||||
|
||||
static void tc_008_recover(void) {
|
||||
printf("TC-WATCHDOG-008 恢复:重新全员到位 → 喂狗 + streak 归零\n");
|
||||
printf("TC-WATCHDOG-008 recovery: all present again -> feed + streak reset\n");
|
||||
start_and_clear();
|
||||
for (int i = 0; i < 8; i++) poll_advance(100); /* 触发 stall */
|
||||
beat_all();
|
||||
poll_advance(100);
|
||||
CHECK(s_feed_count == 1, "恢复后立即喂狗");
|
||||
CHECK(s_feed_count == 1, "feeds immediately after recovery");
|
||||
int err0 = g_wdtest_err_count;
|
||||
poll_advance(100); /* 再缺一次 */
|
||||
CHECK(g_wdtest_err_count == err0, "streak 已归零:单次缺席不报 stall");
|
||||
CHECK(g_wdtest_err_count == err0, "streak already reset: a single miss does not report stall");
|
||||
}
|
||||
|
||||
static void tc_009_throttle_boundary(void) {
|
||||
printf("TC-WATCHDOG-009 节流窗口边界 99/100/199/200\n");
|
||||
printf("TC-WATCHDOG-009 throttle window edges 99/100/199/200\n");
|
||||
start_and_clear();
|
||||
beat_all();
|
||||
poll_advance(99); CHECK(s_feed_count == 0, "99ms 不判");
|
||||
poll_advance(1); CHECK(s_feed_count == 1, "100ms 判(喂狗)");
|
||||
poll_advance(99); CHECK(s_feed_count == 0, "99ms: no decision");
|
||||
poll_advance(1); CHECK(s_feed_count == 1, "100ms: decides (feed)");
|
||||
beat_all();
|
||||
poll_advance(99); CHECK(s_feed_count == 1, "再 99ms 不判");
|
||||
poll_advance(1); CHECK(s_feed_count == 2, "累计 100ms 判");
|
||||
poll_advance(99); CHECK(s_feed_count == 1, "another 99ms: no decision");
|
||||
poll_advance(1); CHECK(s_feed_count == 2, "accumulated 100ms: decides");
|
||||
}
|
||||
|
||||
static void tc_010_tick_wrap(void) {
|
||||
printf("TC-WATCHDOG-010 tick 回绕安全\n");
|
||||
printf("TC-WATCHDOG-010 tick wraparound is safe\n");
|
||||
start_and_clear();
|
||||
s_tick = 0xFFFFFF00u; /* 推到接近 2^32 回绕 */
|
||||
task_wd_poll(); /* 建立基线(此处可能不判,取决节流) */
|
||||
beat_all();
|
||||
s_feed_count = 0;
|
||||
poll_advance(0x100u); /* 跨越回绕:0xFFFFFF00 + 0x100 = 0 */
|
||||
CHECK(s_feed_count == 1, "回绕处 100ms 仍正确判(无符号差)");
|
||||
CHECK(s_feed_count == 1, "across the wraparound 100ms still decides correctly (unsigned diff)");
|
||||
}
|
||||
|
||||
static void tc_011_observers(void) {
|
||||
printf("TC-WATCHDOG-011 观测接口语义\n");
|
||||
printf("TC-WATCHDOG-011 observation API semantics\n");
|
||||
start_and_clear();
|
||||
CHECK(task_wd_is_armed() == 1, "is_armed == 1(已启动)");
|
||||
CHECK(task_wd_is_armed() == 1, "is_armed == 1 (started)");
|
||||
beat_all();
|
||||
poll_advance(100);
|
||||
CHECK(task_wd_last_mask() == 0x1F7u, "last_mask 反映最近一次抽样");
|
||||
CHECK(task_wd_miss_count() == 0u, "全程达标 → miss_count == 0");
|
||||
CHECK(task_wd_last_mask() == 0x1F7u, "last_mask reflects the most recent sample");
|
||||
CHECK(task_wd_miss_count() == 0u, "all rounds met -> miss_count == 0");
|
||||
}
|
||||
|
||||
static void tc_013_bad_id(void) {
|
||||
printf("TC-WATCHDOG-013 非法任务 ID 被忽略\n");
|
||||
printf("TC-WATCHDOG-013 invalid task id is ignored\n");
|
||||
start_and_clear();
|
||||
task_wd_beat((task_wd_id_t)(TASK_WD_ID_MAX + 3)); /* 非法:应被忽略 */
|
||||
poll_advance(100);
|
||||
CHECK(s_feed_count == 0, "非法 ID 未污染位图(必需位全缺 → 不喂)");
|
||||
CHECK(task_wd_last_mask() == 0u, "位图保持 0(非法位未置)");
|
||||
CHECK(s_feed_count == 0, "invalid id does not pollute the bitmap (all required bits missing -> no feed)");
|
||||
CHECK(task_wd_last_mask() == 0u, "bitmap stays 0 (invalid bit never set)");
|
||||
}
|
||||
|
||||
static void tc_014_start_idempotent(void) {
|
||||
printf("TC-WATCHDOG-014 start 幂等\n");
|
||||
printf("TC-WATCHDOG-014 start is idempotent\n");
|
||||
task_wd_start();
|
||||
task_wd_start();
|
||||
CHECK(s_start_count == 1, "重复 start 只启动 IWDG 一次");
|
||||
CHECK(s_start_count == 1, "repeated start launches IWDG only once");
|
||||
CHECK(task_wd_is_armed() == 1, "is_armed == 1");
|
||||
}
|
||||
|
||||
static void tc_015_start_feeds_once(void) {
|
||||
printf("TC-WATCHDOG-015 start 先喂一次(首个窗口从启动时刻起算)\n");
|
||||
printf("TC-WATCHDOG-015 start feeds once first (first window counted from the start instant)\n");
|
||||
task_wd_start();
|
||||
CHECK(s_start_count == 1, "start 启动 IWDG 一次");
|
||||
CHECK(s_feed_count == 1, "start 内部先喂狗一次(避免启动即超时)");
|
||||
CHECK(s_start_count == 1, "start launches IWDG once");
|
||||
CHECK(s_feed_count == 1, "start feeds once internally (avoids a timeout right at boot)");
|
||||
}
|
||||
|
||||
static void tc_016_stall_not_armed_no_feed(void) {
|
||||
printf("TC-WATCHDOG-016 未启动时即便全员到位也不喂\n");
|
||||
printf("TC-WATCHDOG-016 even with all tasks present, nothing feeds before start\n");
|
||||
/* 未 start:poll 直接返回 */
|
||||
beat_all();
|
||||
poll_advance(500);
|
||||
CHECK(s_feed_count == 0, "未启动 → 不喂狗(无论位图)");
|
||||
CHECK(task_wd_last_mask() == 0u, "未启动 → 不更新 last_mask");
|
||||
CHECK(s_feed_count == 0, "not started -> no feed (whatever the bitmap)");
|
||||
CHECK(task_wd_last_mask() == 0u, "not started -> last_mask not updated");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== Task Watchdog 主机侧单元测试 ===\n");
|
||||
printf("被测:App/util/task_watchdog.c(判定状态机)\n\n");
|
||||
printf("=== Task Watchdog host-side unit tests ===\n");
|
||||
printf("Target: App/util/task_watchdog.c (decision state machine)\n\n");
|
||||
|
||||
fresh(); tc_001_not_armed();
|
||||
fresh(); tc_016_stall_not_armed_no_feed();
|
||||
|
||||
Reference in New Issue
Block a user