test: 按模块重写测试代码,删除旧平铺结构

按"测试规则"重写 tests/:一个模块一个目录、用例按数据→过程→验证三段书写、
不保留全局 conftest.py、测试过程只调用真实生产代码。

结构(73 个文件、30 个模块目录、477 用例):
- tests/nodes/  15 个模块目录(srt/whisper/ass/ffmpeg/frame_extract/vlm/
  subtitle_ocr/llm/llm_filter/subtitle_cleanup/subtitle_correction/
  proper_nouns/adaptive_pool/vad_profiler/echo);
- tests/app/    11 个模块目录(db/scheduler/batch/maintenance/registry/seed/
  storage/config/logging/main/routers 三组 API);
- tests/sdk/test_models、tests/web/test_crop、tests/shared(公共设施)。

测试数据随模块目录入库(tests/**/data/),删除根级 testdata/;.gitignore
的 data/ 改为 /data/,否则会连带忽略 tests/**/data/ 导致测试数据无法入库。

顺带发现并修复三个真实缺陷:
- nodes/srt.py:相邻条目缺少空行时把下一条时间轴吞进正文(静默错位),
  改为正文行遇时间戳行即报错;
- src/wov_app/scheduler.py:_file_size 只捕获 OSError,含 \x00 的产物 URI
  抛 ValueError 导致任务误判失败,改为同时捕获;
- nodes/subtitle_correction.py:生产代码依赖测试包解析 SRT,
  改用生产模块 nodes/srt.py。

