Files
wildminder-ComfyUI-DyPE/tests/test_node_validation.py
T

210 lines
7.8 KiB
Python

"""Tests for resolution snapping + input validation (Tier 1: pure unit tests)."""
import pathlib
import pytest
from src.patch_utils import _snap_to_multiple
@pytest.mark.unit
class TestSnapToMultiple:
def test_exact_multiple_unchanged(self):
assert _snap_to_multiple(1024) == 1024
def test_exact_multiple_4096(self):
assert _snap_to_multiple(4096) == 4096
def test_rounds_up_above_half(self):
# 1000 / 16 = 62.5 → round(62.5) = 62 (banker's rounding) or 63
# Python round(62.5) = 62 (rounds to even), so 62*16 = 992
# Actually round(1000/16) = round(62.5) = 62 → 992
result = _snap_to_multiple(1000)
assert result in (992, 1008) # Either is acceptable rounding behavior
assert result % 16 == 0
def test_rounds_down_below_half(self):
# 999 / 16 = 62.4375 → round = 62 → 992
assert _snap_to_multiple(999) == 992
def test_rounds_up_clearly_above(self):
# 1001 / 16 = 62.5625 → round = 63 → 1008
assert _snap_to_multiple(1001) == 1008
def test_minimum_is_multiple(self):
assert _snap_to_multiple(1) == 16
assert _snap_to_multiple(0) == 16
assert _snap_to_multiple(8) == 16
assert _snap_to_multiple(15) == 16
def test_value_16_unchanged(self):
assert _snap_to_multiple(16) == 16
def test_value_17_snaps_to_16(self):
# 17/16 = 1.0625 → round = 1 → 16
assert _snap_to_multiple(17) == 16
def test_value_24_snaps_to_16_or_32(self):
# 24/16 = 1.5 → round(1.5) = 2 (banker's) → 32
result = _snap_to_multiple(24)
assert result in (16, 32)
assert result % 16 == 0
def test_large_value(self):
assert _snap_to_multiple(4095) == 4096
assert _snap_to_multiple(4097) == 4096
def test_custom_multiple_32(self):
assert _snap_to_multiple(100, 32) == 96
assert _snap_to_multiple(113, 32) == 128
def test_custom_multiple_8(self):
# 100/8 = 12.5 → round(12.5) = 12 (banker's rounding) → 96
assert _snap_to_multiple(100, 8) == 96
# 101/8 = 12.625 → round = 13 → 104
assert _snap_to_multiple(101, 8) == 104
def test_always_returns_int(self):
assert isinstance(_snap_to_multiple(1000), int)
assert isinstance(_snap_to_multiple(0), int)
def test_never_returns_zero(self):
assert _snap_to_multiple(0) >= 16
assert _snap_to_multiple(-5) >= 16
@pytest.mark.unit
class TestValidateResolution:
"""W5.1 — shared resolution validator (IMP-002)."""
@pytest.mark.parametrize("w,h", [
(1024, 1024), # standard
(16, 16), # lower bound
(8192, 8192), # upper bound
(2048, 1024), # asymmetric valid
(4096, 4096), # 4K
])
def test_valid_resolutions_pass(self, w, h):
from src.validation import validate_resolution
assert validate_resolution(w, h) is True
@pytest.mark.parametrize("w,h", [
(504, 2000), # REGRESSION (2026-08-25): /8-aligned but not /16 —
# runtime snaps to /16 via _snap_to_multiple; the
# graph validator must NOT reject these.
(1000, 1000), # /8-aligned (1000 % 8 == 0) → accepted
(999, 1024), # 999 not /8 → rejected
(1023, 16),
])
def test_alignment_rules(self, w, h):
"""/8 is the hard VAE requirement; /16-only values are accepted and
snapped at apply time. Only non-/8 values are rejected."""
from src.validation import validate_resolution
result = validate_resolution(w, h)
if w % 8 or h % 8:
assert isinstance(result, str)
assert "multiples of 8" in result
else:
assert result is True
@pytest.mark.parametrize("w,h", [
(0, 1024), # below min
(8208, 1024), # above max (/8-aligned)
(16384, 16384), # way above max
])
def test_out_of_range_rejected(self, w, h):
from src.validation import validate_resolution
result = validate_resolution(w, h)
if w % 8 or h % 8:
assert "multiples of 8" in result
else:
assert isinstance(result, str)
assert "[16, 8192]" in result
def test_boundary_values_exact(self):
from src.validation import validate_resolution
assert validate_resolution(16, 16) is True
assert validate_resolution(8192, 8192) is True
# One step outside.
assert isinstance(validate_resolution(8200, 1024), str)
def test_custom_bounds(self):
from src.validation import validate_resolution
assert validate_resolution(32, 32, min_px=32, max_px=64) is True
assert isinstance(validate_resolution(16, 32, min_px=32, max_px=64), str)
def test_non_integer_inputs_rejected(self):
from src.validation import validate_resolution
assert isinstance(validate_resolution("abc", 1024), str)
assert isinstance(validate_resolution(None, 1024), str)
@pytest.mark.unit
class TestNodeValidateInputsWiring:
"""W5.2 — the resolution nodes implement validate_inputs via the shared
validator (graph-time rejection; runtime snapping retained as
defense-in-depth)."""
_NODES = ("DyPE_FLUX", "SEGA", "SPA")
@pytest.fixture
def init_mod(self):
import importlib.util
import pathlib
root = pathlib.Path(__file__).parent.parent
spec = importlib.util.spec_from_file_location(
"dype_init_wiring", root / "__init__.py")
mod = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(mod)
except Exception:
pytest.skip("__init__.py requires full comfy_api runtime")
return mod
def test_resolution_nodes_have_validate_inputs(self, init_mod):
for node_name in self._NODES:
node_cls = getattr(init_mod, node_name, None)
assert node_cls is not None, f"{node_name} missing"
assert callable(getattr(node_cls, "validate_inputs", None)), (
f"{node_name} does not implement validate_inputs")
def test_validate_inputs_delegates_to_shared_validator(self, init_mod):
from src.validation import validate_resolution
for node_name in self._NODES:
node_cls = getattr(init_mod, node_name)
result = node_cls.validate_inputs(width=1000, height=1000)
assert result == validate_resolution(1000, 1000), (
f"{node_name}.validate_inputs did not delegate to the shared "
f"validator")
def test_validate_inputs_signature_has_no_kwargs(self, init_mod):
"""REGRESSION (2026-08-25): a ``**kwargs`` validate_inputs signature
makes ComfyUI route EVERY input through validation and re-report one
failing string once per input name (16x duplicate errors on SEGA).
The validator must declare named width/height params so failures are
attributed to width/height only."""
import inspect
for node_name in self._NODES:
node_cls = getattr(init_mod, node_name)
sig = inspect.getfullargspec(node_cls.validate_inputs)
assert sig.varkw is None, (
f"{node_name}.validate_inputs must NOT use **kwargs "
f"(ComfyUI duplicates the error per input name)")
assert "width" in sig.args and "height" in sig.args, (
f"{node_name}.validate_inputs must take named width/height")
def test_hap_has_no_resolution_validation(self):
"""HAP has NO width/height inputs by design — no graph-I/O validation
is added there (plan W5.2 decision). This replaces the pre-W5 guard
that forbade validate_inputs entirely."""
content = (pathlib.Path(__file__).parent.parent / "__init__.py").read_text(encoding="utf-8")
assert "class HAP(" in content