Add the first real test suite for protocol/
56 fast checks, 0.2 s, covering packet.py and the new bpsk_radio.py. test_bpsk_radio exists mainly to prove the extraction changed nothing. It loads the original function definitions out of Lab018, Lab019 and Lab023 in isolation, compiling only the wanted AST nodes so the labs' module level file writing never runs, then compares outputs: bit conversion, modulation, shaping filter, frame marker and the carrier estimator over random phase and frequency offsets. It also covers the frame functions written fresh for the module: roundtrip at four payload sizes, size and type rejection, broken sync word, truncation, carrying a real CRC32 packet, and a shaped-signal search that recovers the packet through matched filtering. test_packet covers roundtrip across all four message types and five payload sizes, sync word, length arithmetic, sequence range, truncation, oversized payload, and single-bit corruption at six positions, which is the property CRC32 is there to provide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
97
tests/test_packet.py
Normal file
97
tests/test_packet.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""Быстрые проверки пакетного протокола: protocol/packet.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
import pytest
|
||||
|
||||
from protocol.packet import (
|
||||
CRC_SIZE,
|
||||
HEADER_SIZE,
|
||||
MAX_PAYLOAD_SIZE,
|
||||
MESSAGE_TYPE_ACK,
|
||||
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
MESSAGE_TYPE_TELEMETRY,
|
||||
MESSAGE_TYPE_TEXT,
|
||||
PROTOCOL_VERSION,
|
||||
SYNC_WORD,
|
||||
CRCError,
|
||||
PacketError,
|
||||
build_packet,
|
||||
parse_packet,
|
||||
)
|
||||
|
||||
|
||||
MESSAGE_TYPES = (
|
||||
MESSAGE_TYPE_TEXT,
|
||||
MESSAGE_TYPE_TELEMETRY,
|
||||
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||
MESSAGE_TYPE_ACK,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message_type", MESSAGE_TYPES)
|
||||
@pytest.mark.parametrize("payload", [b"", b"H", b"HELLO SDR", bytes(range(256)), b"\x00" * 1024])
|
||||
def test_roundtrip_preserves_payload(payload: bytes, message_type: int) -> None:
|
||||
parsed = parse_packet(build_packet(payload, message_type, 7))
|
||||
assert parsed.payload == payload
|
||||
assert parsed.message_type == message_type
|
||||
assert parsed.sequence_number == 7
|
||||
assert parsed.version == PROTOCOL_VERSION
|
||||
|
||||
|
||||
def test_packet_starts_with_sync_word() -> None:
|
||||
packet = build_packet(b"data", MESSAGE_TYPE_TEXT, 1)
|
||||
assert struct.unpack(">H", packet[:2])[0] == SYNC_WORD
|
||||
|
||||
|
||||
def test_length_is_header_plus_payload_plus_crc() -> None:
|
||||
payload = b"x" * 300
|
||||
packet = build_packet(payload, MESSAGE_TYPE_TEXT, 1)
|
||||
assert len(packet) == HEADER_SIZE + len(payload) + CRC_SIZE
|
||||
|
||||
|
||||
def test_sequence_number_survives_full_range() -> None:
|
||||
for sequence_number in (0, 1, 65534, 65535):
|
||||
assert parse_packet(build_packet(b"a", MESSAGE_TYPE_TEXT, sequence_number)).sequence_number == sequence_number
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bit_index", [0, 7, 8, 23, 64, 100])
|
||||
def test_single_bit_corruption_is_detected(bit_index: int) -> None:
|
||||
"""CRC32 обязан ловить одиночное искажение бита в любом месте пакета."""
|
||||
|
||||
packet = bytearray(build_packet(b"HELLO SDR TELEMETRY", MESSAGE_TYPE_TELEMETRY, 3))
|
||||
byte_index, offset = divmod(bit_index, 8)
|
||||
assert byte_index < len(packet)
|
||||
packet[byte_index] ^= 1 << offset
|
||||
|
||||
with pytest.raises((CRCError, PacketError)):
|
||||
parse_packet(bytes(packet))
|
||||
|
||||
|
||||
def test_truncated_packet_is_rejected() -> None:
|
||||
packet = build_packet(b"HELLO", MESSAGE_TYPE_TEXT, 1)
|
||||
for cut in (0, 1, HEADER_SIZE - 1, len(packet) - 1):
|
||||
with pytest.raises(PacketError):
|
||||
parse_packet(packet[:cut])
|
||||
|
||||
|
||||
def test_oversized_payload_is_rejected() -> None:
|
||||
with pytest.raises((PacketError, ValueError)):
|
||||
build_packet(b"\x00" * (MAX_PAYLOAD_SIZE + 1), MESSAGE_TYPE_TEXT, 1)
|
||||
|
||||
|
||||
def test_wrong_sync_word_is_rejected() -> None:
|
||||
packet = bytearray(build_packet(b"HELLO", MESSAGE_TYPE_TEXT, 1))
|
||||
packet[0] ^= 0xFF
|
||||
with pytest.raises(PacketError):
|
||||
parse_packet(bytes(packet))
|
||||
|
||||
|
||||
def test_payload_is_not_shared_with_caller() -> None:
|
||||
"""Разбор не должен возвращать ссылку на изменяемый буфер."""
|
||||
|
||||
packet = bytearray(build_packet(b"HELLO", MESSAGE_TYPE_TEXT, 1))
|
||||
parsed = parse_packet(bytes(packet))
|
||||
assert isinstance(parsed.payload, bytes)
|
||||
Reference in New Issue
Block a user