1+ # tests/test_encoder.py
2+
3+ import os
4+ import sys
5+ from pathlib import Path
6+ import pytest
7+ import unittest .mock
8+ import base64
9+ import io
10+ from PIL import Image
11+ import tempfile
12+
13+ # Add the src directory to Python path
14+ src_path = Path (__file__ ).parent .parent / "src"
15+ sys .path .insert (0 , str (src_path ))
16+
17+ # Set up environment variables to avoid connection issues
18+ os .environ .setdefault ('MILVUS_ENDPOINT' , 'http://localhost:19530' )
19+ os .environ .setdefault ('MILVUS_TOKEN' , 'test_token' )
20+ os .environ .setdefault ('COLLECTION_NAME' , 'test_collection' )
21+ os .environ .setdefault ('MODEL_DIM' , '512' )
22+ os .environ .setdefault ('MQTT_BROKER' , 'localhost' )
23+ os .environ .setdefault ('MQTT_PORT' , '1883' )
24+ os .environ .setdefault ('MQTT_TOPIC' , 'test_topic' )
25+ os .environ .setdefault ('CONFIDENCE_THRESHOLD' , '0.4' )
26+
27+ # Import the encoder module directly to avoid loading the server module
28+ import importlib .util
29+ encoder_spec = importlib .util .spec_from_file_location ("encoder" , src_path / "feature_matching" / "encoder.py" )
30+ encoder = importlib .util .module_from_spec (encoder_spec )
31+ encoder_spec .loader .exec_module (encoder )
32+
33+
34+ class TestBase64ImageProcessor :
35+ """Test cases for Base64ImageProcessor"""
36+
37+ def setup_method (self ):
38+ """Set up test fixtures"""
39+ self .processor = encoder .Base64ImageProcessor ()
40+ self .custom_processor = encoder .Base64ImageProcessor (size = (128 , 128 ))
41+
42+ def create_test_image (self , size = (100 , 100 ), mode = "RGB" , color = (255 , 0 , 0 )):
43+ """Helper method to create a test image"""
44+ return Image .new (mode , size , color )
45+
46+ def test_processor_initialization_default_size (self ):
47+ """Test Base64ImageProcessor initialization with default size"""
48+ processor = encoder .Base64ImageProcessor ()
49+ assert processor .size == (224 , 224 )
50+
51+ def test_processor_initialization_custom_size (self ):
52+ """Test Base64ImageProcessor initialization with custom size"""
53+ custom_size = (128 , 256 )
54+ processor = encoder .Base64ImageProcessor (size = custom_size )
55+ assert processor .size == custom_size
56+
57+ def test_process_image_to_base64_basic (self ):
58+ """Test basic image processing to base64"""
59+ # Create a simple test image
60+ test_image = self .create_test_image (size = (100 , 100 ), color = (255 , 0 , 0 ))
61+
62+ # Process the image
63+ result = self .processor .process_image_to_base64 (test_image )
64+
65+ # Verify the result is a string
66+ assert isinstance (result , str )
67+
68+ # Verify it's valid base64
69+ try :
70+ decoded = base64 .b64decode (result )
71+ assert len (decoded ) > 0
72+ except Exception :
73+ pytest .fail ("Result is not valid base64" )
74+
75+ def test_process_image_to_base64_with_custom_size (self ):
76+ """Test image processing with custom processor size"""
77+ test_image = self .create_test_image (size = (200 , 200 ), color = (0 , 255 , 0 ))
78+
79+ # Process with custom processor
80+ result = self .custom_processor .process_image_to_base64 (test_image )
81+
82+ # Verify the result is a string
83+ assert isinstance (result , str )
84+
85+ # Decode the base64 and verify it's a valid image
86+ decoded = base64 .b64decode (result )
87+ processed_image = Image .open (io .BytesIO (decoded ))
88+
89+ # Verify the image has been resized to the custom size
90+ assert processed_image .size == (128 , 128 )
91+
92+ def test_process_image_rgb_conversion (self ):
93+ """Test that images are converted to RGB mode"""
94+ # Create a grayscale image
95+ grayscale_image = self .create_test_image (size = (50 , 50 ), mode = "L" , color = 128 )
96+
97+ # Process the image
98+ result = self .processor .process_image_to_base64 (grayscale_image )
99+
100+ # Decode and verify the result is RGB
101+ decoded = base64 .b64decode (result )
102+ processed_image = Image .open (io .BytesIO (decoded ))
103+
104+ assert processed_image .mode == "RGB"
105+ assert processed_image .size == (224 , 224 )
106+
107+ def test_process_image_rgba_conversion (self ):
108+ """Test that RGBA images are converted to RGB"""
109+ # Create an RGBA image with transparency
110+ rgba_image = self .create_test_image (size = (75 , 75 ), mode = "RGBA" , color = (255 , 0 , 0 , 128 ))
111+
112+ # Process the image
113+ result = self .processor .process_image_to_base64 (rgba_image )
114+
115+ # Decode and verify the result is RGB
116+ decoded = base64 .b64decode (result )
117+ processed_image = Image .open (io .BytesIO (decoded ))
118+
119+ assert processed_image .mode == "RGB"
120+ assert processed_image .size == (224 , 224 )
121+
122+ def test_process_image_resize_functionality (self ):
123+ """Test that images are properly resized"""
124+ # Test with various input sizes
125+ test_sizes = [(50 , 50 ), (100 , 200 ), (300 , 100 ), (500 , 500 )]
126+
127+ for input_size in test_sizes :
128+ test_image = self .create_test_image (size = input_size , color = (0 , 0 , 255 ))
129+ result = self .processor .process_image_to_base64 (test_image )
130+
131+ # Decode and verify size
132+ decoded = base64 .b64decode (result )
133+ processed_image = Image .open (io .BytesIO (decoded ))
134+
135+ assert processed_image .size == (224 , 224 ), f"Failed for input size { input_size } "
136+
137+ def test_process_image_format_output (self ):
138+ """Test that output format is JPEG"""
139+ test_image = self .create_test_image (size = (100 , 100 ), color = (255 , 255 , 0 ))
140+ result = self .processor .process_image_to_base64 (test_image )
141+
142+ # Decode and verify format
143+ decoded = base64 .b64decode (result )
144+ processed_image = Image .open (io .BytesIO (decoded ))
145+
146+ assert processed_image .format == "JPEG"
147+
148+ def test_process_image_deterministic_output (self ):
149+ """Test that the same image produces the same base64 output"""
150+ test_image = self .create_test_image (size = (100 , 100 ), color = (128 , 128 , 128 ))
151+
152+ # Process the same image multiple times
153+ result1 = self .processor .process_image_to_base64 (test_image )
154+ result2 = self .processor .process_image_to_base64 (test_image )
155+
156+ # Results should be identical for the same input
157+ assert result1 == result2
158+
159+ def test_process_image_different_inputs_different_outputs (self ):
160+ """Test that different images produce different base64 outputs"""
161+ image1 = self .create_test_image (size = (100 , 100 ), color = (255 , 0 , 0 ))
162+ image2 = self .create_test_image (size = (100 , 100 ), color = (0 , 255 , 0 ))
163+
164+ result1 = self .processor .process_image_to_base64 (image1 )
165+ result2 = self .processor .process_image_to_base64 (image2 )
166+
167+ # Different images should produce different outputs
168+ assert result1 != result2
169+
170+ def test_process_image_with_real_image_file (self ):
171+ """Test processing with a real image file"""
172+ # Create a temporary image file
173+ with tempfile .NamedTemporaryFile (suffix = ".png" , delete = False ) as temp_file :
174+ temp_image = self .create_test_image (size = (150 , 150 ), color = (100 , 150 , 200 ))
175+ temp_image .save (temp_file .name , "PNG" )
176+ temp_file_path = temp_file .name
177+
178+ try :
179+ # Load the image from file
180+ loaded_image = Image .open (temp_file_path )
181+
182+ # Process it
183+ result = self .processor .process_image_to_base64 (loaded_image )
184+
185+ # Verify the result
186+ assert isinstance (result , str )
187+ assert len (result ) > 0
188+
189+ # Verify it can be decoded back to an image
190+ decoded = base64 .b64decode (result )
191+ processed_image = Image .open (io .BytesIO (decoded ))
192+ assert processed_image .size == (224 , 224 )
193+ assert processed_image .mode == "RGB"
194+
195+ finally :
196+ # Clean up the temporary file
197+ os .unlink (temp_file_path )
198+
199+ def test_process_image_lanczos_resampling (self ):
200+ """Test that LANCZOS resampling is used (this is more of a behavior test)"""
201+ # Create a detailed test image with patterns
202+ test_image = Image .new ("RGB" , (100 , 100 ))
203+ # Create a checkerboard pattern
204+ for x in range (100 ):
205+ for y in range (100 ):
206+ if (x // 10 + y // 10 ) % 2 :
207+ test_image .putpixel ((x , y ), (255 , 255 , 255 ))
208+ else :
209+ test_image .putpixel ((x , y ), (0 , 0 , 0 ))
210+
211+ # Process the image
212+ result = self .processor .process_image_to_base64 (test_image )
213+
214+ # Verify the result is valid
215+ assert isinstance (result , str )
216+ decoded = base64 .b64decode (result )
217+ processed_image = Image .open (io .BytesIO (decoded ))
218+ assert processed_image .size == (224 , 224 )
219+
220+ def test_process_image_preserves_aspect_ratio_behavior (self ):
221+ """Test the behavior of resize (note: PIL resize doesn't preserve aspect ratio by default)"""
222+ # Create a rectangular image
223+ rectangular_image = self .create_test_image (size = (100 , 200 ), color = (255 , 128 , 0 ))
224+
225+ # Process it
226+ result = self .processor .process_image_to_base64 (rectangular_image )
227+
228+ # Decode and verify
229+ decoded = base64 .b64decode (result )
230+ processed_image = Image .open (io .BytesIO (decoded ))
231+
232+ # The image should be exactly the target size (aspect ratio not preserved)
233+ assert processed_image .size == (224 , 224 )
234+
235+ def test_process_image_edge_cases (self ):
236+ """Test edge cases and boundary conditions"""
237+ # Test with very small image
238+ tiny_image = self .create_test_image (size = (1 , 1 ), color = (255 , 255 , 255 ))
239+ result = self .processor .process_image_to_base64 (tiny_image )
240+ assert isinstance (result , str )
241+
242+ # Test with square image matching target size
243+ exact_size_image = self .create_test_image (size = (224 , 224 ), color = (128 , 64 , 192 ))
244+ result = self .processor .process_image_to_base64 (exact_size_image )
245+ assert isinstance (result , str )
246+
247+ # Verify the exact size image
248+ decoded = base64 .b64decode (result )
249+ processed_image = Image .open (io .BytesIO (decoded ))
250+ assert processed_image .size == (224 , 224 )
251+
252+ def test_process_image_invalid_input (self ):
253+ """Test error handling for invalid inputs"""
254+ # Test with None input
255+ with pytest .raises (AttributeError ):
256+ self .processor .process_image_to_base64 (None )
257+
258+ # Test with non-PIL Image object
259+ with pytest .raises (AttributeError ):
260+ self .processor .process_image_to_base64 ("not_an_image" )
261+
262+ def test_multiple_processors_independence (self ):
263+ """Test that multiple processor instances work independently"""
264+ processor1 = encoder .Base64ImageProcessor (size = (64 , 64 ))
265+ processor2 = encoder .Base64ImageProcessor (size = (128 , 128 ))
266+
267+ test_image = self .create_test_image (size = (100 , 100 ), color = (200 , 100 , 50 ))
268+
269+ result1 = processor1 .process_image_to_base64 (test_image )
270+ result2 = processor2 .process_image_to_base64 (test_image )
271+
272+ # Results should be different due to different target sizes
273+ assert result1 != result2
274+
275+ # Verify the actual sizes
276+ decoded1 = base64 .b64decode (result1 )
277+ decoded2 = base64 .b64decode (result2 )
278+
279+ image1 = Image .open (io .BytesIO (decoded1 ))
280+ image2 = Image .open (io .BytesIO (decoded2 ))
281+
282+ assert image1 .size == (64 , 64 )
283+ assert image2 .size == (128 , 128 )
284+
285+
286+ class TestBase64ImageProcessorIntegration :
287+ """Integration tests for Base64ImageProcessor"""
288+
289+ def test_base64_roundtrip_consistency (self ):
290+ """Test that the base64 encoding/decoding roundtrip works correctly"""
291+ processor = encoder .Base64ImageProcessor (size = (100 , 100 ))
292+
293+ # Create a test image with known properties
294+ original_image = Image .new ("RGB" , (50 , 50 ), (123 , 234 , 45 ))
295+
296+ # Process to base64
297+ base64_string = processor .process_image_to_base64 (original_image )
298+
299+ # Decode back to image
300+ decoded_bytes = base64 .b64decode (base64_string )
301+ decoded_image = Image .open (io .BytesIO (decoded_bytes ))
302+
303+ # Verify properties
304+ assert decoded_image .mode == "RGB"
305+ assert decoded_image .size == (100 , 100 )
306+ assert decoded_image .format == "JPEG"
307+
308+ def test_processor_with_various_color_modes (self ):
309+ """Test processor with different PIL image color modes"""
310+ processor = encoder .Base64ImageProcessor ()
311+
312+ # Test different color modes
313+ modes_and_colors = [
314+ ("RGB" , (255 , 128 , 64 )),
315+ ("L" , 128 ), # Grayscale
316+ ("RGBA" , (255 , 128 , 64 , 200 )),
317+ ("P" , 5 ), # Palette mode
318+ ]
319+
320+ for mode , color in modes_and_colors :
321+ if mode == "P" :
322+ # Palette mode requires special handling
323+ test_image = Image .new ("P" , (50 , 50 ))
324+ test_image .putpalette ([i % 256 for i in range (768 )]) # Simple palette
325+ test_image .putpixel ((25 , 25 ), color )
326+ else :
327+ test_image = Image .new (mode , (50 , 50 ), color )
328+
329+ # Process the image
330+ result = processor .process_image_to_base64 (test_image )
331+
332+ # Verify the result
333+ assert isinstance (result , str )
334+
335+ # Decode and verify it's RGB
336+ decoded = base64 .b64decode (result )
337+ processed_image = Image .open (io .BytesIO (decoded ))
338+ assert processed_image .mode == "RGB"
339+ assert processed_image .size == (224 , 224 )
0 commit comments