真实模型/服务集成测试按外部状态跳过:新增 tests/shared/gpu_memory.py
(运行时探测显存、CUDA OOM 转跳过)与 tests/shared/llm_service.py
(无 Key / 余额 / 限流转跳过)。全量 477 passed。
This commit is contained in:
2026-09-13 15:40:56 +08:00
parent 966f3e6b4b
commit 8a715a8064
139 changed files with 20810 additions and 10733 deletions
View File
View File
+180
View File
@@ -0,0 +1,180 @@
"""web/assets/crop.js 的模块级测试(数据 → 测试过程 → 验证结果)。
被测模块:`web/assets/crop.js`(前端框选矩形 ↔ crop 比例的纯函数,含
object-fit: contain 的 letterbox 处理),被 OCR 框选面板复用。
实现方式:在 Node.js 子进程中加载真实 JS 模块并调用真实函数(不重写逻辑),
验证换算结果;环境无 node 时跳过(保持跨平台可运行)。
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import pytest
# 仓库根与被测脚本。
WORKSPACE = Path(__file__).resolve().parents[3]
CROP_JS = WORKSPACE / "web" / "assets" / "crop.js"
# 用真实 JS 引擎执行调用的封装:把用例数据交给 crop.js 的真实实现。
_CALL_SCRIPT = """
const crop = require(process.argv[1]);
const payload = JSON.parse(process.argv[2]);
const fn = crop[payload.fn];
const result = fn(...payload.args);
process.stdout.write(JSON.stringify(result));
"""
def _run(fn: str, *args) -> object:
"""在 node 中调用 crop.js 的真实函数并返回解析后的结果。"""
node = shutil.which("node")
if node is None:
pytest.skip("环境没有 node,跳过 JS 模块测试")
completed = subprocess.run(
[node, "-e", _CALL_SCRIPT, str(CROP_JS), json.dumps({"fn": fn, "args": list(args)})],
capture_output=True,
text=True,
)
assert completed.returncode == 0, completed.stderr
return json.loads(completed.stdout)
# ---------------------------------------------------------------------------
# videoDisplayRectcontain 显示区域
# ---------------------------------------------------------------------------
def test_display_rect_matches_box_when_aspect_equal() -> None:
"""视频与容器宽高比一致时铺满容器,无留边。"""
# 数据:1920x1080 视频放进 960x540 容器。
# 测试过程
rect = _run("videoDisplayRect", 1920, 1080, 960, 540)
# 验证结果
assert rect == {"x": 0, "y": 0, "w": 960, "h": 540}
def test_display_rect_letterboxes_on_wide_container() -> None:
"""容器比视频更宽时左右留边(横屏视频放进方形容器)。"""
# 数据:1920x1080 视频放进 1000x1000 容器。
# 测试过程
rect = _run("videoDisplayRect", 1920, 1080, 1000, 1000)
# 验证结果:宽 1000、高 562.5,上下居中留边。
assert rect["w"] == pytest.approx(1000)
assert rect["h"] == pytest.approx(562.5)
assert rect["x"] == pytest.approx(0)
assert rect["y"] == pytest.approx((1000 - 562.5) / 2)
def test_display_rect_letterboxes_on_tall_container() -> None:
"""容器比视频更高时上下留边。"""
# 数据:1000x1000 视频放进 500x1000 容器。
# 测试过程
rect = _run("videoDisplayRect", 1000, 1000, 500, 1000)
# 验证结果:宽高均为 500,水平居中。
assert rect["w"] == pytest.approx(500)
assert rect["h"] == pytest.approx(500)
assert rect["x"] == pytest.approx(0)
assert rect["y"] == pytest.approx(250)
# ---------------------------------------------------------------------------
# rectToCrop:框选 → 比例
# ---------------------------------------------------------------------------
def test_rect_to_crop_full_frame() -> None:
"""框选整个显示区域得到 [0,0,1,1]。"""
# 数据:显示区为整个容器。
# 测试过程
crop = _run("rectToCrop", {"x": 0, "y": 0, "w": 960, "h": 540}, 1920, 1080, 960, 540)
# 验证结果
assert crop == [0, 0, 1, 1]
def test_rect_to_crop_bottom_quarter() -> None:
"""框选底部 1/4 得到默认字幕区比例 [0,0.75,1,0.25]。"""
# 数据:底部四分之一的显示坐标。
# 测试过程
crop = _run("rectToCrop", {"x": 0, "y": 405, "w": 960, "h": 135}, 1920, 1080, 960, 540)
# 验证结果
assert crop == [0, 0.75, 1, 0.25]
def test_rect_to_crop_accounts_for_letterbox_offset() -> None:
"""存在留边时,框选坐标先减去显示区偏移(否则比例整体偏移)。"""
# 数据:1920x1080 放 1000x1000 容器(上下各留 218.75),框选显示区上半。
rect = {"x": 0, "y": 218.75, "w": 1000, "h": 281.25}
# 测试过程
crop = _run("rectToCrop", rect, 1920, 1080, 1000, 1000)
# 验证结果:y 从 0 开始、高度占一半。
assert crop[0] == pytest.approx(0)
assert crop[1] == pytest.approx(0)
assert crop[2] == pytest.approx(1)
assert crop[3] == pytest.approx(0.5)
def test_rect_to_crop_clamps_out_of_range_to_0_1() -> None:
"""越界框选被钳制到 0~1(不产生非法 crop 参数)。"""
# 数据:超出显示区的矩形(含负坐标)。
# 测试过程
crop = _run("rectToCrop", {"x": -500, "y": -500, "w": 5000, "h": 5000}, 1920, 1080, 960, 540)
# 验证结果:全部落在 [0,1]。
assert all(0 <= value <= 1 for value in crop)
assert crop[0] == 0 and crop[1] == 0
assert crop[2] == 1 and crop[3] == 1
def test_rect_to_crop_rounds_to_three_decimals() -> None:
"""比例保留 3 位小数(与帧清单时间戳精度约定一致)。"""
# 数据:会产生长小数的框选。
# 测试过程
crop = _run("rectToCrop", {"x": 1, "y": 2, "w": 333, "h": 77}, 1920, 1080, 960, 540)
# 验证结果:每个值最多 3 位小数。
for value in crop:
assert round(value, 3) == value
# ---------------------------------------------------------------------------
# cropToRect:比例 → 框选(回显)
# ---------------------------------------------------------------------------
def test_crop_to_rect_is_inverse_of_rect_to_crop() -> None:
"""比例转回矩形与原始框选一致(回显正确,误差在 3 位小数内)。"""
# 数据:一个中间框选。
rect = {"x": 120, "y": 300, "w": 480, "h": 135}
# 测试过程
crop = _run("rectToCrop", rect, 1920, 1080, 960, 540)
restored = _run("cropToRect", crop, 1920, 1080, 960, 540)
# 验证结果:各分量误差极小。
for key in ("x", "y", "w", "h"):
assert restored[key] == pytest.approx(rect[key], abs=1.0)
def test_crop_to_rect_bottom_quarter() -> None:
"""默认 crop 比例回显为底部四分之一矩形。"""
# 数据:默认 crop。
# 测试过程
rect = _run("cropToRect", [0, 0.75, 1, 0.25], 1920, 1080, 960, 540)
# 验证结果
assert rect["x"] == pytest.approx(0)
assert rect["y"] == pytest.approx(405)
assert rect["w"] == pytest.approx(960)
assert rect["h"] == pytest.approx(135)