mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 09:01:14 +00:00
[AZ-416] [AZ-417] [AZ-419] Test batch 72: FT-P-09 AP/iNav + FT-P-11 cold start
- AZ-416 (FT-P-09-AP): fills mavproxy_tlog_reader.iter_messages with pymavlink body (AZ-406 surface kept); adds ap_contract_evaluator covering AC-1 (signing handshake <=5s), AC-2 (GPS_INPUT >=4.5 Hz), AC-3 (EK3_SRC1_POSXY=3), AC-4 (GPS_RAW_INT health >=80%); scenario forces fc_adapter=ardupilot. - AZ-417 (FT-P-09-iNav): msp_frame_observer covering AC-2 (MSP rate) and AC-3 (fix_type/provider/numSat); scenario forces fc_adapter=inav. - AZ-419 (FT-P-11): cold_start_evaluator covering AC-1 (operator manifest origin), AC-2 (FC EKF fallback), AC-3 (no-origin abort), AC-4 (bounded-delta conflict, ADR-010 Principle #11 amended); scenario parametrized on origin_source plus dedicated no-origin abort scenario. - All scenarios skip-gated on upstream frame_source_replay / imu_replay / fdr_reader / sitl_observer extensions. - +67 unit tests; full e2e unit suite: 460 passed. - K=3 cumulative review fired: PASS for batches 70-72. See _docs/03_implementation/batch_72_report.md, _docs/03_implementation/reviews/batch_72_review.md, _docs/03_implementation/cumulative_review_batches_70-72_cycle1_report.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"""Unit tests for ``runner.helpers.mavproxy_tlog_reader.iter_messages``.
|
||||
|
||||
AZ-416 fills in the pymavlink-backed body; AZ-406 committed the public
|
||||
surface. These tests synthesise a tiny tlog on the fly so the parser
|
||||
can be exercised without needing a captured `.tlog` artifact.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pymavlink.dialects.v20 import ardupilotmega as mavlink
|
||||
|
||||
from runner.helpers.mavproxy_tlog_reader import (
|
||||
TlogMessage,
|
||||
count_by_type,
|
||||
iter_messages,
|
||||
)
|
||||
|
||||
_SRC_SYSTEM = 1
|
||||
_SRC_COMPONENT = mavlink.MAV_COMP_ID_AUTOPILOT1
|
||||
_BASE_TS_US = 1_700_000_000_000_000
|
||||
|
||||
|
||||
def _write_tlog(tlog_path: Path, records: list[tuple[int, bytes]]) -> Path:
|
||||
"""Write a synthetic tlog: ``[8B big-endian ts_us][raw frame]`` per record."""
|
||||
with tlog_path.open("wb") as fh:
|
||||
for ts_us, payload in records:
|
||||
fh.write(struct.pack(">Q", ts_us))
|
||||
fh.write(payload)
|
||||
return tlog_path
|
||||
|
||||
|
||||
def _make_mav() -> mavlink.MAVLink:
|
||||
return mavlink.MAVLink(
|
||||
file=None,
|
||||
srcSystem=_SRC_SYSTEM,
|
||||
srcComponent=_SRC_COMPONENT,
|
||||
)
|
||||
|
||||
|
||||
def _heartbeat(mav: mavlink.MAVLink) -> bytes:
|
||||
return mav.heartbeat_encode(
|
||||
type=mavlink.MAV_TYPE_FIXED_WING,
|
||||
autopilot=mavlink.MAV_AUTOPILOT_ARDUPILOTMEGA,
|
||||
base_mode=mavlink.MAV_MODE_FLAG_AUTO_ENABLED,
|
||||
custom_mode=10,
|
||||
system_status=mavlink.MAV_STATE_ACTIVE,
|
||||
).pack(mav)
|
||||
|
||||
|
||||
def _gps_raw_int(mav: mavlink.MAVLink, *, fix_type: int = 3, eph: int = 100) -> bytes:
|
||||
return mav.gps_raw_int_encode(
|
||||
time_usec=_BASE_TS_US,
|
||||
fix_type=fix_type,
|
||||
lat=487750000,
|
||||
lon=375940000,
|
||||
alt=280000,
|
||||
eph=eph,
|
||||
epv=200,
|
||||
vel=12000,
|
||||
cog=18000,
|
||||
satellites_visible=12,
|
||||
).pack(mav)
|
||||
|
||||
|
||||
def _gps_input(mav: mavlink.MAVLink) -> bytes:
|
||||
return mav.gps_input_encode(
|
||||
time_usec=_BASE_TS_US,
|
||||
gps_id=0,
|
||||
ignore_flags=0,
|
||||
time_week_ms=0,
|
||||
time_week=0,
|
||||
fix_type=3,
|
||||
lat=487750000,
|
||||
lon=375940000,
|
||||
alt=280.0,
|
||||
hdop=1.0,
|
||||
vdop=2.0,
|
||||
vn=10.0,
|
||||
ve=5.0,
|
||||
vd=0.5,
|
||||
speed_accuracy=0.3,
|
||||
horiz_accuracy=1.0,
|
||||
vert_accuracy=2.0,
|
||||
satellites_visible=12,
|
||||
).pack(mav)
|
||||
|
||||
|
||||
def test_iter_messages_raises_on_missing_file(tmp_path: Path) -> None:
|
||||
# Act / Assert
|
||||
with pytest.raises(FileNotFoundError, match="tlog not found"):
|
||||
list(iter_messages(tmp_path / "absent.tlog"))
|
||||
|
||||
|
||||
def test_iter_messages_yields_message_type_and_fields(tmp_path: Path) -> None:
|
||||
"""A single heartbeat round-trips through iter_messages."""
|
||||
# Arrange
|
||||
mav = _make_mav()
|
||||
tlog = _write_tlog(tmp_path / "single.tlog", [(_BASE_TS_US, _heartbeat(mav))])
|
||||
|
||||
# Act
|
||||
msgs = list(iter_messages(tlog))
|
||||
|
||||
# Assert
|
||||
assert len(msgs) == 1
|
||||
m = msgs[0]
|
||||
assert isinstance(m, TlogMessage)
|
||||
assert m.msg_type == "HEARTBEAT"
|
||||
assert m.fields["autopilot"] == mavlink.MAV_AUTOPILOT_ARDUPILOTMEGA
|
||||
assert "mavpackettype" not in m.fields # excluded by the impl
|
||||
|
||||
|
||||
def test_iter_messages_preserves_order(tmp_path: Path) -> None:
|
||||
"""Multiple records are yielded oldest-first."""
|
||||
# Arrange
|
||||
mav = _make_mav()
|
||||
tlog = _write_tlog(
|
||||
tmp_path / "ordered.tlog",
|
||||
[
|
||||
(_BASE_TS_US + 0, _heartbeat(mav)),
|
||||
(_BASE_TS_US + 1_000_000, _gps_raw_int(mav)),
|
||||
(_BASE_TS_US + 2_000_000, _gps_input(mav)),
|
||||
],
|
||||
)
|
||||
|
||||
# Act
|
||||
types = [m.msg_type for m in iter_messages(tlog)]
|
||||
|
||||
# Assert
|
||||
assert types == ["HEARTBEAT", "GPS_RAW_INT", "GPS_INPUT"]
|
||||
|
||||
|
||||
def test_iter_messages_timestamp_in_microseconds(tmp_path: Path) -> None:
|
||||
"""``msg._timestamp`` is seconds; we expose microseconds."""
|
||||
# Arrange
|
||||
mav = _make_mav()
|
||||
tlog = _write_tlog(tmp_path / "ts.tlog", [(_BASE_TS_US + 5_000_000, _heartbeat(mav))])
|
||||
|
||||
# Act
|
||||
msg = next(iter_messages(tlog))
|
||||
|
||||
# Assert — pymavlink rounds to its frame timestamp; tolerate ±1ms slop.
|
||||
assert abs(msg.timestamp_us - (_BASE_TS_US + 5_000_000)) <= 1_000
|
||||
|
||||
|
||||
def test_iter_messages_signed_flag_default_false(tmp_path: Path) -> None:
|
||||
"""Plain pymavlink-encoded frame is NOT signed → signed=False."""
|
||||
# Arrange
|
||||
mav = _make_mav()
|
||||
tlog = _write_tlog(tmp_path / "u.tlog", [(_BASE_TS_US, _heartbeat(mav))])
|
||||
|
||||
# Act
|
||||
msg = next(iter_messages(tlog))
|
||||
|
||||
# Assert
|
||||
assert msg.signed is False
|
||||
|
||||
|
||||
def test_count_by_type_tallies_correctly(tmp_path: Path) -> None:
|
||||
"""count_by_type runs iter_messages and aggregates the type counts."""
|
||||
# Arrange
|
||||
mav = _make_mav()
|
||||
tlog = _write_tlog(
|
||||
tmp_path / "mixed.tlog",
|
||||
[
|
||||
(_BASE_TS_US + 0, _heartbeat(mav)),
|
||||
(_BASE_TS_US + 1, _heartbeat(mav)),
|
||||
(_BASE_TS_US + 2, _gps_raw_int(mav)),
|
||||
],
|
||||
)
|
||||
|
||||
# Act
|
||||
counts = count_by_type(tlog)
|
||||
|
||||
# Assert
|
||||
assert counts["HEARTBEAT"] == 2
|
||||
assert counts["GPS_RAW_INT"] == 1
|
||||
Reference in New Issue
Block a user