f754d59218
分层:流程与规范/ 单元测试/ 板级测试/ 专项验证/ 测试工具/ 测试数据/, 顶层只留新增的目录导航 README.md(归层判据 + 硬约束 + 本次处置记录)。 - 搬迁 28 个路径:测试数据三个目录 58 个文件为纯改名(R100,内容逐字节不变) - 全库引用同步 124 个文件:AGENTS / docs 索引 / skills / 门禁注释 / scripts - 门禁面:scripts/check_test_traceability.py 的 glob 改 rglob (分层后不递归扫描=追溯门禁直接瞎掉) - scripts/rename_ref.py:收尾的 ℹ 提示在 GBK 代码页抛 UnicodeEncodeError, 让进程以退出码 1 收场而改写其实已落盘(RC 与真实结果相反);改为把 stdout/stderr reconfigure 成 UTF-8,与 check_doc_links.py 等同法 - docs/文档规范.md §1.1 补「子目录文件过多时再分一层」;§3.6 补 rename_ref 搬整目录的两个实测坑:① 会改写目录内部文件的内容 ② 内部文件的 ../ 出链 不补层级(本次 23 个被误改的证据文件已按 HEAD 原始字节逐字节还原) - docs/README.md §6 改为与六个子目录一一对应,并补登原先漏登记的 「启动日志原子性复验流程」一行 - .cubemx_survival_baseline.json:App/util/task_watchdog.c 的注释路径也随分层更新, 该文件是存活审计的 watchdog-module 漂移锚点,按脚本自己的提示跑 --write 重建基线 (实测重建后仅 watchdog-module 一条哈希变化) - 铁律 4 例外(本次在任务树里改了单写者文件):AGENTS.md 与 .pre-commit-config.yaml 只被 rename_ref 改写路径字面量(后者全在注释行), 任务表块与钩子声明一字未动 验证:check_doc_links.py --sections-strict 覆盖全部 88 个改动 .md 全绿; check_test_traceability.py 输出与主树逐行一致;196 个文本文件无 BOM、无新增 CR、无 U+FFFD、无行尾空白;陈旧引用面由主树 25 处降到 6 处(无新增断链)。
168 lines
9.1 KiB
Python
168 lines
9.1 KiB
Python
#!/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)。
|
||
# 规格正文 = Drivers/BSP/app_assert.h 头注释(原专项文档已删除)。
|
||
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))
|