"""Tests for Image Input Pipeline (F05).""" import cv2 import numpy as np import pytest from gps_denied.core.pipeline import ImageInputPipeline, QueueFullError from gps_denied.schemas.image import ImageBatch @pytest.fixture def pipeline(tmp_path): storage = str(tmp_path / "images") return ImageInputPipeline(storage_dir=storage, max_queue_size=2) def test_batch_validation(pipeline): # VO-05: minimum batch size is now 1 (not 10) # Zero images is still invalid b0 = ImageBatch(images=[], filenames=[], start_sequence=1, end_sequence=0, batch_number=1) val0 = pipeline.validate_batch(b0) assert not val0.valid assert "Batch is empty" in val0.errors # Single image is now valid b1 = ImageBatch(images=[b"fake"], filenames=["AD000001.jpg"], start_sequence=1, end_sequence=1, batch_number=1) val1 = pipeline.validate_batch(b1) assert val1.valid, f"Single-image batch should be valid; errors: {val1.errors}" # 2-image batch — also valid under new rule b2 = ImageBatch( images=[b"1", b"2"], filenames=["AD000001.jpg", "AD000002.jpg"], start_sequence=1, end_sequence=2, batch_number=1, ) val2 = pipeline.validate_batch(b2) assert val2.valid # Large valid batch fake_imgs = [b"fake"] * 10 fake_names = [f"AD{i:06d}.jpg" for i in range(1, 11)] b10 = ImageBatch(images=fake_imgs, filenames=fake_names, start_sequence=1, end_sequence=10, batch_number=1) val10 = pipeline.validate_batch(b10) assert val10.valid @pytest.mark.asyncio async def test_queue_and_process(pipeline): flight_id = "test_f1" # Create valid fake images fake_img_np = np.zeros((10, 10, 3), dtype=np.uint8) _, encoded = cv2.imencode(".jpg", fake_img_np) fake_bytes = encoded.tobytes() fake_imgs = [fake_bytes] * 10 fake_names = [f"AD{i:06d}.jpg" for i in range(1, 11)] b = ImageBatch(images=fake_imgs, filenames=fake_names, start_sequence=1, end_sequence=10, batch_number=1) pipeline.queue_batch(flight_id, b) # Process processed = await pipeline.process_next_batch(flight_id) assert processed is not None assert len(processed.images) == 10 assert processed.images[0].sequence == 1 assert processed.images[-1].sequence == 10 # Status st = pipeline.get_processing_status(flight_id) assert st.total_images == 10 assert st.processed_images == 10 # Sequential get next_img = pipeline.get_next_image(flight_id) assert next_img is not None assert next_img.sequence == 1 # Second get next_img2 = pipeline.get_next_image(flight_id) assert next_img2 is not None assert next_img2.sequence == 2 @pytest.mark.asyncio async def test_exact_sequence_lookup_no_collision(pipeline, tmp_path): """VO-05: sequence 1 must NOT match AD000011.jpg or AD000010.jpg.""" flight_id = "exact_test" fake_img_np = np.zeros((10, 10, 3), dtype=np.uint8) _, encoded = cv2.imencode(".jpg", fake_img_np) fake_bytes = encoded.tobytes() # Sequences 1 and 11 stored in the same flight names = ["AD000001.jpg", "AD000011.jpg"] imgs = [fake_bytes, fake_bytes] b = ImageBatch(images=imgs, filenames=names, start_sequence=1, end_sequence=11, batch_number=1) pipeline.queue_batch(flight_id, b) await pipeline.process_next_batch(flight_id) img1 = pipeline.get_image_by_sequence(flight_id, 1) img11 = pipeline.get_image_by_sequence(flight_id, 11) assert img1 is not None assert img1.filename == "AD000001.jpg", f"Expected AD000001.jpg, got {img1.filename}" assert img11 is not None assert img11.filename == "AD000011.jpg", f"Expected AD000011.jpg, got {img11.filename}" def test_queue_full(pipeline): flight_id = "test_full" fake_imgs = [b"fake"] * 10 fake_names = [f"AD{i:06d}.jpg" for i in range(1, 11)] b = ImageBatch(images=fake_imgs, filenames=fake_names, start_sequence=1, end_sequence=10, batch_number=1) pipeline.queue_batch(flight_id, b) pipeline.queue_batch(flight_id, b) with pytest.raises(QueueFullError): pipeline.queue_batch(flight_id, b)