mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
122 lines
4.4 KiB
Python
122 lines
4.4 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
|
|
|
|
original_setting = main.get_runtime_setting
|
|
topology_settings = {
|
|
"API_WORKERS": 2,
|
|
"MOVIEPILOT_SAFE_MODE": False,
|
|
}
|
|
monkeypatch.setattr(
|
|
main,
|
|
"get_runtime_setting",
|
|
lambda key: topology_settings[key] if key in topology_settings else original_setting(key),
|
|
)
|
|
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
|