feat: implement mqtt ingestion, parsing, and archive pipeline
This commit is contained in:
54
backend/tests/mqtt/test_archive_job.py
Normal file
54
backend/tests/mqtt/test_archive_job.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
|
||||
from app.models import TerminalMessageRaw, get_datetime_utc
|
||||
from app.mqtt.archive_job import run_archive_once
|
||||
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_archive_writes_raw_bytes_and_manifest(tmp_path: Path) -> None:
|
||||
with _session() as session:
|
||||
current = get_datetime_utc()
|
||||
old_ts = current - timedelta(days=91)
|
||||
|
||||
insert_raw_message(
|
||||
session=session,
|
||||
device_id="device-001",
|
||||
topic="terminal/device-001/raw",
|
||||
payload=b"abc",
|
||||
qos=1,
|
||||
retain=False,
|
||||
received_at=old_ts,
|
||||
)
|
||||
insert_raw_message(
|
||||
session=session,
|
||||
device_id="device-001",
|
||||
topic="terminal/device-001/raw",
|
||||
payload=b"\x01\x02",
|
||||
qos=1,
|
||||
retain=False,
|
||||
received_at=old_ts,
|
||||
)
|
||||
|
||||
manifests = run_archive_once(
|
||||
session=session,
|
||||
archive_base_dir=tmp_path,
|
||||
retention_days=90,
|
||||
now=current,
|
||||
)
|
||||
|
||||
assert len(manifests) == 1
|
||||
archived_file = Path(manifests[0].file_path)
|
||||
assert archived_file.exists()
|
||||
assert archived_file.read_bytes() == b"abc\x01\x02"
|
||||
|
||||
remaining = session.exec(select(TerminalMessageRaw)).all()
|
||||
assert remaining == []
|
||||
27
backend/tests/mqtt/test_mixed_parser.py
Normal file
27
backend/tests/mqtt/test_mixed_parser.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from app.mqtt.parsers.mixed import parse_mixed_payload
|
||||
from app.mqtt.parsers.nmea import parse_nmea_sentences
|
||||
|
||||
|
||||
def test_parse_nmea_sentences_extracts_gga_and_rmc() -> None:
|
||||
payload = (
|
||||
b"$GPGGA,123519,3723.2475,N,12158.3416,W,1,12,0.8,10.0,M,-25.0,M,,*65\r\n"
|
||||
b"$GPRMC,123520,A,3723.2475,N,12158.3416,W,0.13,309.62,120598,,,A*74\r\n"
|
||||
)
|
||||
|
||||
events = parse_nmea_sentences(payload)
|
||||
|
||||
assert {e["event_type"] for e in events} >= {"nmea_gga", "nmea_rmc"}
|
||||
|
||||
|
||||
def test_parse_mixed_payload_returns_rtcm_summary() -> None:
|
||||
payload = (
|
||||
b"$GPGSV,1,1,04,02,17,213,40,05,29,121,37,12,66,314,44,25,14,048,39*71\r\n"
|
||||
+ b"\xd3\x00\x04"
|
||||
+ b"\x43\x50\x00\x00"
|
||||
+ b"\x00\x00\x00"
|
||||
)
|
||||
|
||||
events = parse_mixed_payload(payload)
|
||||
|
||||
event_types = {event["event_type"] for event in events}
|
||||
assert "rtcm_summary" in event_types
|
||||
53
backend/tests/mqtt/test_parser_worker.py
Normal file
53
backend/tests/mqtt/test_parser_worker.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
|
||||
from app.models import TerminalDeviceState, TerminalMessageRaw, TerminalObservationEvent
|
||||
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_pending_status() -> 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.parse_status == "pending"
|
||||
assert row.payload_size > 0
|
||||
assert len(row.payload_sha256) == 64
|
||||
|
||||
|
||||
def test_worker_marks_success_and_writes_events() -> 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.parse_status == "succeeded"
|
||||
|
||||
events = session.exec(select(TerminalObservationEvent)).all()
|
||||
assert len(events) >= 1
|
||||
|
||||
state = session.get(TerminalDeviceState, "device-001")
|
||||
assert state is not None
|
||||
Reference in New Issue
Block a user