Files
MoviePilot/tests/test_scheduler_contracts.py
T

89 lines
2.7 KiB
Python

"""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