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