# 测试 测试代码位于 `tests/`,运行方式: ```bash uv run pytest # 全部测试 uv run pytest tests/nodes/test_whisper # 只跑某个模块目录 uv run pytest tests/app/test_routers # 只跑某组 API 测试 uv run pytest --cov=src --cov=nodes # 需要覆盖率时手动追加 ``` 测试的**规则要求**写在 [../AGENTS.md](../AGENTS.md#测试规则):模块边界、 按模块组织、数据/过程/验证三段结构、功能覆盖优先、真实数据与真实生产代码。 ## 目录结构与模块对应 测试按**模块**组织:被测代码的层级与模块名直接映射到 `tests/` 下的目录。 一个模块 = 一个模块目录;单文件过长时在模块目录**内部**按行为拆文件。 ``` tests/ ├── nodes/ # 对应 nodes/ │ ├── test_srt/ # SRT 解析/序列化(parsing + data/) │ ├── test_whisper/ # 转写:模型解析/分块/时间轴/幻觉清洗 │ ├── test_ass/ # SRT → VR 双目 ASS │ ├── test_ffmpeg/ # ffmpeg 定位 + 提音(真实视频) │ ├── test_frame_extract/ # 抽帧/crop/帧号排序(含 frames_boundary 回归) │ ├── test_vlm/ # 单帧 OCR(真实图片 + Ollama 集成) │ ├── test_subtitle_ocr/ # 逐帧 OCR 汇总、断点、暂停(14236 帧夹具) │ ├── test_llm/ # 翻译:ID 对齐/重试/专名注入 │ ├── test_llm_filter/ # 规则层 + LLM 层(真实 OCR 回归数据) │ ├── test_subtitle_cleanup/ # 幻觉删除 + 短呻吟过滤 │ ├── test_subtitle_correction/# 领域纠错(上下文/误听泛化) │ ├── test_proper_nouns/ # 专名/隐语规则表 │ ├── test_adaptive_pool/ # 自适应并发额度 │ ├── test_vad_profiler/ # VAD 信号分析与评分 │ └── test_echo/ # 示例节点 ├── app/ # 对应 src/wov_app/ │ ├── test_db/ # SQLite Repository │ ├── test_scheduler/ # DAG 调度、断点、暂停 │ ├── test_batch/ # 批量扫描/放置/引擎 │ ├── test_maintenance/ # 孤儿清理 │ ├── test_registry/ # 节点注册表(唯一调用入口) │ ├── test_seed/ # 内置工作流加载 │ ├── test_storage/ # 原子复制 │ ├── test_config/ # 环境变量与路径(子进程验证) │ ├── test_logging/ # 日志配置 │ ├── test_main/ # 应用装配与生命周期 │ └── test_routers/ # 三组 API(apps / workflows / batch) ├── sdk/test_models/ # 对应 src/wov_sdk/(协议数据模型) ├── web/test_crop/ # 对应 web/assets/(框选几何换算) └── shared/ # 跨模块公共设施 ├── realdata_contract.py # 真实数据契约与对齐量化 ├── srt_entries.py # 按秒解析 SRT ├── env_isolation.py # 环境/临时目录隔离 ├── data/alignment/ # 跨模块共享的真实素材 + 参考字幕 ├── test_srt_entries/ # 公共设施自身的模块测试 └── test_alignment/ # 真实语音时间对齐集成测试 ``` 规则要点(完整表述见 [../AGENTS.md](../AGENTS.md#测试规则)): - 每个模块目录含 `__init__.py`,因此不同模块下的同名测试文件可共存。 - 每个测试写成**数据 → 测试过程 → 验证结果**三段,可单独运行。 - **不保留全局 `conftest.py`**(已删除):隔离由模块自己的 fixture 完成 (`tests/shared/env_isolation.py` 或模块内 `fixture`)。 - 测试过程调用真实生产代码(`nodes/`、`wov_app`、`wov_sdk`), 只自行构建测试数据并验证输出;mock 仅用于 I/O 边界与模型推理。 ## 测试数据 测试媒体一次性准备后**随模块目录入库**,测试直接复用,禁止在执行时现场生成; 缺失时跳过而非现场生成。小体量数据(JSON 片段、期望输出)直接写在测试代码内。 | 素材 | 内容 | 归属模块 | | --- | --- | --- | | `data/speech_60s.wav` | 真实语音(合法 16kHz 单声道 WAV) | `tests/nodes/test_whisper/` | | `data/clip_with_audio.mp4` | 10 秒真实视频 + 真实语音音轨(提音用例) | `tests/nodes/test_ffmpeg/` | | `data/subtitle_10s.mp4` | 烧录 SUB 001/SUB 002 的 10s 视频(无音轨,验证失败路径) | `tests/nodes/test_ffmpeg/`、`tests/nodes/test_frame_extract/` | | `data/frames_boundary/*.png` | 跨越 9999 帧边界的真实帧文件名(14236 帧事故回归) | `tests/nodes/test_frame_extract/` | | `data/ocr_text.png` / `ocr_notext.png` | 有文字帧 / 无文字帧 | `tests/nodes/test_vlm/` | | `data/test_real_hav_sub.png` | 真实视频字幕截图(期望识别出"还有没有什么困扰 或者奇怪的地方吗") | `tests/nodes/test_vlm/` | | `data/ocr_srt_run_ac7f480a3ccb.srt` | 真实任务 1666 条 OCR 输出(规则层回归基线) | `tests/nodes/test_llm_filter/` | | `data/frames_manifest_full.json` + `ocr_frames_full.json` | 真实任务 **全部 14236 帧**清单与逐帧 OCR 文本 | `tests/nodes/test_subtitle_ocr/` | | `data/sample.reference.srt` | 真实参考字幕(5 条,含装饰行) | `tests/nodes/test_srt/` | | `data/alignment/*.mp4` + `*.reference.srt` | 真实音视频 + 人工校对参考字幕(大体积 mp4 已 gitignored) | `tests/shared/data/alignment/` | | `scripts/data/translate_eval/*.jsonl` | 翻译模型评测集(评测脚本专用) | `scripts/` | ## 真实模型集成测试 使用真实模型/服务 + 真实素材验证端到端行为;本地缺模型或素材时跳过, 有则必须执行,作为对替身单测的校准。 **外部环境状态的两种处理**(都不算代码缺陷): | 状态 | 处理 | | --- | --- | | 未配置密钥、账户余额/配额、限流(HTTP 401/402/403/429)、服务不可达 | `tests/shared/llm_service.py` 统一识别并跳过 | | GPU 显存不足 / CUDA OOM | `tests/shared/gpu_memory.py` 运行时探测并跳过 | `gpu_memory` 的两层防护:运行前按权重体量估算(实测 V2 权重 2.87GB → 峰值 增量约 4.1GB,系数 1.45)判断能否跑完;运行中若仍发生 CUDA OOM(临界卡上 的分配碎片化)则转为跳过。`fits_with_margin()` 进一步区分"显存充裕"与"临界": 充裕时走生产默认的分块路径,临界时退化为整段单次推理,避免整组跳过。 **权重版本**:测试只使用 **V2** 权重。V3 已全面停用(均改用 V2),即使留在 盘上也不得被测试使用;判据是 `preprocessor_config.json` 的 `feature_size` (V2=80,V3=128),不依赖目录名。 | 模块目录 | 覆盖内容 | | --- | --- | | `tests/nodes/test_whisper/` | 真实 faster-whisper **V2** 模型端到端转写 | | `tests/nodes/test_vlm/` | 真实 Ollama glm-ocr(有文字帧 + 无文字帧) | | `tests/nodes/test_subtitle_ocr/` | 真实 14236 帧清单重组装一致性 | | `tests/nodes/test_llm/` | 真实 LLM 翻译(专名不硬译) | | `tests/nodes/test_llm_filter/` | 真实 OCR 输出规则层回归(保留 863 / 删除 803 基线) | | `tests/nodes/test_subtitle_correction/` | 真实 LLM 误听泛化 | | `tests/shared/test_alignment/` | 真实语音转写 vs 人工参考字幕的时间对齐量化 | ## 功能模块覆盖清单 规则要求**追求功能性代码覆盖率 100%、每个独立功能模块必须被测试覆盖** (不追求行覆盖率 100%,大模块内辅助函数由主功能用例顺带覆盖即可, 完整表述见 [../AGENTS.md](../AGENTS.md#功能覆盖优先不追求代码覆盖率))。 本表是新增/修改模块时的核对清单:**新增独立功能模块必须同时补测试并更新本表**。 | 模块 | 职责 | 覆盖测试 | 状态 | | --- | --- | --- | --- | | `nodes/echo.py` | 示例回显节点 | `tests/nodes/test_echo/` | 已覆盖 | | `nodes/ffmpeg.py` | 提音与 ffmpeg 定位 | `tests/nodes/test_ffmpeg/`(真实视频) | 已覆盖 | | `nodes/whisper.py` | 转写、分块、模型解析、清洗入口 | `tests/nodes/test_whisper/`(含真实模型)+ `tests/shared/test_alignment/` | 已覆盖 | | `nodes/llm.py` | LLM 翻译节点 | `tests/nodes/test_llm/`(含真实 LLM) | 已覆盖 | | `nodes/ass.py` | SRT → VR 双目 ASS | `tests/nodes/test_ass/` | 已覆盖 | | `nodes/vlm.py` | 单帧 OCR(Ollama) | `tests/nodes/test_vlm/`(含真实模型) | 已覆盖 | | `nodes/frame_extract.py` | 抽帧与 crop | `tests/nodes/test_frame_extract/`(真实视频 + 帧号边界) | 已覆盖 | | `nodes/subtitle_ocr.py` | 逐帧 OCR → 汇总 SRT、断点/暂停 | `tests/nodes/test_subtitle_ocr/`(14236 帧) | 已覆盖 | | `nodes/adaptive_pool.py` | 自适应并发线程池 | `tests/nodes/test_adaptive_pool/` | 已覆盖 | | `nodes/llm_filter.py` | 字幕规则/LLM 两级过滤 | `tests/nodes/test_llm_filter/` | 已覆盖 | | `nodes/subtitle_cleanup.py` | 幻觉与短呻吟清洗 | `tests/nodes/test_subtitle_cleanup/` | 已覆盖 | | `nodes/subtitle_correction.py` | 字幕领域纠错 | `tests/nodes/test_subtitle_correction/` | 已覆盖 | | `nodes/proper_nouns.py` | 专名/隐语规则表 | `tests/nodes/test_proper_nouns/` | 已覆盖 | | `nodes/srt.py` | SRT 解析/序列化/时间戳 | `tests/nodes/test_srt/` | 已覆盖 | | `nodes/vad_profiler.py` | VAD 信号分析与评分 | `tests/nodes/test_vad_profiler/` | 已覆盖 | | `wov_app/registry.py` | 节点注册表(唯一调用入口) | `tests/app/test_registry/` | 已覆盖 | | `wov_app/scheduler.py` | DAG 调度与断点续跑 | `tests/app/test_scheduler/` | 已覆盖 | | `wov_app/batch.py` | 文件夹批量引擎 | `tests/app/test_batch/` | 已覆盖 | | `wov_app/db.py` | SQLite Repository | `tests/app/test_db/` | 已覆盖 | | `wov_app/main.py` | 应用装配与生命周期 | `tests/app/test_main/` | 已覆盖 | | `wov_app/routers/apps.py` | 用户端 API | `tests/app/test_routers/test_apps_api.py` | 已覆盖 | | `wov_app/routers/workflows.py` | 管理端工作流 API | `tests/app/test_routers/test_workflows_api.py` | 已覆盖 | | `wov_app/routers/batch.py` | 批量任务 API | `tests/app/test_routers/test_batch_api.py` | 已覆盖 | | `wov_app/seed.py` | 内置工作流加载 | `tests/app/test_seed/` | 已覆盖 | | `wov_app/maintenance.py` | 孤儿数据清理 | `tests/app/test_maintenance/` | 已覆盖 | | `wov_app/storage.py` | 原子复制 | `tests/app/test_storage/` | 已覆盖 | | `wov_app/config.py` | 路径与环境配置 | `tests/app/test_config/`(子进程验证真实解析) | 已覆盖 | | `wov_app/logging.py` | 日志装配 | `tests/app/test_logging/` | 已覆盖 | | `wov_app/schemas.py` | 请求模型(Pydantic) | `tests/app/test_main/`(schema 用例) | 已覆盖 | | `wov_sdk/models.py` | 协议数据模型 | `tests/sdk/test_models/` | 已覆盖 | | `web/assets/crop.js` | 框选几何换算 | `tests/web/test_crop/`(真实 node 执行) | 已覆盖 | | `tests/shared/srt_entries.py` | 按秒解析 SRT(测试公共设施) | `tests/shared/test_srt_entries/` | 已覆盖 | | `tests/shared/realdata_contract.py` | 真实数据契约与对齐量化 | `tests/shared/test_alignment/` | 已覆盖 | | `tests/shared/env_isolation.py` | 环境/临时目录隔离 | 被 `tests/app/test_config` 等间接覆盖 | 已覆盖(间接) | 核对方式(查看某模块是否真被执行): ```bash uv run pytest tests/ -q --cov=wov_app --cov-report=term-missing ``` ## 重构历史(已完成) 按新规则对 `tests/` 做了一次完整重写:旧平铺测试(40 个文件、约 10900 行) 已删除,旧内容仅作为**参考与测试数据来源**;现有测试全部按模块目录重写。 重构期间发现并修复的真实缺陷: | 缺陷 | 发现方式 | 修复 | | --- | --- | --- | | `parse_srt` 在相邻条目缺少空行时把下一条时间轴吞进正文(静默错位) | `tests/nodes/test_srt/` 的红灯用例 | 正文行遇时间戳行即报错(`nodes/srt.py`) | | `scheduler._file_size` 只捕获 `OSError`,含 `\\x00` 的产物 URI 抛 `ValueError` 导致任务误判失败 | `tests/app/test_batch/` 端到端用例 | 同时捕获 `ValueError`(`src/wov_app/scheduler.py`) | | 生产节点 `subtitle_correction` 依赖测试包 `tests.realdata_contract` 解析 SRT | 模块化拆分时发现 | 改用生产模块 `nodes/srt.py` 的严格解析器 | ## 相关文档 - 测试规则(模块边界、三段结构、功能覆盖、TDD):[../AGENTS.md](../AGENTS.md#测试规则) - 缺陷回归要求与验收标准:[代码审查问题跟踪.md](./代码审查问题跟踪.md) - 模型/参数选择的实验依据:[decisions.md](./decisions.md)