Split monolithic __init__.py into six modules matching the Rust client structure: protocol.py, codec.py, discovery.py, transport.py, connect.py, client.py. All symbols re-exported from top-level for backwards compat. New features: - Ping-on-connect validation (200ms handshake, matches Rust v0.2.0) - try_connect(), connect_default(), connect_mux_auto(), validate() - connect() commits to mux when socket exists (no USB fall-through) - Bandwidth/ErrorCode IntEnums, RadioConfig TypedDict, py.typed marker - MAX_PAYLOAD, RADIO_CONFIG_SIZE, tag/error constants - Payload size validation on Transmit - 46 pytest tests ported from Rust (protocol, codec, client, firmware compat) - GitHub Actions CI (ruff + pytest on 3.10-3.13 + PyPI publish) - Strict ruff linting (E, W, F, I, UP, B, C4, ARG, SIM, RUF) Breaking: package renamed back to donglora (from donglora-python). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
3.5 KiB
Python
136 lines
3.5 KiB
Python
"""Tests for donglora.client — send/recv/drain_rx/validate."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import struct
|
|
from io import BytesIO
|
|
from typing import Any
|
|
|
|
from donglora.client import _rx_queue, drain_rx, recv, send, validate
|
|
from donglora.codec import cobs_encode
|
|
|
|
|
|
def _make_mock(responses: list[bytes]) -> Any:
|
|
"""Create a mock transport that returns pre-encoded COBS frames."""
|
|
wire = b""
|
|
for resp in responses:
|
|
wire += cobs_encode(resp)
|
|
|
|
class Mock:
|
|
def __init__(self) -> None:
|
|
self._stream = BytesIO(wire)
|
|
self.timeout = 2.0
|
|
self._written: list[bytes] = []
|
|
|
|
def read(self, n: int = 1) -> bytes:
|
|
return self._stream.read(n)
|
|
|
|
def write(self, data: bytes) -> int:
|
|
self._written.append(data)
|
|
return len(data)
|
|
|
|
def flush(self) -> None:
|
|
pass
|
|
|
|
return Mock()
|
|
|
|
|
|
def _pong() -> bytes:
|
|
return bytes([0])
|
|
|
|
|
|
def _ok() -> bytes:
|
|
return bytes([4])
|
|
|
|
|
|
def _rx_packet(payload: bytes, rssi: int = -80, snr: int = 10) -> bytes:
|
|
return (
|
|
bytes([2])
|
|
+ struct.pack("<h", rssi)
|
|
+ struct.pack("<h", snr)
|
|
+ struct.pack("<H", len(payload))
|
|
+ payload
|
|
)
|
|
|
|
|
|
class TestSend:
|
|
def setup_method(self) -> None:
|
|
_rx_queue.clear()
|
|
|
|
def test_basic_command(self) -> None:
|
|
mock = _make_mock([_pong()])
|
|
resp = send(mock, "Ping")
|
|
assert resp["type"] == "Pong"
|
|
|
|
def test_buffers_rx_packets(self) -> None:
|
|
mock = _make_mock([_rx_packet(b"pkt1"), _rx_packet(b"pkt2"), _ok()])
|
|
resp = send(mock, "StartRx")
|
|
assert resp["type"] == "Ok"
|
|
assert len(_rx_queue) == 2
|
|
|
|
def test_timeout(self) -> None:
|
|
mock = _make_mock([])
|
|
resp = send(mock, "Ping")
|
|
assert resp["type"] == "Timeout"
|
|
|
|
|
|
class TestRecv:
|
|
def setup_method(self) -> None:
|
|
_rx_queue.clear()
|
|
|
|
def test_from_buffer(self) -> None:
|
|
_rx_queue.append({"type": "RxPacket", "rssi": -80, "snr": 10, "payload": b"buf"})
|
|
mock = _make_mock([])
|
|
pkt = recv(mock)
|
|
assert pkt is not None
|
|
assert pkt["payload"] == b"buf"
|
|
|
|
def test_from_wire(self) -> None:
|
|
mock = _make_mock([_rx_packet(b"wire")])
|
|
pkt = recv(mock)
|
|
assert pkt is not None
|
|
assert pkt["payload"] == b"wire"
|
|
|
|
def test_timeout(self) -> None:
|
|
mock = _make_mock([])
|
|
assert recv(mock) is None
|
|
|
|
|
|
class TestDrainRx:
|
|
def setup_method(self) -> None:
|
|
_rx_queue.clear()
|
|
|
|
def test_drains_buffer_and_wire(self) -> None:
|
|
_rx_queue.append({"type": "RxPacket", "rssi": 0, "snr": 0, "payload": b"buf"})
|
|
mock = _make_mock([_rx_packet(b"wire")])
|
|
packets = drain_rx(mock)
|
|
assert len(packets) == 2
|
|
assert packets[0]["payload"] == b"buf"
|
|
assert packets[1]["payload"] == b"wire"
|
|
assert len(_rx_queue) == 0
|
|
|
|
|
|
class TestValidate:
|
|
def setup_method(self) -> None:
|
|
_rx_queue.clear()
|
|
|
|
def test_success(self) -> None:
|
|
mock = _make_mock([_pong()])
|
|
validate(mock)
|
|
|
|
def test_failure(self) -> None:
|
|
mock = _make_mock([_ok()])
|
|
try:
|
|
validate(mock)
|
|
assert False, "should have raised" # noqa: B011
|
|
except ConnectionError:
|
|
pass
|
|
|
|
def test_timeout(self) -> None:
|
|
mock = _make_mock([])
|
|
try:
|
|
validate(mock)
|
|
assert False, "should have raised" # noqa: B011
|
|
except ConnectionError:
|
|
pass
|