Files
SDR-Rover/tests/test_packet.py
LittleSam129 2da80d0908 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>
2026-08-10 14:38:12 +03:00

98 lines
3.1 KiB
Python

"""Быстрые проверки пакетного протокола: 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)