Files
vrsub/tests/app/test_registry/test_registry.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

193 lines
6.5 KiB
Python

"""src/wov_app/registry.py 的模块级测试(数据 → 测试过程 → 验证结果)。
被测模块:`src/wov_app/registry.py`(节点注册表:节点调用的唯一入口),
可独立调用。注册表是进程内全局状态,用例通过保存/恢复快照隔离,不依赖
全局 conftest(原 tests/conftest.py 的 autouse 夹具已按规则取消)。
"""
from __future__ import annotations
from pathlib import Path
import pytest
from wov_app import registry
from wov_sdk.models import InvokeRequest, InvokeResponse, NodeManifest
# 仓库根目录(用于定位真实 manifests/)。
WORKSPACE = Path(__file__).resolve().parents[3]
@pytest.fixture(autouse=True)
def _restore_registry():
"""用例前清空注册表、用例后恢复快照:保证用例看到的是干净基线,
不受其他模块(如 main 生命周期 register_all)的注册结果影响。"""
snapshot = dict(registry._registry)
registry._registry.clear()
yield
registry._registry.clear()
registry._registry.update(snapshot)
def _manifest(node_id: str = "demo-node") -> NodeManifest:
"""构造一个最小合法清单(真实字段结构)。"""
return NodeManifest(
id=node_id,
name="演示节点",
version="0.1.0",
capability="demo",
command=["python", "-m", "demo"],
)
def _request(tmp_path: Path) -> InvokeRequest:
"""构造真实调用请求。"""
return InvokeRequest(
run_id="run-test",
node_instance_id="n1",
params={},
inputs={"text": "hi"},
output_dir=str(tmp_path),
)
# ---------------------------------------------------------------------------
# 注册与查询
# ---------------------------------------------------------------------------
def test_register_and_get_node() -> None:
"""注册后可查询到清单,未注册返回 None。"""
# 数据:一个清单 + 一个真实处理器。
registry.register(_manifest("echo-x"), lambda request: InvokeResponse(status="completed"))
# 测试过程
found = registry.get_node("echo-x")
missing = registry.get_node("not-registered")
# 验证结果
assert found is not None and found.id == "echo-x"
assert missing is None
def test_register_overwrites_same_id() -> None:
"""同一 ID 重复注册按后者覆盖(启动时 register_all 幂等)。"""
# 数据:同一 ID 注册两次,名称不同。
first = _manifest("node-x")
second = NodeManifest(
id="node-x", name="覆盖后", version="0.2.0", capability="demo",
command=["python", "-m", "demo"],
)
registry.register(first, lambda request: InvokeResponse(status="completed"))
registry.register(second, lambda request: InvokeResponse(status="completed"))
# 测试过程
found = registry.get_node("node-x")
# 验证结果
assert found.name == "覆盖后"
assert found.version == "0.2.0"
def test_register_rejects_invalid_manifest() -> None:
"""非法清单(空 ID)注册时校验失败并抛错。"""
# 数据:id 为空的清单。
invalid = NodeManifest(id="", name="x", version="1", capability="c", command=["python"])
# 测试过程与验证结果
with pytest.raises(ValueError):
registry.register(invalid, lambda request: InvokeResponse(status="completed"))
def test_register_all_registers_builtin_nodes() -> None:
"""register_all 从真实 manifests/ 注册全部内置节点(含关键节点)。"""
# 数据:真实仓库清单目录。
assert (WORKSPACE / "manifests").is_dir()
# 测试过程
registry.register_all()
ids = {manifest.id for manifest in registry.list_nodes()}
# 验证结果:字幕流水线所需的节点全部在册(ID 取自 manifests/*.json)。
for expected in (
"echo", "ffmpeg-extract", "faster-whisper", "llm-translate",
"vlm-ocr", "frame-extract", "subtitle-ocr", "llm-filter",
"subtitle-correction", "srt-to-dual-eye-ass",
):
assert expected in ids, f"内置节点未注册:{expected}"
def test_register_all_is_idempotent() -> None:
"""重复调用 register_all 不产生重复条目(按 ID 覆盖)。"""
# 数据:无。
# 测试过程
registry.register_all()
first = len(registry.list_nodes())
registry.register_all()
second = len(registry.list_nodes())
# 验证结果
assert first == second
# ---------------------------------------------------------------------------
# invoke:唯一调用入口
# ---------------------------------------------------------------------------
def test_invoke_routes_to_registered_handler(tmp_path: Path) -> None:
"""invoke 把请求路由到注册的处理器并原样返回其响应。"""
# 数据:记录收到的请求的处理器。
received: list[InvokeRequest] = []
def handler(request: InvokeRequest) -> InvokeResponse:
received.append(request)
return InvokeResponse(status="completed", outputs={"text": "ok"})
registry.register(_manifest("node-x"), handler)
# 测试过程
response = registry.invoke("node-x", _request(tmp_path))
# 验证结果:响应来自处理器,且请求原样传递。
assert response.status == "completed"
assert response.outputs == {"text": "ok"}
assert received[0].inputs == {"text": "hi"}
def test_invoke_raises_for_unregistered_node(tmp_path: Path) -> None:
"""未注册节点调用抛 ValueError(不静默返回空结果)。"""
# 数据:未注册的 node_id。
# 测试过程与验证结果
with pytest.raises(ValueError, match="not registered"):
registry.invoke("nope", _request(tmp_path))
def test_invoke_returns_failed_response_unchanged(tmp_path: Path) -> None:
"""处理器返回 failed 时原样透传(注册表不改变节点语义)。"""
# 数据:总是失败的处理器。
registry.register(
_manifest("node-x"),
lambda request: InvokeResponse(status="failed", error="boom"),
)
# 测试过程
response = registry.invoke("node-x", _request(tmp_path))
# 验证结果
assert response.status == "failed"
assert response.error == "boom"
def test_list_nodes_returns_sorted_by_id() -> None:
"""list_nodes 按节点 ID 排序返回(前端展示稳定)。"""
# 数据:乱序注册的节点。
for node_id in ("z-node", "a-node", "m-node"):
registry.register(_manifest(node_id), lambda request: InvokeResponse(status="completed"))
# 测试过程
ids = [m.id for m in registry.list_nodes()]
# 验证结果
assert ids == ["a-node", "m-node", "z-node"]