Files
gps-denied-onboard/test_f02_1_flight_lifecycle_manager.py
T
Denys Zaitsev d7e1066c60 Initial commit
2026-04-03 23:25:54 +03:00

254 lines
11 KiB
Python

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()