import pytest from unittest.mock import Mock from datetime import datetime, timedelta from f02_1_flight_lifecycle_manager import ( FlightLifecycleManager, Flight, GPSPoint, CameraParameters, UserFixRequest, FlightStatusUpdate, Waypoint, Geofences, Polygon ) @pytest.fixture def manager(): return FlightLifecycleManager(coordinate_transformer=Mock()) @pytest.fixture def valid_flight_data(): return { "flight_name": "Test Mission", "start_gps": {"lat": 48.0, "lon": 37.0}, "altitude_m": 400.0, "camera_params": { "focal_length_mm": 25.0, "sensor_width_mm": 23.5, "resolution": {"width": 6000, "height": 4000} } } @pytest.fixture def full_manager(): """Manager with all dependency mocks injected for initialization testing.""" return FlightLifecycleManager( db_adapter=Mock(), orchestrator=Mock(), config_manager=Mock(), model_manager=Mock(), satellite_manager=Mock(), place_recognition=Mock() ) class TestFlightLifecycleManager: # --- Feature 02.1.01: Flight & Waypoint Management --- def test_create_flight_success(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) assert flight_id is not None flight = manager.get_flight(flight_id) assert flight is not None assert flight.flight_name == "Test Mission" assert flight.state == "prefetching" def test_create_flight_invalid_gps(self, manager, valid_flight_data): valid_flight_data["start_gps"]["lat"] = 100.0 # Invalid latitude > 90 with pytest.raises(ValueError): manager.create_flight(valid_flight_data) def test_get_flight_not_found(self, manager): assert manager.get_flight("nonexistent") is None def test_delete_flight(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) assert manager.delete_flight(flight_id) is True assert manager.get_flight(flight_id) is None def test_delete_flight_not_found(self, manager): assert manager.delete_flight("nonexistent") is False def test_update_flight_status(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) success = manager.update_flight_status(flight_id, FlightStatusUpdate(status="completed")) assert success is True assert manager.get_flight(flight_id).state == "completed" def test_get_flight_metadata(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) metadata = manager.get_flight_metadata(flight_id) assert metadata is not None assert metadata["flight_name"] == "Test Mission" assert "created_at" in metadata def test_validate_waypoint(self, manager): valid_wp = Waypoint(id="w1", lat=45.0, lon=30.0, confidence=1.0, timestamp=datetime.utcnow()) invalid_wp = Waypoint(id="w2", lat=95.0, lon=30.0, confidence=1.0, timestamp=datetime.utcnow()) assert manager.validate_waypoint(valid_wp).is_valid is True assert manager.validate_waypoint(invalid_wp).is_valid is False def test_update_waypoint(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) valid_wp = Waypoint(id="w1", lat=45.0, lon=30.0, confidence=1.0, timestamp=datetime.utcnow()) invalid_wp = Waypoint(id="w2", lat=95.0, lon=30.0, confidence=1.0, timestamp=datetime.utcnow()) assert manager.update_waypoint(flight_id, "w1", valid_wp) is True assert manager.update_waypoint(flight_id, "w2", invalid_wp) is False def test_batch_update_waypoints(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) wps = [ Waypoint(id="w1", lat=45.0, lon=30.0, confidence=1.0, timestamp=datetime.utcnow()), Waypoint(id="w2", lat=95.0, lon=30.0, confidence=1.0, timestamp=datetime.utcnow()) # Invalid ] result = manager.batch_update_waypoints(flight_id, wps) assert result.success is False assert result.updated_count == 1 assert "w2" in result.failed_ids def test_validate_geofence(self, manager): valid_geo = Geofences(polygons=[ Polygon(north_west=GPSPoint(lat=50.0, lon=30.0), south_east=GPSPoint(lat=40.0, lon=40.0)) ]) invalid_geo = Geofences(polygons=[ Polygon(north_west=GPSPoint(lat=95.0, lon=30.0), south_east=GPSPoint(lat=40.0, lon=40.0)) ]) assert manager.validate_geofence(valid_geo).is_valid is True assert manager.validate_geofence(invalid_geo).is_valid is False def test_validate_flight_continuity(self, manager): base_time = datetime.utcnow() wps_valid = [ Waypoint(id="w1", lat=45.0, lon=30.0, confidence=1.0, timestamp=base_time), Waypoint(id="w2", lat=45.1, lon=30.1, confidence=1.0, timestamp=base_time + timedelta(seconds=10)) ] wps_invalid = [ Waypoint(id="w1", lat=45.0, lon=30.0, confidence=1.0, timestamp=base_time), Waypoint(id="w2", lat=45.1, lon=30.1, confidence=1.0, timestamp=base_time + timedelta(seconds=400)) # Gap > 300s ] assert manager.validate_flight_continuity(wps_valid).is_valid is True assert manager.validate_flight_continuity(wps_invalid).is_valid is False # --- Feature 02.1.02: Processing Delegation --- def test_queue_images_success(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) # Queuing triggers processing delegation success = manager.queue_images(flight_id, ["image1", "image2"]) assert success is True flight = manager.get_flight(flight_id) assert flight.state == "processing" assert flight_id in manager.active_engines def test_queue_images_reuses_engine(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) manager.queue_images(flight_id, ["batch_1"]) engine_ref1 = manager._get_active_engine(flight_id) manager.queue_images(flight_id, ["batch_2"]) engine_ref2 = manager._get_active_engine(flight_id) assert engine_ref1 is engine_ref2 # Ensure engine instances are reused def test_queue_images_invalid_flight(self, manager): assert manager.queue_images("nonexistent", []) is False def test_handle_user_fix_success(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) manager.queue_images(flight_id, []) # Manually transition flight state to simulate tracked loss block flight = manager.get_flight(flight_id) flight.state = "blocked" fix_data = UserFixRequest( frame_id=1, uav_pixel=(500.0, 500.0), satellite_gps=GPSPoint(lat=48.0, lon=37.0) ) result = manager.handle_user_fix(flight_id, fix_data) assert result["status"] == "success" # State should be restored to processing assert manager.get_flight(flight_id).state == "processing" def test_handle_user_fix_not_blocked(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) # The initial state is prefetching (not blocked) fix_data = UserFixRequest(frame_id=1, uav_pixel=(500.0, 500.0), satellite_gps=GPSPoint(lat=48.0, lon=37.0)) result = manager.handle_user_fix(flight_id, fix_data) assert result["status"] == "error" assert "not in blocked state" in result["message"].lower() def test_handle_user_fix_invalid_data(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) flight = manager.get_flight(flight_id) flight.state = "blocked" # Invalid pixel mapping (-100, 500) fix_data = UserFixRequest(frame_id=1, uav_pixel=(-100.0, 500.0), satellite_gps=GPSPoint(lat=48.0, lon=37.0)) result = manager.handle_user_fix(flight_id, fix_data) assert result["status"] == "error" assert "invalid fix data" in result["message"].lower() def test_handle_user_fix_no_active_engine(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) flight = manager.get_flight(flight_id) flight.state = "blocked" # Do NOT queue images, so no engine is created fix_data = UserFixRequest(frame_id=1, uav_pixel=(500.0, 500.0), satellite_gps=GPSPoint(lat=48.0, lon=37.0)) result = manager.handle_user_fix(flight_id, fix_data) assert result["status"] == "error" assert "no active engine" in result["message"].lower() def test_convert_object_to_gps(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) manager.queue_images(flight_id, []) # Require an active engine instance manager.f13_transformer.image_object_to_gps.return_value = GPSPoint(lat=48.0, lon=37.0) gps = manager.convert_object_to_gps(flight_id, 1, (100.0, 100.0)) assert gps is not None assert hasattr(gps, 'lat') def test_create_client_stream(self, manager, valid_flight_data): flight_id = manager.create_flight(valid_flight_data) stream = manager.create_client_stream(flight_id, "client_abc") assert stream is not None # --- Feature 02.1.03: System Initialization --- def test_initialize_system_success(self, full_manager): assert full_manager.is_system_initialized() is False assert full_manager.initialize_system() is True # Verify all components were called full_manager.config_manager.load_config.assert_called_once() full_manager.model_manager.initialize_models.assert_called_once() full_manager.db.initialize_connection.assert_called_once() full_manager.satellite_manager.prepare_cache.assert_called_once() full_manager.place_recognition.load_indexes.assert_called_once() assert full_manager.is_system_initialized() is True def test_initialize_system_config_failure(self, full_manager): full_manager.config_manager.load_config.side_effect = Exception("Config not found") assert full_manager.initialize_system() is False assert full_manager.is_system_initialized() is False full_manager.model_manager.initialize_models.assert_not_called() # Should abort early def test_initialize_system_model_failure(self, full_manager): full_manager.model_manager.initialize_models.side_effect = Exception("CUDA OOM") assert full_manager.initialize_system() is False assert full_manager.is_system_initialized() is False def test_initialize_system_db_failure(self, full_manager): full_manager.db.initialize_connection.side_effect = Exception("Connection refused") assert full_manager.initialize_system() is False assert full_manager.is_system_initialized() is False full_manager.satellite_manager.prepare_cache.assert_not_called()