202 lines
6.2 KiB
Python
202 lines
6.2 KiB
Python
import json
|
|
import os
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
from aiohttp import web
|
|
|
|
from core.log_system import LogLevel
|
|
from core.routes import settings as settings_routes
|
|
from core.services.settings_service import (
|
|
_backend_log_sort_key,
|
|
_collect_backend_log_files,
|
|
_log_level_setting,
|
|
build_backend_log_export,
|
|
)
|
|
|
|
|
|
class _Routes:
|
|
def __init__(self):
|
|
self.handlers = {}
|
|
|
|
def get(self, path):
|
|
def decorator(handler):
|
|
self.handlers[("GET", path)] = handler
|
|
return handler
|
|
|
|
return decorator
|
|
|
|
def post(self, path):
|
|
def decorator(handler):
|
|
self.handlers[("POST", path)] = handler
|
|
return handler
|
|
|
|
return decorator
|
|
|
|
|
|
def _json_api_endpoint(_name, **_kwargs):
|
|
return lambda handler: handler
|
|
|
|
|
|
def _request(payload):
|
|
return SimpleNamespace(json=AsyncMock(return_value=payload))
|
|
|
|
|
|
def _register(monkeypatch, loaded=None, saved=None):
|
|
routes = _Routes()
|
|
load_settings = MagicMock(
|
|
return_value=loaded or {"backend_logs_enabled": True}
|
|
)
|
|
save_settings = MagicMock(
|
|
return_value=saved or {"backend_logs_enabled": True}
|
|
)
|
|
apply_logging = MagicMock()
|
|
monkeypatch.setattr(settings_routes, "load_resolver_settings", load_settings)
|
|
monkeypatch.setattr(settings_routes, "save_resolver_settings", save_settings)
|
|
monkeypatch.setattr(
|
|
settings_routes,
|
|
"_apply_backend_logging_settings",
|
|
apply_logging,
|
|
)
|
|
settings_routes.register_settings_routes(routes, web, _json_api_endpoint)
|
|
return routes.handlers, load_settings, save_settings, apply_logging
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_settings_routes_return_schema_and_persist_valid_payload(monkeypatch):
|
|
handlers, load_settings, save_settings, apply_logging = _register(
|
|
monkeypatch,
|
|
loaded={"download_backend": "python"},
|
|
saved={"download_backend": "aria2"},
|
|
)
|
|
|
|
get_response = await handlers[("GET", "/model_resolver/settings")](
|
|
SimpleNamespace()
|
|
)
|
|
post_response = await handlers[("POST", "/model_resolver/settings")](
|
|
_request({"download_backend": "aria2"})
|
|
)
|
|
|
|
assert json.loads(get_response.text)["settings"] == {
|
|
"download_backend": "python"
|
|
}
|
|
assert json.loads(get_response.text)["schema"]
|
|
assert post_response.status == 200
|
|
assert json.loads(post_response.text) == {"success": True}
|
|
save_settings.assert_called_once_with({"download_backend": "aria2"})
|
|
assert load_settings.call_count == 2
|
|
assert apply_logging.call_count == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("payload", [None, [], "settings"])
|
|
async def test_settings_route_rejects_non_object_payload(monkeypatch, payload):
|
|
handlers, _, save_settings, _ = _register(monkeypatch)
|
|
|
|
response = await handlers[("POST", "/model_resolver/settings")](
|
|
_request(payload)
|
|
)
|
|
|
|
assert response.status == 400
|
|
assert json.loads(response.text) == {"error": "Expected JSON object"}
|
|
save_settings.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_backend_log_export_includes_rotated_files_and_skips_unrelated_files(
|
|
monkeypatch,
|
|
tmp_path,
|
|
):
|
|
(tmp_path / "azlogs_MResolver.log").write_text("current log", encoding="utf-8")
|
|
(tmp_path / "azlogs_MResolver.1.log").write_text(
|
|
"rotated log", encoding="utf-8"
|
|
)
|
|
(tmp_path / "azlogs_MResolver.log.1").write_text(
|
|
"legacy log", encoding="utf-8"
|
|
)
|
|
(tmp_path / "other.txt").write_text("ignored", encoding="utf-8")
|
|
backend_controller = SimpleNamespace(
|
|
config={"log_dir": str(tmp_path), "log_to_file": True},
|
|
file_handlers={},
|
|
)
|
|
monkeypatch.setattr(
|
|
settings_routes,
|
|
"backend_log_controller",
|
|
backend_controller,
|
|
)
|
|
handlers, _, _, _ = _register(monkeypatch)
|
|
|
|
response = await handlers[("GET", "/model_resolver/logs/backend/export")](
|
|
SimpleNamespace()
|
|
)
|
|
|
|
assert response.status == 200
|
|
assert response.content_type == "text/plain"
|
|
assert "current log" in response.text
|
|
assert "rotated log" in response.text
|
|
assert "legacy log" not in response.text
|
|
assert "ignored" not in response.text
|
|
assert response.headers["Cache-Control"] == "no-store"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_backend_log_export_reports_missing_directory(monkeypatch, tmp_path):
|
|
backend_controller = SimpleNamespace(
|
|
config={
|
|
"log_dir": os.path.join(str(tmp_path), "missing"),
|
|
"log_to_file": False,
|
|
},
|
|
file_handlers={},
|
|
)
|
|
monkeypatch.setattr(
|
|
settings_routes,
|
|
"backend_log_controller",
|
|
backend_controller,
|
|
)
|
|
handlers, _, _, _ = _register(monkeypatch)
|
|
|
|
response = await handlers[("GET", "/model_resolver/logs/backend/export")](
|
|
SimpleNamespace()
|
|
)
|
|
|
|
assert "No backend log files found." in response.text
|
|
assert "File logging: False" in response.text
|
|
|
|
|
|
def test_settings_service_handles_flush_read_and_sorting_edge_cases(
|
|
monkeypatch,
|
|
tmp_path,
|
|
):
|
|
(tmp_path / "azlogs_MResolver.log").write_text("current", encoding="utf-8")
|
|
(tmp_path / "azlogs_Directory.log").mkdir()
|
|
handler = MagicMock()
|
|
handler.flush.side_effect = OSError("flush failed")
|
|
backend_controller = SimpleNamespace(
|
|
config={"log_dir": str(tmp_path), "log_to_file": True},
|
|
file_handlers={"main": handler},
|
|
)
|
|
|
|
files = _collect_backend_log_files(str(tmp_path))
|
|
assert files == [os.path.abspath(tmp_path / "azlogs_MResolver.log")]
|
|
assert _backend_log_sort_key("unrelated.txt") == ("unrelated.txt", 999)
|
|
assert _log_level_setting("not-a-level", default="INFO") == LogLevel.INFO
|
|
|
|
with monkeypatch.context() as patcher:
|
|
patcher.setattr(
|
|
"core.services.settings_service._collect_backend_log_files",
|
|
lambda _log_dir: files,
|
|
)
|
|
patcher.setattr(
|
|
"core.services.settings_service.os",
|
|
SimpleNamespace(
|
|
path=os.path,
|
|
stat=MagicMock(side_effect=OSError("read failed")),
|
|
listdir=os.listdir,
|
|
),
|
|
)
|
|
content = build_backend_log_export(backend_controller)
|
|
|
|
handler.flush.assert_called_once_with()
|
|
assert "Could not read log file: read failed" in content
|