import pytest import time from unittest.mock import Mock, patch from datetime import datetime, timedelta from f02_1_flight_lifecycle_manager import GPSPoint from f03_flight_database import FrameResult as F03FrameResult from f14_result_manager import ResultManager, FrameResult, RefinedFrameResult @pytest.fixture def f03(): db = Mock() store = {} def mock_execute(ops): for op in ops: op() return True db.execute_transaction.side_effect = mock_execute def save_fr(fid, fr): store[fr.frame_id] = fr return True db.save_frame_result.side_effect = save_fr def get_fr(fid): return list(store.values()) db.get_frame_results.side_effect = get_fr return db @pytest.fixture def f15(): return Mock() @pytest.fixture def rm(f03, f15): return ResultManager(f03_database=f03, f15_streamer=f15) @pytest.fixture def sample_result(): return FrameResult( frame_id=10, gps_center=GPSPoint(lat=48.0, lon=37.0), altitude=400.0, heading=90.0, confidence=0.8, timestamp=datetime.utcnow() ) class TestResultManager: # --- 14.01 Feature: Frame Result Persistence --- def test_update_frame_result_new_frame(self, rm, f03, sample_result): res = rm.update_frame_result("flight_1", 10, sample_result) assert res is True f03.save_frame_result.assert_called_once() def test_update_frame_result_updates_waypoint(self, rm, f03, sample_result): rm.update_frame_result("flight_1", 10, sample_result) f03.insert_waypoint.assert_called_once() def test_update_frame_result_transaction_atomic(self, rm, f03, f15, sample_result): f03.execute_transaction.side_effect = None f03.execute_transaction.return_value = False res = rm.update_frame_result("flight_1", 10, sample_result) assert res is False # Because atomic transaction failed f15.send_frame_result.assert_not_called() def test_update_frame_result_triggers_sse(self, rm, f15, sample_result): rm.update_frame_result("flight_1", 10, sample_result) f15.send_frame_result.assert_called_once() def test_update_frame_result_refined_flag(self, rm, f03, sample_result): sample_result.refined = True rm.update_frame_result("flight_1", 10, sample_result) # Grab the saved F03 object args, _ = f03.save_frame_result.call_args assert args[1].refined is True def test_publish_waypoint_fetches_latest(self, rm, f03, f15, sample_result): rm.update_frame_result("flight_1", 10, sample_result) rm.publish_waypoint_update("flight_1", 10) f03.get_frame_results.assert_called() f15.send_frame_result.assert_called() def test_publish_waypoint_handles_transient_error(self, rm, f03, f15, sample_result): rm.update_frame_result("flight_1", 10, sample_result) # Fail twice, succeed third time f15.send_frame_result.side_effect = [Exception("Net Err"), Exception("Net Err"), True] assert rm.publish_waypoint_update("flight_1", 10) is True assert f15.send_frame_result.call_count == 4 # 1 from update + 3 retries def test_publish_waypoint_logs_on_db_unavailable(self, rm, f03): f03.get_frame_results.side_effect = Exception("DB Down") assert rm.publish_waypoint_update("flight_1", 10) is False # --- 14.03 Feature: Batch Refinement Updates --- def test_mark_refined_updates_all_frames(self, rm, f03, sample_result): rm.update_frame_result("flight_ref", 10, sample_result) ref = RefinedFrameResult(frame_id=10, gps_center=GPSPoint(lat=48.1, lon=37.1), confidence=0.99) assert rm.mark_refined("flight_ref", [ref]) is True # Verify it was updated in the DB f03_res = f03.get_frame_results("flight_ref")[0] assert f03_res.gps_center.lat == 48.1 assert f03_res.confidence == 0.99 def test_mark_refined_sets_refined_flag(self, rm, f03, sample_result): rm.update_frame_result("flight_ref", 10, sample_result) rm.mark_refined("flight_ref", [RefinedFrameResult(frame_id=10, gps_center=GPSPoint(lat=0, lon=0), confidence=0.9)]) assert f03.get_frame_results("flight_ref")[0].refined is True def test_mark_refined_updates_gps_coordinates(self, rm, f03, sample_result): rm.update_frame_result("flight_ref", 10, sample_result) rm.mark_refined("flight_ref", [RefinedFrameResult(frame_id=10, gps_center=GPSPoint(lat=99.0, lon=99.0), confidence=0.9)]) assert f03.get_frame_results("flight_ref")[0].gps_center.lat == 99.0 def test_mark_refined_triggers_sse_per_frame(self, rm, f15, sample_result): rm.update_frame_result("flight_ref", 10, sample_result) f15.reset_mock() rm.mark_refined("flight_ref", [RefinedFrameResult(frame_id=10, gps_center=GPSPoint(lat=0, lon=0), confidence=0.9)]) f15.send_refinement.assert_called_once() def test_mark_refined_updates_waypoints(self, rm, f03, sample_result): rm.update_frame_result("flight_ref", 10, sample_result) f03.insert_waypoint.reset_mock() rm.mark_refined("flight_ref", [RefinedFrameResult(frame_id=10, gps_center=GPSPoint(lat=0, lon=0), confidence=0.9)]) f03.insert_waypoint.assert_called_once() def test_chunk_merge_updates_all_frames(self, rm, f03, sample_result): rm.update_frame_result("flight_chk", 10, sample_result) ref = RefinedFrameResult(frame_id=10, gps_center=GPSPoint(lat=48.5, lon=37.5), confidence=0.9) assert rm.update_results_after_chunk_merge("flight_chk", [ref]) is True assert f03.get_frame_results("flight_chk")[0].gps_center.lat == 48.5 def test_chunk_merge_sets_refined_flag(self, rm, f03, sample_result): rm.update_frame_result("flight_chk", 10, sample_result) ref = RefinedFrameResult(frame_id=10, gps_center=GPSPoint(lat=48.5, lon=37.5), confidence=0.9) rm.update_results_after_chunk_merge("flight_chk", [ref]) assert f03.get_frame_results("flight_chk")[0].refined is True def test_chunk_merge_triggers_sse(self, rm, f15, sample_result): rm.update_frame_result("flight_chk", 10, sample_result) f15.reset_mock() rm.update_results_after_chunk_merge("flight_chk", [RefinedFrameResult(frame_id=10, gps_center=GPSPoint(lat=0, lon=0), confidence=0.9)]) f15.send_refinement.assert_called_once() def test_batch_transaction_atomic(self, rm, f03, sample_result): rm.update_frame_result("flight_chk", 10, sample_result) f03.execute_transaction.side_effect = None f03.execute_transaction.return_value = False res = rm.mark_refined("flight_chk", [RefinedFrameResult(frame_id=10, gps_center=GPSPoint(lat=0, lon=0), confidence=0.9)]) assert res is False # --- 14.02 Feature: Result Retrieval --- def test_get_flight_results_returns_all_frames(self, rm, sample_result): rm.update_frame_result("flight_all", 10, sample_result) res2 = sample_result.model_copy(update={'frame_id': 11}) rm.update_frame_result("flight_all", 11, res2) results = rm.get_flight_results("flight_all") assert len(results.frames) == 2 def test_get_flight_results_includes_statistics(self, rm, sample_result): rm.update_frame_result("flight_stats", 10, sample_result) results = rm.get_flight_results("flight_stats") assert results.statistics.total_frames == 1 assert results.statistics.mean_confidence == 0.8 def test_get_flight_results_empty_flight(self, rm): results = rm.get_flight_results("flight_empty") assert len(results.frames) == 0 assert results.statistics.total_frames == 0 def test_get_flight_results_performance(self, rm, f03): f03.get_frame_results.side_effect = None f03.get_frame_results.return_value = [F03FrameResult(frame_id=i, gps_center=GPSPoint(lat=0, lon=0), altitude=0.0, heading=0.0, confidence=1.0, timestamp=datetime.utcnow(), updated_at=datetime.utcnow()) for i in range(2000)] start = time.time() res = rm.get_flight_results("flight_perf") assert time.time() - start < 0.200 assert len(res.frames) == 2000 def test_get_changed_frames_returns_modified(self, rm, sample_result): rm.update_frame_result("flight_change", 10, sample_result) t1 = datetime.utcnow() time.sleep(0.01) res2 = sample_result.model_copy(update={'frame_id': 11, 'updated_at': datetime.utcnow()}) rm.update_frame_result("flight_change", 11, res2) changed = rm.get_changed_frames("flight_change", t1) assert len(changed) == 1 assert changed[0] == 11 def test_get_changed_frames_empty_result(self, rm, sample_result): rm.update_frame_result("flight_change", 10, sample_result) changed = rm.get_changed_frames("flight_change", datetime.utcnow() + timedelta(days=1)) assert len(changed) == 0 def test_get_changed_frames_includes_refined(self, rm, sample_result): rm.update_frame_result("flight_change", 10, sample_result) t1 = datetime.utcnow() time.sleep(0.01) rm.mark_refined("flight_change", [RefinedFrameResult(frame_id=10, gps_center=GPSPoint(lat=0, lon=0), confidence=0.9)]) changed = rm.get_changed_frames("flight_change", t1) assert len(changed) == 1 def test_export_results_json(self, rm, sample_result): rm.update_frame_result("flight_exp", 10, sample_result) out = rm.export_results("flight_exp", "json") assert "flight_exp" in out assert "48.0" in out def test_export_results_csv(self, rm, sample_result): rm.update_frame_result("flight_exp", 10, sample_result) out = rm.export_results("flight_exp", "csv") assert "image,sequence,lat,lon" in out assert "AD000010.jpg,10,48.0,37.0" in out def test_export_results_kml(self, rm, sample_result): out = rm.export_results("flight_exp", "kml") assert "" in out