mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
- Remove prewarm_cache startup hook from BaseRecipeRoutes - Add post-scan task management to RecipeScanner for proper cleanup - Ensure LoRA scanner initialization completes before recipe enrichment - Schedule post-scan enrichment after cache initialization - Improve error handling and task cancellation during shutdown
215 lines
6.6 KiB
Python
215 lines
6.6 KiB
Python
"""Smoke tests for the recipe routing scaffolding.
|
|
|
|
The cases keep the registrar/controller contract aligned with
|
|
``docs/architecture/recipe_routes.md`` so future refactors can focus on handler
|
|
logic.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import types
|
|
from collections import Counter
|
|
from typing import Any, Awaitable, Callable, Dict
|
|
|
|
import pytest
|
|
from aiohttp import web
|
|
|
|
from py.routes import base_recipe_routes, recipe_route_registrar, recipe_routes
|
|
from py.services import service_registry
|
|
from py.services.server_i18n import server_i18n
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_service_registry(monkeypatch: pytest.MonkeyPatch):
|
|
"""Ensure each test starts from a clean registry state."""
|
|
|
|
registry = service_registry.ServiceRegistry
|
|
previous_services = dict(registry._services)
|
|
previous_locks = dict(registry._locks)
|
|
registry._services.clear()
|
|
registry._locks.clear()
|
|
try:
|
|
yield
|
|
finally:
|
|
registry._services = previous_services
|
|
registry._locks = previous_locks
|
|
|
|
|
|
def _make_stub_scanner():
|
|
class _StubScanner:
|
|
def __init__(self):
|
|
self._cache = types.SimpleNamespace()
|
|
|
|
async def _lora_get_cached_data(): # pragma: no cover - smoke hook
|
|
return None
|
|
|
|
self._lora_scanner = types.SimpleNamespace(
|
|
get_cached_data=_lora_get_cached_data,
|
|
_hash_index=types.SimpleNamespace(_hash_to_path={}),
|
|
)
|
|
|
|
async def get_cached_data(self, force_refresh: bool = False):
|
|
return self._cache
|
|
|
|
return _StubScanner()
|
|
|
|
|
|
def test_attach_dependencies_resolves_services_once(monkeypatch: pytest.MonkeyPatch):
|
|
registry = service_registry.ServiceRegistry
|
|
|
|
scanner = _make_stub_scanner()
|
|
civitai_client = object()
|
|
filter_calls = Counter()
|
|
|
|
async def fake_get_recipe_scanner():
|
|
return scanner
|
|
|
|
async def fake_get_civitai_client():
|
|
return civitai_client
|
|
|
|
def fake_create_filter():
|
|
filter_calls["create_filter"] += 1
|
|
return object()
|
|
|
|
monkeypatch.setattr(registry, "get_recipe_scanner", fake_get_recipe_scanner)
|
|
monkeypatch.setattr(registry, "get_civitai_client", fake_get_civitai_client)
|
|
monkeypatch.setattr(server_i18n, "create_template_filter", fake_create_filter)
|
|
|
|
async def scenario():
|
|
routes = base_recipe_routes.BaseRecipeRoutes()
|
|
|
|
await routes.attach_dependencies()
|
|
await routes.attach_dependencies() # idempotent
|
|
|
|
assert routes.recipe_scanner is scanner
|
|
assert routes.lora_scanner is scanner._lora_scanner
|
|
assert routes.civitai_client is civitai_client
|
|
assert routes.template_env.filters["t"] is not None
|
|
assert filter_calls["create_filter"] == 1
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
def test_register_startup_hooks_appends_once():
|
|
routes = base_recipe_routes.BaseRecipeRoutes()
|
|
|
|
app = web.Application()
|
|
routes.register_startup_hooks(app)
|
|
routes.register_startup_hooks(app)
|
|
|
|
startup_bound_to_routes = [
|
|
callback for callback in app.on_startup if getattr(callback, "__self__", None) is routes
|
|
]
|
|
|
|
assert routes.attach_dependencies in startup_bound_to_routes
|
|
assert len(startup_bound_to_routes) == 1
|
|
|
|
|
|
def test_to_route_mapping_uses_handler_set():
|
|
class DummyHandlerSet:
|
|
def __init__(self):
|
|
self.calls = 0
|
|
|
|
def to_route_mapping(self):
|
|
self.calls += 1
|
|
|
|
async def render_page(request): # pragma: no cover - simple coroutine
|
|
return web.Response(text="ok")
|
|
|
|
return {"render_page": render_page}
|
|
|
|
class DummyRoutes(base_recipe_routes.BaseRecipeRoutes):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.created = 0
|
|
|
|
def _create_handler_set(self): # noqa: D401 - simple override for test
|
|
self.created += 1
|
|
return DummyHandlerSet()
|
|
|
|
routes = DummyRoutes()
|
|
mapping = routes.to_route_mapping()
|
|
|
|
assert set(mapping.keys()) == {"render_page"}
|
|
assert asyncio.iscoroutinefunction(mapping["render_page"])
|
|
# Cached mapping reused on subsequent calls
|
|
assert routes.to_route_mapping() is mapping
|
|
# Handler set cached for get_handler_owner callers
|
|
assert isinstance(routes.get_handler_owner(), DummyHandlerSet)
|
|
assert routes.created == 1
|
|
|
|
|
|
def test_recipe_route_registrar_binds_every_route():
|
|
class FakeRouter:
|
|
def __init__(self):
|
|
self.calls: list[tuple[str, str, Callable[..., Awaitable[Any]]]] = []
|
|
|
|
def add_get(self, path, handler):
|
|
self.calls.append(("GET", path, handler))
|
|
|
|
def add_post(self, path, handler):
|
|
self.calls.append(("POST", path, handler))
|
|
|
|
def add_put(self, path, handler):
|
|
self.calls.append(("PUT", path, handler))
|
|
|
|
def add_delete(self, path, handler):
|
|
self.calls.append(("DELETE", path, handler))
|
|
|
|
class FakeApp:
|
|
def __init__(self):
|
|
self.router = FakeRouter()
|
|
|
|
app = FakeApp()
|
|
registrar = recipe_route_registrar.RecipeRouteRegistrar(app)
|
|
|
|
handler_mapping = {
|
|
definition.handler_name: object()
|
|
for definition in recipe_route_registrar.ROUTE_DEFINITIONS
|
|
}
|
|
|
|
registrar.register_routes(handler_mapping)
|
|
|
|
assert {
|
|
(method, path)
|
|
for method, path, _ in app.router.calls
|
|
} == {(d.method, d.path) for d in recipe_route_registrar.ROUTE_DEFINITIONS}
|
|
|
|
|
|
def test_recipe_routes_setup_routes_uses_registrar(monkeypatch: pytest.MonkeyPatch):
|
|
registered_mappings: list[Dict[str, Callable[..., Awaitable[Any]]]] = []
|
|
|
|
class DummyRegistrar:
|
|
def __init__(self, app):
|
|
self.app = app
|
|
|
|
def register_routes(self, mapping):
|
|
registered_mappings.append(mapping)
|
|
|
|
monkeypatch.setattr(recipe_routes, "RecipeRouteRegistrar", DummyRegistrar)
|
|
|
|
expected_mapping = {name: object() for name in ("render_page", "list_recipes")}
|
|
|
|
def fake_to_route_mapping(self):
|
|
return expected_mapping
|
|
|
|
monkeypatch.setattr(base_recipe_routes.BaseRecipeRoutes, "to_route_mapping", fake_to_route_mapping)
|
|
monkeypatch.setattr(
|
|
base_recipe_routes.BaseRecipeRoutes,
|
|
"_HANDLER_NAMES",
|
|
tuple(expected_mapping.keys()),
|
|
)
|
|
|
|
app = web.Application()
|
|
recipe_routes.RecipeRoutes.setup_routes(app)
|
|
|
|
assert registered_mappings == [expected_mapping]
|
|
recipe_callbacks = {
|
|
cb
|
|
for cb in app.on_startup
|
|
if isinstance(getattr(cb, "__self__", None), recipe_routes.RecipeRoutes)
|
|
}
|
|
assert {type(cb.__self__) for cb in recipe_callbacks} == {recipe_routes.RecipeRoutes}
|
|
assert {cb.__name__ for cb in recipe_callbacks} == {"attach_dependencies"}
|