返回系列

测试、调试与打包:从 pytest 到 PyInstaller exe

复盘 RespiraScope 如何通过单元测试覆盖滤波、峰谷、Socket.IO、记录状态和 HTTP 默认值,并解释源码运行、onefile exe、onedir 包的取舍。

背景问题

实时系统最怕“看起来能跑”。只要传感器有数据,页面有曲线,很多问题会被掩盖:队列满了是否丢数据,Socket 新连接是否补快照,Record 前置条件是否正确报错,短数据是否会误滤波,端口占用是否会自动避让。

RespiraScope 的测试和打包目标不是追求覆盖率数字,而是覆盖这些最容易破坏用户体验的工程边界。

测试方案对比

方案优点问题项目判断
只手动打开页面看波形最接近使用体验不可重复,难覆盖错误状态不足够
单元测试算法函数快速稳定不能覆盖 HTTP 和状态机必须有但不够
FastAPI TestClient不启动真实服务也能测接口不覆盖浏览器行为当前采用
端到端浏览器测试最完整成本高,依赖运行环境可作为后续增强

当前测试重点放在后端核心行为:滤波、峰谷、队列、Socket.IO、HTTP 默认参数、记录状态、异常质量事件和端口选择。

当前测试覆盖

测试文件覆盖重点
test_signal_processing.py实时/离线滤波、短数据保护
test_peak_valley_detector.py峰谷识别、BPM 和稳定性
test_runtime_resilience.py队列 dropped、高水位、快照、质量事件、record 状态
test_http_defaults.py空请求体默认值、/applyFilter 空数据
test_socket_io_service.py多客户端广播和新连接快照
test_port_selection.py端口可用性和自动选择

这里最值得学习的是测试对象选择。项目没有试图在单元测试里启动完整浏览器,而是把可重复的核心逻辑拆出来测。比如 DataQueueManager.snapshot_messages() 可以直接测试,不必通过浏览器刷新来观察。

调试路径

当系统没有数据时,不应该先怀疑图表。推荐按这条路径排查:

  1. GET /health:后端是否运行,mock 是否启用,传感器地址是什么。
  2. GET /runtime/config:前端看到的 backend port 是否和真实端口一致。
  3. GET /stream/status:是否 started,socket 是否 connected,received_count 是否增长。
  4. Socket.IO:是否收到 raw 类型消息。
  5. Filter:raw 有数据但 filtered 没数据时,检查是否少于 FILTER_STARTUP_DELAY = 300。
  6. Record:记录按钮不可用时,检查是否已经开始接收并收到 raw 点。
/health runtime config stream status Socket raw filtered 先确认后端和数据流,再看滤波和前端展示

打包方案对比

方案优点问题适合场景
源码运行 uv run RespiraScope方便开发,依赖可复现用户需要安装 Python 和 uv开发调试
PyInstaller onefile分发简单,单个 exe首次启动解包慢,调试不方便演示和交付
PyInstaller onedir文件多但结构清楚分发不如单文件方便打包问题排查
Docker环境一致Windows 设备联调和本地传感器访问不一定方便服务部署

项目默认使用 PyInstaller onefile 输出 dist/RespiraScope.exe。如果需要排查静态资源或隐藏导入问题,可以用 --onedir 生成展开目录包。

为什么打包脚本要显式收集依赖和前端资源

SciPy、NumPy、uvicorn、socketio、engineio 都有动态导入或二进制组件。PyInstaller 不一定能完全自动发现,所以脚本里使用了:

--collect-all numpy
--collect-all scipy
--collect-submodules socketio
--collect-submodules engineio
--collect-submodules uvicorn

前端静态资源也必须通过 --add-data 嵌入:frontend-console、frontend-monitor、frontend-lab、frontend-guide、frontend-api-docs 和配置模板都要进入 exe。否则后端启动了,Console 可能找不到静态文件。

代价和边界

单元测试不能证明真实设备一定稳定,只能证明核心逻辑在可控输入下行为正确。真实传感器还涉及网络、设备固件、线缆、采样间隔和现场干扰。

PyInstaller 也不是部署银弹。onefile exe 方便分发,但启动时会解包,杀毒软件也可能影响启动速度。生产环境如果需要持续运行、日志采集和自动重启,仍应考虑 Windows 服务、进程守护或容器化部署。

关键函数表

函数或工具作用设计点
pytest运行单元测试快速验证核心行为
TestClient测 FastAPI 路由不启动真实网络服务也能测接口
asyncio.run()测异步滤波和 Socket 行为让异步逻辑可重复测试
uv sync同步依赖锁定 Python 依赖
uv run RespiraScope源码启动开发和调试主入口
PyInstaller构建 exe分发给非 Python 用户
--add-data打包静态资源确保 Console 和配置模板可用

读源码建议

先读 tests/test_runtime_resilience.py,它覆盖了队列、快照、质量事件和 record 状态,是理解系统边界最好的测试文件。再读 scripts/build_exe.py,看为什么打包脚本要显式收集依赖和静态资源。最后执行 uv run pytest,把测试输出和源码行为对应起来。