Files
Artificial-Sweetener-Simple…/tests/test_image_file_loader.py
T

124 lines
3.8 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 Comfy authored-image file adapter."""
from __future__ import annotations
from importlib import import_module
from pathlib import Path
import pytest
import torch
from PIL import Image
from simple_syrup.runtime.image_file_loader import ImageFileLoader
def patch_path(
monkeypatch: pytest.MonkeyPatch,
path: Path,
*,
exists: bool = True,
) -> None:
"""Route Comfy annotated-path helpers to one temporary file."""
folder_paths = import_module("folder_paths")
monkeypatch.setattr(import_module("nodes"), "folder_paths", folder_paths)
monkeypatch.setattr(folder_paths, "exists_annotated_filepath", lambda value: exists)
monkeypatch.setattr(folder_paths, "get_annotated_filepath", lambda value: str(path))
def test_load_uses_native_image_loader_and_returns_one_singleton(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Static files are decoded through Comfy's LoadImage behavior."""
path = tmp_path / "image.png"
Image.new("RGB", (3, 2), color=(255, 0, 0)).save(path)
patch_path(monkeypatch, path)
calls: list[str] = []
class FakeNativeLoader:
"""Return one recognizable native image."""
@classmethod
def VALIDATE_INPUTS(cls, value: str) -> bool:
"""Accept the annotated path through the native contract."""
del cls, value
return True
def load_image(self, value: str) -> tuple[torch.Tensor, torch.Tensor]:
"""Record the native loader argument."""
calls.append(value)
return torch.full((1, 2, 3, 3), 0.75), torch.zeros((1, 2, 3))
monkeypatch.setattr(import_module("nodes"), "LoadImage", FakeNativeLoader)
result = ImageFileLoader().load("image.png")
assert calls == ["image.png"]
assert tuple(result.shape) == (1, 2, 3, 3)
assert torch.all(result == 0.75)
def test_available_files_uses_native_image_loader_choices(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The node's file combo is sourced from native LoadImage inputs."""
class FakeNativeLoader:
"""Expose recognizable native widget choices."""
@classmethod
def INPUT_TYPES(cls) -> dict[str, dict[str, tuple[list[str], dict[str, bool]]]]:
"""Return the native image-upload declaration shape."""
del cls
return {"required": {"image": (["z.png", "a.png"], {"image_upload": True})}}
monkeypatch.setattr(import_module("nodes"), "LoadImage", FakeNativeLoader)
assert ImageFileLoader().available_files() == ("z.png", "a.png")
def test_rejects_multiframe_and_missing_files(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""One selected file cannot silently expand into several list items."""
path = tmp_path / "animated.gif"
first = Image.new("RGB", (2, 2), color=0)
second = Image.new("RGB", (2, 2), color=255)
first.save(path, save_all=True, append_images=[second])
patch_path(monkeypatch, path)
loader = ImageFileLoader()
with pytest.raises(ValueError, match="contains 2 frames"):
loader.load("animated.gif")
patch_path(monkeypatch, path, exists=False)
with pytest.raises(ValueError, match="does not exist"):
loader.fingerprint("missing.png")
def test_fingerprint_reads_validated_file_contents(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Content changes invalidate the image-list node cache."""
path = tmp_path / "image.png"
path.write_bytes(b"first")
patch_path(monkeypatch, path)
loader = ImageFileLoader()
first = loader.fingerprint("image.png")
path.write_bytes(b"second")
assert loader.fingerprint("image.png") != first