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,494 @@
|
||||
"""src/wov_app/scheduler.py 的模块级测试(数据 → 测试过程 → 验证结果)。
|
||||
|
||||
被测模块:`src/wov_app/scheduler.py`(DAG 拓扑调度、断点续跑、暂停语义),
|
||||
可独立调用。用例使用真实 SQLite、真实存储目录与真实节点(echo/echo 派生),
|
||||
仅对需要外部服务的节点不做测试。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from wov_app import registry
|
||||
from wov_app.db import Database
|
||||
from wov_app.scheduler import WorkflowScheduler, topological_sort
|
||||
from wov_sdk.models import WorkflowDefinition
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_registry():
|
||||
"""用例前清空注册表、用例后恢复快照:保证用例看到的是干净基线,
|
||||
不受其他模块(如 main 生命周期 register_all)的注册结果影响。"""
|
||||
snapshot = dict(registry._registry)
|
||||
registry._registry.clear()
|
||||
yield
|
||||
registry._registry.clear()
|
||||
registry._registry.update(snapshot)
|
||||
|
||||
|
||||
def _definition(nodes: list[dict], edges: list[dict], **extra) -> WorkflowDefinition:
|
||||
"""构造真实 WorkflowDefinition(节点 ID/类型/输入与边)。"""
|
||||
payload = {
|
||||
"name": "测试流程",
|
||||
"version": 1,
|
||||
"nodes": nodes,
|
||||
"edges": edges,
|
||||
"entry_inputs": {"video_uri": "file"},
|
||||
"final_outputs": extra.pop("final_outputs", {}),
|
||||
**extra,
|
||||
}
|
||||
return WorkflowDefinition.from_dict(payload)
|
||||
|
||||
|
||||
def _db_with_workflow(tmp_path: Path, definition: WorkflowDefinition, workflow_id: str = "wf") -> Database:
|
||||
"""建好工作流 + 版本记录的临时库(任务表有外键约束)。"""
|
||||
db = Database(tmp_path / "wov.db")
|
||||
db.upsert_workflow({"id": workflow_id, "name": "流程", "description": ""})
|
||||
db.create_workflow_version(workflow_id, 1, definition.to_dict())
|
||||
return db
|
||||
|
||||
|
||||
def _run(run_id: str, input_uri: str, **overrides) -> dict:
|
||||
"""构造真实任务记录。"""
|
||||
record = {
|
||||
"id": run_id, "workflow_id": "wf", "workflow_version": 1, "status": "QUEUED",
|
||||
"current_node_id": None, "progress": 0.0, "error": None, "input_uri": input_uri,
|
||||
"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 _scheduler(db: Database, storage: Path) -> WorkflowScheduler:
|
||||
"""构造调度器(不启动后台线程,直接调用 execute_run)。"""
|
||||
return WorkflowScheduler(db, storage, interval_seconds=999)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 拓扑排序
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_topological_sort_linear_chain() -> None:
|
||||
"""线性链按依赖顺序返回。"""
|
||||
# 数据:a → b → c。
|
||||
definition = _definition(
|
||||
nodes=[
|
||||
{"id": "a", "node_type": "echo", "inputs": {}},
|
||||
{"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}},
|
||||
{"id": "c", "node_type": "echo", "inputs": {"file_uri": "b.file_uri"}},
|
||||
],
|
||||
edges=[{"from": "a", "to": "b"}, {"from": "b", "to": "c"}],
|
||||
)
|
||||
|
||||
# 测试过程
|
||||
order = topological_sort(definition)
|
||||
|
||||
# 验证结果
|
||||
assert order == ["a", "b", "c"]
|
||||
|
||||
|
||||
def test_topological_sort_diamond() -> None:
|
||||
"""菱形依赖中,汇合节点排在其全部前驱之后。"""
|
||||
# 数据:a → (b, c) → d。
|
||||
definition = _definition(
|
||||
nodes=[
|
||||
{"id": "a", "node_type": "echo", "inputs": {}},
|
||||
{"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}},
|
||||
{"id": "c", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}},
|
||||
{"id": "d", "node_type": "echo", "inputs": {"file_uri": "b.file_uri"}},
|
||||
],
|
||||
edges=[
|
||||
{"from": "a", "to": "b"}, {"from": "a", "to": "c"},
|
||||
{"from": "b", "to": "d"}, {"from": "c", "to": "d"},
|
||||
],
|
||||
)
|
||||
|
||||
# 测试过程
|
||||
order = topological_sort(definition)
|
||||
|
||||
# 验证结果:a 最先、d 最后,b/c 在中间。
|
||||
assert order[0] == "a"
|
||||
assert order[-1] == "d"
|
||||
assert set(order[1:3]) == {"b", "c"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 执行:成功路径
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_execute_run_completes_and_records_artifacts(tmp_path: Path) -> None:
|
||||
"""单节点任务执行成功:状态 COMPLETED、产物登记、进度到位。"""
|
||||
# 数据:echo 单节点工作流 + 真实输入文件。
|
||||
definition = _definition(
|
||||
nodes=[{"id": "step", "node_type": "echo", "inputs": {"file_uri": "input.video_uri"}}],
|
||||
edges=[],
|
||||
final_outputs={"result": "step.file_uri"},
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
db = _db_with_workflow(tmp_path, definition)
|
||||
source = tmp_path / "input.txt"
|
||||
source.write_text("输入内容", encoding="utf-8")
|
||||
db.create_run(_run("run-1", str(source)))
|
||||
registry.register_all()
|
||||
|
||||
# 测试过程
|
||||
_scheduler(db, storage).execute_run("run-1")
|
||||
|
||||
# 验证结果:任务完成、进度 1.0、节点产物与最终别名都已登记。
|
||||
stored = db.get_run("run-1")
|
||||
assert stored["status"] == "COMPLETED"
|
||||
assert stored["progress"] == 1.0
|
||||
names = {a["name"] for a in db.list_artifacts("run-1")}
|
||||
assert "step.file_uri" in names
|
||||
assert "result" in names
|
||||
|
||||
|
||||
def test_execute_run_multi_node_chain_passes_artifacts(tmp_path: Path) -> None:
|
||||
"""多节点链:后序节点通过 URI 拿到前序产物(节点间只经产物交换数据)。"""
|
||||
# 数据:step1 → step2 两节点链。
|
||||
definition = _definition(
|
||||
nodes=[
|
||||
{"id": "step1", "node_type": "echo", "inputs": {"file_uri": "input.video_uri"}},
|
||||
{"id": "step2", "node_type": "echo", "inputs": {"file_uri": "step1.file_uri"}},
|
||||
],
|
||||
edges=[{"from": "step1", "to": "step2"}],
|
||||
final_outputs={"out": "step2.file_uri"},
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
db = _db_with_workflow(tmp_path, definition)
|
||||
source = tmp_path / "input.txt"
|
||||
source.write_text("链式内容", encoding="utf-8")
|
||||
db.create_run(_run("run-2", str(source)))
|
||||
registry.register_all()
|
||||
|
||||
# 测试过程
|
||||
_scheduler(db, storage).execute_run("run-2")
|
||||
|
||||
# 验证结果:两节点产物都存在,且 step2 的产物内容来自 step1(传递一致)。
|
||||
assert db.get_run("run-2")["status"] == "COMPLETED"
|
||||
artifacts = {a["name"]: a["uri"] for a in db.list_artifacts("run-2")}
|
||||
assert "step1.file_uri" in artifacts and "step2.file_uri" in artifacts
|
||||
assert Path(artifacts["step2.file_uri"]).read_text(encoding="utf-8") == "链式内容"
|
||||
assert Path(artifacts["step2.file_uri"]).parent != Path(artifacts["step1.file_uri"]).parent
|
||||
|
||||
|
||||
def test_execute_run_creates_final_alias_with_stable_name(tmp_path: Path) -> None:
|
||||
"""最终产物按 上传文件名.标识.时间戳 生成别名,并保留节点原始文件。"""
|
||||
# 数据:单节点 + target_language 参数(决定别名标识)。
|
||||
definition = _definition(
|
||||
nodes=[{
|
||||
"id": "step", "node_type": "echo",
|
||||
"inputs": {"file_uri": "input.video_uri"},
|
||||
"params": {"target_language": "zh-CN"},
|
||||
}],
|
||||
edges=[],
|
||||
final_outputs={"cn_srt_uri": "step.file_uri"},
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
db = _db_with_workflow(tmp_path, definition)
|
||||
source = tmp_path / "test01.mp4"
|
||||
source.write_text("数据", encoding="utf-8")
|
||||
db.create_run(_run("run-3", str(source)))
|
||||
registry.register_all()
|
||||
|
||||
# 测试过程
|
||||
_scheduler(db, storage).execute_run("run-3")
|
||||
|
||||
# 验证结果:别名指向 finals 下的稳定路径,含 zh-CN 标识;节点原文件仍在。
|
||||
artifacts = {a["name"]: a["uri"] for a in db.list_artifacts("run-3")}
|
||||
final = Path(artifacts["cn_srt_uri"])
|
||||
assert final.is_file()
|
||||
assert "zh-CN" in final.name
|
||||
assert "finals" in final.parts
|
||||
assert Path(artifacts["step.file_uri"]).is_file()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 失败与无效 DAG
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_execute_run_fails_when_workflow_version_missing(tmp_path: Path) -> None:
|
||||
"""工作流版本记录丢失时任务失败(不留 QUEUED 堵塞队列)。"""
|
||||
# 数据:工作流存在但没有 v1 版本记录(外键仍满足)。
|
||||
db = Database(tmp_path / "wov.db")
|
||||
storage = tmp_path / "storage"
|
||||
db.upsert_workflow({"id": "wf-no-version", "name": "流程", "description": ""})
|
||||
db.create_run(_run("run-x", "input", workflow_id="wf-no-version"))
|
||||
|
||||
# 测试过程
|
||||
_scheduler(db, storage).execute_run("run-x")
|
||||
|
||||
# 验证结果
|
||||
stored = db.get_run("run-x")
|
||||
assert stored["status"] == "FAILED"
|
||||
assert "workflow version not found" in stored["error"]
|
||||
|
||||
|
||||
def test_execute_run_marks_failed_on_cycle_and_does_not_block_queue(tmp_path: Path) -> None:
|
||||
"""环形 DAG(历史无效版本)立即失败且不堵塞后续任务(R04 回归)。"""
|
||||
# 数据:A→B→A 的环形定义。
|
||||
definition = _definition(
|
||||
nodes=[
|
||||
{"id": "a", "node_type": "echo", "inputs": {"file_uri": "b.file_uri"}},
|
||||
{"id": "b", "node_type": "echo", "inputs": {"file_uri": "a.file_uri"}},
|
||||
],
|
||||
edges=[{"from": "a", "to": "b"}, {"from": "b", "to": "a"}],
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
db = _db_with_workflow(tmp_path, definition)
|
||||
db.create_run(_run("run-cycle", "input"))
|
||||
registry.register_all()
|
||||
|
||||
# 测试过程
|
||||
_scheduler(db, storage).execute_run("run-cycle")
|
||||
|
||||
# 验证结果:任务 FAILED,且队首前移(不再返回该任务)。
|
||||
assert db.get_run("run-cycle")["status"] == "FAILED"
|
||||
assert db.next_queued_run() is None
|
||||
|
||||
|
||||
def test_execute_run_fails_when_input_reference_missing(tmp_path: Path) -> None:
|
||||
"""输入引用无法解析(前序产物缺失)时任务失败并记录原因。"""
|
||||
# 数据:节点引用不存在的产物。
|
||||
definition = _definition(
|
||||
nodes=[{"id": "step", "node_type": "echo", "inputs": {"file_uri": "ghost.file_uri"}}],
|
||||
edges=[],
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
db = _db_with_workflow(tmp_path, definition)
|
||||
db.create_run(_run("run-4", "input"))
|
||||
registry.register_all()
|
||||
|
||||
# 测试过程
|
||||
_scheduler(db, storage).execute_run("run-4")
|
||||
|
||||
# 验证结果
|
||||
stored = db.get_run("run-4")
|
||||
assert stored["status"] == "FAILED"
|
||||
assert "missing input" in stored["error"]
|
||||
|
||||
|
||||
def test_execute_run_fails_when_node_returns_failed(tmp_path: Path) -> None:
|
||||
"""节点返回 failed 时任务失败并保留错误信息。"""
|
||||
# 数据:注册一个总是失败的节点。
|
||||
from wov_sdk.models import InvokeResponse, NodeManifest
|
||||
|
||||
definition = _definition(
|
||||
nodes=[{"id": "step", "node_type": "boom", "inputs": {}}],
|
||||
edges=[],
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
db = _db_with_workflow(tmp_path, definition)
|
||||
db.create_run(_run("run-5", "input"))
|
||||
registry.register(
|
||||
NodeManifest(id="boom", name="失败节点", version="1", capability="c", command=["python"]),
|
||||
lambda request: InvokeResponse(status="failed", error="节点内部错误"),
|
||||
)
|
||||
|
||||
# 测试过程
|
||||
_scheduler(db, storage).execute_run("run-5")
|
||||
|
||||
# 验证结果
|
||||
stored = db.get_run("run-5")
|
||||
assert stored["status"] == "FAILED"
|
||||
assert "节点内部错误" in stored["error"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 暂停语义
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_execute_run_leaves_paused_run_untouched(tmp_path: Path) -> None:
|
||||
"""以 PAUSED 进入时直接返回保持暂停(修复"点击暂停反而开始任务")。"""
|
||||
# 数据:PAUSED 状态的任务。
|
||||
definition = _definition(
|
||||
nodes=[{"id": "step", "node_type": "echo", "inputs": {}}],
|
||||
edges=[],
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
db = _db_with_workflow(tmp_path, definition)
|
||||
db.create_run(_run("run-6", "input", status="PAUSED"))
|
||||
registry.register_all()
|
||||
|
||||
# 测试过程
|
||||
_scheduler(db, storage).execute_run("run-6")
|
||||
|
||||
# 验证结果:仍为 PAUSED,且未产生任何产物。
|
||||
assert db.get_run("run-6")["status"] == "PAUSED"
|
||||
assert db.list_artifacts("run-6") == []
|
||||
|
||||
|
||||
def test_execute_run_stops_at_node_boundary_when_paused(tmp_path: Path) -> None:
|
||||
"""运行中被暂停:在当前节点边界停下保持 PAUSED,不标 FAILED。"""
|
||||
# 数据:两节点链;第一个节点执行时把任务置 PAUSED。
|
||||
from wov_sdk.models import InvokeResponse, NodeManifest
|
||||
|
||||
definition = _definition(
|
||||
nodes=[
|
||||
{"id": "step1", "node_type": "pause-me", "inputs": {}},
|
||||
{"id": "step2", "node_type": "echo", "inputs": {}},
|
||||
],
|
||||
edges=[{"from": "step1", "to": "step2"}],
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
db = _db_with_workflow(tmp_path, definition)
|
||||
db.create_run(_run("run-7", "input"))
|
||||
registry.register_all()
|
||||
|
||||
def pause_handler(request):
|
||||
"""模拟用户在该节点执行期间点击暂停。"""
|
||||
db.pause_run("run-7", "t2")
|
||||
return InvokeResponse(status="completed", outputs={"file_uri": "/tmp/x"})
|
||||
|
||||
registry.register(
|
||||
NodeManifest(id="pause-me", name="暂停节点", version="1", capability="c", command=["python"]),
|
||||
pause_handler,
|
||||
)
|
||||
|
||||
# 测试过程
|
||||
_scheduler(db, storage).execute_run("run-7")
|
||||
|
||||
# 验证结果:保持 PAUSED,step2 未执行。
|
||||
assert db.get_run("run-7")["status"] == "PAUSED"
|
||||
names = {a["name"] for a in db.list_artifacts("run-7")}
|
||||
assert not any(name.startswith("step2") for name in names)
|
||||
|
||||
|
||||
def test_execute_run_clears_stale_pause_flag(tmp_path: Path) -> None:
|
||||
"""执行前清理残留的 paused.flag(避免误触发节点内暂停)。"""
|
||||
# 数据:单节点任务 + 已存在的 paused.flag。
|
||||
definition = _definition(
|
||||
nodes=[{"id": "step", "node_type": "echo", "inputs": {}}],
|
||||
edges=[],
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
db = _db_with_workflow(tmp_path, definition)
|
||||
db.create_run(_run("run-8", "input"))
|
||||
registry.register_all()
|
||||
run_root = storage / "runs" / "run-8"
|
||||
run_root.mkdir(parents=True)
|
||||
(run_root / "paused.flag").write_text("", encoding="utf-8")
|
||||
|
||||
# 测试过程
|
||||
_scheduler(db, storage).execute_run("run-8")
|
||||
|
||||
# 验证结果:标志被清除,任务正常完成。
|
||||
assert not (run_root / "paused.flag").exists()
|
||||
assert db.get_run("run-8")["status"] == "COMPLETED"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 断点续跑与参数覆盖
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_execute_run_resumes_from_existing_artifacts(tmp_path: Path) -> None:
|
||||
"""断点续跑:已有产物的节点被跳过,只执行剩余节点。"""
|
||||
# 数据:两节点链,step1 产物已登记。
|
||||
calls: list[str] = []
|
||||
|
||||
def tracking_handler(node_id: str):
|
||||
def handler(request):
|
||||
from wov_sdk.models import InvokeResponse
|
||||
|
||||
calls.append(node_id)
|
||||
return InvokeResponse(status="completed", outputs={"file_uri": f"/tmp/{node_id}"})
|
||||
|
||||
return handler
|
||||
|
||||
definition = _definition(
|
||||
nodes=[
|
||||
{"id": "step1", "node_type": "track1", "inputs": {}},
|
||||
{"id": "step2", "node_type": "track2", "inputs": {}},
|
||||
],
|
||||
edges=[{"from": "step1", "to": "step2"}],
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
db = _db_with_workflow(tmp_path, definition)
|
||||
db.create_run(_run("run-9", "input"))
|
||||
db.create_artifact({
|
||||
"run_id": "run-9", "node_id": "step1", "name": "step1.file_uri",
|
||||
"uri": "/tmp/step1", "kind": "file",
|
||||
})
|
||||
from wov_sdk.models import NodeManifest
|
||||
|
||||
registry.register(
|
||||
NodeManifest(id="track1", name="t1", version="1", capability="c", command=["python"]),
|
||||
tracking_handler("step1"),
|
||||
)
|
||||
registry.register(
|
||||
NodeManifest(id="track2", name="t2", version="1", capability="c", command=["python"]),
|
||||
tracking_handler("step2"),
|
||||
)
|
||||
|
||||
# 测试过程
|
||||
_scheduler(db, storage).execute_run("run-9")
|
||||
|
||||
# 验证结果:只调用了 step2。
|
||||
assert calls == ["step2"]
|
||||
assert db.get_run("run-9")["status"] == "COMPLETED"
|
||||
|
||||
|
||||
def test_execute_run_applies_param_overrides(tmp_path: Path) -> None:
|
||||
"""param_overrides 按节点 ID 合并进节点参数(前端框选 crop 的通道)。"""
|
||||
# 数据:节点参数与覆盖值同时存在。
|
||||
received: list[dict] = []
|
||||
|
||||
def handler(request):
|
||||
from wov_sdk.models import InvokeResponse
|
||||
|
||||
received.append(dict(request.params))
|
||||
return InvokeResponse(status="completed", outputs={"file_uri": "/tmp/x"})
|
||||
|
||||
definition = _definition(
|
||||
nodes=[{
|
||||
"id": "step", "node_type": "param-node",
|
||||
"inputs": {}, "params": {"interval_seconds": 0.5, "crop": [0, 0, 1, 1]},
|
||||
}],
|
||||
edges=[],
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
db = _db_with_workflow(tmp_path, definition)
|
||||
db.create_run(_run("run-10", "input", param_overrides={"step": {"crop": [0, 0.75, 1, 0.25]}}))
|
||||
from wov_sdk.models import NodeManifest
|
||||
|
||||
registry.register(
|
||||
NodeManifest(id="param-node", name="p", version="1", capability="c", command=["python"]),
|
||||
handler,
|
||||
)
|
||||
|
||||
# 测试过程
|
||||
_scheduler(db, storage).execute_run("run-10")
|
||||
|
||||
# 验证结果:覆盖值生效,未覆盖的参数保持原样。
|
||||
assert received == [{"interval_seconds": 0.5, "crop": [0, 0.75, 1, 0.25]}]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 线程生命周期
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_start_and_stop_are_idempotent(tmp_path: Path) -> None:
|
||||
"""start 重复调用不产生多余线程;stop 正常结束。"""
|
||||
# 数据:空库 + 调度器。
|
||||
db = Database(tmp_path / "wov.db")
|
||||
scheduler = WorkflowScheduler(db, tmp_path / "storage", interval_seconds=999)
|
||||
|
||||
# 测试过程
|
||||
scheduler.start()
|
||||
first = scheduler._thread
|
||||
scheduler.start()
|
||||
second = scheduler._thread
|
||||
scheduler.stop()
|
||||
|
||||
# 验证结果
|
||||
assert first is second
|
||||
assert scheduler._thread is None
|
||||
Reference in New Issue
Block a user