Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions src/compas_xr/mqtt/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,40 @@ def __init__(
time_stamp: str = None,
):
super(Header, self).__init__()
self["sequence_id"] = sequence_id or self._ensure_sequence_id()
self["response_id"] = response_id or self._ensure_response_id(increment_response_ID)
self["device_id"] = device_id or self._get_device_id()
self["time_stamp"] = time_stamp or self._get_time_stamp()
# ``is None`` rather than falsiness: sequence_id and response_id are counters that
# legitimately start at 0, and ``0 or default`` would silently discard them.
self["sequence_id"] = sequence_id if sequence_id is not None else self._ensure_sequence_id()
self["response_id"] = (
response_id if response_id is not None else self._ensure_response_id(increment_response_ID)
)
self["device_id"] = device_id if device_id is not None else self._get_device_id()
self["time_stamp"] = time_stamp if time_stamp is not None else self._get_time_stamp()

@classmethod
def parse(cls, value):
"""Parse the header information
from the input value
"""
instance = cls(value["sequence_id"], value["response_id"], value["device_id"], value["time_stamp"])
# ``parse`` is called with the decoded JSON dict off the wire, but also with a
# Header instance when a message is re-parsed locally. Unwrap to the plain mapping.
value = getattr(value, "data", value)

missing = [
key
for key in ("sequence_id", "response_id", "device_id", "time_stamp")
if value.get(key) is None
]
if missing:
raise ValueError("Information for Header parsing missing: {}.".format(", ".join(missing)))

# Keyword arguments on purpose: the first positional parameter is
# ``increment_response_ID``, so passing these positionally shifts every field.
instance = cls(
sequence_id=value["sequence_id"],
response_id=value["response_id"],
device_id=value["device_id"],
time_stamp=value["time_stamp"],
)
instance._update_sequence_counter_from_message(value["sequence_id"])
instance._update_response_id_from_message(value["response_id"])
return instance
Expand Down
63 changes: 63 additions & 0 deletions tests/compas_xr/mqtt/test_messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import pytest

from compas_xr.mqtt.messages import GetTrajectoryRequest
from compas_xr.mqtt.messages import Header


def test_header_parse_preserves_every_field():
"""Header.parse used to pass values positionally into a signature whose first
parameter is ``increment_response_ID``, shifting every field by one."""
wire = {
"sequence_id": 7,
"response_id": 3,
"device_id": "device-abc",
"time_stamp": "2026-09-08T10:00:00",
}

header = Header.parse(wire)

assert header["sequence_id"] == 7
assert header["response_id"] == 3
assert header["device_id"] == "device-abc"
assert header["time_stamp"] == "2026-09-08T10:00:00"


def test_header_parse_keeps_zero_valued_counters():
"""sequence_id and response_id are counters that legitimately start at 0, which a
falsiness check would discard and regenerate."""
wire = {
"sequence_id": 0,
"response_id": 0,
"device_id": "device-abc",
"time_stamp": "2026-09-08T10:00:00",
}

header = Header.parse(wire)

assert header["sequence_id"] == 0
assert header["response_id"] == 0


@pytest.mark.parametrize("missing", ["sequence_id", "response_id", "device_id", "time_stamp"])
def test_header_parse_rejects_incomplete_headers(missing):
wire = {
"sequence_id": 1,
"response_id": 2,
"device_id": "device-abc",
"time_stamp": "2026-09-08T10:00:00",
}
del wire[missing]

with pytest.raises(ValueError, match=missing):
Header.parse(wire)


def test_message_roundtrip_preserves_header():
request = GetTrajectoryRequest(element_id="elem-7", robot_name="ur10e")

parsed = GetTrajectoryRequest.parse(request.data)

assert parsed["element_id"] == "elem-7"
assert parsed["robot_name"] == "ur10e"
for key in ("sequence_id", "response_id", "device_id", "time_stamp"):
assert parsed["header"][key] == request["header"][key]
Loading