73941da6b1
为 task_watchdog.c 的判定逻辑(位图取走清零 / 必需掩码 0x37 判定 / miss_streak 连续 8 次停喂 / 100ms 抽样节流 / tick 回绕安全)补一层 主机侧纯逻辑单元测试,弥补此前只能靠真机或模拟器才能发现回归的空白。 架构(沿用项目既有"共享核心 + 平台后端"做法,参照 cfg_file_store): - task_watchdog.h 末段新增"平台后端(内部)"声明 3 个钩子 (wd_platform_feed / _start / _tick_ms),MCU 端以 __weak 提供默认实现, 行为与重构前逐字一致(@build 0 错误 0 警告,Flash +20B)。 - 主机侧单测以同名强符号覆盖三钩子,即可离线注入/观测,无需碰 MCU 代码。 - 测试文件 #include 生产源文件本身,保证测的是真代码而非副本。 - 测试专用复位 task_wd_test_reset() 仅 TASK_WATCHDOG_UNIT_TEST 下编入, MCU 构建零代码零符号。 测试(test/host/watchdog/,MSVC 离线编译,纯标准库驱动): - 39 项断言全过(退出码 0);注入故障可复现退出码 1。 - run_unit_test.py 直接拼 INCLUDE/LIB 调 cl.exe,绕开本机被禁的 reg.exe。 - stub/ 三桩头覆盖 main.h / iwdg.h / dbg_log.h。 门禁:pre-commit 新增 watchdog-unit-test 本地钩子(L0.5 层),仅被测文件 或其测试变动时触发;测试开发标准流程.md 新增 L0.5 层说明。 顺带改善:task_watchdog.c 的节流基线 s_last_tick 由函数内 static 提为文件 static(可测性 + 模块状态归一)。
171 lines
6.2 KiB
Python
171 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""看门狗判定状态机的主机侧单元测试运行器(纯标准库)。
|
||
|
||
做什么:
|
||
用本机 MSVC 编译 test/host/watchdog/watchdog_test.c(它以 #include 纳入
|
||
**生产源文件** App/util/task_watchdog.c),在 PC 上离线跑完整判定状态机,
|
||
失败即非 0 退出。零硬件、零烧录、毫秒级。
|
||
|
||
为什么要主机侧单测(而不是只用板端套件):
|
||
task_watchdog.c 的判定逻辑(位图取走清零 / 掩码 / miss_streak 累计 / 节流)
|
||
是纯逻辑,与硬件无关;板端套件只能"在真机上跑一遍",无法穷举边界(节流窗口
|
||
99/100ms、tick 回绕、连续 8 次缺席的第 8/9 次分界)。主机侧单测把这些一次性
|
||
钉死,且不占 MCU 的 RAM(项目 RAM 已 95%)。
|
||
|
||
实现要点(为何不用 vcvars64.bat):
|
||
vcvars64.bat 内部调用 reg.exe 探测环境;本机策略禁止 reg.exe。故这里**直接
|
||
拼 INCLUDE/LIB 环境变量**并绝对路径调用 cl.exe——只需 3 个已知目录
|
||
(MSVC 版本 / Windows SDK include / SDK lib),无副作用、可重复。
|
||
|
||
用法(仓库根目录):
|
||
python test/host/watchdog/run_unit_test.py
|
||
或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/watchdog/run_unit_test.py
|
||
|
||
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败)。
|
||
|
||
设计依据:docs/测试说明/看门狗单元测试说明.md
|
||
创建日期:2026-10-01
|
||
"""
|
||
import glob
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
|
||
# ----------------------------- 路径 -----------------------------
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||
STUB = os.path.join(HERE, "stub")
|
||
SRC = os.path.join(HERE, "watchdog_test.c")
|
||
APP_UTIL = os.path.join(ROOT, "App", "util") # task_watchdog.c 所在目录
|
||
|
||
VS_ROOTS = [
|
||
r"C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools",
|
||
r"C:\Program Files\Microsoft Visual Studio\2022\BuildTools",
|
||
r"C:\Program Files\Microsoft Visual Studio\2022\Community",
|
||
r"C:\Program Files\Microsoft Visual Studio\2022\Professional",
|
||
]
|
||
SDK_ROOTS = [r"C:\Program Files (x86)\Windows Kits\10"]
|
||
|
||
|
||
def _first(pattern):
|
||
hits = sorted(glob.glob(pattern))
|
||
return hits[-1] if hits else None
|
||
|
||
|
||
def _decode(b):
|
||
"""把 subprocess 字节输出解码为字符串:本机 MSVC 输出为 GBK(代码页 936),
|
||
测试程序自身输出为 UTF-8;按 UTF-8 优先、失败回退 GBK 处理。"""
|
||
if isinstance(b, str):
|
||
return b
|
||
try:
|
||
return b.decode("utf-8")
|
||
except UnicodeDecodeError:
|
||
return b.decode("gbk", errors="replace")
|
||
|
||
|
||
def locate_toolchain():
|
||
"""返回 (cl.exe, include_dirs, lib_dirs);找不到返回 (None, [], [])。"""
|
||
cl = None
|
||
for vs in VS_ROOTS:
|
||
c = _first(os.path.join(vs, r"VC\Tools\MSVC\*\bin\Hostx64\x64\cl.exe"))
|
||
if c:
|
||
cl = c
|
||
break
|
||
if not cl:
|
||
return None, [], []
|
||
|
||
msvc_ver_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(cl))))
|
||
# cl.exe = <vs>\VC\Tools\MSVC\<ver>\bin\Hostx64\x64\cl.exe
|
||
msvc_ver_dir = os.path.dirname(os.path.dirname(os.path.dirname(
|
||
os.path.dirname(cl)))) # -> ...\MSVC\<ver>
|
||
inc = [
|
||
os.path.join(msvc_ver_dir, "include"),
|
||
]
|
||
lib = [
|
||
os.path.join(msvc_ver_dir, "lib", "x64"),
|
||
]
|
||
|
||
for sdk in SDK_ROOTS:
|
||
sdk_inc = _first(os.path.join(sdk, r"Include\*"))
|
||
sdk_lib = _first(os.path.join(sdk, r"Lib\*"))
|
||
if sdk_inc:
|
||
inc += [
|
||
os.path.join(sdk_inc, "ucrt"),
|
||
os.path.join(sdk_inc, "shared"),
|
||
os.path.join(sdk_inc, "um"),
|
||
]
|
||
if sdk_lib:
|
||
lib += [
|
||
os.path.join(sdk_lib, "ucrt", "x64"),
|
||
os.path.join(sdk_lib, "um", "x64"),
|
||
]
|
||
return cl, inc, lib
|
||
|
||
|
||
def main():
|
||
cl, inc, lib = locate_toolchain()
|
||
if not cl:
|
||
print("[ENV] 找不到 MSVC cl.exe(需要 VS2019/2022 的 C++ 工具链)",
|
||
file=sys.stderr)
|
||
return 2
|
||
|
||
tmp = tempfile.gettempdir()
|
||
objdir = os.path.join(tmp, "wd_test_obj")
|
||
os.makedirs(objdir, exist_ok=True)
|
||
exe = os.path.join(tmp, "watchdog_unit_test.exe")
|
||
|
||
env = dict(os.environ)
|
||
env["INCLUDE"] = os.pathsep.join(inc)
|
||
env["LIB"] = os.pathsep.join(lib)
|
||
env["PATH"] = os.path.dirname(cl) + os.pathsep + env.get("PATH", "")
|
||
|
||
cmd = [
|
||
cl, "/nologo", "/W4", "/std:c11", "/utf-8",
|
||
# C4819("源代码包含不能在当前代码页中表示的字符")是**纯编码诊断**:
|
||
# 本文件确为 UTF-8(带 BOM,已核验),且已显式传 /utf-8,字节解码无误、
|
||
# 编译产物正确。但在"只设 INCLUDE/LIB、不调 vcvars"的调用方式下,
|
||
# MSVC 的代码页探测会退化为 `代码页(0)` → 恒定误报 10 条。
|
||
# 该文件仅主机侧编译(ARMCC 从不编译 test/),故显式屏蔽,保持门禁输出干净。
|
||
"/wd4819",
|
||
"/I", STUB, # stub 头优先于真实头(覆盖 main.h/iwdg.h/dbg_log.h)
|
||
"/I", APP_UTIL, # task_watchdog.c / task_watchdog.h
|
||
"/I", HERE, # (保留)本目录
|
||
SRC,
|
||
"/Fe:" + exe,
|
||
"/Fo:" + objdir + os.sep,
|
||
]
|
||
|
||
print("=== 编译并运行看门狗单元测试 ===")
|
||
cp = subprocess.run(cmd, env=env, capture_output=True)
|
||
cout = _decode(cp.stdout)
|
||
cerr = _decode(cp.stderr)
|
||
if cout:
|
||
sys.stdout.write(cout)
|
||
if cerr:
|
||
sys.stderr.write(cerr)
|
||
if cp.returncode != 0:
|
||
print("[ENV] 编译失败", file=sys.stderr)
|
||
return 2
|
||
|
||
run = subprocess.run([exe], env=env, capture_output=True)
|
||
out = _decode(run.stdout)
|
||
sys.stdout.write(out)
|
||
errout = _decode(run.stderr)
|
||
if errout:
|
||
sys.stderr.write(errout)
|
||
|
||
if "=== Task Watchdog Tests:" not in out:
|
||
print("[ENV] 未产生测试汇总行", file=sys.stderr)
|
||
return 2
|
||
if run.returncode == 0:
|
||
print("[OK] 单元测试全部通过")
|
||
return 0
|
||
print("[FAIL] 有测试用例未通过", file=sys.stderr)
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|