56893709fe
规格:docs/任务规划/测试与架构专项.md §2.1(模块级轻量断言宏,必须零生产开销)。
本提交只做道C-1:新增断言宏 + 2 处试点,不开 net 层大口子、不新增 test/host 目录。
改动
- 新增 Drivers/BSP/app_assert.h:APP_ASSERT 两级裁剪。
· 关闭态(默认,产品构建):编译期消失、零符号零代码;条件仍进 sizeof 的
不求值上下文,写错变量名/比较类型会当场报错,不会出现"裁掉=从没被编译器看过"。
· 打开态:失败交 configASSERT 用的同一个钩子 vApplicationAssertFailed()
(裸控制台打 [ERR] ASSERT FAILED file:line 后软复位);宿主端(模拟器 /
主机单测)落 stderr + abort(),非零退出码不会被一键回归静默吞掉。
- 开关 APP_ASSERT_ENABLE 默认注释,策略与 ENABLE_DESTRUCTIVE_TESTS 同级。
- 纯宏、无函数、无数据,不新增 .c → 不涉及双端注册(.h 靠 include 路径找到)。
- Drivers/BSP/NET/net_socket.c:2 处试点,都落在"函数头写了限定条件、函数体
一项都没检查"的缺口上。
· net_close_locked:控制块必须落在 s_net_socks 数组内且 in_use——此前给出
越界或已释放指针会一路走到 memset(sizeof(net_sock_t)),直接踩脏 fd 表。
· net_drop_conn_locked:进入时必须 in_use + TCP 流 + 会话处于 ESTABLISHED
或 CONNECTING——函数体无条件置 CLOSED 并登记重建,将来绕过
NET_MSG_DROP_CONN 分派门的新调用路径会拆掉一个健康监听器(症状与 N-30
一族同源难查),断言把这条状态机不变量钉成机器可查。
两处均 netTask 上下文、非中断、非高频;条件纯读无副作用;既有错误返回分支
一条都没改(断言不替代错误码,见 app_assert.h 第四节纪律)。
- test/host/net_socket/run_unit_test.py:include_dirs 补 Drivers/BSP 一行,使该
套单测编 net_socket.c 时用真 app_assert.h,不造桩——假件与真头漂移正是本
runner「常量闸」要防的那类自洽假绿;stub 目录仍在最前,同名头命中不受影响。
- Drivers/BSP/README.md:根级文件表补 app_assert.h 一行。
两处有意偏离示意稿(理由已写进头注释)
1) 断言点不再额外发一条 DBG_ERROR:钩子本身已打印 file:line,而每条 DBG_*
调用点约 90 B Flash + 216 B 栈(AGENTS「日志不是免费的」);断言点又常在
「内核状态已不可信」的现场,走裸控制台比走日志行缓冲稳。
2) 头落 Drivers/BSP(与 dbg_log.h、bsp_err.h 同级)而非示意稿的 App/util:它是
纯宏叶子能力,落 App/util 会让 BSP 新增一条反向 include App 层头的边,而
layer-deps 存量基线只减不增、加不进去;落 Drivers/BSP 已在 MCU 与模拟器双端
include 路径内 → 双端零工程文件改动(uvprojx / CMakeLists 一字未动)。
验证(工作树 _wt/S12-assert,分支 task/S12-assert)
- @build 产品态:BUILD_OK,0 错误 0 警告。
- 体积与 RAM:Code=213780 RO=18860 RW=1204 ZI=131852,与 scripts/ram_baseline.tsv
的 2026-10-24 基线行完全相同 → Flash/RAM 增量 0 B;.map 中 app_assert 相关
符号 0 条(规格完成判据"无新增符号")。gate 全绿,无需 mem-budget 例外。
- 打开态实测(临时取消注释编译,跑完已还原成注释态):0 错 0 警,
Code=213916(2 处断言共 +136 B ≈ 68 B/处,无每调用点栈代价),RW/ZI 不变。
- 宿主 MSVC 三态实测:关闭态断言不求值但条件仍做类型检查(错误标识符报
C2065);打开态真条件不触发、假条件 stderr 打 [ASSERT] FAILED 后 abort;
net_socket 主机单测 452/452 通过(用真头,开关默认关)。
- 门禁实跑:cppcheck(warning/perf/portability/style)、no-mojibake、coding-standard、
layer-deps、bsp-err、id-check、doc-links、registration-check、mem-budget 全 0。
- 台架 @test 未跑:bench 锁归 Lead;产品态本改动零行为差异,无需烧录回归。
遗留
- 打开态未在台架上真跑一轮(需 bench 锁 + 一个故意造假的注入点):待台架实测。
- 文档销项(测试与架构专项.md §2.1、并行分道方案.md 对应行、AGENTS 任务表)
归主工作树统一处理,本分支未碰 docs/任务规划/**。
168 lines
9.1 KiB
Python
168 lines
9.1 KiB
Python
#!/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 放本模块头。
|
||
# Drivers/BSP 放**最后**(只补 app_assert.h 这一个真头,其余同名头仍由 stub 先命中):
|
||
# net_socket.c 的 S12 断言试点(2 处 APP_ASSERT)要包含它。这里刻意**用真头不造桩**——
|
||
# 桩与真头漂移正是本 runner「常量闸」要防的那类自洽假绿(stub/dbg_log.h 那种必须做桩
|
||
# 的是平台层,断言宏不是平台层:它无状态、开关默认关,真头在宿主下只剩一条 #define)。
|
||
# 依据 docs/任务规划/测试与架构专项.md §2.1。
|
||
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"),
|
||
os.path.join(ROOT, "Drivers", "BSP"),
|
||
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))
|