8.7 KiB
Test Environment
Overview
System under test: GPS-Denied Visual Navigation System — a real-time position estimation service running on Jetson Orin Nano Super. Public interfaces: FastAPI REST/SSE endpoints (port 8000), MAVLink GPS_INPUT messages over serial/UDP, MAVLink telemetry messages.
Consumer app purpose: Standalone Python test runner (pytest) that exercises the GPS-denied system through its public interfaces (HTTP API, MAVLink message inspection, SSE stream consumption) without access to internal modules, ESKF state, or GPU buffers.
Test Modes
This is embedded robotics software targeting Jetson Orin Nano Super. A pure Docker environment cannot exercise GPU-dependent paths (TRT inference, cuVSLAM, CUDA streams). The test environment supports two modes:
Mode 1 — Docker SITL (CI/dev): Full system in Docker containers with ArduPilot SITL providing MAVLink + IMU at 200Hz. Camera images replayed from input_data/. Satellite tiles served from a mock HTTP server. GPS-denied system runs in CPU-mode with stubbed TRT/cuVSLAM inference (functionally equivalent but slower). Tests all integration paths, API, MAVLink, resilience, and security.
Mode 2 — Jetson Hardware (nightly/pre-deploy): GPS-denied system runs natively on Jetson Orin Nano Super with real CUDA/TRT/cuVSLAM. ArduPilot SITL runs on the Jetson or a companion x86 host, connected via UART or UDP. Camera frames injected via USB camera emulator or replay service. Tests real-time performance, GPU memory, thermal, TRT correctness, and CUDA stream isolation.
Docker Environment (Mode 1)
Services
| Service | Image / Build | Purpose | Ports |
|---|---|---|---|
| gps-denied-system | ./Dockerfile (build context: project root) |
GPS-denied navigation system in CPU-mode (TRT stubs, cuVSLAM stub returning synthetic VO poses derived from ground truth trajectory) | 8000 (FastAPI), 14550/udp (MAVLink) |
| ardupilot-sitl | ardupilot/sitl:plane-4.5 (fixed-wing) |
ArduPilot SITL: flies waypoint mission following coordinates.csv trajectory, generates IMU at 200Hz via MAVLink, GPS_TYPE=14 accepts GPS_INPUT, provides GLOBAL_POSITION_INT at startup | 5760 (MAVLink TCP), 14551/udp |
| camera-replay | ./tests/docker/camera-replay/Dockerfile |
Replays AD000001-060.jpg from input_data/ at configurable FPS (default 0.7fps) with timestamps synchronized to SITL clock. Supports fault injection: frame skip, corrupted JPEG, pause. | 8001 (frame server) |
| satellite-tile-server | ./tests/docker/tile-server/Dockerfile |
HTTP server for pre-cached satellite tiles (zoom 18, ~50-100 tiles covering test area). Supports fault injection: 404 for specific tiles, 503 for full outage, slow responses. | 8002 |
| mavlink-inspector | ./tests/docker/mavlink-inspector/Dockerfile |
Passively captures all MAVLink traffic (GPS_INPUT, NAMED_VALUE_FLOAT, STATUSTEXT, COMMAND_LONG) for post-test assertion. Can inject operator re-localization hints. | 14552/udp |
| e2e-consumer | ./tests/e2e/Dockerfile |
Black-box test runner (pytest). Communicates only via HTTP API + MAVLink inspector. | — |
Networks
| Network | Services | Purpose |
|---|---|---|
| e2e-net | all | Isolated test network; MAVLink UDP multicast between gps-denied-system, ardupilot-sitl, mavlink-inspector |
Volumes
| Volume | Mounted to | Purpose |
|---|---|---|
| input-data | camera-replay:/data, e2e-consumer:/test-data | Camera frames (AD000001-060.jpg), coordinates.csv, data_parameters.md, gmaps reference images |
| satellite-tiles | satellite-tile-server:/tiles, gps-denied-system:/tiles | Pre-processed satellite tiles for test area (zoom 18, 48.249-48.276°N, 37.340-37.386°E) |
| sitl-mission | ardupilot-sitl:/mission | Waypoint mission file derived from coordinates.csv (SITL flies this trajectory, generating physically consistent 200Hz IMU data) |
| test-results | e2e-consumer:/results | Test result CSV output |
| mavlink-capture | mavlink-inspector:/capture | Recorded MAVLink messages for post-test assertions |
IMU Data Flow
ArduPilot SITL is the primary source of IMU data. It flies a waypoint mission derived from coordinates.csv and internally generates physically consistent accelerometer + gyroscope readings at 200Hz, delivered to the GPS-denied system via MAVLink (RAW_IMU, SCALED_IMU2). This eliminates the need for pre-recorded IMU data files and ensures IMU/trajectory consistency.
coordinates.csv → mission_generator script → ArduPilot waypoint file
↓
ArduPilot SITL flies trajectory
↓
IMU @ 200Hz + heartbeat + GLOBAL_POSITION_INT
↓ (MAVLink UDP)
gps-denied-system receives IMU for ESKF
docker-compose structure
services:
ardupilot-sitl:
# ArduPilot SITL fixed-wing, outputs IMU at 200Hz via MAVLink
# GPS_TYPE=14 (MAVLink), pre-configured for GPS_INPUT acceptance
satellite-tile-server:
# HTTP tile server with tiles for test area (48.249-48.276°N, 37.340-37.386°E)
camera-replay:
# Replays AD000001-060.jpg at 0.7fps, serves via HTTP or shared volume
depends_on:
- satellite-tile-server
gps-denied-system:
# The system under test
depends_on:
- ardupilot-sitl
- satellite-tile-server
- camera-replay
mavlink-inspector:
# Captures GPS_INPUT, NAMED_VALUE_FLOAT, STATUSTEXT messages
depends_on:
- ardupilot-sitl
e2e-consumer:
# pytest runner — executes after system reaches steady state
depends_on:
- gps-denied-system
- mavlink-inspector
Consumer Application
Tech stack: Python 3.11, pytest, httpx (HTTP client), pymavlink (MAVLink inspection), sseclient-py (SSE stream)
Entry point: pytest tests/e2e/ --tb=short --csv=results/report.csv
Communication with system under test
| Interface | Protocol | Endpoint / Topic | Authentication |
|---|---|---|---|
| Position API | HTTP REST | http://gps-denied-system:8000/sessions | JWT token |
| Position stream | HTTP SSE | http://gps-denied-system:8000/sessions/{id}/stream | JWT token |
| Object localization | HTTP REST | http://gps-denied-system:8000/objects/locate | JWT token |
| Health check | HTTP REST | http://gps-denied-system:8000/health | None |
| GPS_INPUT inspection | MAVLink UDP | mavlink-inspector:14552 (recorded messages) | None |
| Telemetry inspection | MAVLink UDP | mavlink-inspector:14552 (NAMED_VALUE_FLOAT, STATUSTEXT) | None |
What the consumer does NOT have access to
- No direct access to ESKF internal state, covariance matrices, or error vectors
- No direct access to cuVSLAM tracking state or feature maps
- No direct access to GPU memory, CUDA streams, or TRT engine internals
- No direct access to the system's file system or configuration files
- No direct database or state store access
Jetson Hardware Environment (Mode 2)
Tests tagged @pytest.mark.jetson require actual Jetson Orin Nano Super hardware. These run natively (no Docker for the GPS-denied system) to exercise real GPU paths.
Hardware setup:
- Jetson Orin Nano Super (JetPack 6.2, CUDA 12.x, TensorRT 10.3)
- ArduPilot SITL on same Jetson (or x86 companion connected via UART/UDP)
- Camera frames injected via: USB camera emulator (v4l2loopback feeding frames from input_data/) or HTTP replay service
- Satellite tiles on local SSD (same path as production deployment)
- Active cooling attached (required for sustained load tests)
Tests in this mode:
- NFT-PERF-01 through NFT-PERF-06 (real GPU latency, throughput)
- NFT-RES-LIM-01 (GPU+CPU shared memory monitoring via tegrastats)
- NFT-RES-LIM-02 (thermal monitoring via thermal_zone sysfs)
- NFT-RES-LIM-05 (CUDA stream isolation with real concurrent GPU work)
- TRT engine build and inference correctness (expected_results #42-44)
Jetson CI runner: Self-hosted GitHub Actions runner on a dedicated Jetson Orin Nano Super, triggered for nightly builds and pre-deploy gates.
CI/CD Integration
When to run: On every PR to dev, nightly full suite, before production deploy
Pipeline stages:
- Unit tests (no Docker, no hardware) — on every commit
- Docker blackbox tests (SITL + CPU mode) — on PR merge to dev
- Hardware tests (Jetson runner) — nightly + pre-deploy
Gate behavior: Docker tests block merge; hardware tests are advisory (nightly) or blocking (pre-deploy) Timeout: Docker suite: 15 minutes; Hardware suite: 30 minutes
Reporting
Format: CSV
Columns: Test ID, Test Name, Execution Time (ms), Result (PASS/FAIL/SKIP), Error Message (if FAIL)
Output path: ./tests/e2e-results/report.csv