mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-23 02:06:36 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user