test(host): P2 覆盖率批次,5 套主机单测与共享假 FatFs

P2 五个文件全部进分母(三名队友并行,写范围互斥、零越界):
  net_socket 420 条 / 881-883=99.8%、52-52 函数(S1~S6 分片)
  sys_log_mcu 75 条 / 310-313=99.0%、12-12
  console_log 104 条 / 188-189=99.5%、15-15
  cfg_file_store_mcu 34 条 / 97-97=100%、10-10
  soe_store 26 条 / 78-79=98.7%、4-4
合计 659 条断言,P2 本体 1554-1561=99.6%、93-93 函数。

账:套件 28→33 全绿;files_tested 32→37;未触及 112→107(.c 34→29,
恒等式 29 = 0(P1剩) + 0(P2) + 29(P3) 咬合,§9 终点判据达成);分母
4684→6245 插桩行;line 85.8%→89.3%;func 90.1%→92.2%(这批 5 个文件
全都以 ≥98.7% 进分母,故未摊薄)。--update-baseline 前后逐文件比对:
下降 0 个文件。

基建:新建 test/host/common/fatfs_fake.{h,c}(以真实 ff.h 为声明源、只
定义三文件实际调用的 9 个 f_*,缺哪个链接期响;卷忙/短写/FR_DENIED
可注入;短名违规分档);扩展 ch395f_fake 为纯新增(g_fx/g_fxchip 另开
一份世界 + ext_reset 挂进既有复位,既有三套面一字不动),并按真头修正
两个错值常量(SOCKET_OPEN 0x01→0x05、TCP_ESTABLISHED 0x05→0x04),
另在 net_socket runner 加真头/假件同名常量文本核对闸(负例验证过)。
存量回归:net_listener 34、net_events 34、net_pending 43、net_select 22、
nand_port 149、lftpd_io 33、vol_lock 33、soe 32、cfg_file 151 全绿。

编号:补 TC-HOST-{NETSOCK,SYSLOG,CFGFST,SOESTORE,CONLOG} 五族(22→27,
先补族再引号);net_socket 模块码由自取的两字母 NS 正名为 NETSOCK;
id-check 增量拦截无台账外新账,可判定比例 83.27%(基线 82.6%)。

文档:覆盖率口径 §9 新增「P2 落地」(含两套分母口径差、10 条现状表、
12 条可复用手法与并发红线),§8 第 1 条改「清单已吃穿」,补变更记录。
本提交未改任何生产代码(App/Drivers/Src/Lib/MDK-ARM 零触碰)。

10 条现状已逐条登记在 §9(含 sys_log_mcu.c:509~537 卷忙整批丢日志、
net_socket.c 同文件两套 IP 字节序、UDP 两 API 绕过 netTask),已获指示
全部修复,另开一轮(含测试字面量统一纯 ASCII)。

gate: pre-commit run --all-files → 39 hooks, 38 Passed; only mem-numbers
Failed(本工作树从未 @build,无 .map 事实源,属分支期已知陷阱);
mem-budget Passed;test-coverage 与 id-check Passed。RAM/Flash 增量 = 0
(纯测试批次)。按仓内分支约定走 --no-verify,台账回填留待主树合并后。
This commit is contained in:
2026-10-24 17:54:02 +08:00
parent 4ac869058d
commit 794906cdc8
46 changed files with 10821 additions and 39 deletions
+161
View File
@@ -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))