326 lines
11 KiB
Python
326 lines
11 KiB
Python
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from aiohttp import web
|
|
|
|
from core.path_utils import is_path_within
|
|
from core.routes.context import RouteContext
|
|
from core.routes.directories import register_directory_routes
|
|
from core.type_utils import to_bool
|
|
|
|
|
|
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 _dedupe(paths, **_kwargs):
|
|
return list(dict.fromkeys(paths))
|
|
|
|
|
|
def _build_routes(**overrides):
|
|
routes = _Routes()
|
|
values = {
|
|
"TEMPLATE_KEY_ALIASES": {"diffusion_models": ("unet",)},
|
|
"asyncio": asyncio,
|
|
"dedupe_local_base_directories": _dedupe,
|
|
"get_category_folder_keys": lambda category: [category],
|
|
"get_comfy_root_path": MagicMock(return_value=r"C:\ComfyUI"),
|
|
"get_default_root_for_category": MagicMock(return_value=None),
|
|
"get_download_directory": MagicMock(return_value=r"C:\models"),
|
|
"get_enabled_download_categories": MagicMock(
|
|
side_effect=lambda categories: list(categories)
|
|
),
|
|
"get_local_path_identity": lambda path: os.path.normcase(
|
|
os.path.abspath(path)
|
|
),
|
|
"is_path_within": is_path_within,
|
|
"get_model_files": MagicMock(return_value=[]),
|
|
"infer_download_path_templates": MagicMock(return_value=[]),
|
|
"invalidate_local_hash_match_cache": MagicMock(),
|
|
"is_civarchive_available": MagicMock(return_value=False),
|
|
"is_lora_manager_archive_available": MagicMock(return_value=False),
|
|
"json_api_endpoint": _json_api_endpoint,
|
|
"load_resolver_settings": MagicMock(return_value={}),
|
|
"normalize_download_category": lambda category: str(category).lower(),
|
|
"prefer_local_base_directory": MagicMock(return_value=True),
|
|
"register_version_routes": MagicMock(),
|
|
"routes": routes,
|
|
"self": SimpleNamespace(logger=MagicMock()),
|
|
"split_path_segments": lambda value: [
|
|
part for part in str(value).replace("\\", "/").split("/") if part
|
|
],
|
|
"to_bool": to_bool,
|
|
"web": web,
|
|
}
|
|
values.update(overrides)
|
|
register_directory_routes(RouteContext(values))
|
|
return routes.handlers, values
|
|
|
|
|
|
def _request(category="checkpoints"):
|
|
return SimpleNamespace(
|
|
match_info={"category": category},
|
|
query={},
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_directories_route_returns_only_configured_download_directories():
|
|
handlers, values = _build_routes(
|
|
get_download_directory=MagicMock(
|
|
side_effect=lambda category: {
|
|
"checkpoints": r"C:\models\checkpoints",
|
|
"loras": "",
|
|
}.get(category)
|
|
)
|
|
)
|
|
folder_paths = SimpleNamespace(
|
|
folder_names_and_paths={
|
|
"checkpoints": ([], set()),
|
|
"loras": ([], set()),
|
|
}
|
|
)
|
|
|
|
with patch.dict(sys.modules, {"folder_paths": folder_paths}):
|
|
response = await handlers[("GET", "/model_resolver/directories")](
|
|
_request()
|
|
)
|
|
|
|
assert json.loads(response.text) == {
|
|
"checkpoints": r"C:\models\checkpoints"
|
|
}
|
|
values["get_enabled_download_categories"].assert_called_once_with(
|
|
["checkpoints", "loras"]
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_root_directories_normalizes_ultralytics_paths_and_skips_yolo():
|
|
handlers, values = _build_routes(
|
|
get_enabled_download_categories=MagicMock(
|
|
return_value=["ultralytics"]
|
|
),
|
|
get_default_root_for_category=MagicMock(
|
|
return_value=r"C:\models\ultralytics"
|
|
),
|
|
)
|
|
folder_paths = MagicMock()
|
|
folder_paths.__file__ = r"C:\ComfyUI\folder_paths.py"
|
|
folder_paths.folder_names_and_paths = {
|
|
"ultralytics": (
|
|
[
|
|
r"C:\models\ultralytics\bbox",
|
|
r"C:\models\ultralytics\segm",
|
|
r"C:\models\ultralytics\yolo",
|
|
],
|
|
set(),
|
|
)
|
|
}
|
|
folder_paths.get_folder_paths.side_effect = lambda category: (
|
|
folder_paths.folder_names_and_paths.get(category, ([], set()))[0]
|
|
)
|
|
|
|
with patch.dict(sys.modules, {"folder_paths": folder_paths}):
|
|
response = await handlers[
|
|
("GET", "/model_resolver/root-directories")
|
|
](_request())
|
|
|
|
roots = json.loads(response.text)
|
|
assert roots["ultralytics"] == [r"C:\models\ultralytics"]
|
|
values["get_default_root_for_category"].assert_called_once_with(
|
|
"ultralytics",
|
|
{},
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_root_directories_resolves_aliases_and_preserves_extra_categories():
|
|
checkpoint_directory = r"C:\models\checkpoints"
|
|
extra_directory = r"C:\models\extra"
|
|
handlers, _ = _build_routes(
|
|
TEMPLATE_KEY_ALIASES={"custom_category": ("checkpoints",)},
|
|
get_enabled_download_categories=MagicMock(
|
|
return_value=["custom_category"]
|
|
),
|
|
)
|
|
folder_paths = MagicMock()
|
|
folder_paths.folder_names_and_paths = {
|
|
"checkpoints": ([checkpoint_directory], set()),
|
|
"extra": ([extra_directory], set()),
|
|
}
|
|
folder_paths.get_folder_paths.side_effect = lambda category: (
|
|
folder_paths.folder_names_and_paths.get(category, ([], set()))[0]
|
|
)
|
|
|
|
with patch.dict(sys.modules, {"folder_paths": folder_paths}):
|
|
response = await handlers[
|
|
("GET", "/model_resolver/root-directories")
|
|
](_request())
|
|
|
|
roots = json.loads(response.text)
|
|
assert roots["custom_category"] == [checkpoint_directory]
|
|
assert roots["extra"] == [extra_directory]
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_capabilities_route_returns_optional_sources_and_node_rules():
|
|
handlers, values = _build_routes()
|
|
|
|
with patch(
|
|
"core.workflow.widgets.NODE_TYPE_MODEL_WIDGET_CATEGORIES",
|
|
{"CheckpointLoaderSimple": ["checkpoints"]},
|
|
):
|
|
response = await handlers[("GET", "/model_resolver/capabilities")](
|
|
_request()
|
|
)
|
|
|
|
body = json.loads(response.text)
|
|
assert body["sources"] == {
|
|
"civarchive": False,
|
|
"lora_manager_archive": False,
|
|
}
|
|
assert body["node_rules"] == {"CheckpointLoaderSimple": ["checkpoints"]}
|
|
from core.type_utils import get_model_category_aliases
|
|
|
|
assert body["category_aliases"] == get_model_category_aliases()
|
|
values["is_civarchive_available"].assert_called_once_with()
|
|
values["is_lora_manager_archive_available"].assert_called_once_with()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subfolders_route_returns_empty_for_unknown_category(tmp_path):
|
|
handlers, values = _build_routes(
|
|
get_category_folder_keys=MagicMock(),
|
|
normalize_download_category=lambda category: "unknown",
|
|
)
|
|
folder_paths = SimpleNamespace(folder_names_and_paths={})
|
|
|
|
with patch.dict(sys.modules, {"folder_paths": folder_paths}):
|
|
response = await handlers[
|
|
("GET", "/model_resolver/subfolders/{category}")
|
|
](_request("not-a-category"))
|
|
|
|
assert json.loads(response.text) == []
|
|
values["get_category_folder_keys"].assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subfolders_route_logs_and_returns_empty_for_unavailable_category():
|
|
get_category_folder_keys = MagicMock(return_value=["checkpoints"])
|
|
handlers, values = _build_routes(
|
|
get_category_folder_keys=get_category_folder_keys
|
|
)
|
|
folder_paths = SimpleNamespace(folder_names_and_paths={"loras": ([], set())})
|
|
|
|
with patch.dict(sys.modules, {"folder_paths": folder_paths}):
|
|
response = await handlers[
|
|
("GET", "/model_resolver/subfolders/{category}")
|
|
](_request("checkpoints"))
|
|
|
|
assert json.loads(response.text) == []
|
|
values["self"].logger.debug.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subfolders_route_ignores_invalid_and_unresolvable_entries(tmp_path):
|
|
base_directory = Path(tmp_path) / "checkpoints"
|
|
base_directory.mkdir()
|
|
missing_directory = Path(tmp_path) / "missing"
|
|
folder_paths = MagicMock()
|
|
folder_paths.folder_names_and_paths = {
|
|
"checkpoints": (
|
|
[str(base_directory), "", str(missing_directory)],
|
|
set(),
|
|
)
|
|
}
|
|
folder_paths.get_folder_paths.side_effect = lambda category: (
|
|
folder_paths.folder_names_and_paths.get(category, ([], set()))[0]
|
|
)
|
|
folder_paths.get_filename_list.return_value = [
|
|
None,
|
|
"model.safetensors",
|
|
"nested/model.safetensors",
|
|
]
|
|
folder_paths.get_full_path.side_effect = RuntimeError("not indexed")
|
|
handlers, _ = _build_routes()
|
|
|
|
with patch.dict(sys.modules, {"folder_paths": folder_paths}):
|
|
response = await handlers[
|
|
("GET", "/model_resolver/subfolders/{category}")
|
|
](_request())
|
|
|
|
assert json.loads(response.text) == [
|
|
{
|
|
"value": "nested",
|
|
"label": "nested",
|
|
"base_directory": "",
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subfolders_route_discovers_nested_directories(tmp_path):
|
|
base_directory = Path(tmp_path) / "checkpoints"
|
|
nested_directory = base_directory / "character" / "pony"
|
|
nested_directory.mkdir(parents=True)
|
|
(base_directory / "character" / "model.safetensors").touch()
|
|
|
|
folder_paths = MagicMock()
|
|
folder_paths.folder_names_and_paths = {
|
|
"checkpoints": ([str(base_directory)], set())
|
|
}
|
|
folder_paths.get_folder_paths.side_effect = lambda category: (
|
|
folder_paths.folder_names_and_paths.get(category, ([], set()))[0]
|
|
)
|
|
folder_paths.get_filename_list.return_value = [
|
|
"character/model.safetensors"
|
|
]
|
|
folder_paths.get_full_path.side_effect = (
|
|
lambda _category, relative_path: str(base_directory / relative_path)
|
|
)
|
|
values = {
|
|
"get_category_folder_keys": MagicMock(return_value=["checkpoints"]),
|
|
"get_comfy_root_path": MagicMock(return_value=str(tmp_path)),
|
|
"get_default_root_for_category": MagicMock(return_value=None),
|
|
"get_download_directory": MagicMock(return_value=str(base_directory)),
|
|
}
|
|
handlers, _ = _build_routes(**values)
|
|
|
|
with patch.dict(sys.modules, {"folder_paths": folder_paths}):
|
|
response = await handlers[
|
|
("GET", "/model_resolver/subfolders/{category}")
|
|
](_request())
|
|
|
|
items = json.loads(response.text)
|
|
assert [item["value"] for item in items] == [
|
|
"character",
|
|
"character/pony",
|
|
]
|
|
assert all(item["base_directory"] == str(base_directory) for item in items)
|