108 lines
3.7 KiB
Python
108 lines
3.7 KiB
Python
# SimpleSyrup - workflow-focused ComfyUI extensions for image generation
|
|
# Copyright (C) 2026 Artificial Sweetener and contributors
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
"""Tests for the Load Image List Comfy v3 node."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import ClassVar
|
|
|
|
import torch
|
|
|
|
from simple_syrup.nodes_v3.load_image_list import LoadImageListV3
|
|
|
|
|
|
class FakeService:
|
|
"""Provide deterministic image-list behavior at the node boundary."""
|
|
|
|
loaded: ClassVar[list[str] | None] = None
|
|
|
|
def available_files(self) -> tuple[str, ...]:
|
|
"""Return node combo choices."""
|
|
|
|
return ("b.png", "a.png")
|
|
|
|
def validate(self, files: str | list[str]) -> None:
|
|
"""Accept deterministic widget values."""
|
|
|
|
del files
|
|
|
|
def load(self, files: str | list[str]) -> list[torch.Tensor]:
|
|
"""Record order and return independently sized singleton images."""
|
|
|
|
ordered = [files] if isinstance(files, str) else files
|
|
type(self).loaded = ordered
|
|
return [torch.zeros((1, 2, 3, 3)), torch.ones((1, 4, 2, 3))]
|
|
|
|
def fingerprint(self, files: str | list[str]) -> str:
|
|
"""Return an order-visible node fingerprint."""
|
|
|
|
ordered = [files] if isinstance(files, str) else files
|
|
return "|".join(ordered)
|
|
|
|
|
|
def test_schema_exposes_native_ordered_image_upload_and_list_output() -> None:
|
|
"""The widget persists filenames while the IMAGE socket is a Comfy list."""
|
|
|
|
original = LoadImageListV3.service_class
|
|
LoadImageListV3.service_class = FakeService # type: ignore[assignment]
|
|
try:
|
|
schema = LoadImageListV3.define_schema()
|
|
finally:
|
|
LoadImageListV3.service_class = original
|
|
|
|
inputs = {value.id: value for value in schema.inputs}
|
|
image = inputs["image"].as_dict()
|
|
assert schema.node_id == "SimpleSyrup.LoadImageList"
|
|
assert schema.display_name == "Load Image List"
|
|
assert schema.has_intermediate_output is False
|
|
assert image["image_upload"] is True
|
|
assert image["allow_batch"] is True
|
|
assert image["image_folder"] == "input"
|
|
assert image["multiselect"] is True
|
|
assert image["default"] == []
|
|
assert schema.outputs[0].io_type == "IMAGE"
|
|
assert schema.outputs[0].is_output_list is True
|
|
|
|
|
|
def test_execute_returns_exact_ordered_python_list_without_batching() -> None:
|
|
"""Comfy receives one singleton tensor for every ordered image position."""
|
|
|
|
original = LoadImageListV3.service_class
|
|
LoadImageListV3.service_class = FakeService # type: ignore[assignment]
|
|
try:
|
|
output = LoadImageListV3.execute(["b.png", "a.png"])
|
|
fingerprint = LoadImageListV3.fingerprint_inputs(["b.png", "a.png"])
|
|
finally:
|
|
LoadImageListV3.service_class = original
|
|
|
|
assert FakeService.loaded == ["b.png", "a.png"]
|
|
assert output.result is not None
|
|
images = output.result[0]
|
|
assert isinstance(images, list)
|
|
assert [tuple(image.shape) for image in images] == [(1, 2, 3, 3), (1, 4, 2, 3)]
|
|
assert fingerprint == "b.png|a.png"
|
|
|
|
|
|
def test_validation_surfaces_actionable_service_failures() -> None:
|
|
"""Comfy validation receives the service's workflow-facing reason."""
|
|
|
|
class RejectingService(FakeService):
|
|
"""Reject an empty image list."""
|
|
|
|
def validate(self, files: str | list[str]) -> None:
|
|
"""Raise one deterministic validation failure."""
|
|
|
|
del files
|
|
raise ValueError("Load Image List requires at least one image file.")
|
|
|
|
original = LoadImageListV3.service_class
|
|
LoadImageListV3.service_class = RejectingService # type: ignore[assignment]
|
|
try:
|
|
result: bool | str = LoadImageListV3.validate_inputs([])
|
|
finally:
|
|
LoadImageListV3.service_class = original
|
|
|
|
assert result == "Load Image List requires at least one image file."
|