Files
Jet Huang 1baa1dc48a chore(pack): ruff format normalization (25 file whitespace-only, post-v0.6.0)
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-20 8adfd48 ratified 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 peel c5ec29f must 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
2026-04-20 17:24:43 +07:00

224 lines
5.9 KiB
Python

"""Integration tests for the ComfyUI node wrapper ``JHPixelProSubPixelMaskRefiner``.
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
def _disk_mask(batch: int, size: int, radius: int) -> torch.Tensor:
coords = torch.arange(size, dtype=torch.float32)
yy, xx = torch.meshgrid(coords, coords, indexing="ij")
center = (size - 1) / 2.0
mask = ((yy - center) ** 2 + (xx - center) ** 2 <= radius**2).to(dtype=torch.float32)
return mask.unsqueeze(0).repeat(batch, 1, 1)
@pytest.fixture(scope="module")
def pack():
return _load_pack()
@pytest.fixture(scope="module")
def node_cls(pack):
return pack.NODE_CLASS_MAPPINGS["JHPixelProSubPixelMaskRefiner"]
def test_node_registered_in_mappings(pack):
assert "JHPixelProSubPixelMaskRefiner" in pack.NODE_CLASS_MAPPINGS
assert (
pack.NODE_DISPLAY_NAME_MAPPINGS["JHPixelProSubPixelMaskRefiner"] == "Sub-Pixel Mask Refiner"
)
def test_class_metadata(node_cls):
assert node_cls.CATEGORY == "ComfyUI-JH-PixelPro/mask"
assert node_cls.RETURN_TYPES == ("MASK",)
assert node_cls.RETURN_NAMES == ("refined_mask",)
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()) == {
"mask",
"erosion_radius",
"dilation_radius",
"feather_sigma",
"threshold",
}
assert required["mask"] == ("MASK",)
er_type, er_meta = required["erosion_radius"]
assert er_type == "INT"
assert er_meta["default"] == 2
assert er_meta["min"] == 0
assert er_meta["max"] == 64
dr_type, dr_meta = required["dilation_radius"]
assert dr_type == "INT"
assert dr_meta["default"] == 4
assert dr_meta["min"] == 0
assert dr_meta["max"] == 64
sigma_type, sigma_meta = required["feather_sigma"]
assert sigma_type == "FLOAT"
assert sigma_meta["default"] == 2.0
assert sigma_meta["min"] == 0.1
assert sigma_meta["max"] == 32.0
assert sigma_meta["step"] == 0.1
thr_type, thr_meta = required["threshold"]
assert thr_type == "FLOAT"
assert thr_meta["default"] == 0.5
assert thr_meta["min"] == 0.0
assert thr_meta["max"] == 1.0
assert thr_meta["step"] == 0.01
def test_run_shape_and_dtype_bhw(node_cls):
node = node_cls()
mask = _disk_mask(batch=2, size=64, radius=20)
(refined,) = node.run(
mask=mask,
erosion_radius=2,
dilation_radius=4,
feather_sigma=2.0,
threshold=0.5,
)
assert refined.shape == mask.shape
assert refined.ndim == 3
assert refined.dtype == torch.float32
def test_run_output_in_unit_range(node_cls):
node = node_cls()
mask = _disk_mask(batch=1, size=48, radius=14)
(refined,) = node.run(
mask=mask,
erosion_radius=2,
dilation_radius=4,
feather_sigma=2.0,
threshold=0.5,
)
assert refined.min().item() >= 0.0
assert refined.max().item() <= 1.0
def test_run_preserves_bc1hw_rank(node_cls):
"""Core preserves channel dim when given BC1HW; wrapper must pass it through."""
node = node_cls()
mask_bhw = _disk_mask(batch=1, size=48, radius=14)
mask_bc1hw = mask_bhw.unsqueeze(1)
(refined,) = node.run(
mask=mask_bc1hw,
erosion_radius=2,
dilation_radius=4,
feather_sigma=2.0,
threshold=0.5,
)
assert refined.shape == mask_bc1hw.shape
assert refined.ndim == 4
def test_run_zero_radii_is_clamped_blur(node_cls):
"""er=dr=0 is the documented edge case: no protected zone, output = clamped blur."""
node = node_cls()
mask = _disk_mask(batch=1, size=48, radius=14)
(refined,) = node.run(
mask=mask,
erosion_radius=0,
dilation_radius=0,
feather_sigma=1.5,
threshold=0.5,
)
assert refined.shape == mask.shape
assert refined.dtype == torch.float32
assert refined.min().item() >= 0.0
assert refined.max().item() <= 1.0
def test_run_preserves_device(node_cls):
node = node_cls()
mask = _disk_mask(batch=1, size=32, radius=10)
(refined,) = node.run(
mask=mask,
erosion_radius=1,
dilation_radius=2,
feather_sigma=1.0,
threshold=0.5,
)
assert refined.device == mask.device
def test_run_inside_core_pinned_to_one(node_cls):
"""Pixels well inside the disk (after erosion) must stay exactly 1.0."""
node = node_cls()
mask = _disk_mask(batch=1, size=64, radius=20)
(refined,) = node.run(
mask=mask,
erosion_radius=3,
dilation_radius=6,
feather_sigma=1.5,
threshold=0.5,
)
center = refined.shape[-1] // 2
assert refined[0, center, center].item() == pytest.approx(1.0, abs=1e-6)
def test_run_outside_core_pinned_to_zero(node_cls):
"""Pixels far outside the dilated disk must stay exactly 0.0."""
node = node_cls()
mask = _disk_mask(batch=1, size=64, radius=10)
(refined,) = node.run(
mask=mask,
erosion_radius=2,
dilation_radius=4,
feather_sigma=1.5,
threshold=0.5,
)
assert refined[0, 0, 0].item() == pytest.approx(0.0, abs=1e-6)