mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-15 11:04:12 +08:00
fix(database): fail startup on migration errors (#6279)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from configparser import ConfigParser as _ConfigParser
|
||||
import traceback
|
||||
|
||||
from alembic.command import upgrade
|
||||
from alembic.config import Config
|
||||
@@ -38,5 +39,8 @@ def update_db():
|
||||
|
||||
alembic_cfg.set_main_option('sqlalchemy.url', db_url)
|
||||
upgrade(alembic_cfg, 'head')
|
||||
except Exception as e:
|
||||
logger.error(f'数据库更新失败:{str(e)}')
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f'数据库更新失败:{str(error)} - {traceback.format_exc()}'
|
||||
)
|
||||
raise
|
||||
|
||||
63
tests/test_database_migration_startup.py
Normal file
63
tests/test_database_migration_startup.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from app.db import init as db_init
|
||||
|
||||
|
||||
LOCAL_SETUP_PATH = (
|
||||
Path(__file__).resolve().parents[1] / "scripts" / "local_setup.py"
|
||||
)
|
||||
|
||||
|
||||
def _load_local_setup_module():
|
||||
"""加载隔离的本地安装脚本实例,避免测试间共享模块状态。"""
|
||||
module_name = f"moviepilot_local_setup_migration_{uuid.uuid4().hex}"
|
||||
spec = importlib.util.spec_from_file_location(module_name, LOCAL_SETUP_PATH)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_update_db_preserves_migration_error_and_traceback(monkeypatch) -> None:
|
||||
"""迁移失败日志应保留堆栈,同时向调用方传播原始异常。"""
|
||||
migration_error = RuntimeError("migration failed")
|
||||
logged_errors: list[str] = []
|
||||
|
||||
def fail_upgrade(*_args, **_kwargs) -> None:
|
||||
raise migration_error
|
||||
|
||||
monkeypatch.setattr(db_init, "upgrade", fail_upgrade)
|
||||
monkeypatch.setattr(db_init.logger, "error", logged_errors.append)
|
||||
|
||||
with pytest.raises(RuntimeError) as raised:
|
||||
db_init.update_db()
|
||||
|
||||
assert raised.value is migration_error
|
||||
assert len(logged_errors) == 1
|
||||
assert "数据库更新失败:migration failed" in logged_errors[0]
|
||||
assert "RuntimeError: migration failed" in logged_errors[0]
|
||||
|
||||
|
||||
def test_local_setup_returns_failure_when_database_migration_fails(
|
||||
monkeypatch,
|
||||
capsys,
|
||||
) -> None:
|
||||
"""本地维护命令不得在迁移失败后继续访问业务表。"""
|
||||
module = _load_local_setup_module()
|
||||
migration_error = RuntimeError("migration failed")
|
||||
|
||||
def fail_sync() -> None:
|
||||
raise migration_error
|
||||
|
||||
monkeypatch.setattr(sys, "argv", [str(LOCAL_SETUP_PATH), "sync-superuser"])
|
||||
monkeypatch.setattr(module, "_resolve_interactive_config_dir", lambda *_: None)
|
||||
monkeypatch.setattr(module, "configure_config_dir", lambda **_: Path("config"))
|
||||
monkeypatch.setattr(module, "_sync_superuser_account_inner", fail_sync)
|
||||
|
||||
assert module.main() == 1
|
||||
assert "migration failed" in capsys.readouterr().err
|
||||
@@ -261,3 +261,16 @@ def test_backend_ready_timeout_accepts_leading_zero_decimal(tmp_path: Path) -> N
|
||||
|
||||
assert "MOVIEPILOT_BACKEND_READY_TIMEOUT=08 无效" not in output
|
||||
assert "MoviePilot Web 已可访问" in output
|
||||
|
||||
|
||||
def test_backend_failure_keepalive_contract_is_explicit() -> None:
|
||||
"""后端异常默认保活诊断,显式关闭后才退出容器。"""
|
||||
content = (ROOT / "docker" / "entrypoint.sh").read_text(encoding="utf-8")
|
||||
function = content.split(
|
||||
"function diagnostic_keepalive() {", 1
|
||||
)[1].split("\n}", 1)[0]
|
||||
|
||||
assert 'MOVIEPILOT_DOCKER_KEEPALIVE_ON_FAILURE:-true' in function
|
||||
assert 'if [ "${keepalive}" = "false" ]' in function
|
||||
assert 'graceful_exit "$exit_code" "python_exit"' in function
|
||||
assert "容器将保持运行以便执行 moviepilot doctor" in function
|
||||
|
||||
@@ -154,6 +154,25 @@ def test_application_preserves_stop_requested_before_startup(monkeypatch):
|
||||
]
|
||||
|
||||
|
||||
def test_application_does_not_start_server_after_migration_failure(monkeypatch):
|
||||
"""数据库迁移失败时不得启动 API 服务。"""
|
||||
from app import main
|
||||
|
||||
migration_error = RuntimeError("migration failed")
|
||||
server_run = MagicMock()
|
||||
monkeypatch.setattr(main.signal, "signal", MagicMock())
|
||||
monkeypatch.setattr(main, "start_tray", MagicMock())
|
||||
monkeypatch.setattr(main, "init_db", MagicMock())
|
||||
monkeypatch.setattr(main, "update_db", MagicMock(side_effect=migration_error))
|
||||
monkeypatch.setattr(main.Server, "run", server_run)
|
||||
|
||||
with pytest.raises(RuntimeError) as raised:
|
||||
main.run_application()
|
||||
|
||||
assert raised.value is migration_error
|
||||
server_run.assert_not_called()
|
||||
|
||||
|
||||
def test_uvicorn_preserves_stop_requested_before_serve(monkeypatch):
|
||||
"""Uvicorn 启动不能清除数据库初始化阶段已经发布的停止请求"""
|
||||
from app import main
|
||||
|
||||
Reference in New Issue
Block a user