Files
limbicnation fa7e9b7f20 fix: GrabCutRefinement mask unsqueeze, batch fallbacks, FFT div-by-zero, dead field removal
- Move mask.unsqueeze(0) outside else branch in GrabCutRefinement.refine_mask
  (same bug pattern fixed in AutoGrabCutRemover, line 841)
- Replace bare `continue` in batch validation loop with fallback tensor append
  to prevent torch.stack crash on empty list and bbox_metadata misalignment
- Derive exception-handler fallback h,w from input tensor instead of first
  processed item to avoid shape mismatch across batch items
- Guard FFT peak_ratio division against zero mean on uniform images
- Remove dead edge_threshold field from GrabCutParams (never passed from nodes)
- Log warning when Pydantic validation is unavailable instead of silent skip
2026-03-29 08:08:22 +02:00

189 lines
7.1 KiB
Python

"""Tests for Pydantic validation models in src/validation.py."""
from __future__ import annotations
import pytest
from pydantic import ValidationError
class TestMaskParamsEdgeBlur:
"""edge_blur_amount must accept float values without truncation."""
def test_edge_blur_amount_stores_float(self):
"""MaskParams.edge_blur_amount preserves float (e.g. 1.7)."""
from src.validation import MaskParams
p = MaskParams(edge_blur_amount=1.7)
assert isinstance(p.edge_blur_amount, float)
assert p.edge_blur_amount == 1.7
def test_edge_blur_amount_float_half_step(self):
"""MaskParams accepts 0.5 (was previously truncated to 0 by int() cast)."""
from src.validation import MaskParams
p = MaskParams(edge_blur_amount=0.5)
assert p.edge_blur_amount == 0.5
def test_edge_blur_amount_boundary_zero(self):
"""edge_blur_amount accepts 0.0."""
from src.validation import MaskParams
p = MaskParams(edge_blur_amount=0.0)
assert p.edge_blur_amount == 0.0
def test_edge_blur_amount_boundary_max(self):
"""edge_blur_amount accepts 20.0 (upper bound)."""
from src.validation import MaskParams
p = MaskParams(edge_blur_amount=20.0)
assert p.edge_blur_amount == 20.0
def test_edge_blur_amount_rejects_above_max(self):
"""edge_blur_amount > 20.0 raises ValidationError."""
from src.validation import MaskParams
with pytest.raises(ValidationError):
MaskParams(edge_blur_amount=20.1)
def test_edge_blur_amount_rejects_negative(self):
"""edge_blur_amount < 0.0 raises ValidationError."""
from src.validation import MaskParams
with pytest.raises(ValidationError):
MaskParams(edge_blur_amount=-0.1)
class TestGrabCutParams:
"""Tests for GrabCutParams model."""
def test_iterations_min(self):
"""iterations=1 is the minimum accepted value."""
from src.validation import GrabCutParams
p = GrabCutParams(iterations=1)
assert p.iterations == 1
def test_iterations_max(self):
"""iterations=100 is the maximum accepted value."""
from src.validation import GrabCutParams
p = GrabCutParams(iterations=100)
assert p.iterations == 100
def test_iterations_rejects_zero(self):
"""iterations=0 is rejected."""
from src.validation import GrabCutParams
with pytest.raises(ValidationError):
GrabCutParams(iterations=0)
def test_confidence_threshold_range(self):
"""confidence_threshold is clamped to [0.0, 1.0]."""
from src.validation import GrabCutParams
p = GrabCutParams(confidence_threshold=0.99)
assert p.confidence_threshold == 0.99
with pytest.raises(ValidationError):
GrabCutParams(confidence_threshold=1.01)
class TestBBoxParams:
"""Tests for BBoxParams model."""
def test_bbox_safety_margin_range(self):
"""bbox_safety_margin accepts 0-200."""
from src.validation import BBoxParams
p = BBoxParams(bbox_safety_margin=150)
assert p.bbox_safety_margin == 150
with pytest.raises(ValidationError):
BBoxParams(bbox_safety_margin=201)
def test_fallback_margin_percent_range(self):
"""fallback_margin_percent must be between 0.01 and 0.5."""
from src.validation import BBoxParams
p = BBoxParams(fallback_margin_percent=0.3)
assert p.fallback_margin_percent == 0.3
with pytest.raises(ValidationError):
BBoxParams(fallback_margin_percent=0.6)
class TestScalingParams:
"""Tests for ScalingParams model."""
def test_target_long_edge_range(self):
"""target_long_edge accepts 256-4096."""
from src.validation import ScalingParams
p = ScalingParams(target_long_edge=2048)
assert p.target_long_edge == 2048
with pytest.raises(ValidationError):
ScalingParams(target_long_edge=128) # below 256
with pytest.raises(ValidationError):
ScalingParams(target_long_edge=8192) # above 4096
def test_scaling_method_enum(self):
"""scaling_method must be one of the valid set."""
from src.validation import ScalingParams
for method in ("auto", "nearest", "bilinear", "bicubic", "lanczos", "power-of-8"):
p = ScalingParams(scaling_method=method)
assert p.scaling_method == method
with pytest.raises(ValidationError):
ScalingParams(scaling_method="cubic") # not in enum
def test_scaling_method_case_insensitive(self):
"""scaling_method accepts uppercase (node-supplied) values."""
from src.validation import ScalingParams
p = ScalingParams(scaling_method="NEAREST")
assert p.scaling_method == "nearest"
p2 = ScalingParams(scaling_method="BILINEAR")
assert p2.scaling_method == "bilinear"
class TestGrabCutNodeParamsCombined:
"""Tests for the combined GrabCutNodeParams model."""
def test_full_params_combined_model(self):
"""All params pass validation together."""
from src.validation import GrabCutNodeParams
p = GrabCutNodeParams(
iterations=5,
margin=20,
confidence_threshold=0.5,
target_long_edge=1024,
maintain_aspect=True,
scaling_method="auto",
edge_blur_amount=1.5,
invert_mask=False,
edge_refinement_strength=0.7,
bbox_safety_margin=30,
min_bbox_size=64,
fallback_margin_percent=0.2,
binary_threshold=200,
output_format="RGBA",
auto_adjust=False,
)
assert p.iterations == 5
assert p.edge_blur_amount == 1.5
assert isinstance(p.edge_blur_amount, float)
def test_output_format_enum(self):
"""output_format must be RGBA or MASK."""
from src.validation import GrabCutNodeParams
p = GrabCutNodeParams(output_format="MASK")
assert p.output_format == "MASK"
with pytest.raises(ValidationError):
GrabCutNodeParams(output_format="RGB")
def test_invert_mask_bool(self):
"""invert_mask is stored as bool."""
from src.validation import GrabCutNodeParams
p_true = GrabCutNodeParams(invert_mask=True)
p_false = GrabCutNodeParams(invert_mask=False)
assert p_true.invert_mask is True
assert p_false.invert_mask is False
class TestConvenienceValidators:
"""Tests for the convenience validator functions."""
def test_validate_grabcut_params(self):
"""validate_grabcut_params returns GrabCutParams."""
from src.validation import validate_grabcut_params
result = validate_grabcut_params(iterations=3, margin=15)
assert result.iterations == 3
assert result.margin == 15
def test_validate_node_params_raises_on_bad_input(self):
"""validate_node_params raises ValidationError on invalid params."""
from src.validation import validate_node_params
with pytest.raises(ValidationError):
validate_node_params(iterations=0) # iterations must be >= 1