- 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
189 lines
7.1 KiB
Python
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
|