Files
MoviePilot/tests/test_process_topology.py
T

114 lines
4.2 KiB
Python

"""V3 API 数据面与宿主控制面进程拓扑测试。"""
import asyncio
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from pydantic import ValidationError
from app.doctor import checks
from app.doctor.models import DoctorFindingStatus
from app.doctor.runner import DoctorRunner
from app.runtime.config import ConfigModel, settings
from app.runtime.topology import (
UnsupportedProcessTopologyError,
process_topology_issue,
validate_process_topology,
)
from app.startup import lifecycle
@pytest.mark.parametrize("safe_mode", [False, True])
def test_single_worker_is_supported_in_every_mode(safe_mode: bool):
"""单 worker 是正常模式和安全模式共同支持的默认拓扑。"""
assert process_topology_issue(workers=1, safe_mode=safe_mode) is None
validate_process_topology(workers=1, safe_mode=safe_mode)
def test_full_runtime_rejects_multiple_workers():
"""全功能模式不得复制插件、调度器、监控器和工作流。"""
with pytest.raises(
UnsupportedProcessTopologyError,
match="全功能模式仅支持 API_WORKERS=1",
):
validate_process_topology(workers=2, safe_mode=False)
def test_safe_mode_temporarily_allows_multiple_workers():
"""安全模式跳过控制面时允许多 worker 作为故障诊断手段。"""
assert process_topology_issue(workers=2, safe_mode=True) is None
validate_process_topology(workers=2, safe_mode=True)
@pytest.mark.parametrize("workers", [0, -1, "invalid"])
def test_api_workers_configuration_rejects_invalid_values(workers):
"""worker 数量的边界或类型错误必须在配置解析阶段暴露。"""
with pytest.raises(ValidationError):
ConfigModel(API_WORKERS=workers)
def test_main_rejects_topology_before_startup_side_effects(monkeypatch):
"""主入口应在注册信号、迁移数据库和启动服务器前拒绝错误拓扑。"""
from app import main
monkeypatch.setattr(main.settings, "API_WORKERS", 2)
monkeypatch.setattr(main.settings, "MOVIEPILOT_SAFE_MODE", False)
signal_handler = MagicMock()
start_tray = MagicMock()
server_run = MagicMock()
monkeypatch.setattr(main.signal, "signal", signal_handler)
monkeypatch.setattr(main, "start_tray", start_tray)
monkeypatch.setattr(main, "run_api_server", server_run)
with pytest.raises(UnsupportedProcessTopologyError):
main.run_application()
signal_handler.assert_not_called()
start_tray.assert_not_called()
server_run.assert_not_called()
def test_asgi_lifespan_rejects_topology_before_runtime_initialization(monkeypatch):
"""外部 ASGI supervisor 也必须在生命周期副作用前执行同一校验。"""
monkeypatch.setattr(settings, "API_WORKERS", 2)
monkeypatch.setattr(settings, "MOVIEPILOT_SAFE_MODE", False)
set_loop = MagicMock()
monkeypatch.setattr(lifecycle.global_vars, "set_loop", set_loop)
async def run_lifespan() -> None:
async with lifecycle.lifespan(FastAPI()):
pass
with pytest.raises(UnsupportedProcessTopologyError):
asyncio.run(run_lifespan())
set_loop.assert_not_called()
def test_doctor_fails_unsupported_full_runtime_topology(monkeypatch):
"""Doctor 应把正常模式多 worker 报告为影响整体状态的失败。"""
monkeypatch.setattr(settings, "API_WORKERS", 2)
monkeypatch.setattr(settings, "MOVIEPILOT_SAFE_MODE", False)
runner = DoctorRunner()
checks._check_process_topology(runner)
finding = runner.report.find("startup.process_topology")
assert finding is not None
assert finding.status == DoctorFindingStatus.Failed
assert finding.context == {"api_workers": 2, "safe_mode": False}
def test_doctor_marks_safe_mode_multi_worker_as_degraded(monkeypatch):
"""安全模式多 worker 可运行,但 Doctor 必须提醒它不是正式扩容方案。"""
monkeypatch.setattr(settings, "API_WORKERS", 2)
monkeypatch.setattr(settings, "MOVIEPILOT_SAFE_MODE", True)
runner = DoctorRunner()
checks._check_process_topology(runner)
finding = runner.report.find("startup.process_topology")
assert finding is not None
assert finding.status == DoctorFindingStatus.Degraded