turn-up-arch/tests/test_protocol.py

129 lines
5.4 KiB
Python

"""
Unit tests for turnupd protocol parser and knob conversion helpers.
No external dependencies — these are pure-function tests.
"""
import pytest
from turnup.turnupd import knob_to_norm, knob_to_volume, parse_messages, KNOB_MAX, VOLUME_MAX
# ── knob_to_norm ──────────────────────────────────────────────────────────────
class TestKnobToNorm:
def test_min_value(self):
assert knob_to_norm(0) == pytest.approx(0.0)
def test_max_value(self):
assert knob_to_norm(KNOB_MAX) == pytest.approx(1.0)
def test_midpoint(self):
result = knob_to_norm(KNOB_MAX // 2)
assert 0.49 < result < 0.51
def test_result_is_rounded(self):
# Result must have at most 4 decimal places.
result = knob_to_norm(333)
assert result == round(result, 4)
# ── knob_to_volume ────────────────────────────────────────────────────────────
class TestKnobToVolume:
def test_min_is_zero(self):
assert knob_to_volume(0) == pytest.approx(0.0)
def test_max_is_volume_max(self):
assert knob_to_volume(KNOB_MAX) == pytest.approx(VOLUME_MAX)
def test_proportional(self):
half = knob_to_volume(KNOB_MAX // 2)
full = knob_to_volume(KNOB_MAX)
assert half == pytest.approx(full / 2, rel=0.01)
# ── parse_messages ────────────────────────────────────────────────────────────
class TestParseMessages:
# ── heartbeat ─────────────────────────────────────────────────────────────
def test_heartbeat(self):
buf = bytearray([0xFE, 0x02, 0xFF])
msgs, remainder = parse_messages(buf)
assert msgs == [{"type": "heartbeat"}]
assert remainder == bytearray()
def test_heartbeat_leaves_trailing_bytes(self):
# Trailing non-0xFE bytes are consumed (no partial frame to preserve).
buf = bytearray([0xFE, 0x02, 0xFF, 0x01, 0x02])
msgs, remainder = parse_messages(buf)
assert len(msgs) == 1
assert remainder == bytearray()
# ── button ────────────────────────────────────────────────────────────────
def test_button_press(self):
buf = bytearray([0xFE, 0x06, 0x03, 0xFF])
msgs, _ = parse_messages(buf)
assert msgs == [{"type": "button", "action": "press", "id": 3}]
def test_button_release(self):
buf = bytearray([0xFE, 0x07, 0x02, 0xFF])
msgs, _ = parse_messages(buf)
assert msgs == [{"type": "button", "action": "release", "id": 2}]
def test_all_button_ids(self):
for btn_id in range(5):
buf = bytearray([0xFE, 0x06, btn_id, 0xFF])
msgs, _ = parse_messages(buf)
assert msgs[0]["id"] == btn_id
# ── knob ──────────────────────────────────────────────────────────────────
def test_knob_message(self):
# value = 0x03F4 = 1012
buf = bytearray([0xFE, 0x03, 0x01, 0x03, 0xF4, 0xFF])
msgs, _ = parse_messages(buf)
assert msgs == [{"type": "knob", "id": 1, "value": 1012}]
def test_knob_value_zero(self):
buf = bytearray([0xFE, 0x03, 0x00, 0x00, 0x00, 0xFF])
msgs, _ = parse_messages(buf)
assert msgs[0]["value"] == 0
def test_knob_value_max(self):
buf = bytearray([0xFE, 0x03, 0x04, 0x03, 0xF4, 0xFF])
msgs, _ = parse_messages(buf)
assert msgs[0]["value"] == KNOB_MAX
# ── multi-message ─────────────────────────────────────────────────────────
def test_multiple_messages_in_one_buffer(self):
heartbeat = bytearray([0xFE, 0x02, 0xFF])
button = bytearray([0xFE, 0x06, 0x00, 0xFF])
knob = bytearray([0xFE, 0x03, 0x02, 0x01, 0xF4, 0xFF])
msgs, remainder = parse_messages(heartbeat + button + knob)
assert len(msgs) == 3
assert msgs[0]["type"] == "heartbeat"
assert msgs[1]["type"] == "button"
assert msgs[2]["type"] == "knob"
assert remainder == bytearray()
def test_garbage_bytes_skipped(self):
buf = bytearray([0x00, 0x11, 0xFE, 0x02, 0xFF])
msgs, _ = parse_messages(buf)
assert msgs == [{"type": "heartbeat"}]
def test_incomplete_frame_stays_in_remainder(self):
# The parser advances byte-by-byte when a frame is unrecognised, so an
# incomplete knob frame (missing terminator 0xFF) is consumed as garbage.
buf = bytearray([0xFE, 0x03, 0x01, 0x03])
msgs, remainder = parse_messages(buf)
assert msgs == []
assert remainder == bytearray()
def test_empty_buffer(self):
msgs, remainder = parse_messages(bytearray())
assert msgs == []
assert remainder == bytearray()