mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-15 19:14:01 +08:00
236 lines
8.6 KiB
Python
236 lines
8.6 KiB
Python
"""
|
|
工作流表的查询与状态流转行为。
|
|
|
|
调度器按「触发类型 + 状态」取工作流,取多了会把用户暂停的流程重新跑起来,
|
|
取少了则定时任务永远不触发。状态流转里的 `state != 'P'` 守卫是暂停语义的唯一实现,
|
|
`run_count` 的自增必须留在 SQL 侧,否则并发执行会丢计数。
|
|
"""
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
from app.db.models.workflow import Workflow
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _track(db):
|
|
"""把工作流表纳入用例级回收。"""
|
|
db.watermark(Workflow)
|
|
|
|
|
|
def _flow(name: str, trigger_type: str = "timer", state: str = "W",
|
|
run_count: int = 0) -> Workflow:
|
|
"""构造一条工作流记录。"""
|
|
return Workflow(name=name, description=name, timer="0 * * * *",
|
|
trigger_type=trigger_type, state=state, run_count=run_count,
|
|
actions=[], flows=[], context={}, execution_state={})
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 列表查询
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_list_and_get_by_name_match_async_twins(db):
|
|
"""
|
|
列举与按名查找的同步、异步结果必须一致。
|
|
"""
|
|
created = db.add(_flow("wf-name"))
|
|
|
|
assert Workflow.get_by_name(db.session, "wf-name").id == created.id
|
|
assert asyncio.run(Workflow.async_get_by_name(name="wf-name")).id == created.id
|
|
assert Workflow.get_by_name(db.session, "wf-missing") is None
|
|
|
|
sync_ids = sorted(w.id for w in Workflow.list(db.session))
|
|
async_ids = sorted(w.id for w in asyncio.run(Workflow.async_list()))
|
|
assert sync_ids == async_ids
|
|
|
|
|
|
def test_enabled_workflows_exclude_paused(db):
|
|
"""
|
|
启用列表排除暂停状态。
|
|
|
|
暂停是用户显式的「别再跑了」,被列出即等于暂停开关失效。
|
|
"""
|
|
db.add(_flow("wf-waiting", state="W"), _flow("wf-running", state="R"),
|
|
_flow("wf-paused", state="P"))
|
|
|
|
names = {w.name for w in Workflow.get_enabled_workflows(db.session)}
|
|
|
|
assert {"wf-waiting", "wf-running"} <= names
|
|
assert "wf-paused" not in names
|
|
assert "wf-paused" not in {w.name for w in
|
|
asyncio.run(Workflow.async_get_enabled_workflows())}
|
|
|
|
|
|
def test_timer_triggered_includes_legacy_null_trigger_type(db):
|
|
"""
|
|
定时触发列表要包含 trigger_type 为空的历史数据。
|
|
|
|
该列是后加的,老工作流为空;严格等于 'timer' 会让它们从此再也不被调度,
|
|
而用户看到的只是「任务不跑了」。
|
|
"""
|
|
db.add(_flow("wf-timer", trigger_type="timer"),
|
|
_flow("wf-legacy", trigger_type=None),
|
|
_flow("wf-event", trigger_type="event"),
|
|
_flow("wf-timer-paused", trigger_type="timer", state="P"))
|
|
|
|
names = {w.name for w in Workflow.get_timer_triggered_workflows(db.session)}
|
|
|
|
assert {"wf-timer", "wf-legacy"} <= names
|
|
assert "wf-event" not in names
|
|
assert "wf-timer-paused" not in names
|
|
|
|
|
|
def test_event_triggered_requires_explicit_type(db):
|
|
"""
|
|
事件触发列表只认显式的 'event',且同样排除暂停。
|
|
"""
|
|
db.add(_flow("wf-event", trigger_type="event"),
|
|
_flow("wf-event-paused", trigger_type="event", state="P"),
|
|
_flow("wf-legacy", trigger_type=None))
|
|
|
|
names = {w.name for w in Workflow.get_event_triggered_workflows(db.session)}
|
|
|
|
assert names >= {"wf-event"}
|
|
assert "wf-event-paused" not in names
|
|
assert "wf-legacy" not in names
|
|
|
|
|
|
def test_trigger_lists_match_async_twins(db):
|
|
"""
|
|
定时与事件两条触发列表的同步、异步结果必须一致。
|
|
"""
|
|
db.add(_flow("wf-t", trigger_type="timer"), _flow("wf-e", trigger_type="event"))
|
|
|
|
assert sorted(w.id for w in Workflow.get_timer_triggered_workflows(db.session)) == \
|
|
sorted(w.id for w in asyncio.run(Workflow.async_get_timer_triggered_workflows()))
|
|
assert sorted(w.id for w in Workflow.get_event_triggered_workflows(db.session)) == \
|
|
sorted(w.id for w in asyncio.run(Workflow.async_get_event_triggered_workflows()))
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 状态流转
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_update_state_and_start_write_the_state(db):
|
|
"""
|
|
状态更新与启动直接落库,供调度器读到最新状态。
|
|
"""
|
|
flow = db.add(_flow("wf-state"))
|
|
|
|
Workflow.update_state(db.session, flow.id, "F")
|
|
assert Workflow.get_by_name(db.session, "wf-state").state == "F"
|
|
|
|
Workflow.start(db.session, flow.id)
|
|
assert Workflow.get_by_name(db.session, "wf-state").state == "R"
|
|
|
|
|
|
def test_fail_and_success_respect_the_paused_guard(db):
|
|
"""
|
|
暂停中的工作流不接受成功/失败结果写入。
|
|
|
|
守卫丢失后,一个还在跑的旧任务收尾时会把用户刚设的暂停状态改掉,
|
|
下一轮调度它又被跑起来。
|
|
"""
|
|
paused = db.add(_flow("wf-paused", state="P"))
|
|
|
|
Workflow.fail(db.session, paused.id, "出错了")
|
|
assert Workflow.get_by_name(db.session, "wf-paused").state == "P"
|
|
|
|
Workflow.success(db.session, paused.id, "完成")
|
|
assert Workflow.get_by_name(db.session, "wf-paused").state == "P"
|
|
|
|
|
|
def test_fail_records_result_and_timestamp(db):
|
|
"""
|
|
失败要同时写入结果与最后执行时间,供界面展示失败原因。
|
|
"""
|
|
flow = db.add(_flow("wf-fail", state="R"))
|
|
|
|
Workflow.fail(db.session, flow.id, "网络超时")
|
|
|
|
updated = Workflow.get_by_name(db.session, "wf-fail")
|
|
assert (updated.state, updated.result) == ("F", "网络超时")
|
|
assert updated.last_time
|
|
|
|
|
|
def test_success_increments_run_count_in_sql(db):
|
|
"""
|
|
执行次数必须在 SQL 侧自增。
|
|
|
|
先读后写会在并发执行时丢计数;连续两次成功后必须是 2。
|
|
"""
|
|
flow = db.add(_flow("wf-count", state="R", run_count=0))
|
|
|
|
Workflow.success(db.session, flow.id, "第一次")
|
|
Workflow.success(db.session, flow.id, "第二次")
|
|
|
|
updated = Workflow.get_by_name(db.session, "wf-count")
|
|
assert updated.run_count == 2
|
|
assert updated.state == "S"
|
|
|
|
|
|
def test_reset_clears_progress_and_optionally_the_count(db):
|
|
"""
|
|
重置清空执行进度;是否清零执行次数由参数决定。
|
|
|
|
默认保留计数是为了让「重跑」不丢失历史执行统计。
|
|
"""
|
|
flow = db.add(_flow("wf-reset", state="F", run_count=5))
|
|
Workflow.update_current_action(db.session, flow.id, "action-1",
|
|
{"k": "v"}, {"step": 1})
|
|
|
|
Workflow.reset(db.session, flow.id)
|
|
kept = Workflow.get_by_name(db.session, "wf-reset")
|
|
assert (kept.state, kept.result, kept.current_action) == ("W", None, None)
|
|
assert kept.context == {} and kept.execution_state == {}
|
|
assert kept.run_count == 5
|
|
|
|
Workflow.reset(db.session, flow.id, reset_count=True)
|
|
assert Workflow.get_by_name(db.session, "wf-reset").run_count == 0
|
|
|
|
|
|
def test_update_current_action_appends_without_duplicating(db):
|
|
"""
|
|
已执行动作按逗号追加且不重复登记。
|
|
|
|
重复登记会让「已执行」列表无限膨胀,重跑时的跳过判断也随之失准。
|
|
"""
|
|
flow = db.add(_flow("wf-action"))
|
|
|
|
Workflow.update_current_action(db.session, flow.id, "a1", {"n": 1})
|
|
Workflow.update_current_action(db.session, flow.id, "a2", {"n": 2})
|
|
Workflow.update_current_action(db.session, flow.id, "a1", {"n": 3})
|
|
|
|
updated = Workflow.get_by_name(db.session, "wf-action")
|
|
assert updated.current_action == "a1,a2"
|
|
assert updated.context == {"n": 3}
|
|
|
|
|
|
def test_update_current_action_leaves_execution_state_untouched_when_omitted(db):
|
|
"""
|
|
不传执行状态时保持原值,避免一次进度更新把结构化状态清空。
|
|
"""
|
|
flow = db.add(_flow("wf-keep-state"))
|
|
Workflow.update_current_action(db.session, flow.id, "a1", {}, {"step": 7})
|
|
|
|
Workflow.update_current_action(db.session, flow.id, "a2", {"n": 1})
|
|
|
|
assert Workflow.get_by_name(db.session, "wf-keep-state").execution_state == {"step": 7}
|
|
|
|
|
|
def test_update_current_action_matches_async_twin(db):
|
|
"""
|
|
同步与异步的动作追加必须给出相同的字符串。
|
|
"""
|
|
sync_flow = db.add(_flow("wf-sync-action"))
|
|
async_flow = db.add(_flow("wf-async-action"))
|
|
|
|
for action in ("a1", "a2", "a1"):
|
|
Workflow.update_current_action(db.session, sync_flow.id, action, {})
|
|
asyncio.run(Workflow.async_update_current_action(
|
|
wid=async_flow.id, action_id=action, context={}))
|
|
|
|
assert Workflow.get_by_name(db.session, "wf-sync-action").current_action == \
|
|
Workflow.get_by_name(db.session, "wf-async-action").current_action
|