feat: VRSub 单体应用(WOV 单机版)初始提交
为视频生成 VR 双眼字幕的单体实现:FastAPI 后端、调度器与全部节点 (提音/转写/翻译/ASS/抽帧/OCR/LLM 过滤)在单进程内运行。 - 节点协议(wov_sdk 数据模型)与分布式版保持一致,预留回退桥梁 - 工作流即数据:DAG 存于 workflows/*.json,模型/链路改动只改数据 - 调度器:拓扑顺序执行、断点续跑(产物重建)、任务暂停/继续 - 抽帧按帧间隔(select 按帧号精确取帧),VLM OCR 与 LLM 过滤使用 自适应线程池弹性并发,并打印数据处理速度进度日志 - 100% 行覆盖率(pytest --cov-fail-under=100)
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
"""数据库层单元测试。
|
||||
|
||||
直接对 Database 方法调用真实 SQLite 路径,覆盖工作流、版本、任务与产物的
|
||||
增删改查。节点注册表已改为进程内内存态,不再落库。
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from wov_app.db import Database
|
||||
|
||||
|
||||
def test_workflow_crud(tmp_path) -> None:
|
||||
"""验证工作流概要的插入、发布标记更新与删除。"""
|
||||
db = Database(tmp_path / "wov.db")
|
||||
workflow = {
|
||||
"id": "demo",
|
||||
"name": "Demo",
|
||||
"description": "desc",
|
||||
"published": 0,
|
||||
"latest_version": 0,
|
||||
}
|
||||
db.upsert_workflow(workflow)
|
||||
assert db.get_workflow("demo")["name"] == "Demo"
|
||||
assert [item["id"] for item in db.list_workflows()] == ["demo"]
|
||||
|
||||
db.upsert_workflow({**workflow, "published": 1, "latest_version": 1})
|
||||
assert db.get_workflow("demo")["published"] == 1
|
||||
|
||||
db.delete_workflow("demo")
|
||||
assert db.get_workflow("demo") is None
|
||||
|
||||
|
||||
def test_workflow_versions(tmp_path) -> None:
|
||||
"""验证工作流版本的写入、最新版本查询与列表。"""
|
||||
db = Database(tmp_path / "wov.db")
|
||||
db.upsert_workflow(
|
||||
{"id": "demo", "name": "Demo", "published": 1, "latest_version": 2}
|
||||
)
|
||||
definition = {"name": "Demo", "version": 1, "nodes": [], "edges": []}
|
||||
db.create_workflow_version("demo", 1, definition)
|
||||
db.create_workflow_version("demo", 2, {**definition, "version": 2})
|
||||
|
||||
latest = db.get_latest_workflow_version("demo")
|
||||
assert latest["version"] == 2
|
||||
assert latest["definition"]["version"] == 2
|
||||
|
||||
version = db.get_workflow_version("demo", 1)
|
||||
assert version["version"] == 1
|
||||
assert db.get_workflow_version("demo", 99) is None
|
||||
assert len(db.list_workflow_versions("demo")) == 2
|
||||
|
||||
empty_db = Database(tmp_path / "empty.db")
|
||||
assert empty_db.get_latest_workflow_version("missing") is None
|
||||
|
||||
|
||||
def test_run_and_artifact_crud(tmp_path) -> None:
|
||||
"""验证任务与产物的创建、查询、更新与删除。"""
|
||||
db = Database(tmp_path / "wov.db")
|
||||
db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1})
|
||||
now = "2026-01-01T00:00:00+00:00"
|
||||
db.create_run(
|
||||
{
|
||||
"id": "run_1",
|
||||
"workflow_id": "demo",
|
||||
"workflow_version": 1,
|
||||
"status": "QUEUED",
|
||||
"progress": 0,
|
||||
"input_uri": "in.txt",
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
)
|
||||
assert db.get_run("run_1")["status"] == "QUEUED"
|
||||
assert db.next_queued_run()["id"] == "run_1"
|
||||
|
||||
db.update_run("run_1", status="RUNNING", progress=0.5, updated_at=now)
|
||||
db.update_run("run_1")
|
||||
assert db.get_run("run_1")["status"] == "RUNNING"
|
||||
assert db.get_run("run_1")["progress"] == 0.5
|
||||
assert db.next_queued_run() is None
|
||||
assert len(db.list_runs()) == 1
|
||||
|
||||
db.create_artifact(
|
||||
{
|
||||
"run_id": "run_1",
|
||||
"node_id": "echo",
|
||||
"name": "result",
|
||||
"uri": "out.txt",
|
||||
"mime_type": "text/plain",
|
||||
"size": 3,
|
||||
}
|
||||
)
|
||||
assert db.get_artifact("run_1", "result")["uri"] == "out.txt"
|
||||
assert db.get_artifact("run_1", "missing") is None
|
||||
assert len(db.list_artifacts("run_1")) == 1
|
||||
|
||||
db.delete_run_artifacts("run_1")
|
||||
assert db.list_artifacts("run_1") == []
|
||||
|
||||
|
||||
def test_reset_run_clears_error_and_artifacts(tmp_path) -> None:
|
||||
"""验证 reset_run 会把失败任务恢复到排队状态并清空旧产物。"""
|
||||
db = Database(tmp_path / "wov.db")
|
||||
db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1})
|
||||
now = "2026-01-01T00:00:00+00:00"
|
||||
db.create_run(
|
||||
{
|
||||
"id": "run_1",
|
||||
"workflow_id": "demo",
|
||||
"workflow_version": 1,
|
||||
"status": "FAILED",
|
||||
"progress": 0.75,
|
||||
"current_node_id": "translate",
|
||||
"error": "timed out",
|
||||
"input_uri": "in.txt",
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
)
|
||||
db.create_artifact(
|
||||
{
|
||||
"run_id": "run_1",
|
||||
"node_id": "asr",
|
||||
"name": "asr.srt_uri",
|
||||
"uri": "out.srt",
|
||||
"mime_type": "application/x-subrip",
|
||||
"size": 3,
|
||||
}
|
||||
)
|
||||
|
||||
db.reset_run("run_1", "2026-01-02T00:00:00+00:00")
|
||||
|
||||
run = db.get_run("run_1")
|
||||
assert run["status"] == "QUEUED"
|
||||
assert run["progress"] == 0
|
||||
assert run["current_node_id"] is None
|
||||
assert run["error"] is None
|
||||
assert run["updated_at"] == "2026-01-02T00:00:00+00:00"
|
||||
assert run["created_at"] == now
|
||||
assert db.list_artifacts("run_1") == []
|
||||
|
||||
|
||||
def test_delete_run(tmp_path) -> None:
|
||||
"""验证 delete_run 会删除任务记录及其产物记录。"""
|
||||
db = Database(tmp_path / "wov.db")
|
||||
db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1})
|
||||
now = "2026-01-01T00:00:00+00:00"
|
||||
db.create_run(
|
||||
{
|
||||
"id": "run_1",
|
||||
"workflow_id": "demo",
|
||||
"workflow_version": 1,
|
||||
"status": "COMPLETED",
|
||||
"progress": 1,
|
||||
"input_uri": "in.txt",
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
)
|
||||
db.create_artifact(
|
||||
{
|
||||
"run_id": "run_1",
|
||||
"node_id": "asr",
|
||||
"name": "asr.srt_uri",
|
||||
"uri": "out.srt",
|
||||
"mime_type": "application/x-subrip",
|
||||
"size": 3,
|
||||
}
|
||||
)
|
||||
db.delete_run("run_1")
|
||||
assert db.get_run("run_1") is None
|
||||
assert db.list_artifacts("run_1") == []
|
||||
|
||||
|
||||
def test_list_run_ids(tmp_path) -> None:
|
||||
"""验证 list_run_ids 返回全部任务 ID,供孤儿清理对照使用。"""
|
||||
db = Database(tmp_path / "wov.db")
|
||||
db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1})
|
||||
now = "2026-01-01T00:00:00+00:00"
|
||||
assert db.list_run_ids() == []
|
||||
for run_id in ("run_a", "run_b"):
|
||||
db.create_run(
|
||||
{
|
||||
"id": run_id,
|
||||
"workflow_id": "demo",
|
||||
"workflow_version": 1,
|
||||
"status": "QUEUED",
|
||||
"progress": 0,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
)
|
||||
assert set(db.list_run_ids()) == {"run_a", "run_b"}
|
||||
|
||||
|
||||
def test_run_param_overrides_persist(tmp_path) -> None:
|
||||
"""验证 param_overrides 随任务持久化并可读回。"""
|
||||
db = Database(tmp_path / "wov.db")
|
||||
db.upsert_workflow({"id": "demo", "name": "D", "published": 1, "latest_version": 1})
|
||||
now = "2026-01-01T00:00:00+00:00"
|
||||
db.create_run(
|
||||
{
|
||||
"id": "run_ov",
|
||||
"workflow_id": "demo",
|
||||
"workflow_version": 1,
|
||||
"status": "QUEUED",
|
||||
"progress": 0,
|
||||
"param_overrides": {"extract": {"crop": [0, 0.5, 1, 0.5]}},
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
)
|
||||
run = db.get_run("run_ov")
|
||||
assert run["param_overrides"] == {"extract": {"crop": [0, 0.5, 1, 0.5]}}
|
||||
assert db.next_queued_run()["param_overrides"] == {"extract": {"crop": [0, 0.5, 1, 0.5]}}
|
||||
|
||||
|
||||
def test_db_migration_adds_param_overrides(tmp_path) -> None:
|
||||
"""旧库迁移:缺少 param_overrides 列的库打开后自动补列。"""
|
||||
import sqlite3
|
||||
|
||||
db_path = tmp_path / "old.db"
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute(
|
||||
"CREATE TABLE workflow_runs (id TEXT PRIMARY KEY, workflow_id TEXT NOT NULL,"
|
||||
" workflow_version INTEGER NOT NULL, status TEXT NOT NULL, current_node_id TEXT,"
|
||||
" progress REAL NOT NULL DEFAULT 0, error TEXT, input_uri TEXT,"
|
||||
" created_at TEXT NOT NULL, updated_at TEXT NOT NULL)"
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
Database(db_path)
|
||||
conn = sqlite3.connect(db_path)
|
||||
columns = [row[1] for row in conn.execute("PRAGMA table_info(workflow_runs)")]
|
||||
conn.close()
|
||||
assert "param_overrides" in columns
|
||||
|
||||
|
||||
def test_pause_resume_run(tmp_path) -> None:
|
||||
"""验证 pause_run/resume_run 的状态流转与 PAUSED 任务可被调度器取到。"""
|
||||
db = Database(tmp_path / "wov.db")
|
||||
db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1})
|
||||
now = "2026-01-01T00:00:00+00:00"
|
||||
db.create_run(
|
||||
{
|
||||
"id": "run_p",
|
||||
"workflow_id": "demo",
|
||||
"workflow_version": 1,
|
||||
"status": "QUEUED",
|
||||
"progress": 0,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
)
|
||||
db.pause_run("run_p", now)
|
||||
assert db.get_run("run_p")["status"] == "PAUSED"
|
||||
# PAUSED 任务会被 next_queued_run 取到(等待续跑)。
|
||||
assert db.next_queued_run()["id"] == "run_p"
|
||||
db.resume_run("run_p", now)
|
||||
assert db.get_run("run_p")["status"] == "QUEUED"
|
||||
assert db.next_queued_run()["id"] == "run_p"
|
||||
|
||||
|
||||
def test_restore_run_outputs(tmp_path) -> None:
|
||||
"""验证从产物重建节点输出(断点续跑的依据)。"""
|
||||
db = Database(tmp_path / "wov.db")
|
||||
db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1})
|
||||
now = "2026-01-01T00:00:00+00:00"
|
||||
db.create_run(
|
||||
{
|
||||
"id": "run_r",
|
||||
"workflow_id": "demo",
|
||||
"workflow_version": 1,
|
||||
"status": "PAUSED",
|
||||
"progress": 0,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
)
|
||||
db.create_artifact(
|
||||
{
|
||||
"run_id": "run_r",
|
||||
"node_id": "extract",
|
||||
"name": "frames_manifest",
|
||||
"uri": "frames.json",
|
||||
"mime_type": "application/json",
|
||||
"size": 1,
|
||||
}
|
||||
)
|
||||
assert db.restore_run_outputs("run_r") == {
|
||||
"extract": {"frames_manifest": "frames.json"}
|
||||
}
|
||||
assert db.restore_run_outputs("run_none") == {}
|
||||
Reference in New Issue
Block a user