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
+332
View File
@@ -0,0 +1,332 @@
"""src/wov_app/db.py 的模块级测试(数据 → 测试过程 → 验证结果)。
被测模块:`src/wov_app/db.py`SQLite Repository:工作流/版本/任务/产物/
批量任务),可独立调用。每个用例在临时目录创建独立数据库文件(真实 SQLite),
不依赖全局 conftest。
"""
from __future__ import annotations
from pathlib import Path
import pytest
from wov_app.db import Database
@pytest.fixture()
def db(tmp_path: Path) -> Database:
"""每个用例一个独立 SQLite 库(真实文件,非内存桩)。"""
return Database(tmp_path / "wov.db")
@pytest.fixture()
def db_with_workflow(db: Database) -> Database:
"""已建好工作流(含 v1 版本)的库:任务表对 workflow_id 有外键约束。"""
db.upsert_workflow(_workflow("wf"))
db.create_workflow_version("wf", 1, {"nodes": []})
return db
def _workflow(workflow_id: str = "wf", name: str = "流程") -> dict:
"""构造真实工作流记录字段。"""
return {"id": workflow_id, "name": name, "description": ""}
def _run(run_id: str = "run-1", **overrides) -> dict:
"""构造真实任务记录字段(默认 upload 来源、QUEUED 状态)。"""
record = {
"id": run_id,
"workflow_id": "wf",
"workflow_version": 1,
"status": "QUEUED",
"current_node_id": None,
"progress": 0.0,
"error": None,
"input_uri": None,
"param_overrides": None,
"source": "upload",
"created_at": "2026-09-01T00:00:00+00:00",
"updated_at": "2026-09-01T00:00:00+00:00",
}
record.update(overrides)
return record
# ---------------------------------------------------------------------------
# 工作流与版本
# ---------------------------------------------------------------------------
def test_upsert_and_get_workflow(db: Database) -> None:
"""工作流写入后可读回,重复写入同 ID 覆盖而不报错。"""
# 数据:一条工作流记录。
db.upsert_workflow(_workflow("wf-1", "初版"))
# 测试过程
stored = db.get_workflow("wf-1")
db.upsert_workflow(_workflow("wf-1", "改名"))
renamed = db.get_workflow("wf-1")
# 验证结果
assert stored["name"] == "初版"
assert renamed["name"] == "改名"
def test_list_and_delete_workflow(db: Database) -> None:
"""列出全部工作流;删除后不再出现在列表与查询中。"""
# 数据:两条工作流。
db.upsert_workflow(_workflow("wf-1"))
db.upsert_workflow(_workflow("wf-2"))
# 测试过程
before = {w["id"] for w in db.list_workflows()}
db.delete_workflow("wf-1")
after = {w["id"] for w in db.list_workflows()}
# 验证结果
assert before == {"wf-1", "wf-2"}
assert after == {"wf-2"}
assert db.get_workflow("wf-1") is None
def test_workflow_versions_and_latest(db: Database) -> None:
"""版本按序保存,latest 返回最高版本,可按版本号精确读取。"""
# 数据:同一工作流的 v1 与 v2 定义。
db.upsert_workflow(_workflow("wf-1"))
db.create_workflow_version("wf-1", 1, {"nodes": [{"id": "a"}]})
db.create_workflow_version("wf-1", 2, {"nodes": [{"id": "a"}, {"id": "b"}]})
# 测试过程
latest = db.get_latest_workflow_version("wf-1")
first = db.get_workflow_version("wf-1", 1)
versions = db.list_workflow_versions("wf-1")
# 验证结果
assert latest["version"] == 2
assert len(latest["definition"]["nodes"]) == 2
assert first["definition"]["nodes"] == [{"id": "a"}]
assert [v["version"] for v in versions] == [2, 1]
def test_workflow_version_missing_returns_none(db: Database) -> None:
"""不存在的工作流/版本返回 None(不做隐式创建)。"""
# 数据:空库。
# 测试过程与验证结果
assert db.get_latest_workflow_version("nope") is None
assert db.get_workflow_version("nope", 1) is None
# ---------------------------------------------------------------------------
# 任务:创建、读写、param_overrides
# ---------------------------------------------------------------------------
def test_create_and_get_run_with_overrides(db_with_workflow: Database) -> None:
"""任务创建后可读回,param_overrides 以 JSON 存储并解析回字典。"""
# 数据:带参数覆盖的 upload 任务。
db_with_workflow.create_run(_run("r1", param_overrides={"frame-extract": {"crop": [0, 0.75, 1, 0.25]}}))
# 测试过程
stored = db_with_workflow.get_run("r1")
# 验证结果
assert stored["status"] == "QUEUED"
assert stored["source"] == "upload"
assert stored["param_overrides"] == {"frame-extract": {"crop": [0, 0.75, 1, 0.25]}}
def test_update_run_fields(db_with_workflow: Database) -> None:
"""update_run 按字段更新(状态/进度/错误)。"""
# 数据:一条任务。
db_with_workflow.create_run(_run("r1"))
# 测试过程
db_with_workflow.update_run("r1", status="RUNNING", progress=0.5, error=None, updated_at="t2")
stored = db_with_workflow.get_run("r1")
# 验证结果
assert stored["status"] == "RUNNING"
assert stored["progress"] == 0.5
def test_list_runs_orders_by_created_at_desc(db_with_workflow: Database) -> None:
"""任务列表按创建时间倒序返回(新的在前)。"""
# 数据:三条不同创建时间的任务。
db_with_workflow.create_run(_run("old", created_at="2026-09-01T00:00:00+00:00"))
db_with_workflow.create_run(_run("mid", created_at="2026-09-02T00:00:00+00:00"))
db_with_workflow.create_run(_run("new", created_at="2026-09-03T00:00:00+00:00"))
# 测试过程
ids = [r["id"] for r in db_with_workflow.list_runs()]
# 验证结果
assert ids == ["new", "mid", "old"]
def test_delete_run_removes_record_and_artifacts(db_with_workflow: Database) -> None:
"""删除任务同时清理其产物记录。"""
# 数据:任务 + 一条产物。
db_with_workflow.create_run(_run("r1"))
db_with_workflow.create_artifact({
"run_id": "r1", "node_id": "a", "name": "a.data_uri", "uri": "/tmp/x", "kind": "file",
})
# 测试过程
db_with_workflow.delete_run("r1")
# 验证结果
assert db_with_workflow.get_run("r1") is None
assert db_with_workflow.list_artifacts("r1") == []
# ---------------------------------------------------------------------------
# 调度用查询:只有 QUEUED 会被拾起
# ---------------------------------------------------------------------------
def test_next_queued_run_returns_oldest_queued(db_with_workflow: Database) -> None:
"""按创建时间返回最早的 QUEUED 任务。"""
# 数据:一条较早的 QUEUED。
db_with_workflow.create_run(_run("a", created_at="2026-09-01T00:00:00+00:00"))
# 测试过程
picked = db_with_workflow.next_queued_run()
# 验证结果
assert picked["id"] == "a"
def test_next_queued_run_skips_paused(db_with_workflow: Database) -> None:
"""PAUSED 不被拾起(必须显式 resume;修复"点击暂停反而开始任务"回归)。"""
# 数据:一条 PAUSED(更早)+ 一条 QUEUED(更晚)。
db_with_workflow.create_run(_run("paused", status="PAUSED", created_at="2026-09-01T00:00:00+00:00"))
db_with_workflow.create_run(_run("queued", created_at="2026-09-02T00:00:00+00:00"))
# 测试过程
picked = db_with_workflow.next_queued_run()
# 验证结果:取的是 QUEUED 那条。
assert picked["id"] == "queued"
def test_next_queued_run_skips_batch_source(db_with_workflow: Database) -> None:
"""source=batch 的任务由批量引擎执行,主调度器不拾起(存储目录不同)。"""
# 数据:一条 batch 来源的 QUEUED。
db_with_workflow.create_run(_run("batch-1", source="batch"))
# 测试过程与验证结果
assert db_with_workflow.next_queued_run() is None
def test_pause_and_resume_run(db_with_workflow: Database) -> None:
"""暂停置 PAUSED,继续置回 QUEUED(等待调度器断点续跑)。"""
# 数据:一条 QUEUED。
db_with_workflow.create_run(_run("r1"))
# 测试过程
db_with_workflow.pause_run("r1", "t2")
paused = db_with_workflow.get_run("r1")["status"]
db_with_workflow.resume_run("r1", "t3")
resumed = db_with_workflow.get_run("r1")["status"]
# 验证结果
assert paused == "PAUSED"
assert resumed == "QUEUED"
def test_recover_interrupted_runs_requeues_running_only(db_with_workflow: Database) -> None:
"""重启恢复:RUNNING → QUEUEDPAUSED 保持不变。"""
# 数据:RUNNING 与 PAUSED 各一条。
db_with_workflow.create_run(_run("running", status="RUNNING"))
db_with_workflow.create_run(_run("paused", status="PAUSED"))
# 测试过程
count = db_with_workflow.recover_interrupted_runs("t2")
# 验证结果:只恢复 1 条,PAUSED 不变。
assert count == 1
assert db_with_workflow.get_run("running")["status"] == "QUEUED"
assert db_with_workflow.get_run("paused")["status"] == "PAUSED"
def test_recover_interrupted_batch_jobs_requeues_running(db_with_workflow: Database) -> None:
"""重启恢复:RUNNING 的批量任务 → QUEUED(否则永久无人拾起)。"""
# 数据:一条 RUNNING 批量任务(批量明细表对 run 有外键,先建任务记录)。
db_with_workflow.create_run(_run("bv-run-1"))
db_with_workflow.create_batch_job({
"id": "job-1", "folder_path": "/videos", "workflow_id": "wf", "recursive": False,
"status": "RUNNING", "created_at": "t1", "updated_at": "t1",
})
# 测试过程
count = db_with_workflow.recover_interrupted_batch_jobs("t2")
# 验证结果
assert count == 1
assert db_with_workflow.get_batch_job("job-1")["status"] == "QUEUED"
# ---------------------------------------------------------------------------
# 产物与断点恢复
# ---------------------------------------------------------------------------
def test_create_and_get_artifact(db_with_workflow: Database) -> None:
"""产物按 run + name 记录,可按名精确读取。"""
# 数据:一条任务 + 一条产物(产物对 run 有外键)。
db_with_workflow.create_run(_run("r1"))
db_with_workflow.create_artifact({
"run_id": "r1", "node_id": "ocr", "name": "ocr.srt_uri",
"uri": "/tmp/out/subtitle.srt", "kind": "file",
})
# 测试过程
stored = db_with_workflow.get_artifact("r1", "ocr.srt_uri")
# 验证结果
assert stored["uri"] == "/tmp/out/subtitle.srt"
assert db_with_workflow.get_artifact("r1", "missing") is None
def test_restore_run_outputs_strips_node_prefix(db_with_workflow: Database) -> None:
"""恢复产物时剥去"节点ID."前缀,还原为 {输出名: URI}(断点续跑依赖)。"""
# 数据:两个节点的产物。
db_with_workflow.create_run(_run("r1"))
db_with_workflow.create_artifact({"run_id": "r1", "node_id": "extract", "name": "extract.audio_uri", "uri": "/a.wav", "kind": "file"})
db_with_workflow.create_artifact({"run_id": "r1", "node_id": "asr", "name": "asr.srt_uri", "uri": "/a.srt", "kind": "file"})
# 测试过程
outputs = db_with_workflow.restore_run_outputs("r1")
# 验证结果
assert outputs == {
"extract": {"audio_uri": "/a.wav"},
"asr": {"srt_uri": "/a.srt"},
}
def test_reset_run_clears_state_and_artifacts(db_with_workflow: Database) -> None:
"""reset_run 清空产物与错误、回到 QUEUED(供失败任务重跑)。"""
# 数据:一条 FAILED 任务带产物与错误。
db_with_workflow.create_run(_run("r1", status="FAILED", error="boom"))
db_with_workflow.create_artifact({"run_id": "r1", "node_id": "a", "name": "a.x", "uri": "/x", "kind": "file"})
# 测试过程
db_with_workflow.reset_run("r1", "t2")
stored = db_with_workflow.get_run("r1")
# 验证结果
assert stored["status"] == "QUEUED"
assert stored["error"] is None
assert db_with_workflow.list_artifacts("r1") == []
def test_list_run_ids(db_with_workflow: Database) -> None:
"""列出全部任务 ID(供孤儿清理比对文件系统)。"""
# 数据:两条任务。
db_with_workflow.create_run(_run("r1"))
db_with_workflow.create_run(_run("r2"))
# 测试过程与验证结果
assert sorted(db_with_workflow.list_run_ids()) == ["r1", "r2"]