Files
client-py/tests/test_client.py
T
swaitsandClaude Opus 4.6 7a9e67ac1c
CI / lint (push) Canceled after 0s
CI / test (3.10) (push) Canceled after 0s
CI / test (3.11) (push) Canceled after 0s
CI / test (3.12) (push) Canceled after 0s
CI / test (3.13) (push) Canceled after 0s
CI / publish (push) Canceled after 0s
feat!: v0.2.0 — module split, Rust parity, PyPI-ready
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>
2026-04-08 13:33:37 -06:00

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