178 lines
5.5 KiB
Python
178 lines
5.5 KiB
Python
"""Integration tests for the ComfyUI node wrapper ``JHPixelProFrequencySeparation``.
|
|
|
|
Does NOT require a running ComfyUI instance — loads the pack via
|
|
``importlib.util.spec_from_file_location`` the same way ComfyUI does, so
|
|
relative imports inside the pack resolve correctly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
_PACK_ROOT = Path(__file__).resolve().parent.parent
|
|
_PACK_MODULE_NAME = "comfyui_jh_pixelpro_under_test"
|
|
|
|
|
|
def _load_pack():
|
|
if _PACK_MODULE_NAME in sys.modules:
|
|
return sys.modules[_PACK_MODULE_NAME]
|
|
spec = importlib.util.spec_from_file_location(
|
|
_PACK_MODULE_NAME,
|
|
_PACK_ROOT / "__init__.py",
|
|
submodule_search_locations=[str(_PACK_ROOT)],
|
|
)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[_PACK_MODULE_NAME] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def pack():
|
|
return _load_pack()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def node_cls(pack):
|
|
return pack.NODE_CLASS_MAPPINGS["JHPixelProFrequencySeparation"]
|
|
|
|
|
|
def test_node_registered_in_mappings(pack):
|
|
assert "JHPixelProFrequencySeparation" in pack.NODE_CLASS_MAPPINGS
|
|
assert (
|
|
pack.NODE_DISPLAY_NAME_MAPPINGS["JHPixelProFrequencySeparation"]
|
|
== "GPU Frequency Separation"
|
|
)
|
|
|
|
|
|
def test_class_metadata(node_cls):
|
|
assert node_cls.CATEGORY == "ComfyUI-JH-PixelPro/filters"
|
|
assert node_cls.RETURN_TYPES == ("IMAGE", "IMAGE")
|
|
assert node_cls.RETURN_NAMES == ("low", "high")
|
|
assert node_cls.FUNCTION == "run"
|
|
|
|
|
|
def test_input_types_structure(node_cls):
|
|
spec = node_cls.INPUT_TYPES()
|
|
assert "required" in spec
|
|
required = spec["required"]
|
|
assert set(required.keys()) == {"image", "radius", "sigma", "precision"}
|
|
|
|
assert required["image"] == ("IMAGE",)
|
|
|
|
radius_type, radius_meta = required["radius"]
|
|
assert radius_type == "INT"
|
|
assert radius_meta["default"] == 8
|
|
assert radius_meta["min"] == 1
|
|
assert radius_meta["max"] == 128
|
|
|
|
sigma_type, sigma_meta = required["sigma"]
|
|
assert sigma_type == "FLOAT"
|
|
assert sigma_meta["default"] == 0.0
|
|
assert sigma_meta["min"] == 0.0
|
|
assert sigma_meta["max"] == 50.0
|
|
|
|
precision_enum, precision_meta = required["precision"]
|
|
assert precision_enum == ["float32", "float16"]
|
|
assert precision_meta["default"] == "float32"
|
|
|
|
|
|
def test_run_shape_and_dtype_float32(node_cls):
|
|
node = node_cls()
|
|
image_bhwc = torch.rand(1, 64, 96, 3)
|
|
|
|
low, high = node.run(image=image_bhwc, radius=8, sigma=0.0, precision="float32")
|
|
|
|
assert low.shape == image_bhwc.shape
|
|
assert high.shape == image_bhwc.shape
|
|
assert low.dtype == torch.float32
|
|
assert high.dtype == torch.float32
|
|
|
|
|
|
def test_run_reconstruction_invariant_bhwc(node_cls):
|
|
node = node_cls()
|
|
image_bhwc = torch.rand(2, 48, 48, 3)
|
|
|
|
low, high = node.run(image=image_bhwc, radius=6, sigma=0.0, precision="float32")
|
|
reconstructed = low + high
|
|
|
|
assert torch.allclose(reconstructed, image_bhwc, atol=1e-5)
|
|
|
|
|
|
def test_run_float16_precision(node_cls):
|
|
node = node_cls()
|
|
image_bhwc = torch.rand(1, 64, 64, 3)
|
|
|
|
low, high = node.run(image=image_bhwc, radius=4, sigma=0.0, precision="float16")
|
|
|
|
assert low.dtype == torch.float16
|
|
assert high.dtype == torch.float16
|
|
assert low.shape == image_bhwc.shape
|
|
reconstructed = (low + high).float()
|
|
assert torch.allclose(reconstructed, image_bhwc, atol=1e-3)
|
|
|
|
|
|
def test_run_rejects_rgba(node_cls):
|
|
node = node_cls()
|
|
image_bhwc_rgba = torch.rand(1, 32, 32, 4)
|
|
|
|
with pytest.raises(ValueError, match="SplitAlpha|4"):
|
|
node.run(image=image_bhwc_rgba, radius=4, sigma=0.0, precision="float32")
|
|
|
|
|
|
def test_run_rejects_grayscale(node_cls):
|
|
node = node_cls()
|
|
image_bhwc_gray = torch.rand(1, 32, 32, 1)
|
|
|
|
with pytest.raises(ValueError, match="3-channel"):
|
|
node.run(image=image_bhwc_gray, radius=4, sigma=0.0, precision="float32")
|
|
|
|
|
|
def test_run_rejects_zero_radius(node_cls):
|
|
node = node_cls()
|
|
image_bhwc = torch.rand(1, 32, 32, 3)
|
|
|
|
with pytest.raises(ValueError, match="radius must be >= 1"):
|
|
node.run(image=image_bhwc, radius=0, sigma=0.0, precision="float32")
|
|
|
|
|
|
def test_run_output_is_contiguous(node_cls):
|
|
"""ComfyUI downstream nodes may assume contiguous BHWC layout."""
|
|
node = node_cls()
|
|
image_bhwc = torch.rand(1, 32, 32, 3)
|
|
|
|
low, high = node.run(image=image_bhwc, radius=4, sigma=0.0, precision="float32")
|
|
|
|
assert low.is_contiguous()
|
|
assert high.is_contiguous()
|
|
|
|
|
|
def test_run_preserves_device(node_cls):
|
|
node = node_cls()
|
|
image_bhwc = torch.rand(1, 32, 32, 3)
|
|
|
|
low, high = node.run(image=image_bhwc, radius=4, sigma=0.0, precision="float32")
|
|
|
|
assert low.device == image_bhwc.device
|
|
assert high.device == image_bhwc.device
|
|
|
|
|
|
def test_recombine_node_round_trip(pack) -> None:
|
|
split = pack.NODE_CLASS_MAPPINGS["JHPixelProFrequencySeparation"]()
|
|
merge = pack.NODE_CLASS_MAPPINGS["JHPixelProFrequencyRecombine"]
|
|
assert pack.NODE_DISPLAY_NAME_MAPPINGS["JHPixelProFrequencyRecombine"] == (
|
|
"Frequency Recombine (low + high)"
|
|
)
|
|
assert merge.CATEGORY == "ComfyUI-JH-PixelPro/filters"
|
|
assert merge.INPUT_TYPES()["required"]["detail"][1]["default"] == 1.0
|
|
image = torch.rand(1, 40, 40, 3, generator=torch.Generator().manual_seed(3))
|
|
low, high = split.run(image, 4, 0.0, "float32")
|
|
(rebuilt,) = merge().run(low, high, 1.0)
|
|
assert rebuilt.shape == image.shape
|
|
assert torch.allclose(rebuilt, image, atol=1e-5)
|