279 lines
10 KiB
Python
279 lines
10 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""Tests for admin reload models functionality."""
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
import omlx.server # noqa: F401 — ensure server module is imported first
|
|
import omlx.admin.routes as admin_routes
|
|
|
|
|
|
def _setup_mocks(
|
|
engine_pool=None,
|
|
settings_manager=None,
|
|
global_settings=None,
|
|
):
|
|
"""Patch module-level getters and return originals for restoration."""
|
|
originals = {
|
|
"pool": admin_routes._get_engine_pool,
|
|
"settings_manager": admin_routes._get_settings_manager,
|
|
"global_settings": admin_routes._get_global_settings,
|
|
}
|
|
admin_routes._get_engine_pool = lambda: engine_pool
|
|
admin_routes._get_settings_manager = lambda: settings_manager
|
|
admin_routes._get_global_settings = lambda: global_settings
|
|
return originals
|
|
|
|
|
|
def _restore_mocks(originals):
|
|
admin_routes._get_engine_pool = originals["pool"]
|
|
admin_routes._get_settings_manager = originals["settings_manager"]
|
|
admin_routes._get_global_settings = originals["global_settings"]
|
|
|
|
|
|
class TestReloadModels:
|
|
"""Tests for _reload_models() helper."""
|
|
|
|
def test_reload_success(self):
|
|
"""Successful reload: re-reads settings, re-discovers, preloads pinned."""
|
|
pool = MagicMock()
|
|
pool.model_count = 5
|
|
pool.preload_pinned_models = AsyncMock()
|
|
|
|
settings_manager = MagicMock()
|
|
settings_manager._load = MagicMock()
|
|
settings_manager.get_pinned_model_ids = MagicMock(return_value=[])
|
|
|
|
global_settings = MagicMock()
|
|
global_settings.model.model_dirs = ["/path/to/models"]
|
|
global_settings.model.model_dir = "/path/to/models"
|
|
global_settings.get_effective_model_dirs = MagicMock(
|
|
return_value=["/path/to/models"]
|
|
)
|
|
|
|
originals = _setup_mocks(pool, settings_manager, global_settings)
|
|
|
|
mock_server_state = MagicMock()
|
|
mock_server_state.engine_pool = pool
|
|
mock_server_state.settings_manager = settings_manager
|
|
|
|
try:
|
|
with patch.object(omlx.server, "_server_state", mock_server_state):
|
|
with patch(
|
|
"omlx.admin.routes._apply_model_dirs_runtime",
|
|
new_callable=AsyncMock,
|
|
return_value=(True, "Re-discovered 5 models from 1 directory"),
|
|
) as mock_apply:
|
|
success, msg = asyncio.run(admin_routes._reload_models())
|
|
|
|
assert success is True
|
|
assert "5 models" in msg
|
|
settings_manager._load.assert_called_once()
|
|
mock_apply.assert_called_once_with(["/path/to/models"])
|
|
pool.preload_pinned_models.assert_called_once()
|
|
finally:
|
|
_restore_mocks(originals)
|
|
|
|
def test_reload_engine_pool_none(self):
|
|
"""Reload fails when engine pool is not initialized."""
|
|
mock_server_state = MagicMock()
|
|
mock_server_state.engine_pool = None
|
|
|
|
originals = _setup_mocks(
|
|
engine_pool=None,
|
|
settings_manager=None,
|
|
global_settings=MagicMock(),
|
|
)
|
|
|
|
try:
|
|
with patch.object(omlx.server, "_server_state", mock_server_state):
|
|
success, msg = asyncio.run(admin_routes._reload_models())
|
|
assert success is False
|
|
assert "not initialized" in msg
|
|
finally:
|
|
_restore_mocks(originals)
|
|
|
|
def test_reload_global_settings_none(self):
|
|
"""Reload fails when global settings is not available."""
|
|
pool = MagicMock()
|
|
|
|
mock_server_state = MagicMock()
|
|
mock_server_state.engine_pool = pool
|
|
|
|
originals = _setup_mocks(
|
|
engine_pool=pool,
|
|
settings_manager=None,
|
|
global_settings=None,
|
|
)
|
|
|
|
try:
|
|
with patch.object(omlx.server, "_server_state", mock_server_state):
|
|
success, msg = asyncio.run(admin_routes._reload_models())
|
|
assert success is False
|
|
assert "not initialized" in msg
|
|
finally:
|
|
_restore_mocks(originals)
|
|
|
|
def test_reload_apply_dirs_fails(self):
|
|
"""Reload propagates error from _apply_model_dirs_runtime."""
|
|
pool = MagicMock()
|
|
pool.preload_pinned_models = AsyncMock()
|
|
|
|
settings_manager = MagicMock()
|
|
settings_manager._load = MagicMock()
|
|
|
|
global_settings = MagicMock()
|
|
global_settings.model.model_dirs = ["/bad/path"]
|
|
global_settings.model.model_dir = "/bad/path"
|
|
global_settings.get_effective_model_dirs = MagicMock(return_value=["/bad/path"])
|
|
|
|
originals = _setup_mocks(pool, settings_manager, global_settings)
|
|
|
|
mock_server_state = MagicMock()
|
|
mock_server_state.engine_pool = pool
|
|
|
|
try:
|
|
with patch.object(omlx.server, "_server_state", mock_server_state):
|
|
with patch(
|
|
"omlx.admin.routes._apply_model_dirs_runtime",
|
|
new_callable=AsyncMock,
|
|
return_value=(False, "Model directory does not exist: /bad/path"),
|
|
):
|
|
success, msg = asyncio.run(admin_routes._reload_models())
|
|
assert success is False
|
|
assert "does not exist" in msg
|
|
pool.preload_pinned_models.assert_not_called()
|
|
finally:
|
|
_restore_mocks(originals)
|
|
|
|
def test_reload_fallback_to_model_dir(self):
|
|
"""When model_dirs is empty, falls back to model_dir."""
|
|
pool = MagicMock()
|
|
pool.preload_pinned_models = AsyncMock()
|
|
|
|
settings_manager = MagicMock()
|
|
settings_manager._load = MagicMock()
|
|
|
|
global_settings = MagicMock()
|
|
global_settings.model.model_dirs = []
|
|
global_settings.model.model_dir = "/fallback/path"
|
|
global_settings.get_effective_model_dirs = MagicMock(
|
|
return_value=["/fallback/path"]
|
|
)
|
|
|
|
originals = _setup_mocks(pool, settings_manager, global_settings)
|
|
|
|
mock_server_state = MagicMock()
|
|
mock_server_state.engine_pool = pool
|
|
|
|
try:
|
|
with patch.object(omlx.server, "_server_state", mock_server_state):
|
|
with patch(
|
|
"omlx.admin.routes._apply_model_dirs_runtime",
|
|
new_callable=AsyncMock,
|
|
return_value=(True, "Re-discovered 3 models from 1 directory"),
|
|
) as mock_apply:
|
|
success, msg = asyncio.run(admin_routes._reload_models())
|
|
assert success is True
|
|
mock_apply.assert_called_once_with(["/fallback/path"])
|
|
finally:
|
|
_restore_mocks(originals)
|
|
|
|
|
|
class TestApplyModelDirsRuntime:
|
|
"""Tests for runtime model directory application."""
|
|
|
|
def test_rejects_unreadable_model_dir(self, tmp_path, monkeypatch):
|
|
"""Unreadable model dirs should return a user-facing error."""
|
|
model_dir = tmp_path / "models"
|
|
model_dir.mkdir()
|
|
|
|
original_iterdir = Path.iterdir
|
|
|
|
def fake_iterdir(path):
|
|
if path == model_dir.resolve():
|
|
raise PermissionError("Operation not permitted")
|
|
return original_iterdir(path)
|
|
|
|
monkeypatch.setattr(Path, "iterdir", fake_iterdir)
|
|
|
|
mock_server_state = MagicMock()
|
|
mock_server_state.engine_pool = MagicMock()
|
|
mock_server_state.settings_manager = None
|
|
|
|
with patch.object(omlx.server, "_server_state", mock_server_state):
|
|
success, msg = asyncio.run(
|
|
admin_routes._apply_model_dirs_runtime([str(model_dir)])
|
|
)
|
|
|
|
assert success is False
|
|
assert "not readable" in msg
|
|
mock_server_state.engine_pool.discover_models.assert_not_called()
|
|
|
|
def test_skips_unreadable_secondary_model_dir(self, tmp_path, monkeypatch):
|
|
"""Unreadable secondary dirs should not block applying the primary dir."""
|
|
primary = tmp_path / "primary"
|
|
secondary = tmp_path / "secondary"
|
|
primary.mkdir()
|
|
secondary.mkdir()
|
|
|
|
original_iterdir = Path.iterdir
|
|
|
|
def fake_iterdir(path):
|
|
if path == secondary.resolve():
|
|
raise PermissionError("Operation not permitted")
|
|
return original_iterdir(path)
|
|
|
|
monkeypatch.setattr(Path, "iterdir", fake_iterdir)
|
|
monkeypatch.setattr(admin_routes, "_hf_downloader", None)
|
|
monkeypatch.setattr(admin_routes, "_ms_downloader", None)
|
|
monkeypatch.setattr(admin_routes, "_oq_manager", None)
|
|
monkeypatch.setattr(admin_routes, "_hf_uploader", None)
|
|
|
|
pool = MagicMock()
|
|
pool.get_loaded_model_ids.return_value = []
|
|
pool.model_count = 0
|
|
mock_server_state = MagicMock()
|
|
mock_server_state.engine_pool = pool
|
|
mock_server_state.settings_manager = None
|
|
|
|
with patch.object(omlx.server, "_server_state", mock_server_state):
|
|
success, msg = asyncio.run(
|
|
admin_routes._apply_model_dirs_runtime(
|
|
[str(primary), str(secondary)]
|
|
)
|
|
)
|
|
|
|
assert success is True
|
|
assert "from 1 directory" in msg
|
|
pool.discover_models.assert_called_once_with([str(primary.resolve())], [])
|
|
|
|
def test_creates_missing_primary_model_dir(self, tmp_path, monkeypatch):
|
|
"""The primary download target should be created if it is missing."""
|
|
primary = tmp_path / "new-primary"
|
|
|
|
monkeypatch.setattr(admin_routes, "_hf_downloader", None)
|
|
monkeypatch.setattr(admin_routes, "_ms_downloader", None)
|
|
monkeypatch.setattr(admin_routes, "_oq_manager", None)
|
|
monkeypatch.setattr(admin_routes, "_hf_uploader", None)
|
|
|
|
pool = MagicMock()
|
|
pool.get_loaded_model_ids.return_value = []
|
|
pool.model_count = 0
|
|
mock_server_state = MagicMock()
|
|
mock_server_state.engine_pool = pool
|
|
mock_server_state.settings_manager = None
|
|
|
|
with patch.object(omlx.server, "_server_state", mock_server_state):
|
|
success, msg = asyncio.run(
|
|
admin_routes._apply_model_dirs_runtime([str(primary)])
|
|
)
|
|
|
|
assert success is True
|
|
assert "from 1 directory" in msg
|
|
assert primary.is_dir()
|
|
pool.discover_models.assert_called_once_with([str(primary.resolve())], [])
|