150 lines
4.9 KiB
Python
150 lines
4.9 KiB
Python
from sqlmodel import Session, SQLModel, create_engine
|
|
|
|
from app.models import TerminalDeviceState, TerminalMessageRaw
|
|
from app.mqtt.parser_worker import process_pending_batch
|
|
from app.mqtt.repository import insert_raw_message
|
|
|
|
|
|
def _session() -> Session:
|
|
engine = create_engine("sqlite://")
|
|
SQLModel.metadata.create_all(engine)
|
|
return Session(engine)
|
|
|
|
|
|
def test_insert_raw_message_sets_unparsed_defaults() -> None:
|
|
with _session() as session:
|
|
row = insert_raw_message(
|
|
session=session,
|
|
device_id="device-001",
|
|
topic="terminal/device-001/raw",
|
|
payload=b"$GPRMC,123520,A,3723.2475,N,12158.3416,W,0.13,309.62,120598,,,A*74\r\n",
|
|
qos=1,
|
|
retain=False,
|
|
)
|
|
|
|
assert row.parsed_at is None
|
|
assert row.parse_error is None
|
|
assert row.payload_size > 0
|
|
assert len(row.payload_sha256) == 64
|
|
|
|
|
|
def test_worker_marks_success_and_updates_state() -> None:
|
|
with _session() as session:
|
|
raw = insert_raw_message(
|
|
session=session,
|
|
device_id="device-001",
|
|
topic="terminal/device-001/raw",
|
|
payload=b"$GPGGA,123519,3723.2475,N,12158.3416,W,1,12,0.8,10.0,M,-25.0,M,,*65\r\n",
|
|
qos=1,
|
|
retain=False,
|
|
)
|
|
|
|
processed = process_pending_batch(session=session, batch_size=10)
|
|
|
|
assert processed == 1
|
|
|
|
refreshed = session.get(TerminalMessageRaw, raw.id)
|
|
assert refreshed is not None
|
|
assert refreshed.parsed_at is not None
|
|
assert refreshed.parse_error is None
|
|
|
|
state = session.get(TerminalDeviceState, "device-001")
|
|
assert state is not None
|
|
assert state.lat is not None
|
|
assert state.lon is not None
|
|
|
|
|
|
def test_worker_marks_failed_without_retry_schedule(monkeypatch) -> None:
|
|
def _raise_parse_error(_payload: bytes) -> list[dict[str, object]]:
|
|
raise ValueError("boom")
|
|
|
|
monkeypatch.setattr(
|
|
"app.mqtt.parser_worker.parse_mixed_payload", _raise_parse_error
|
|
)
|
|
|
|
with _session() as session:
|
|
raw = insert_raw_message(
|
|
session=session,
|
|
device_id="device-err",
|
|
topic="terminal/device-err/raw",
|
|
payload=b"$GPGGA,broken*00\r\n",
|
|
qos=1,
|
|
retain=False,
|
|
)
|
|
|
|
processed = process_pending_batch(session=session, batch_size=10)
|
|
|
|
assert processed == 1
|
|
refreshed = session.get(TerminalMessageRaw, raw.id)
|
|
assert refreshed is not None
|
|
assert refreshed.parsed_at is not None
|
|
assert refreshed.parse_error == "boom"
|
|
processed_again = process_pending_batch(session=session, batch_size=10)
|
|
assert processed_again == 0
|
|
|
|
|
|
def test_schema_excludes_terminal_observation_event_table() -> None:
|
|
assert "terminal_observation_event" not in SQLModel.metadata.tables
|
|
|
|
|
|
def test_ingest_and_parse_real_nmea_sample() -> None:
|
|
from pathlib import Path
|
|
|
|
sample_path = Path(__file__).parent / "nmea.txt"
|
|
payload = sample_path.read_bytes()
|
|
|
|
with _session() as session:
|
|
raw = insert_raw_message(
|
|
session=session,
|
|
device_id="device-real-sample",
|
|
topic="terminal/device-real-sample/raw",
|
|
payload=payload,
|
|
qos=1,
|
|
retain=False,
|
|
)
|
|
|
|
processed = process_pending_batch(session=session, batch_size=10)
|
|
assert processed == 1
|
|
|
|
refreshed = session.get(TerminalMessageRaw, raw.id)
|
|
assert refreshed is not None
|
|
assert refreshed.parsed_at is not None
|
|
assert refreshed.parse_error is None
|
|
|
|
state = session.get(TerminalDeviceState, "device-real-sample")
|
|
assert state is not None
|
|
assert state.lat is not None
|
|
assert state.lon is not None
|
|
|
|
|
|
def test_worker_builds_satellite_distribution_summary() -> None:
|
|
payload = (
|
|
b"$GNGGA,123519,3723.2475,N,12158.3416,W,4,12,0.8,10.0,M,-25.0,M,,*65\r\n"
|
|
b"$GPGSA,A,3,02,05,12,,,,,,,,,,1.50,0.90,1.20*00\r\n"
|
|
b"$GPGSV,1,1,04,02,17,213,40,05,29,121,37,12,66,314,44,25,14,048,39*71\r\n"
|
|
)
|
|
|
|
with _session() as session:
|
|
insert_raw_message(
|
|
session=session,
|
|
device_id="device-gsv",
|
|
topic="terminal/device-gsv/raw",
|
|
payload=payload,
|
|
qos=1,
|
|
retain=False,
|
|
)
|
|
|
|
processed = process_pending_batch(session=session, batch_size=10)
|
|
|
|
assert processed == 1
|
|
state = session.get(TerminalDeviceState, "device-gsv")
|
|
assert state is not None
|
|
assert state.fix_status == "4"
|
|
summary = state.satellite_signal_summary
|
|
assert summary is not None
|
|
assert summary["satellites_in_view"] == 4
|
|
assert summary["snr_values"] == [40, 37, 44, 39]
|
|
assert summary["used_satellite_ids"] == ["02", "05", "12"]
|
|
assert summary["constellations"]["GPS"]["count"] == 4
|
|
assert summary["constellations"]["GPS"]["used_in_navigation"] == 3
|