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:
@@ -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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# videoDisplayRect:contain 显示区域
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user