Files
MoviePilot/tests/test_scheduler_cache_expiry.py
T

225 lines
7.7 KiB
Python

import asyncio
import threading
from unittest.mock import AsyncMock, Mock
import pytest
from app import scheduler as scheduler_module
from app.scheduler import Scheduler
from app.application.configuration import SchedulerRuntimeConfig
from app.startup.initializers import scheduler as scheduler_initializer
class _BackgroundSchedulerStub:
"""记录系统定时任务注册结果的调度器替身。"""
def __init__(self):
"""初始化任务记录。"""
self.jobs = []
self.started = False
def add_job(self, func, trigger, **kwargs):
"""记录一次任务注册。"""
self.jobs.append({"func": func, "trigger": trigger, **kwargs})
def start(self):
"""记录调度器已启动。"""
self.started = True
def test_scheduler_constructor_does_not_start_background_jobs(monkeypatch):
"""取得调度器单例不应绕过应用生命周期启动后台任务。"""
init = Mock()
monkeypatch.setattr(Scheduler, "init", init)
scheduler = object.__new__(Scheduler)
scheduler.__init__()
init.assert_not_called()
assert scheduler._scheduler is None
def test_scheduler_initializer_starts_background_jobs(monkeypatch):
"""应用启动入口负责显式启动已经构造的调度器。"""
scheduler = Mock()
monkeypatch.setattr(scheduler_initializer, "Scheduler", Mock(return_value=scheduler))
scheduler_initializer.init_scheduler()
scheduler.init.assert_called_once_with()
def test_scheduler_initializer_stop_preserves_sync_abi(monkeypatch):
"""同步调用停止入口仍应立即关闭 Scheduler,不返回 coroutine。"""
scheduler = Mock()
monkeypatch.setattr(scheduler_initializer, "Scheduler", Mock(return_value=scheduler))
assert scheduler_initializer.stop_scheduler() is None
scheduler.stop.assert_called_once_with()
scheduler.stop_async.assert_not_called()
def test_scheduler_initializer_stop_awaits_in_running_loop(monkeypatch):
"""生命周期事件循环中的停止入口应返回可等待的异步收口。"""
scheduler = Mock()
scheduler.stop_async = AsyncMock()
monkeypatch.setattr(scheduler_initializer, "Scheduler", Mock(return_value=scheduler))
async def scenario():
result = scheduler_initializer.stop_scheduler()
assert result is not None
await result
asyncio.run(scenario())
scheduler.stop_async.assert_awaited_once_with()
scheduler.stop.assert_not_called()
def test_clear_cache_is_manual_only(monkeypatch):
"""缓存清理任务应仅手动执行,不注册到调度器自动运行。"""
background_scheduler = _BackgroundSchedulerStub()
generic_chain = Mock()
for name in [
"MediaServerChain",
"RecommendChain",
"SchedulerChain",
"SiteChain",
"SubscribeChain",
"TransferChain",
"WallpaperHelper",
"WorkflowChain",
"get_plugin_manager",
]:
monkeypatch.setattr(scheduler_module, name, lambda: generic_chain)
monkeypatch.setattr(
scheduler_module,
"get_mediaserver_configs",
lambda **_kwargs: [],
)
monkeypatch.setattr(
scheduler_module,
"BackgroundScheduler",
lambda **kwargs: background_scheduler,
)
monkeypatch.setattr(Scheduler, "stop", lambda self: None)
monkeypatch.setattr(Scheduler, "init_workflow_jobs", lambda self: None)
monkeypatch.setattr(Scheduler, "init_agent_task_jobs", lambda self: None)
monkeypatch.setattr(Scheduler, "init_plugin_jobs", lambda self: None)
monkeypatch.setattr(
scheduler_module,
"get_scheduler_runtime_config",
lambda: SchedulerRuntimeConfig(
False, "Asia/Shanghai", 1, False, "", 0, None, False, 24,
"rss", 30, False, 0, 0, False, None, False, None,
),
)
scheduler = object.__new__(Scheduler)
scheduler._scheduler = None
scheduler._event = threading.Event()
scheduler._lock = threading.RLock()
scheduler._jobs = {}
scheduler._lifecycle_state = "new"
scheduler._handles = {}
scheduler._job_generations = {}
scheduler._active_job_generations = {}
scheduler._agent_task_reservations = {}
scheduler._agent_task_interruptions_reconciled = True
scheduler._auth_count = 0
scheduler._auth_message = False
scheduler.init()
scheduled_job_ids = {job["id"] for job in background_scheduler.jobs}
assert "clear_cache" not in scheduled_job_ids
assert "clear_cache" in scheduler._jobs
assert scheduler._jobs["clear_cache"]["manual"] is True
assert background_scheduler.started is True
def test_user_auth_refreshes_plugin_routes_after_runtime_reinitialization(monkeypatch):
"""自动认证重建插件实例后必须同步刷新动态路由投影。"""
scheduler = object.__new__(Scheduler)
scheduler._auth_count = 1
scheduler._auth_message = False
scheduler._auth_plugin_routes_pending = False
plugin_manager = Mock()
plugin_jobs = Mock()
refresh_routes = Mock()
message_chain = Mock()
monkeypatch.setattr(
scheduler_module,
"get_scheduler_runtime_config",
lambda: Mock(site_link="https://example.invalid"),
)
monkeypatch.setattr(
scheduler_module,
"SitesHelper",
lambda: Mock(auth_level=0, check_user=Mock(return_value=(True, "demo"))),
)
monkeypatch.setattr(
scheduler_module,
"get_configured_system_config",
lambda: Mock(get=Mock(return_value=None)),
)
monkeypatch.setattr(scheduler_module, "SchedulerChain", lambda: message_chain)
monkeypatch.setattr(
scheduler_module,
"get_plugin_manager",
lambda: plugin_manager,
)
monkeypatch.setattr(scheduler, "init_plugin_jobs", plugin_jobs)
monkeypatch.setattr(scheduler_module, "register_plugin_api", refresh_routes)
scheduler.user_auth()
plugin_manager.init_config.assert_called_once_with()
plugin_jobs.assert_called_once_with()
refresh_routes.assert_called_once_with()
assert scheduler._auth_plugin_routes_pending is False
def test_user_auth_retries_pending_plugin_route_projection(monkeypatch):
"""认证已成功但路由投影失败时,后续认证任务只重试未完成的投影。"""
scheduler = object.__new__(Scheduler)
scheduler._auth_count = 1
scheduler._auth_message = False
scheduler._auth_plugin_routes_pending = False
plugin_manager = Mock()
plugin_jobs = Mock()
refresh_routes = Mock(side_effect=[RuntimeError("loop unavailable"), None])
message_chain = Mock()
sites = Mock(auth_level=0, check_user=Mock(return_value=(True, "demo")))
monkeypatch.setattr(
scheduler_module,
"get_scheduler_runtime_config",
lambda: Mock(site_link="https://example.invalid"),
)
monkeypatch.setattr(scheduler_module, "SitesHelper", lambda: sites)
monkeypatch.setattr(
scheduler_module,
"get_configured_system_config",
lambda: Mock(get=Mock(return_value=None)),
)
monkeypatch.setattr(scheduler_module, "SchedulerChain", lambda: message_chain)
monkeypatch.setattr(
scheduler_module,
"get_plugin_manager",
lambda: plugin_manager,
)
monkeypatch.setattr(scheduler, "init_plugin_jobs", plugin_jobs)
monkeypatch.setattr(scheduler_module, "register_plugin_api", refresh_routes)
with pytest.raises(RuntimeError, match="loop unavailable"):
scheduler.user_auth()
assert scheduler._auth_plugin_routes_pending is True
sites.auth_level = 2
scheduler.user_auth()
assert scheduler._auth_plugin_routes_pending is False
assert refresh_routes.call_count == 2
plugin_manager.init_config.assert_called_once_with()
plugin_jobs.assert_called_once_with()
sites.check_user.assert_called_once_with()