Run `ruff format .` at pack root -- 25 pre-existing format-drift files auto-reformatted (whitespace-only: indent + line-break + quote consistency). AST-safe deterministic transform, zero logic delta. Scope (25 file cross-cut): - core/ 4: color_matcher, facial_aligner, luminosity, lut. - nodes/ 4: frequency_node, lens_distortion_node, tone_curve_node, unwrap_face_node. - tests/bench_*.py 11: bench_color_matcher, bench_detail_masker, bench_face_detect, bench_facial_aligner, bench_frequency, bench_lens_distortion, bench_luminosity, bench_mask_refiner, bench_smoother, bench_tone_curve, bench_unwrap_face. - tests/test_*.py 6: test_lens_distortion, test_lut, test_mask_refiner, test_mask_refiner_node, test_smoother, test_smoother_node. Post-format sanity gate: - ruff format --check . -> 57 files already formatted (0 drift). - ruff check . -> All checks passed (format did not introduce lint). - pytest tests/ -q -> 214 passed / 10 skipped (T-20 baseline, zero regression). Sec 2b one-time ownership exception: Claude Code single-agent cross-cuts core/ + nodes/ + tests/ because `ruff format` is a canonical deterministic build-tool transform, not authored logic. Precedent T-208adfd48ratified pattern (trivial-math node single-agent full-stack). Codex would produce byte-identical output if they ran the same command on the same input -- there is no authored logic to review. Non-scope: no tag, no CHANGELOG, no README, no workflow JSON, no pyproject bump. Tag v0.6.0 peelc5ec29fmust remain unchanged post-push (historic invariant, T-14 v0.3.0 re-tag incident lesson carry forward). L-3 lesson ref (per H-20260420-1230 quirk Q-D6 + memory feedback_grep_verify_precedent.md specialization): future Cowork Gate 6.0 drafts grep T-17 / T-18 precedent command string (`pytest + ruff check` canon) before baking task templates. This commit closes the format-drift debt so a future release can opt into strict `pytest + ruff check + ruff format --check` Gate 6.0 if JH tightens the discipline policy. Refs: T-20260420-23, T-20260420-22
163 lines
4.9 KiB
Python
163 lines
4.9 KiB
Python
"""Integration tests for the ComfyUI node wrapper ``JHPixelProEdgeAwareSmoother``.
|
|
|
|
Loads the pack via ``importlib.util.spec_from_file_location`` so relative
|
|
imports inside the hyphenated pack root resolve correctly without requiring
|
|
a running ComfyUI instance.
|
|
"""
|
|
|
|
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["JHPixelProEdgeAwareSmoother"]
|
|
|
|
|
|
def test_node_registered_in_mappings(pack):
|
|
assert "JHPixelProEdgeAwareSmoother" in pack.NODE_CLASS_MAPPINGS
|
|
assert (
|
|
pack.NODE_DISPLAY_NAME_MAPPINGS["JHPixelProEdgeAwareSmoother"] == "Edge-Aware Skin Smoother"
|
|
)
|
|
|
|
|
|
def test_class_metadata(node_cls):
|
|
assert node_cls.CATEGORY == "ComfyUI-JH-PixelPro/filters"
|
|
assert node_cls.RETURN_TYPES == ("IMAGE",)
|
|
assert node_cls.RETURN_NAMES == ("image",)
|
|
assert node_cls.FUNCTION == "run"
|
|
|
|
|
|
def test_input_types_structure(node_cls):
|
|
types = node_cls.INPUT_TYPES()
|
|
assert set(types["required"].keys()) == {
|
|
"image",
|
|
"strength",
|
|
"sigma_color",
|
|
"sigma_space",
|
|
"device",
|
|
"tile_mode",
|
|
}
|
|
assert set(types["optional"].keys()) == {"mask"}
|
|
|
|
assert types["required"]["image"] == ("IMAGE",)
|
|
assert types["optional"]["mask"] == ("MASK",)
|
|
|
|
assert types["required"]["strength"][0] == "FLOAT"
|
|
assert types["required"]["strength"][1]["default"] == 0.4
|
|
assert types["required"]["strength"][1]["min"] == 0.0
|
|
assert types["required"]["strength"][1]["max"] == 1.0
|
|
|
|
assert types["required"]["sigma_color"][1]["default"] == 0.1
|
|
assert types["required"]["sigma_space"][1]["default"] == 6.0
|
|
assert types["required"]["sigma_space"][1]["max"] == 8.0
|
|
|
|
|
|
def test_node_has_device_and_tile_mode_pins(node_cls):
|
|
types = node_cls.INPUT_TYPES()
|
|
|
|
device_spec = types["required"]["device"]
|
|
assert device_spec[0] == ["auto", "cpu", "cuda"]
|
|
assert device_spec[1]["default"] == "auto"
|
|
assert "tooltip" in device_spec[1]
|
|
|
|
tile_spec = types["required"]["tile_mode"]
|
|
assert tile_spec[0] == "BOOLEAN"
|
|
assert tile_spec[1]["default"] is False
|
|
assert "tooltip" in tile_spec[1]
|
|
|
|
|
|
def test_run_bhwc_roundtrip_no_mask(node_cls):
|
|
torch.manual_seed(0)
|
|
image = torch.rand(1, 64, 64, 3, dtype=torch.float32)
|
|
|
|
out = node_cls().run(image, 0.4, 0.1, 6.0, "auto", False)[0]
|
|
|
|
assert isinstance(out, torch.Tensor)
|
|
assert out.shape == image.shape
|
|
assert out.dtype == torch.float32
|
|
assert out.min().item() >= 0.0
|
|
assert out.max().item() <= 1.0
|
|
|
|
|
|
def test_run_with_mask_bhw(node_cls):
|
|
torch.manual_seed(0)
|
|
image = torch.rand(1, 48, 48, 3, dtype=torch.float32)
|
|
# Right half mask = 0 — those pixels must remain exactly the input.
|
|
mask = torch.zeros(1, 48, 48, dtype=torch.float32)
|
|
mask[:, :, :24] = 1.0
|
|
|
|
out = node_cls().run(image, 1.0, 0.1, 6.0, "auto", False, mask=mask)[0]
|
|
|
|
assert out.shape == image.shape
|
|
assert torch.equal(out[:, :, 24:, :], image[:, :, 24:, :])
|
|
|
|
|
|
def test_run_strength_zero_identity(node_cls):
|
|
torch.manual_seed(0)
|
|
image = torch.rand(1, 32, 32, 3, dtype=torch.float32)
|
|
|
|
out = node_cls().run(image, 0.0, 0.1, 6.0, "auto", False)[0]
|
|
|
|
assert torch.equal(out, image)
|
|
|
|
|
|
def test_run_batch(node_cls):
|
|
torch.manual_seed(0)
|
|
image = torch.rand(4, 32, 32, 3, dtype=torch.float32)
|
|
|
|
out = node_cls().run(image, 0.4, 0.1, 6.0, "auto", False)[0]
|
|
|
|
assert out.shape == (4, 32, 32, 3)
|
|
assert out.dtype == torch.float32
|
|
|
|
|
|
def test_run_forwards_tile_mode_to_core(node_cls):
|
|
"""tile_mode=True must route through the core tile path without crashing."""
|
|
torch.manual_seed(0)
|
|
image = torch.rand(1, 32, 32, 3, dtype=torch.float32)
|
|
|
|
out = node_cls().run(image, 0.4, 0.1, 6.0, "auto", True)[0]
|
|
|
|
assert out.shape == image.shape
|
|
assert out.dtype == torch.float32
|
|
|
|
|
|
def test_run_forwards_device_to_core(node_cls):
|
|
"""Passing device='cuda' on a CPU-only runner must surface the core's RuntimeError."""
|
|
if torch.cuda.is_available():
|
|
pytest.skip("CUDA available on this runner — test targets the CPU-only path")
|
|
|
|
image = torch.rand(1, 16, 16, 3, dtype=torch.float32)
|
|
|
|
with pytest.raises(RuntimeError, match="CUDA"):
|
|
node_cls().run(image, 0.4, 0.1, 6.0, "cuda", False)
|