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,192 @@
|
||||
"""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"]
|
||||
Reference in New Issue
Block a user