Files
wildminder-ComfyUI-KEEP/tests/test_nodes_execute.py
T

225 lines
7.6 KiB
Python

"""P3.2 / P4.1 - the mock-integration execution of the node classes.
The loader node is a V3 node (P4.1) - its ``execute`` is a classmethod that
returns an ``io.NodeOutput``. The tests drive it with a **stubbed loader**
(the model build is stubbed out - no weights are needed) and assert the
contract of the node: the pack comes back, the widgets reach the loader
unchanged and the second call is served from the loader's cache.
"""
from __future__ import annotations
import importlib
import numpy as np
import pytest
import torch
@pytest.fixture(scope="module")
def node_module():
return importlib.import_module("ComfyUI_KEEP.nodes.keep_face_upscale_image")
@pytest.fixture(scope="module")
def loader_module():
return importlib.import_module("ComfyUI_KEEP.src.keep_model_loader")
@pytest.fixture(scope="module")
def loader_node_module():
return importlib.import_module("ComfyUI_KEEP.nodes.keep_model_loader")
@pytest.fixture
def stub_pack(loader_module, monkeypatch):
"""A real ``KEEPModelPack`` (the node checks the type) with stubs."""
pack = loader_module.KEEPModelPack(
keep_net=None,
face_helper=None,
bg_upscale_model=None,
face_upscale_model=None,
model_type_str="KEEP",
)
calls: list[str] = []
monkeypatch.setattr(pack, "load_device", lambda: calls.append("load_device"))
monkeypatch.setattr(pack, "offload", lambda: calls.append("offload"))
pack.calls = calls
return pack
@pytest.fixture
def stubbed_loader(loader_module, monkeypatch, tmp_path):
"""A real ``KEEPModelLoader`` whose model build is stubbed out.
The stub replaces the four heavy parts of the build (the architecture, the
checkpoint download, the checkpoint load and the face helper) - the cache
logic of the loader itself stays **real**.
"""
class _StubArch:
def __init__(self, **kwargs):
self.kwargs = kwargs
self.loaded = None
def to(self, device):
return self
def load_state_dict(self, state_dict, strict=True):
self.loaded = state_dict
return self
def eval(self):
return self
class _StubHelper:
def __init__(self, **kwargs):
self.kwargs = kwargs
monkeypatch.setattr(loader_module, "ARCH_REGISTRY", {"KEEP": _StubArch})
monkeypatch.setattr(
loader_module,
"load_file_from_url_comfy",
lambda **kwargs: str(tmp_path / "checkpoint.pth"),
)
monkeypatch.setattr(
loader_module.torch,
"load",
lambda *args, **kwargs: {"params_ema": {"weight": torch.zeros(1)}},
)
monkeypatch.setattr(loader_module, "_face_restore_helper", lambda: _StubHelper)
monkeypatch.setattr(
loader_module.keep_paths, "resolve_model_root", lambda *args, **kwargs: str(tmp_path)
)
return loader_module.KEEPModelLoader()
# ---------------------------------------------------------------------------
# P4.1 - the model loader node
# ---------------------------------------------------------------------------
@pytest.mark.mock_integration
def test_the_loader_node_returns_the_pack(loader_node_module, stubbed_loader, monkeypatch):
"""The node returns the pack of the loader as a ``NodeOutput``."""
from comfy_api.latest import io
monkeypatch.setattr(loader_node_module, "GLOBAL_KEEP_MODEL_LOADER", stubbed_loader)
output = loader_node_module.KEEP_ModelLoaderNode.execute(
model="KEEP",
detection_model="retinaface_resnet50",
)
assert isinstance(output, io.NodeOutput)
assert len(output.result) == 1
pack = output.result[0]
assert pack.model_type_str == "KEEP"
assert pack.keep_net is not None
@pytest.mark.mock_integration
def test_the_loader_node_serves_the_second_call_from_the_cache(
loader_node_module, stubbed_loader, monkeypatch
):
"""The second call with the same widgets reuses the cached models."""
monkeypatch.setattr(loader_node_module, "GLOBAL_KEEP_MODEL_LOADER", stubbed_loader)
first = loader_node_module.KEEP_ModelLoaderNode.execute(
model="KEEP", detection_model="retinaface_resnet50"
).result[0]
second = loader_node_module.KEEP_ModelLoaderNode.execute(
model="KEEP", detection_model="retinaface_resnet50"
).result[0]
assert first is not second, "every call returns its own pack"
assert first.keep_net is second.keep_net, "the models are cached"
assert first.face_helper is second.face_helper, "the models are cached"
assert len(stubbed_loader.loaded_models) == 1, stubbed_loader.loaded_models
@pytest.mark.mock_integration
def test_the_loader_node_passes_the_widgets_to_the_loader(
loader_node_module, monkeypatch
):
"""The widget values reach the loader unchanged."""
received: list[dict] = []
class _RecordingLoader:
def load_keep_model_pack(self, **kwargs):
received.append(kwargs)
return "pack"
monkeypatch.setattr(loader_node_module, "GLOBAL_KEEP_MODEL_LOADER", _RecordingLoader())
output = loader_node_module.KEEP_ModelLoaderNode.execute(
model="KEEP",
detection_model="retinaface_mobile0.25",
bg_upscale_model="bg",
face_upscale_model="face",
)
assert output.result == ("pack",)
assert received == [
{
"model_type_str": "KEEP",
"detection_model_str": "retinaface_mobile0.25",
"bg_upscale_model": "bg",
"face_upscale_model": "face",
}
]
# ---------------------------------------------------------------------------
# P3.2 - the single image node (V1 until P4.2)
# ---------------------------------------------------------------------------
@pytest.mark.mock_integration
def test_upscale_face_image_returns_the_processed_image(node_module, stub_pack, monkeypatch):
"""The node returns the processed image and uses the pack once."""
processed = np.full((4, 6, 3), 128, dtype=np.uint8)
processor_calls: list[dict] = []
class _StubProcessor:
def __init__(self, model_pack):
processor_calls.append({"pack": model_pack})
def process_image(self, **kwargs):
processor_calls.append({"kwargs": kwargs})
return processed
monkeypatch.setattr(node_module, "KEEPFaceProcessor", _StubProcessor)
image = torch.rand((1, 4, 6, 3), dtype=torch.float32)
result = node_module.KEEP_FaceUpscaleImageNode().upscale_face_image(
image,
stub_pack,
final_upscale_factor=1.0,
has_aligned_face=True,
only_center_face=True,
draw_bounding_box=False,
)
assert len(result) == 1
output = result[0]
assert tuple(output.shape) == (1, 4, 6, 3), output.shape
# the stub's image (128/255) came back
assert torch.allclose(output, torch.full((1, 4, 6, 3), 128 / 255), atol=1e-3), output
# the processor was built once and called once
assert len(processor_calls) == 2, processor_calls
assert processor_calls[0]["pack"] is stub_pack
assert processor_calls[1]["kwargs"]["final_upscale_factor"] == 1.0
# the pack was loaded and offloaded exactly once each
assert stub_pack.calls == ["load_device", "offload"], stub_pack.calls
@pytest.mark.mock_integration
def test_upscale_face_image_rejects_a_foreign_pack(node_module):
"""A pack of the wrong type is refused (and the error is logged)."""
result = node_module.KEEP_FaceUpscaleImageNode().upscale_face_image(
torch.rand((1, 4, 4, 3), dtype=torch.float32),
object(),
final_upscale_factor=1.0,
has_aligned_face=True,
only_center_face=True,
draw_bounding_box=False,
)
assert result == (None,)