diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 1a66404..35c0f8b 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -61,6 +61,8 @@ def _random_frame(h=200, w=200): # --------------------------------------------------------------- # AC-1: Normal flight — 20 consecutive frames # --------------------------------------------------------------- +@pytest.mark.ac("AC-1.1") +@pytest.mark.ac("AC-4.4") @pytest.mark.asyncio async def test_ac1_normal_flight(wired_processor): """Twenty frames processed without crash; SSE events emitted for each.""" @@ -76,6 +78,8 @@ async def test_ac1_normal_flight(wired_processor): # --------------------------------------------------------------- # AC-2: Tracking loss → recovery cycle # --------------------------------------------------------------- +@pytest.mark.ac("AC-2.1a") +@pytest.mark.ac("AC-3.4") @pytest.mark.asyncio async def test_ac2_tracking_loss_and_recovery(wired_processor, monkeypatch): """ @@ -122,6 +126,7 @@ async def test_ac2_tracking_loss_and_recovery(wired_processor, monkeypatch): # --------------------------------------------------------------- # AC-3: Performance — < 5 s per frame # --------------------------------------------------------------- +@pytest.mark.ac("AC-4.1") @pytest.mark.asyncio async def test_ac3_performance_per_frame(wired_processor): """Each process_frame call must complete in < 5 seconds (mock pipeline).""" @@ -144,6 +149,8 @@ async def test_ac3_performance_per_frame(wired_processor): # --------------------------------------------------------------- # AC-4: User anchor fix # --------------------------------------------------------------- +@pytest.mark.ac("AC-4.4") +@pytest.mark.ac("AC-1.4") @pytest.mark.asyncio async def test_ac4_user_anchor_fix(wired_processor): """ @@ -185,6 +192,8 @@ async def test_ac4_user_anchor_fix(wired_processor): # --------------------------------------------------------------- # AC-5: Sustained throughput — 50 frames # --------------------------------------------------------------- +@pytest.mark.ac("AC-1.1") +@pytest.mark.ac("AC-4.4") @pytest.mark.asyncio async def test_ac5_sustained_throughput(wired_processor): """Process 50 frames back-to-back; no crashes, total < 30 seconds.""" @@ -201,6 +210,7 @@ async def test_ac5_sustained_throughput(wired_processor): # --------------------------------------------------------------- # AC-6: Factor graph optimization converges # --------------------------------------------------------------- +@pytest.mark.ac("AC-3.3") @pytest.mark.asyncio async def test_ac6_graph_optimization_convergence(wired_processor): """After N frames the graph should report convergence.""" diff --git a/tests/test_accuracy.py b/tests/test_accuracy.py index 4ca8d64..58b873d 100644 --- a/tests/test_accuracy.py +++ b/tests/test_accuracy.py @@ -162,6 +162,7 @@ def test_benchmark_summary_non_empty(): # AC-PERF-3: Latency < 400ms # --------------------------------------------------------------- +@pytest.mark.ac("AC-4.1") def test_per_frame_latency_under_400ms(): """AC-PERF-3: p95 per-frame latency < 400ms on synthetic trajectory.""" result = _run_benchmark(num_frames=20) @@ -182,6 +183,7 @@ def test_median_error_with_sat_corrections(): ) +@pytest.mark.ac("AC-1.1") def test_pct_within_50m_with_sat_corrections(): """AC-PERF-1: ≥80% frames within 50m when satellite corrections are active.""" result = _run_benchmark(num_frames=40, with_sat=True) @@ -191,6 +193,7 @@ def test_pct_within_50m_with_sat_corrections(): ) +@pytest.mark.ac("AC-1.2") def test_pct_within_20m_with_sat_corrections(): """AC-PERF-2: ≥60% frames within 20m when satellite corrections are active.""" result = _run_benchmark(num_frames=40, with_sat=True) @@ -239,6 +242,7 @@ def test_waypoint_steering_changes_direction(): # AC-PERF-4: VO drift over 1 km straight segment # --------------------------------------------------------------- +@pytest.mark.ac("AC-1.3") def test_vo_drift_under_100m_over_1km(): """AC-PERF-4: VO drift (no sat correction) over 1 km < 100 m.""" bench = AccuracyBenchmark() @@ -252,6 +256,7 @@ def test_vo_drift_under_100m_over_1km(): # AC-PERF-6: Covariance shrinks after satellite update # --------------------------------------------------------------- +@pytest.mark.ac("AC-2.2") def test_covariance_shrinks_after_satellite_update(): """AC-PERF-6: ESKF position covariance trace decreases after satellite correction.""" from gps_denied.core.eskf import ESKF @@ -274,6 +279,7 @@ def test_covariance_shrinks_after_satellite_update(): # AC-PERF-5: Confidence tier transitions # --------------------------------------------------------------- +@pytest.mark.ac("AC-2.1a") def test_confidence_high_after_fresh_satellite(): """AC-PERF-5: HIGH confidence when satellite correction is recent + covariance small.""" from gps_denied.core.eskf import ESKF @@ -323,6 +329,8 @@ def test_confidence_failed_after_3_consecutive(): # passes_acceptance_criteria integration # --------------------------------------------------------------- +@pytest.mark.ac("AC-1.1") +@pytest.mark.ac("AC-1.2") def test_passes_acceptance_criteria_full_pass(): """passes_acceptance_criteria returns (True, all-True) for ideal data.""" result = BenchmarkResult( @@ -336,6 +344,7 @@ def test_passes_acceptance_criteria_full_pass(): assert all(checks.values()) +@pytest.mark.ac("AC-4.1") def test_passes_acceptance_criteria_latency_fail(): """passes_acceptance_criteria fails when latency exceeds 400ms.""" result = BenchmarkResult( @@ -349,6 +358,7 @@ def test_passes_acceptance_criteria_latency_fail(): assert checks["AC-PERF-3: p95 latency < 400ms"] is False +@pytest.mark.ac("AC-1.1") def test_passes_acceptance_criteria_accuracy_fail(): """passes_acceptance_criteria fails when less than 80% within 50m.""" result = BenchmarkResult( diff --git a/tests/test_gps_input_encoding.py b/tests/test_gps_input_encoding.py index 8bd8d6b..7b3d4dc 100644 --- a/tests/test_gps_input_encoding.py +++ b/tests/test_gps_input_encoding.py @@ -17,7 +17,7 @@ import time import numpy as np import pytest -pytestmark = [pytest.mark.blackbox] +pytestmark = [pytest.mark.blackbox, pytest.mark.ac("AC-4.3")] from gps_denied.core.mavlink import _confidence_to_fix_type, _eskf_to_gps_input from gps_denied.schemas import GPSPoint diff --git a/tests/test_mavlink.py b/tests/test_mavlink.py index 8d0d5a6..4b3602d 100644 --- a/tests/test_mavlink.py +++ b/tests/test_mavlink.py @@ -75,6 +75,7 @@ def test_unix_to_gps_time_recent(): # MAV-02: ESKF → GPS_INPUT field mapping # --------------------------------------------------------------- +@pytest.mark.ac("AC-4.3") def test_confidence_to_fix_type(): """MAV-02: confidence tier → fix_type mapping.""" assert _confidence_to_fix_type(ConfidenceTier.HIGH) == 3 @@ -83,6 +84,7 @@ def test_confidence_to_fix_type(): assert _confidence_to_fix_type(ConfidenceTier.FAILED) == 0 +@pytest.mark.ac("AC-4.3") def test_eskf_to_gps_input_position(): """MAV-02: ENU position → degE7 lat/lon.""" # 1° lat ≈ 111319.5 m; move 111319.5 m North → lat + 1° @@ -93,6 +95,7 @@ def test_eskf_to_gps_input_position(): assert abs(msg.lat - expected_lat) <= 10 # within 1 µ-degree tolerance +@pytest.mark.ac("AC-4.3") def test_eskf_to_gps_input_lon(): """MAV-02: East displacement → longitude shift.""" cos_lat = math.cos(math.radians(ORIGIN.lat)) @@ -212,6 +215,8 @@ def test_consecutive_failure_counter_increments_on_low(bridge): assert bridge._consecutive_failures == 2 +@pytest.mark.ac("AC-5.2") +@pytest.mark.ac("AC-3.4") def test_reloc_request_triggered_after_3_failures(bridge): """MAV-04: after 3 failures the re-localisation callback is called.""" received: list[RelocalizationRequest] = [] diff --git a/tests/test_processor_pipe.py b/tests/test_processor_pipe.py index 8feeb33..cd2ad7b 100644 --- a/tests/test_processor_pipe.py +++ b/tests/test_processor_pipe.py @@ -211,6 +211,7 @@ async def test_failure_counter_resets_on_recovery(): # PIPE-07: ESKF state pushed to MAVLink # --------------------------------------------------------------- +@pytest.mark.ac("AC-4.4") @pytest.mark.asyncio async def test_mavlink_state_pushed_per_frame(): """PIPE-07: MAVLinkBridge.update_state called on every frame with ESKF.""" @@ -283,6 +284,7 @@ async def test_convert_object_to_gps_fallback_without_coord(): # ESKF initialization via create_flight # --------------------------------------------------------------- +@pytest.mark.ac("AC-1.4") @pytest.mark.asyncio async def test_create_flight_initialises_eskf(): """create_flight should seed ESKF for the new flight.""" diff --git a/tests/test_schemas.py b/tests/test_schemas.py index f70c24f..9698e5a 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -22,6 +22,7 @@ from gps_denied.schemas.flight import ( # ── GPSPoint ────────────────────────────────────────────────────────────── +@pytest.mark.ac("AC-6.3") class TestGPSPoint: def test_valid(self): p = GPSPoint(lat=48.275, lon=37.385) @@ -102,6 +103,7 @@ class TestFlightCreateRequest: # ── Waypoint ────────────────────────────────────────────────────────────── +@pytest.mark.ac("AC-6.3") class TestWaypoint: def test_valid(self): wp = Waypoint( diff --git a/tests/test_sitl_integration.py b/tests/test_sitl_integration.py index 7cd8dc0..2a561bc 100644 --- a/tests/test_sitl_integration.py +++ b/tests/test_sitl_integration.py @@ -90,6 +90,7 @@ def _wait_for_tcp(host: str, port: int, timeout: float = 30.0) -> bool: # SITL-01: Connection # --------------------------------------------------------------------------- +@pytest.mark.ac("AC-4.3") def test_sitl_tcp_port_reachable(): """SITL-01: ArduPilot SITL TCP port is reachable before running tests.""" reachable = _wait_for_tcp(SITL_HOST, SITL_PORT, timeout=30.0) @@ -99,6 +100,7 @@ def test_sitl_tcp_port_reachable(): ) +@pytest.mark.ac("AC-4.3") def test_pymavlink_connection_to_sitl(): """SITL-01: pymavlink connects to SITL without error.""" pytest.importorskip("pymavlink", reason="pymavlink not installed") @@ -115,6 +117,8 @@ def test_pymavlink_connection_to_sitl(): # SITL-02: GPS_INPUT accepted by SITL EKF # --------------------------------------------------------------------------- +@pytest.mark.ac("AC-4.3") +@pytest.mark.ac("AC-NEW-2") def test_gps_input_accepted_by_sitl(): """SITL-02: Sending GPS_INPUT produces GPS_RAW_INT with fix_type >= 3.""" pytest.importorskip("pymavlink", reason="pymavlink not installed") @@ -173,6 +177,7 @@ def test_gps_input_accepted_by_sitl(): # SITL-03: MAVLinkBridge lifecycle # --------------------------------------------------------------------------- +@pytest.mark.ac("AC-4.3") @pytest.mark.asyncio async def test_mavlink_bridge_start_stop_with_sitl(): """SITL-03: MAVLinkBridge.start/stop with real SITL TCP connection.""" @@ -228,6 +233,8 @@ async def test_imu_callback_fires_from_sitl(): # SITL-05: GPS_INPUT rate ≥ 5 Hz # --------------------------------------------------------------------------- +@pytest.mark.ac("AC-4.3") +@pytest.mark.ac("AC-4.4") @pytest.mark.asyncio async def test_gps_input_rate_at_least_5hz(): """SITL-05: MAVLinkBridge delivers GPS_INPUT at ≥5 Hz over 1 second.""" @@ -297,6 +304,8 @@ async def test_telemetry_reaches_sitl_at_1hz(): # SITL-07: Reloc request after 3 consecutive failures # --------------------------------------------------------------------------- +@pytest.mark.ac("AC-5.2") +@pytest.mark.ac("AC-3.4") @pytest.mark.asyncio async def test_reloc_request_after_3_failures_with_sitl(): """SITL-07: After 3 FAILED-confidence updates, reloc callback fires."""