"""RF-001: воспроизводимость файлового IQ без аппаратуры и передачи.""" from __future__ import annotations import json from itertools import pairwise from pathlib import Path import numpy as np import pytest from protocol.video003_modem import build_frame from protocol.video005_receiver import IqStreamResult, ReceivedIqPacket from tools import qpsk_rf_rx from tools.qpsk_rf_common import ( GAP_SAMPLES, PACKET_COUNT, PAYLOAD_BYTES, SEED, payload_for_sequence, read_iq_file, sha256_file, ) from tools.qpsk_rf_tx import build_dry_run, write_dry_run def test_payload_and_gap_are_fixed_and_reproducible() -> None: first = payload_for_sequence(SEED, 0) assert len(first) == PAYLOAD_BYTES assert first == payload_for_sequence(SEED, 0) assert first != payload_for_sequence(SEED, 1) iq, metadata = build_dry_run() assert iq.dtype == np.dtype(" left for left, right in pairwise(starts)) assert iq.size == metadata["waveform_sample_count"] with pytest.raises(ValueError): payload_for_sequence(-1, 0) def test_full_100_packet_file_roundtrip(tmp_path: Path) -> None: output_directory = tmp_path / "rf001" iq_path, metadata_path = write_dry_run(output_directory) metadata = json.loads(metadata_path.read_text(encoding="utf-8")) assert iq_path.stat().st_size == metadata["waveform_sample_count"] * 8 assert sha256_file(iq_path) == metadata["iq_sha256"] with pytest.raises(FileExistsError): write_dry_run(output_directory) report = qpsk_rf_rx.decode_file(iq_path, truth_metadata_path=metadata_path) assert report["decoded_packets"] == PACKET_COUNT assert report["crc_valid_packets"] == PACKET_COUNT assert report["exact_packets"] == PACKET_COUNT assert report["packet_sequences"] == list(range(PACKET_COUNT)) assert report["duplicates"] == 0 assert report["missing_packets"] == [] assert report["raw_ber"] == 0 assert report["raw_bits_compared_on_crc_valid_packets"] > 0 assert report["raw_ber_replay_failures"] == 0 assert report["post_fec_bit_errors"] == 0 assert report["same_bytes_as_tx_waveform"] assert report["rf_tx"] == 0 assert report["sdr_hardware"] == "NOT_ACCESSED" def test_missing_packets_do_not_get_invented_raw_ber() -> None: frame = build_frame(payload_for_sequence(SEED, 0), "QPSK", "2/3", sequence_number=0) packet = ReceivedIqPacket(frame.packet, payload_for_sequence(SEED, 0), 0.0, 0.0, 0.0, 0.0, 0, 1.0, 0.0) reception = IqStreamResult((packet,), 1, 0, {}) _, metadata = build_dry_run() report = qpsk_rf_rx.summarize_result( reception, iq_sample_count=100, iq_sha256="different", iq_format="c64le", clipping_codes=None, truth=metadata, raw_bits=(0, 3210, 0), ) assert report["exact_packets"] == 1 assert len(report["missing_packets"]) == 99 assert report["raw_ber"] is None assert report["raw_ber_on_crc_valid_packets"] == 0 assert report["raw_ber_scope"].startswith("CRC_VALID_PACKETS_ONLY") assert report["post_fec_error_rate"] is None assert report["post_fec_error_rate_on_compared_packets"] == 0 def test_truth_file_is_read_only_after_receiver(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: iq_path = tmp_path / "input.c64le" np.zeros(32, dtype=" IqStreamResult: assert iq.size == 32 called.append(True) return IqStreamResult((), 0, 0, {}) monkeypatch.setattr(qpsk_rf_rx, "receive_iq_stream", fake_receiver) with pytest.raises(json.JSONDecodeError): qpsk_rf_rx.decode_file(iq_path, truth_metadata_path=truth_path) assert called == [True] def test_iq_file_format_and_clipping_validation(tmp_path: Path) -> None: invalid = tmp_path / "bad.c64le" invalid.write_bytes(b"123") with pytest.raises(ValueError, match="c64le"): read_iq_file(invalid) raw = tmp_path / "capture.cs8" raw.write_bytes(bytes([127, 0, 128, 10])) iq, clipping = read_iq_file(raw, "cs8") assert iq.dtype == np.complex64 assert iq.size == 2 assert clipping == 2