"""Проверки целостности и ограничения возраста видеокадра.""" from __future__ import annotations from dataclasses import replace import pytest from protocol.video_stream_transport import ( BoundedFrameQueue, FrameReassembler, FrameType, VideoFrame, VideoTransportError, decode_fragment, encode_fragment, is_newer, split_frame, ) from tools.video002_radio_budget import estimate, margin_fits def frame(number: int = 1, capture_ms: int = 0, size: int = 600) -> VideoFrame: return VideoFrame(7, number, FrameType.KEYFRAME, capture_ms, bytes(range(256)) * (size // 256) + bytes(range(size % 256))) def test_header_roundtrip_and_corruption() -> None: part = split_frame(frame(), 256)[0] wire = encode_fragment(part, 256) assert len(wire) == 256 assert decode_fragment(wire, 256) == part with pytest.raises(VideoTransportError): decode_fragment(wire[:-1], 256) with pytest.raises(VideoTransportError): decode_fragment(bytes([0x21]) + wire[1:], 256) with pytest.raises(VideoTransportError): encode_fragment(replace(part, fragment_index=part.fragment_count), 256) def test_out_of_order_reassembly_no_partial_or_replay() -> None: source = frame() parts = [encode_fragment(item, 256) for item in split_frame(source, 256)] receiver = FrameReassembler(7, 256) assert receiver.offer(parts[2], 20) is None assert receiver.offer(parts[0], 21) is None assert receiver.offer(parts[0], 22) is None complete = receiver.offer(parts[1], 23) assert complete is not None and complete.data == source.data assert receiver.offer(parts[1], 24) is None assert receiver.rejected_fragments == 1 def test_incomplete_frame_expires_and_new_frame_advances() -> None: receiver = FrameReassembler(7, 256, max_age_ms=100) first = encode_fragment(split_frame(frame(1), 256)[0], 256) assert receiver.offer(first, 0) is None assert receiver.expire(100) == (1,) assert receiver.offer(first, 101) is None next_frame = frame(2, 101, 25) complete = receiver.offer(encode_fragment(split_frame(next_frame, 256)[0], 256), 102) assert complete is not None and complete.data == next_frame.data def test_sender_bounded_queue_discards_old_frame() -> None: sender = BoundedFrameQueue(256, max_frames=2, max_age_ms=100) sender.enqueue(frame(1, 0), 0) sender.enqueue(frame(2, 20), 20) sender.enqueue(frame(3, 40), 40) assert sender.depth == 2 and sender.capacity_frame_drops == 1 part = sender.next_fragment(40, 50) assert part is not None and part.frame_id == 2 sender.expire(120) assert sender.stale_frame_drops == 1 assert sender.next_fragment(120, 130).frame_id == 3 sender.expire(140) assert sender.depth == 0 and sender.stale_frame_drops == 2 def test_fractional_packet_end_drops_before_receiver_age_rounding() -> None: sender = BoundedFrameQueue(256, max_age_ms=300) sender.enqueue(frame(1, 0, 25), 0) assert sender.next_fragment(299, 299.1) is None assert sender.stale_frame_drops == 1 def test_conflicting_fragment_closes_frame() -> None: receiver = FrameReassembler(7, 256) original = split_frame(frame(), 256)[0] receiver.offer(encode_fragment(original, 256), 0) bad = replace(original, data=b"X" + original.data[1:]) with pytest.raises(VideoTransportError): receiver.offer(encode_fragment(bad, 256), 1) assert receiver.offer(encode_fragment(original, 256), 2) is None def test_wrong_stream_and_wraparound() -> None: receiver = FrameReassembler(7, 256) other = encode_fragment(split_frame(replace(frame(size=1), stream_id=8), 256)[0], 256) assert receiver.offer(other, 0) is None assert is_newer(0, 0xFFFFFFFF) assert not is_newer(0xFFFFFFFE, 1) def test_expired_frame_cannot_replay_after_closed_cache_rollover() -> None: receiver = FrameReassembler(7, 256, max_age_ms=10) for frame_id in range(1025): part = split_frame(frame(frame_id, frame_id * 20, 300), 256)[0] assert receiver.offer(encode_fragment(part, 256), frame_id * 20) is None receiver.expire(frame_id * 20 + 10) assert 0 not in receiver._closed old_parts = [encode_fragment(part, 256) for part in split_frame(frame(0, 0, 300), 256)] assert receiver.offer(old_parts[0], 20_501) is None assert receiver.offer(old_parts[1], 20_502) is None assert receiver.rejected_fragments >= 2 def test_two_new_frames_can_start_out_of_order() -> None: receiver = FrameReassembler(7, 256) newer_parts = [encode_fragment(item, 256) for item in split_frame(frame(11, 10, 300), 256)] older = encode_fragment(split_frame(frame(10, 0, 25), 256)[0], 256) assert receiver.offer(newer_parts[0], 20) is None assert receiver.offer(older, 21).frame_id == 10 assert receiver.offer(newer_parts[1], 22).frame_id == 11 def test_radio_formula_and_margin() -> None: current = estimate("BPSK", 500_000, 256, 5) assert current.coded_body_bits == 4300 assert current.airtime_ms == pytest.approx(11.532) assert current.video_bytes_per_packet == 239 assert current.useful_video_bps == pytest.approx(115_654.488) future = estimate("QPSK", 500_000, 1024, 0) assert not future.within_current_packet_limit assert not future.implemented_in_current_phy assert not margin_fits(future.useful_video_bps, 376_000, 0.2)