"""Unit tests for VIN preprocessor.""" import io from unittest.mock import patch, MagicMock import numpy as np import pytest from PIL import Image from app.preprocessors.vin_preprocessor import VinPreprocessor, vin_preprocessor def create_test_image(width: int = 400, height: int = 100, color: int = 128) -> bytes: """Create a simple test image.""" image = Image.new("RGB", (width, height), (color, color, color)) buffer = io.BytesIO() image.save(buffer, format="PNG") return buffer.getvalue() def create_grayscale_test_image(width: int = 400, height: int = 100) -> bytes: """Create a grayscale test image.""" image = Image.new("L", (width, height), 128) buffer = io.BytesIO() image.save(buffer, format="PNG") return buffer.getvalue() class TestVinPreprocessor: """Tests for VIN-optimized preprocessing.""" def test_preprocess_returns_result(self) -> None: """Test basic preprocessing returns a result.""" preprocessor = VinPreprocessor() image_bytes = create_test_image() result = preprocessor.preprocess(image_bytes) assert result.image_bytes is not None assert len(result.image_bytes) > 0 assert "grayscale" in result.preprocessing_applied def test_preprocess_applies_all_steps(self) -> None: """Test preprocessing applies all requested steps.""" preprocessor = VinPreprocessor() image_bytes = create_test_image() result = preprocessor.preprocess( image_bytes, apply_clahe=True, apply_deskew=True, apply_denoise=True, apply_threshold=True, ) assert "grayscale" in result.preprocessing_applied assert "resolution_check" in result.preprocessing_applied assert "clahe" in result.preprocessing_applied assert "deskew" in result.preprocessing_applied assert "denoise" in result.preprocessing_applied assert "threshold" in result.preprocessing_applied def test_preprocess_skips_disabled_steps(self) -> None: """Test preprocessing skips disabled steps.""" preprocessor = VinPreprocessor() image_bytes = create_test_image() result = preprocessor.preprocess( image_bytes, apply_clahe=False, apply_deskew=False, apply_denoise=False, apply_threshold=False, ) assert "clahe" not in result.preprocessing_applied assert "deskew" not in result.preprocessing_applied assert "denoise" not in result.preprocessing_applied assert "threshold" not in result.preprocessing_applied def test_preprocess_output_is_valid_image(self) -> None: """Test preprocessing output is a valid PNG image.""" preprocessor = VinPreprocessor() image_bytes = create_test_image() result = preprocessor.preprocess(image_bytes) # Should be able to open as image output_image = Image.open(io.BytesIO(result.image_bytes)) assert output_image is not None assert output_image.format == "PNG" def test_preprocess_handles_grayscale_input(self) -> None: """Test preprocessing handles grayscale input.""" preprocessor = VinPreprocessor() image_bytes = create_grayscale_test_image() result = preprocessor.preprocess(image_bytes) assert result.image_bytes is not None assert len(result.image_bytes) > 0 def test_preprocess_handles_rgba_input(self) -> None: """Test preprocessing handles RGBA input.""" preprocessor = VinPreprocessor() # Create RGBA image image = Image.new("RGBA", (400, 100), (128, 128, 128, 255)) buffer = io.BytesIO() image.save(buffer, format="PNG") result = preprocessor.preprocess(buffer.getvalue()) assert result.image_bytes is not None assert "convert_rgb" in result.preprocessing_applied def test_singleton_instance(self) -> None: """Test singleton instance is available.""" assert vin_preprocessor is not None assert isinstance(vin_preprocessor, VinPreprocessor) class TestVinPreprocessorDeskew: """Tests for deskew functionality.""" def test_deskew_no_change_for_straight_image(self) -> None: """Test deskew doesn't change a straight image significantly.""" preprocessor = VinPreprocessor() # Create image with horizontal line (no skew) image = np.zeros((100, 400), dtype=np.uint8) image[50, 50:350] = 255 # Horizontal line result = preprocessor._deskew(image) # Shape should be similar (might change slightly due to processing) assert result.shape[0] > 0 assert result.shape[1] > 0 class TestVinPreprocessorCLAHE: """Tests for CLAHE contrast enhancement.""" def test_clahe_improves_contrast(self) -> None: """Test CLAHE changes the image.""" preprocessor = VinPreprocessor() # Create low contrast image image = np.full((100, 400), 128, dtype=np.uint8) result = preprocessor._apply_clahe(image) # Result should be numpy array of same shape assert result.shape == image.shape class TestVinPreprocessorDenoise: """Tests for denoising functionality.""" def test_denoise_reduces_noise(self) -> None: """Test denoising works on noisy image.""" preprocessor = VinPreprocessor() # Create noisy image image = np.random.randint(0, 256, (100, 400), dtype=np.uint8) result = preprocessor._denoise(image) # Should return array of same shape assert result.shape == image.shape class TestVinPreprocessorThreshold: """Tests for adaptive thresholding.""" def test_threshold_creates_binary_image(self) -> None: """Test thresholding creates binary output.""" preprocessor = VinPreprocessor() # Create grayscale image image = np.full((100, 400), 128, dtype=np.uint8) result = preprocessor._adaptive_threshold(image) # Result should be binary (only 0 and 255) unique_values = np.unique(result) assert len(unique_values) <= 2 class TestVinPreprocessorOtsu: """Tests for Otsu's thresholding preprocessing.""" def test_otsu_threshold_creates_binary_image(self) -> None: """Test Otsu's thresholding creates binary output.""" preprocessor = VinPreprocessor() image = np.full((100, 400), 128, dtype=np.uint8) result = preprocessor._otsu_threshold(image) unique_values = np.unique(result) assert len(unique_values) <= 2 def test_preprocess_otsu_returns_result(self) -> None: """Test Otsu preprocessing pipeline returns valid result.""" preprocessor = VinPreprocessor() image_bytes = create_test_image() result = preprocessor.preprocess_otsu(image_bytes) assert result.image_bytes is not None assert len(result.image_bytes) > 0 assert "otsu_threshold" in result.preprocessing_applied assert "grayscale" in result.preprocessing_applied class TestVinPreprocessorResolution: """Tests for resolution upscaling.""" def test_upscale_small_image(self) -> None: """Test small images are upscaled.""" preprocessor = VinPreprocessor() small_image = np.full((50, 200), 128, dtype=np.uint8) result = preprocessor._ensure_minimum_resolution(small_image) assert result.shape[1] >= preprocessor.MIN_WIDTH_FOR_VIN def test_no_upscale_large_image(self) -> None: """Test large images are not upscaled.""" preprocessor = VinPreprocessor() large_image = np.full((200, 800), 128, dtype=np.uint8) result = preprocessor._ensure_minimum_resolution(large_image) assert result.shape == large_image.shape class TestVinRegionDetection: """Tests for VIN region detection.""" def test_detect_vin_region_returns_none_for_empty(self) -> None: """Test region detection returns None for empty image.""" preprocessor = VinPreprocessor() # Solid color image - no regions to detect image_bytes = create_test_image(color=128) result = preprocessor.detect_vin_region(image_bytes) # May return None for uniform image # This is expected behavior assert result is None or result.width > 0