mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
278 lines
9.7 KiB
Python
278 lines
9.7 KiB
Python
"""Tests for other-model path handling in py/config.py."""
|
|
|
|
import logging
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from py import config as config_module
|
|
from py.services.settings_manager import get_settings_manager
|
|
|
|
|
|
def _normalize(path: str) -> str:
|
|
return os.path.normpath(path).replace(os.sep, "/")
|
|
|
|
|
|
def _make_config(**overrides) -> config_module.Config:
|
|
"""Create a bare Config instance for _prepare_other_paths tests."""
|
|
config = config_module.Config.__new__(config_module.Config)
|
|
config._path_mappings = {}
|
|
config._preview_root_paths = set()
|
|
config._cached_fingerprint = None
|
|
config.base_models_roots = []
|
|
config.embeddings_roots = []
|
|
for key, value in overrides.items():
|
|
setattr(config, key, value)
|
|
return config
|
|
|
|
|
|
class TestPrepareOtherPaths:
|
|
"""Unit tests for Config._prepare_other_paths."""
|
|
|
|
def test_maps_each_folder_key_to_sub_type(self, tmp_path):
|
|
roots = {
|
|
"vae": tmp_path / "vae",
|
|
"upscale_models": tmp_path / "upscale_models",
|
|
"text_encoders": tmp_path / "text_encoders",
|
|
"clip": tmp_path / "clip",
|
|
"clip_vision": tmp_path / "clip_vision",
|
|
"controlnet": tmp_path / "controlnet",
|
|
}
|
|
for root in roots.values():
|
|
root.mkdir()
|
|
|
|
config = _make_config()
|
|
unique, sub_type_map, per_key = config._prepare_other_paths(
|
|
{key: [str(root)] for key, root in roots.items()}
|
|
)
|
|
|
|
assert len(unique) == 6
|
|
assert sub_type_map[_normalize(str(roots["vae"]))] == "vae"
|
|
assert sub_type_map[_normalize(str(roots["upscale_models"]))] == "upscaler"
|
|
assert sub_type_map[_normalize(str(roots["text_encoders"]))] == "text_encoder"
|
|
# Legacy ComfyUI 'clip' key maps to text_encoder as well
|
|
assert sub_type_map[_normalize(str(roots["clip"]))] == "text_encoder"
|
|
assert sub_type_map[_normalize(str(roots["clip_vision"]))] == "clip_vision"
|
|
assert sub_type_map[_normalize(str(roots["controlnet"]))] == "controlnet"
|
|
assert per_key["vae"] == [_normalize(str(roots["vae"]))]
|
|
assert per_key["controlnet"] == [_normalize(str(roots["controlnet"]))]
|
|
|
|
def test_missing_or_unknown_keys_are_skipped(self, tmp_path):
|
|
vae_root = tmp_path / "vae"
|
|
vae_root.mkdir()
|
|
|
|
config = _make_config()
|
|
unique, sub_type_map, per_key = config._prepare_other_paths(
|
|
{
|
|
"vae": [str(vae_root)],
|
|
"does_not_exist_key": [str(tmp_path / "nope_dir")],
|
|
"upscale_models": [],
|
|
}
|
|
)
|
|
|
|
assert unique == [_normalize(str(vae_root))]
|
|
assert set(per_key.keys()) == {"vae"}
|
|
|
|
def test_nonexistent_paths_are_filtered(self, tmp_path):
|
|
config = _make_config()
|
|
unique, _, _ = config._prepare_other_paths(
|
|
{"vae": [str(tmp_path / "missing_vae")]}
|
|
)
|
|
assert unique == []
|
|
|
|
def test_cross_category_overlap_warns_and_keeps_first(
|
|
self, tmp_path, caplog
|
|
):
|
|
"""The same physical folder under two categories warns; first wins."""
|
|
shared = tmp_path / "shared"
|
|
shared.mkdir()
|
|
|
|
config = _make_config()
|
|
with caplog.at_level(logging.WARNING, logger=config_module.logger.name):
|
|
unique, sub_type_map, per_key = config._prepare_other_paths(
|
|
{
|
|
"vae": [str(shared)],
|
|
"upscale_models": [str(shared)],
|
|
}
|
|
)
|
|
|
|
assert unique == [_normalize(str(shared))]
|
|
assert sub_type_map[_normalize(str(shared))] == "vae"
|
|
assert "upscale_models" not in per_key
|
|
|
|
warnings = [
|
|
record.message
|
|
for record in caplog.records
|
|
if record.levelname == "WARNING"
|
|
and "multiple other-model categories" in record.message
|
|
]
|
|
assert len(warnings) == 1
|
|
|
|
def test_cross_scanner_overlap_warns_but_keeps_path(self, tmp_path, caplog):
|
|
"""An other root overlapping a checkpoint root warns but stays managed."""
|
|
shared = tmp_path / "shared_models"
|
|
shared.mkdir()
|
|
|
|
config = _make_config(base_models_roots=[_normalize(str(shared))])
|
|
with caplog.at_level(logging.WARNING, logger=config_module.logger.name):
|
|
unique, sub_type_map, _ = config._prepare_other_paths(
|
|
{"vae": [str(shared)]}
|
|
)
|
|
|
|
# Kept on purpose: dropping would silently unmanage the files
|
|
assert unique == [_normalize(str(shared))]
|
|
assert sub_type_map[_normalize(str(shared))] == "vae"
|
|
|
|
warnings = [
|
|
record.message
|
|
for record in caplog.records
|
|
if record.levelname == "WARNING"
|
|
and "overlaps an existing checkpoints/embeddings root" in record.message
|
|
]
|
|
assert len(warnings) == 1
|
|
|
|
def test_no_warning_for_disjoint_roots(self, tmp_path, caplog):
|
|
checkpoints_root = tmp_path / "checkpoints"
|
|
checkpoints_root.mkdir()
|
|
vae_root = tmp_path / "vae"
|
|
vae_root.mkdir()
|
|
|
|
config = _make_config(base_models_roots=[_normalize(str(checkpoints_root))])
|
|
with caplog.at_level(logging.WARNING, logger=config_module.logger.name):
|
|
unique, _, _ = config._prepare_other_paths({"vae": [str(vae_root)]})
|
|
|
|
assert unique == [_normalize(str(vae_root))]
|
|
warnings = [
|
|
record.message
|
|
for record in caplog.records
|
|
if record.levelname == "WARNING" and "overlap" in record.message.lower()
|
|
]
|
|
assert warnings == []
|
|
|
|
|
|
class TestInitOtherPaths:
|
|
"""Config._init_other_paths with mocked folder_paths (plugin + standalone modes).
|
|
|
|
Config only depends on ``folder_paths.get_folder_paths(key)``: ComfyUI in
|
|
plugin mode, or MockFolderPaths serving ``settings.json.folder_paths`` in
|
|
standalone mode. A dict-backed stub therefore covers both.
|
|
"""
|
|
|
|
def _stub_folder_paths(self, monkeypatch, mapping):
|
|
def get_folder_paths(key):
|
|
value = mapping.get(key, [])
|
|
return [value] if isinstance(value, str) else list(value)
|
|
|
|
monkeypatch.setattr(
|
|
config_module.folder_paths, "get_folder_paths", get_folder_paths
|
|
)
|
|
|
|
def test_default_enabled_keys_exclude_controlnet(self, monkeypatch, tmp_path):
|
|
dirs = {}
|
|
for key in (
|
|
"vae",
|
|
"upscale_models",
|
|
"text_encoders",
|
|
"clip",
|
|
"clip_vision",
|
|
"controlnet",
|
|
):
|
|
path = tmp_path / key
|
|
path.mkdir()
|
|
dirs[key] = str(path)
|
|
|
|
self._stub_folder_paths(monkeypatch, dirs)
|
|
|
|
config = _make_config()
|
|
roots = config._init_other_paths()
|
|
|
|
assert _normalize(dirs["controlnet"]) not in roots
|
|
assert _normalize(dirs["controlnet"]) not in config.other_root_subtypes
|
|
for key in ("vae", "upscale_models", "text_encoders", "clip", "clip_vision"):
|
|
assert _normalize(dirs[key]) in roots
|
|
|
|
def test_controlnet_opt_in_via_setting(self, monkeypatch, tmp_path):
|
|
controlnet_dir = tmp_path / "controlnet"
|
|
controlnet_dir.mkdir()
|
|
|
|
self._stub_folder_paths(monkeypatch, {"controlnet": str(controlnet_dir)})
|
|
get_settings_manager().set("enabled_other_folders", ["controlnet"])
|
|
|
|
config = _make_config()
|
|
roots = config._init_other_paths()
|
|
|
|
assert _normalize(str(controlnet_dir)) in roots
|
|
assert (
|
|
config.other_root_subtypes[_normalize(str(controlnet_dir))]
|
|
== "controlnet"
|
|
)
|
|
|
|
def test_unknown_opt_in_keys_are_ignored(self, monkeypatch, tmp_path):
|
|
vae_dir = tmp_path / "vae"
|
|
vae_dir.mkdir()
|
|
|
|
self._stub_folder_paths(monkeypatch, {"vae": str(vae_dir)})
|
|
get_settings_manager().set("enabled_other_folders", ["not_a_real_key", 42])
|
|
|
|
config = _make_config()
|
|
roots = config._init_other_paths()
|
|
|
|
assert roots == [_normalize(str(vae_dir))]
|
|
|
|
def test_apply_library_paths_picks_up_other_keys(self, monkeypatch, tmp_path):
|
|
vae_dir = tmp_path / "vae"
|
|
vae_dir.mkdir()
|
|
|
|
config = _make_config()
|
|
monkeypatch.setattr(config, "_initialize_symlink_mappings", lambda: None)
|
|
|
|
config._apply_library_paths(
|
|
{
|
|
"loras": [],
|
|
"checkpoints": [],
|
|
"unet": [],
|
|
"embeddings": [],
|
|
"vae": [str(vae_dir)],
|
|
}
|
|
)
|
|
|
|
assert config.other_roots == [_normalize(str(vae_dir))]
|
|
assert config.other_root_subtypes == {
|
|
_normalize(str(vae_dir)): "vae"
|
|
}
|
|
assert config.other_folder_roots == {"vae": [_normalize(str(vae_dir))]}
|
|
|
|
|
|
class TestOtherRootsWiring:
|
|
"""other_roots participates in symlink and preview root bookkeeping."""
|
|
|
|
def test_symlink_roots_include_other_roots(self):
|
|
config = _make_config()
|
|
config.loras_roots = ["/loras"]
|
|
config.embeddings_roots = ["/embeddings"]
|
|
config.other_roots = ["/vae"]
|
|
config.extra_loras_roots = []
|
|
config.extra_checkpoints_roots = []
|
|
config.extra_unet_roots = []
|
|
config.extra_embeddings_roots = []
|
|
|
|
assert "/vae" in config._symlink_roots()
|
|
|
|
def test_preview_roots_include_other_roots(self, tmp_path):
|
|
vae_dir = tmp_path / "vae"
|
|
vae_dir.mkdir()
|
|
|
|
config = _make_config()
|
|
config.loras_roots = []
|
|
config.embeddings_roots = []
|
|
config.other_roots = [_normalize(str(vae_dir))]
|
|
config.extra_loras_roots = []
|
|
config.extra_checkpoints_roots = []
|
|
config.extra_unet_roots = []
|
|
config.extra_embeddings_roots = []
|
|
config.recipes_path = ""
|
|
|
|
config._rebuild_preview_roots()
|
|
|
|
assert config.is_preview_path_allowed(str(vae_dir / "model.preview.png"))
|