"""Быстрые проверки Lab046: фрагментация большого объекта и fail-closed сборка. Тесты не трогают оборудование, не создают файлов и не запускают RF. Полный тракт PHY (кадр Lab045 -> канал -> приём -> сборка) проверяется на маленьких объектах, чтобы оставаться быстрым. """ from __future__ import annotations import struct import numpy as np from experiments import lab046_bpsk_object_transport as lab046 from protocol import image_fragments as fragments # ------------------------------------------------------------ Детерминированный объект def test_deterministic_object_is_reproducible_and_hashable() -> None: first = lab046.make_deterministic_object(4096, seed=46) second = lab046.make_deterministic_object(4096, seed=46) assert first == second assert len(first) == 4096 other = lab046.make_deterministic_object(4096, seed=47) assert other != first assert len(lab046.object_sha256(first)) == 64 # ------------------------------------------------------------ Фрагмент туда-обратно def test_fragment_round_trip_exact_without_channel() -> None: obj = lab046.make_deterministic_object(3000, seed=1) pieces = fragments.split_image_bytes(obj, lab046.DEFAULT_OBJECT_ID, lab046.FRAGMENT_DATA_SIZE) assert fragments.reassemble_image(pieces) == obj def test_last_short_fragment_recovered_by_header_length() -> None: # 3000 не кратно 244, значит последний фрагмент короче. obj = lab046.make_deterministic_object(3000, seed=2) pieces = fragments.split_image_bytes(obj, lab046.DEFAULT_OBJECT_ID, lab046.FRAGMENT_DATA_SIZE) last = pieces[-1] assert len(last.data) < lab046.FRAGMENT_DATA_SIZE padded = lab046._pad_payload_to_packet(fragments.encode_image_fragment(last)) assert len(padded) == lab046.PACKET_PAYLOAD_SIZE recovered = lab046.recover_fragment_from_payload(padded) assert recovered is not None assert recovered.data == last.data def _make_payload(object_id: int, index: int, count: int, original_size: int, data: bytes) -> bytes: header = struct.pack( fragments.FRAGMENT_HEADER_FORMAT, object_id, index, count, original_size, ) return lab046._pad_payload_to_packet(header + data) # ------------------------------------------------------------ recover: fail-closed def test_recover_rejects_truncated_metadata() -> None: # Короче заголовка -> декод не проходит, объект не собирается. assert lab046.recover_fragment_from_payload(b"\x00\x03") is None def test_recover_rejects_impossible_index() -> None: payload = _make_payload(lab046.DEFAULT_OBJECT_ID, index=5, count=3, original_size=600, data=b"\x01" * 200) assert lab046.recover_fragment_from_payload(payload) is None def test_recover_rejects_inconsistent_count_and_size() -> None: # count=1, но original_size больше одного фрагмента -> несогласовано. payload = _make_payload(lab046.DEFAULT_OBJECT_ID, index=0, count=1, original_size=10_000, data=b"\x01" * 244) assert lab046.recover_fragment_from_payload(payload) is None def test_recover_rejects_absurd_original_size_without_huge_alloc() -> None: payload = _make_payload(lab046.DEFAULT_OBJECT_ID, index=0, count=2, original_size=2**31, data=b"\x01" * 244) assert lab046.recover_fragment_from_payload(payload) is None def test_expected_length_returns_none_on_bad_fields() -> None: assert lab046.expected_fragment_data_length(0, 0, 100) is None assert lab046.expected_fragment_data_length(-1, 3, 100) is None assert lab046.expected_fragment_data_length(0, 1, 0) is None # ------------------------------------------------------------ Сборщик: fail-closed def test_assembler_rejects_foreign_object_id() -> None: obj = lab046.make_deterministic_object(600, seed=3) pieces = fragments.split_image_bytes(obj, lab046.DEFAULT_OBJECT_ID, lab046.FRAGMENT_DATA_SIZE) assembler = lab046.ObjectAssembler() assert assembler.offer(pieces[0]) == "accepted" foreign = fragments.ImageFragment( image_id=0xDEADBEEF, fragment_index=pieces[1].fragment_index, fragment_count=pieces[1].fragment_count, original_size=pieces[1].original_size, data=pieces[1].data, ) assert assembler.offer(foreign) == "rejected" assert not assembler.object_complete() def test_assembler_identical_duplicate_ignored_conflicting_rejected() -> None: obj = lab046.make_deterministic_object(600, seed=4) pieces = fragments.split_image_bytes(obj, lab046.DEFAULT_OBJECT_ID, lab046.FRAGMENT_DATA_SIZE) assembler = lab046.ObjectAssembler() assert assembler.offer(pieces[0]) == "accepted" assert assembler.offer(pieces[0]) == "duplicate" conflicting = fragments.ImageFragment( image_id=pieces[0].image_id, fragment_index=pieces[0].fragment_index, fragment_count=pieces[0].fragment_count, original_size=pieces[0].original_size, data=bytes(len(pieces[0].data)), ) assert assembler.offer(conflicting) == "conflicting" def test_assembler_none_is_rejected_not_raised() -> None: assembler = lab046.ObjectAssembler() assert assembler.offer(None) == "rejected" assert assembler.recovered_object() is None def test_assembler_incomplete_returns_no_object() -> None: obj = lab046.make_deterministic_object(600, seed=5) pieces = fragments.split_image_bytes(obj, lab046.DEFAULT_OBJECT_ID, lab046.FRAGMENT_DATA_SIZE) assembler = lab046.ObjectAssembler() assembler.offer(pieces[0]) # намеренно не все assert not assembler.object_complete() assert assembler.recovered_object() is None # ------------------------------------------------------------ Полный тракт PHY: сценарии def test_scenario_a_ideal_channel_exact_object() -> None: obj = lab046.make_deterministic_object(2048, seed=46) result = lab046.run_object_transport(obj, channel=lab046.ideal_channel) assert result.object_complete assert result.sha_match assert result.object_byte_errors == 0 assert result.crc_fail_count == 0 assert result.recovered_object_sha256 == result.object_sha256 def test_scenario_b_distributed_bit_errors_corrected() -> None: # Честный гауссов шум: до FEC ошибки есть, после FEC ноль, объект точен. obj = lab046.make_deterministic_object(2048, seed=46) channel = lab046.make_noisy_channel(noise_sigma=0.40, base_seed=46000) result = lab046.run_object_transport(obj, channel=channel) assert result.pre_fec_coded_bit_errors > 0 assert result.post_fec_protocol_bit_errors == 0 assert result.crc_fail_count == 0 assert result.object_complete assert result.sha_match assert result.object_byte_errors == 0 def test_scenario_c_out_of_order_delivery_still_exact() -> None: obj = lab046.make_deterministic_object(2048, seed=46) frames = lab046.build_object_frames(obj) order = list(reversed(range(len(frames)))) result = lab046.run_object_transport(obj, channel=lab046.ideal_channel, delivery_order=order) assert result.object_complete assert result.sha_match def test_scenario_d_duplicate_fragments_still_exact() -> None: obj = lab046.make_deterministic_object(2048, seed=46) result = lab046.run_object_transport( obj, channel=lab046.ideal_channel, duplicate_indexes=(0, 2, 2), ) assert result.duplicates >= 1 assert result.object_complete assert result.sha_match def test_scenario_e_missing_fragment_means_not_complete() -> None: obj = lab046.make_deterministic_object(2048, seed=46) result = lab046.run_object_transport( obj, channel=lab046.ideal_channel, drop_indexes=(1,), ) assert not result.object_complete assert 1 in result.missing_indexes assert result.recovered_object_sha256 is None assert not result.sha_match def test_scenario_f_corrupted_packet_rejected_no_fake_object() -> None: obj = lab046.make_deterministic_object(2048, seed=46) result = lab046.run_object_transport( obj, channel=lab046.ideal_channel, corrupt_indexes=(0,), ) assert result.crc_fail_count >= 1 assert not result.object_complete assert result.recovered_object_sha256 is None # ------------------------------------------------------------ Метрики кадра def test_object_metrics_match_lab045_frame() -> None: obj = lab046.make_deterministic_object(16384, seed=46) frames = lab046.build_object_frames(obj) assert len(frames) == 68 # ceil(16384 / 244) assert all(f.plan.frame_symbol_count == 5724 for f in frames) assert all(len(f.plan.packet) == 268 for f in frames) # ------------------------------------------------------------ Нет оракула def test_receiver_ignores_original_object_on_corruption() -> None: # Если бы приёмник подглядывал в оригинал, повреждённый кадр «восстановился» # бы. Проверяем, что этого не происходит: объект остаётся неполным. obj = lab046.make_deterministic_object(2048, seed=46) result = lab046.run_object_transport( obj, channel=lab046.ideal_channel, corrupt_indexes=tuple(range(len(lab046.build_object_frames(obj)))), ) assert result.crc_pass_count == 0 assert not result.object_complete assert result.recovered_object_sha256 is None def test_functional_self_check_runs() -> None: lab046.run_functional_tests()