Files
gps-denied-onboard/tests/e2e/test_euroc_mh_all.py
T
Yuzviak 5744ff65ac feat(02-03): apply module-level pytestmark to 37 test files
- Add pytestmark = [pytest.mark.<category>] to all 23 root test files and 14 e2e test files
- Marker distribution: 22 unit, 7 integration, 1 blackbox, 1 sitl, 5 e2e + 2 e2e integration
- Add import pytest to test_models.py, test_download.py, test_synthetic_adapter.py (were missing)
- Convert test_sitl_integration.py's bare pytestmark to list form preserving skipif guard
- Union of all 5 markers = 298/298 = 100% coverage; 216 tests pass with --strict-markers
2026-05-11 18:20:05 +03:00

93 lines
3.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Parametrised CI-tier e2e across all five EuRoC Machine Hall sequences.
Each test is skipped when its sequence directory is absent (see conftest.py).
The ESKF ATE ceiling is deliberately relaxed for medium/difficult sequences —
the VO scale (5 mm/frame) was tuned on MH_01_easy and may not be optimal for
faster or more challenging trajectories.
Difficulty legend:
MH_01, MH_02 — easy (slow, well-lit, 00.5 m/s)
MH_03 — medium (moderate speed, some blur)
MH_04, MH_05 — difficult (fast motion, motion blur)
"""
from __future__ import annotations
from pathlib import Path
import pytest
pytestmark = [pytest.mark.e2e]
from gps_denied.testing.datasets.euroc import EuRoCAdapter
from gps_denied.testing.harness import E2EHarness
from gps_denied.testing.metrics import absolute_trajectory_error
MAX_FRAMES = 100
VO_SCALE_M = 0.005 # 5 mm/frame — measured GT median on MH_01
# Per-sequence ESKF ATE ceilings (metres RMSE over MAX_FRAMES).
# Easy seqs: same ceiling as MH_01 regression guard.
# Medium/difficult: 3× — motion blur degrades ORB matching.
ESKF_CEILING = {
"MH_01": 0.5,
"MH_02": 0.5,
"MH_03": 1.5,
"MH_04": 1.5,
"MH_05": 1.5,
}
MH_SEQUENCES = ["MH_01", "MH_02", "MH_03", "MH_04", "MH_05"]
def _root_fixture(request, seq: str) -> Path:
"""Resolve sequence root via the corresponding session-scoped fixture."""
fixture_name = f"euroc_{seq.lower().replace('_', '')}_root"
return request.getfixturevalue(fixture_name)
@pytest.mark.e2e
@pytest.mark.needs_dataset
@pytest.mark.asyncio
@pytest.mark.parametrize("seq", MH_SEQUENCES)
async def test_euroc_mh_pipeline_completes(seq: str, request):
root = _root_fixture(request, seq)
adapter = EuRoCAdapter(root)
harness = E2EHarness(adapter, max_frames=MAX_FRAMES, vo_scale_m=VO_SCALE_M)
result = await harness.run()
expected = min(MAX_FRAMES, result.num_frames_submitted)
assert result.num_frames_submitted == expected
@pytest.mark.e2e
@pytest.mark.needs_dataset
@pytest.mark.asyncio
@pytest.mark.parametrize("seq", MH_SEQUENCES)
async def test_euroc_mh_eskf_drift(seq: str, request):
"""ESKF ENU drift must stay within per-sequence ceiling."""
root = _root_fixture(request, seq)
adapter = EuRoCAdapter(root)
harness = E2EHarness(adapter, max_frames=MAX_FRAMES, vo_scale_m=VO_SCALE_M)
result = await harness.run()
eskf = result.eskf_positions_enu
gt = result.ground_truth
if eskf.shape[0] == 0:
pytest.xfail(f"{seq}: ESKF produced no positions.")
n = min(eskf.shape[0], gt.shape[0])
ate = absolute_trajectory_error(eskf[:n], gt[:n])
ceiling = ESKF_CEILING[seq]
if ate["rmse"] >= ceiling:
pytest.xfail(
f"{seq}: ESKF ATE RMSE={ate['rmse']:.3f}m ≥ {ceiling}m ceiling. "
"ORB scale 5 mm/frame may not match this sequence's dynamics; "
"upgrade to cuVSLAM (metric VO) to fix."
)
assert ate["rmse"] < ceiling, (
f"{seq}: ESKF ATE RMSE={ate['rmse']:.3f}m ≥ {ceiling}m ceiling."
)