Files
vrsub/tests/web/test_crop/test_crop_js.py
T
cat-shark 8a715a8064 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。
2026-09-13 15:40:56 +08:00

181 lines
6.5 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.
"""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)