mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
89 lines
2.7 KiB
Python
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
|