182 lines
6.2 KiB
Python
182 lines
6.2 KiB
Python
import importlib.util
|
|
import json
|
|
import sys
|
|
import tempfile
|
|
import types
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class _FakeRoutes:
|
|
def get(self, _path):
|
|
return lambda function: function
|
|
|
|
def post(self, _path):
|
|
return lambda function: function
|
|
|
|
|
|
def _load_suite_nodes():
|
|
folder_paths = types.ModuleType("folder_paths")
|
|
folder_paths.filename_list_cache = {}
|
|
folder_paths.supported_pt_extensions = {".safetensors"}
|
|
folder_paths.get_output_directory = lambda: tempfile.gettempdir()
|
|
folder_paths.get_filename_list = lambda _kind: ["a.safetensors"]
|
|
folder_paths.get_folder_paths = lambda _kind: []
|
|
|
|
numpy = types.ModuleType("numpy")
|
|
numpy.ndarray = type("ndarray", (), {})
|
|
numpy.uint8 = int
|
|
|
|
pil = types.ModuleType("PIL")
|
|
pil_image = types.ModuleType("PIL.Image")
|
|
pil_image.Image = type("Image", (), {})
|
|
pil_image.Resampling = types.SimpleNamespace(LANCZOS=1)
|
|
pil_image_ops = types.ModuleType("PIL.ImageOps")
|
|
pil.Image = pil_image
|
|
pil.ImageOps = pil_image_ops
|
|
|
|
aiohttp = types.ModuleType("aiohttp")
|
|
web = types.ModuleType("aiohttp.web")
|
|
web.json_response = lambda payload, status=200: {"payload": payload, "status": status}
|
|
aiohttp.web = web
|
|
|
|
server = types.ModuleType("server")
|
|
|
|
class PromptServer:
|
|
instance = types.SimpleNamespace(
|
|
routes=_FakeRoutes(),
|
|
client_id=None,
|
|
send_sync=lambda *_args, **_kwargs: None,
|
|
)
|
|
|
|
server.PromptServer = PromptServer
|
|
|
|
stubs = {
|
|
"folder_paths": folder_paths,
|
|
"numpy": numpy,
|
|
"PIL": pil,
|
|
"PIL.Image": pil_image,
|
|
"PIL.ImageOps": pil_image_ops,
|
|
"aiohttp": aiohttp,
|
|
"aiohttp.web": web,
|
|
"server": server,
|
|
}
|
|
module_name = "checkpoint_handpicker_suite_nodes_under_test"
|
|
spec = importlib.util.spec_from_file_location(module_name, REPO_ROOT / "suite_nodes.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
with patch.dict(sys.modules, stubs):
|
|
sys.modules[module_name] = module
|
|
try:
|
|
spec.loader.exec_module(module)
|
|
finally:
|
|
sys.modules.pop(module_name, None)
|
|
return module
|
|
|
|
|
|
class CyclerModeTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.suite = _load_suite_nodes()
|
|
|
|
def setUp(self):
|
|
self.checkpoints = ["a.safetensors", "b.safetensors", "c.safetensors"]
|
|
self.statuses = {
|
|
"a.safetensors": "god",
|
|
"b.safetensors": "none",
|
|
"c.safetensors": "favorite",
|
|
}
|
|
self.suite._CYCLER_STATES.clear()
|
|
self.suite._TAB_EXECUTION_STATES.clear()
|
|
self.suite._get_checkpoint_list = lambda: list(self.checkpoints)
|
|
self.suite._get_status = lambda relpath: self.statuses.get(relpath, "none")
|
|
|
|
def cycle(self, *, start="a.safetensors", mode="increment", statuses=None, use_local_list=False):
|
|
return self.suite.CheckpointNameCycler().cycle(
|
|
start,
|
|
mode=mode,
|
|
change_every=1,
|
|
hps_tab_id="test-tab",
|
|
hps_filter_statuses=json.dumps(statuses or []),
|
|
hps_use_local_list="true" if use_local_list else "false",
|
|
hps_settings_revision="1",
|
|
unique_id=1,
|
|
)[0]
|
|
|
|
def cycler_state(self):
|
|
return self.suite._get_cycler_state(self.suite._state_key("test-tab", 1))
|
|
|
|
def test_fixed_ignores_status_filter(self):
|
|
selected = self.cycle(start="b.safetensors", mode="fixed", statuses=["god"])
|
|
self.assertEqual("b.safetensors", selected)
|
|
|
|
def test_increment_uses_status_filter_as_pass_condition(self):
|
|
selected = self.cycle(start="a.safetensors", mode="increment", statuses=["favorite"])
|
|
self.assertEqual("c.safetensors", selected)
|
|
|
|
def test_randomize_selects_from_filtered_candidates(self):
|
|
selected = self.cycle(start="a.safetensors", mode="randomize", statuses=["favorite"])
|
|
self.assertEqual("c.safetensors", selected)
|
|
|
|
def test_shuffle_once_consumes_nonmatching_deck_items(self):
|
|
self.cycler_state()["shuffle_deck"] = ["b.safetensors", "c.safetensors", "a.safetensors"]
|
|
selected = self.cycle(start="a.safetensors", mode="shuffle_once", statuses=["god"])
|
|
self.assertEqual("a.safetensors", selected)
|
|
self.assertEqual([], self.cycler_state()["shuffle_deck"])
|
|
|
|
def test_local_list_overrides_fixed_and_ignores_filter(self):
|
|
self.cycler_state()["local_list"] = ["c.safetensors"]
|
|
selected = self.cycle(
|
|
start="b.safetensors",
|
|
mode="fixed",
|
|
statuses=["god"],
|
|
use_local_list=True,
|
|
)
|
|
self.assertEqual("c.safetensors", selected)
|
|
self.assertEqual([], self.cycler_state()["local_list"])
|
|
|
|
|
|
class NodeContractTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.suite = _load_suite_nodes()
|
|
|
|
def test_public_node_mappings_are_stable(self):
|
|
self.assertEqual(
|
|
{
|
|
"CheckpointListSelector",
|
|
"CheckpointNameCycler",
|
|
"CheckpointStatusTagger",
|
|
"CheckpointTagExportImport",
|
|
"EphemeralPreview",
|
|
"ImageDirPreview",
|
|
},
|
|
set(self.suite.NODE_CLASS_MAPPINGS),
|
|
)
|
|
|
|
def test_node_menu_categories(self):
|
|
main_nodes = (
|
|
self.suite.CheckpointListSelector,
|
|
self.suite.CheckpointNameCycler,
|
|
self.suite.CheckpointStatusTagger,
|
|
self.suite.CheckpointTagExportImport,
|
|
)
|
|
preview_nodes = (self.suite.EphemeralPreview, self.suite.ImageDirPreview)
|
|
self.assertTrue(all(node.CATEGORY == "HandpickerSuite" for node in main_nodes))
|
|
self.assertTrue(all(node.CATEGORY == "HandpickerSuite/Preview" for node in preview_nodes))
|
|
|
|
def test_documented_output_contracts(self):
|
|
self.assertEqual(
|
|
("ckpt_name", "ckpt_name_str", "ckpt_name_safe"),
|
|
self.suite.CheckpointListSelector.RETURN_NAMES,
|
|
)
|
|
self.assertEqual((), self.suite.CheckpointStatusTagger.RETURN_TYPES)
|
|
self.assertEqual((), self.suite.EphemeralPreview.RETURN_TYPES)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|