#!/usr/bin/env python3 # -*- coding: utf-8 -*- """NET 套接字核心(net_socket)的主机侧纯逻辑单元测试运行器(纯标准库)。 做什么: 用本机 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 上离线验证: 跨上下文消息全分派、结果经任务通知值回传、每任务错误码槽、 监听门控与 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];而错误码本身还按任务隔离,跨任务覆写只会让排障读到别人的码。 把队列/通知/时基做成显式量后,"登记了挂起却仍发即时回执"这种缺陷在 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))