Files
limbicnation faab680db5 chore: lint cleanup and artifact removal
- Remove unused imports (ruff F401)
- Fix f-strings without placeholders (ruff F541)
- Resolve E402 import ordering in grabcut_nodes.py
- Fix B108 hardcoded /tmp fallback in nodes.py (use tempfile.gettempdir())
- Delete untracked artifacts: .claude/, node.zip, __pycache__/
- Add .claude/ and *.zip to .gitignore
2026-04-22 04:57:18 +02:00

148 lines
6.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for GrabCutProcessor core functionality."""
from __future__ import annotations
import numpy as np
import pytest
import torch
class TestGrabCutProcessorInit:
"""Tests for GrabCutProcessor.__init__ and YOLO caching."""
def test_yolo_cache_shared_across_instances(self, grabcut_processor):
"""YOLO model is cached at class level and shared between instances."""
from grabcut_remover import GrabCutProcessor
proc_a = grabcut_processor
proc_b = GrabCutProcessor()
# Both should reference the same cached model object
assert proc_a.yolo_model is proc_b.yolo_model
def test_processor_stores_params(self, grabcut_processor):
"""Processor stores all constructor parameters."""
assert grabcut_processor.confidence_threshold == 0.5
assert grabcut_processor.iterations == 2
assert grabcut_processor.margin_pixels == 10
assert grabcut_processor.edge_refinement_strength == 0.0
assert grabcut_processor.edge_blur_amount == 0.0
assert grabcut_processor.binary_threshold == 200
class TestDetectObject:
"""Tests for detect_object method."""
def test_detect_object_no_grad(self, grabcut_processor, sample_image_rgb):
"""detect_object runs inside torch.no_grad() context."""
before = torch.is_grad_enabled()
result = grabcut_processor.detect_object(sample_image_rgb)
after = torch.is_grad_enabled()
assert after == before # gradient mode unchanged
# Result is None when no YOLO model available or no detection
assert result is None or isinstance(result, tuple)
def test_detect_object_returns_tuple_or_none(
self, grabcut_processor, sample_image_rgb
):
"""Return type is either None or (x1, y1, x2, y2, confidence)."""
result = grabcut_processor.detect_object(sample_image_rgb)
if result is not None:
assert isinstance(result, tuple)
assert len(result) == 5
x1, y1, x2, y2, conf = result
assert all(isinstance(v, (int, float)) for v in [x1, y1, x2, y2, conf])
assert 0.0 <= conf <= 1.0
class TestProcessWithGrabCut:
"""Tests for process_with_grabcut method."""
def test_result_dict_structure(self, grabcut_processor, sample_image_rgb):
"""Result dict contains all required keys."""
result = grabcut_processor.process_with_grabcut(sample_image_rgb)
assert isinstance(result, dict)
for key in ("rgba_image", "mask", "bbox", "confidence", "processing_time_ms", "success"):
assert key in result, f"Missing key: {key}"
def test_result_dict_types(self, grabcut_processor, sample_image_rgb):
"""Result values have correct types."""
result = grabcut_processor.process_with_grabcut(sample_image_rgb)
assert result["rgba_image"] is None or isinstance(result["rgba_image"], np.ndarray)
assert result["mask"] is None or isinstance(result["mask"], np.ndarray)
assert isinstance(result["confidence"], float)
assert isinstance(result["processing_time_ms"], int)
assert isinstance(result["success"], bool)
def test_process_with_grabcut_no_grad(self, grabcut_processor, sample_image_rgb):
"""No gradient graph is built during processing."""
before = torch.is_grad_enabled()
grabcut_processor.process_with_grabcut(sample_image_rgb)
after = torch.is_grad_enabled()
assert after == before
class TestBBoxValidation:
"""Tests for _validate_and_fix_bbox method."""
def test_bbox_validates_min_size(self, grabcut_processor):
"""BBox smaller than min_bbox_size is expanded."""
# 10×10 bbox on 256×256 image with min_bbox_size=64
raw_bbox = (100, 100, 110, 110)
fixed = grabcut_processor._validate_and_fix_bbox(raw_bbox, (256, 256))
x1, y1, x2, y2 = fixed
assert (x2 - x1) >= 64
assert (y2 - y1) >= 64
def test_bbox_within_image_bounds(self, grabcut_processor):
"""Fixed bbox is clipped to image dimensions."""
raw_bbox = (0, 0, 300, 300)
h, w = 256, 256
x1, y1, x2, y2 = grabcut_processor._validate_and_fix_bbox(raw_bbox, (h, w))
assert 0 <= x1 < x2 <= w
assert 0 <= y1 < y2 <= h
class TestRefineEdges:
"""Tests for refine_edges method."""
def test_refine_edges_returns_uint8_mask(self, grabcut_processor, sample_mask_hw):
"""refine_edges returns uint8 array with 0/255 values."""
rgb = np.zeros((256, 256, 3), dtype=np.uint8)
result = grabcut_processor.refine_edges(sample_mask_hw, rgb)
assert result.dtype == np.uint8
assert result.shape == sample_mask_hw.shape
def test_refine_edges_zero_strength_returns_input(
self, grabcut_processor, sample_mask_hw
):
"""With zero refinement strength, mask is returned unchanged (type may differ)."""
grabcut_processor.edge_refinement_strength = 0.0
grabcut_processor.edge_blur_amount = 0.0
rgb = np.zeros((256, 256, 3), dtype=np.uint8)
result = grabcut_processor.refine_edges(sample_mask_hw, rgb)
assert result.shape == sample_mask_hw.shape
class TestGPUContext:
"""Tests verifying GPU memory management patterns."""
def test_empty_cache_called_after_detect_object(
self, grabcut_processor, sample_image_rgb
):
"""CUDA cache is cleared after detect_object when CUDA is available."""
if not torch.cuda.is_available():
pytest.skip("CUDA not available")
torch.cuda.empty_cache()
torch.cuda.reset_peak_memory_stats()
grabcut_processor.detect_object(sample_image_rgb)
# Should not raise — empty_cache was called
def test_process_no_grad_active(self, grabcut_processor, sample_image_rgb):
"""torch.no_grad() is active during process_with_grabcut."""
enabled_before = torch.is_grad_enabled()
grabcut_processor.process_with_grabcut(sample_image_rgb)
# Gradient mode should be unchanged after decorated method returns
assert torch.is_grad_enabled() == enabled_before