Files
STM32F4-Base/test/host/watchdog/run_unit_test.py
T
wandering1 73941da6b1 看门狗判定状态机增加主机侧单元测试(L0.5 层)
为 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(可测性 + 模块状态归一)。
2026-10-01 06:57:01 +08:00

171 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())