mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-22 21:46:36 +00:00
254 lines
11 KiB
Python
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() |