mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
refactor: dataize scheduler job contracts
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""Scheduler 声明、catalog 与 execution state 测试。"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.scheduling import (
|
||||
JobCatalog,
|
||||
JobExecutionState,
|
||||
JobOverlapPolicy,
|
||||
JobRecoveryPolicy,
|
||||
JobSpec,
|
||||
)
|
||||
|
||||
|
||||
def test_job_spec_exports_complete_legacy_state() -> None:
|
||||
"""每个声明必须显式暴露 owner、overlap、timeout、manual 和 recovery。"""
|
||||
state = JobSpec(
|
||||
"outbox",
|
||||
"恢复副作用",
|
||||
MagicMock(),
|
||||
"outbox",
|
||||
timeout_seconds=30,
|
||||
recovery=JobRecoveryPolicy.DURABLE_QUEUE,
|
||||
).to_runtime_state()
|
||||
|
||||
assert state["owner"] == "outbox"
|
||||
assert state["overlap"] == JobOverlapPolicy.SKIP.value
|
||||
assert state["timeout_seconds"] == 30
|
||||
assert state["manual"] is False
|
||||
assert state["recovery"] == JobRecoveryPolicy.DURABLE_QUEUE.value
|
||||
|
||||
|
||||
def test_job_catalog_rejects_duplicate_ids() -> None:
|
||||
"""重复 job ID 在 APScheduler 注册前即失败。"""
|
||||
spec = JobSpec("same", "相同", MagicMock(), "test")
|
||||
|
||||
with pytest.raises(ValueError, match="不得重复"):
|
||||
JobCatalog([spec, spec])
|
||||
|
||||
|
||||
def test_execution_state_skips_overlap_and_records_terminal_state() -> None:
|
||||
"""统一 execution state 保持旧的跳过重入并记录失败终态。"""
|
||||
state = JobSpec("job", "任务", MagicMock(), "test").to_runtime_state()
|
||||
|
||||
assert JobExecutionState.begin(state, "start") is True
|
||||
assert JobExecutionState.begin(state, "second") is False
|
||||
JobExecutionState.finish(state, "finish", "failed")
|
||||
|
||||
assert state["running"] is False
|
||||
assert state["last_started_at"] == "start"
|
||||
assert state["last_finished_at"] == "finish"
|
||||
assert state["last_error"] == "failed"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execution_state_cancels_coroutine_after_timeout() -> None:
|
||||
"""声明的 timeout 必须取消协程并向 Facade 抛出超时。"""
|
||||
cancelled = asyncio.Event()
|
||||
|
||||
async def wait_forever() -> None:
|
||||
"""模拟只能由取消信号结束的协程任务。"""
|
||||
try:
|
||||
await asyncio.Event().wait()
|
||||
finally:
|
||||
cancelled.set()
|
||||
|
||||
with pytest.raises(asyncio.TimeoutError):
|
||||
await JobExecutionState.await_result(wait_forever(), timeout_seconds=0.01)
|
||||
|
||||
assert cancelled.is_set()
|
||||
|
||||
|
||||
def test_manual_job_declares_restart_policy() -> None:
|
||||
"""只允许人工触发的任务必须显式声明重启后不自动补跑。"""
|
||||
state = JobSpec(
|
||||
"manual",
|
||||
"人工任务",
|
||||
MagicMock(),
|
||||
"runtime",
|
||||
manual=True,
|
||||
recovery=JobRecoveryPolicy.MANUAL_ONLY,
|
||||
).to_runtime_state()
|
||||
|
||||
assert state["manual"] is True
|
||||
assert state["recovery"] == JobRecoveryPolicy.MANUAL_ONLY.value
|
||||
Reference in New Issue
Block a user