Lab066: validate 500 ksymbol/s full-packet OTA reception
This commit is contained in:
149
tests/test_lab066_full_packet_rf_retest.py
Normal file
149
tests/test_lab066_full_packet_rf_retest.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""Lab066: исправленные паузы, строгий software gate и запрет TX."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from experiments import lab066_full_packet_rf_retest as lab
|
||||
from tools.hackrf_process_check import ProcessCheck, ProcessState
|
||||
|
||||
|
||||
def test_fixed_layout_and_operating_point():
|
||||
waveform, layout = lab.build_waveform()
|
||||
assert len(lab.plans()) == 16
|
||||
assert all(len(frame.plan.payload) == 256 for frame in lab.plans())
|
||||
assert len({frame.plan.payload for frame in lab.plans()}) == 16
|
||||
assert lab.PACKET_SAMPLES == 92_256
|
||||
assert lab.INTER_PACKET_GAP_SAMPLES == 40_000
|
||||
assert lab.NUMBER_OF_GAPS == 15
|
||||
assert lab.GUARD_SAMPLES == 800_000
|
||||
assert lab.BASE_TX_SAMPLES == 2_076_096
|
||||
assert lab.EXPECTED_TX_SAMPLES == 3_676_096
|
||||
assert len(waveform) == 2 * lab.EXPECTED_TX_SAMPLES
|
||||
assert [row["packet_index"] for row in layout] == list(range(1, 17))
|
||||
assert lab.parameters()["tx_vga_db"] == 20
|
||||
assert lab.parameters()["rx_lna_db"] == lab.parameters()["rx_vga_db"] == 24
|
||||
|
||||
|
||||
def test_guards_and_all_gaps_are_exact_zero():
|
||||
waveform, layout = lab.build_waveform()
|
||||
samples = memoryview(waveform)
|
||||
assert not any(samples[:2 * lab.GUARD_SAMPLES])
|
||||
assert not any(samples[-2 * lab.GUARD_SAMPLES:])
|
||||
for row in layout[:-1]:
|
||||
low = 2 * row["following_gap_start_sample"]
|
||||
high = low + 2 * row["following_gap_samples"]
|
||||
assert high - low == 2 * 40_000
|
||||
assert not any(samples[low:high])
|
||||
|
||||
|
||||
def test_transfer_commands_are_rx_only_or_fixed_tx():
|
||||
receive = lab.transfer_command("rx.cs8", 800_000)
|
||||
transmit = lab.transfer_command("tx.cs8", 100, transmit=True)
|
||||
assert "-r" in receive and "-t" not in receive
|
||||
assert "-t" in transmit and "-r" not in transmit
|
||||
assert receive[receive.index("-f") + 1] == "1400000000"
|
||||
assert receive[receive.index("-l") + 1] == receive[receive.index("-g") + 1] == "24"
|
||||
assert transmit[transmit.index("-x") + 1] == "20"
|
||||
|
||||
|
||||
def test_prepare_metadata_and_two_strict_roundtrips(tmp_path, monkeypatch):
|
||||
raw, output = tmp_path / "raw", tmp_path / "processed"
|
||||
monkeypatch.setattr(lab, "RAW", raw); monkeypatch.setattr(lab, "OUTPUT", output)
|
||||
metadata = lab.prepare()
|
||||
assert metadata["tx_samples"] == lab.EXPECTED_TX_SAMPLES
|
||||
assert metadata["tx_sha256"] == hashlib.sha256((raw / "tx_stream.cs8").read_bytes()).hexdigest()
|
||||
result = lab.software_gate()
|
||||
for mode in ("legacy", "t2"):
|
||||
row = result[mode]
|
||||
assert row["header_pass"] == row["fec_pass"] == row["crc_pass"] == 16
|
||||
assert row["packet_output"] == row["exact_payload_match"] == 16
|
||||
assert row["packet_indexes"] == list(range(1, 17))
|
||||
assert row["exact_ordered_payloads"] and row["passed"]
|
||||
assert row["fallback_between_modes"] is False
|
||||
assert result["t2"]["t2_state_scope"] == "NEW_PREAMBLE_ESTIMATE_PER_PACKET"
|
||||
assert result["t2"]["t2_state_reset_evidence"][
|
||||
"tracking_runs_per_accepted_packet"
|
||||
] == [2] * 16
|
||||
assert result["t2_state_reset_verified_by_fresh_preamble_per_packet"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", [None, "clock", "clipping", "strong", "overrun"])
|
||||
def test_preflight_never_transmits_and_fails_closed(tmp_path, monkeypatch, failure):
|
||||
raw, output = tmp_path / "raw", tmp_path / "processed"
|
||||
raw.mkdir(); output.mkdir()
|
||||
monkeypatch.setattr(lab, "RAW", raw); monkeypatch.setattr(lab, "OUTPUT", output)
|
||||
(raw / "tx_stream.cs8").write_bytes(b"test")
|
||||
monkeypatch.setattr(lab, "validate_prepared", lambda: {"tx_sha256": "TEST"})
|
||||
(output / "software_gate.json").write_text('{"passed": true, "tx_sha256": "TEST"}')
|
||||
monkeypatch.setattr(lab.lab054.phy.lab045, "_verify_device_inventory", lambda runner: {"passed": True})
|
||||
|
||||
def clock(point, runner, guard, **kwargs):
|
||||
return {"passed": failure != "clock", "master_reg29": 0x26,
|
||||
"slave_reg29": 0x26, "slave_clkin_status": "DETECTED"}
|
||||
|
||||
def runner(command):
|
||||
assert "-t" not in command
|
||||
if "-r" in command:
|
||||
value = 127 if failure == "clipping" else (110 if failure == "strong" else 1)
|
||||
Path(command[command.index("-r") + 1]).write_bytes(bytes([value, value]) * lab.PREFLIGHT_SAMPLES)
|
||||
log = "1 RX overruns\n0 RX overruns" if failure == "overrun" else "0 RX overruns"
|
||||
return subprocess.CompletedProcess(command, 0, log, "")
|
||||
|
||||
monkeypatch.setattr(lab.lab054, "disable_clock_outputs", lambda runner: [])
|
||||
kwargs = dict(runner=runner, clock_checker=clock,
|
||||
process_checker=lambda: ProcessCheck(ProcessState.CLEAR, ()),
|
||||
sleep_function=lambda _: None)
|
||||
if failure:
|
||||
with pytest.raises(RuntimeError): lab.preflight(**kwargs)
|
||||
else:
|
||||
assert lab.preflight(**kwargs)["passed"]
|
||||
record = json.loads((raw / "preflight_result.json").read_text())
|
||||
assert record["rf_tx_count"] == 0
|
||||
if failure == "overrun":
|
||||
assert record["rx_counter_summary"]["maximum_count"] == 1
|
||||
|
||||
|
||||
def test_capture_refuses_without_authorization_before_hardware(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(lab, "RAW", tmp_path); monkeypatch.setattr(lab, "OUTPUT", tmp_path)
|
||||
with pytest.raises(PermissionError):
|
||||
lab.capture_once(authorization=False, runner=lambda command: pytest.fail("hardware"))
|
||||
assert not (tmp_path / "hardware_attempt.json").exists()
|
||||
|
||||
|
||||
def test_future_analysis_declares_strict_modes_and_physical_comparison():
|
||||
source = Path(lab.__file__).read_text(encoding="utf-8")
|
||||
assert 'decode("legacy", rx_path, diagnostics=True)' in source
|
||||
assert 'decode("t2", rx_path, diagnostics=True)' in source
|
||||
assert "physical_packet_diagnostics(tx, rx, location)" in source
|
||||
assert "if not strict_runs_complete" in source
|
||||
assert '["Lab056 B", "Lab062", "Lab066"]' in source
|
||||
|
||||
|
||||
def test_future_analysis_rejects_capture_from_another_configuration(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(lab, "RAW", tmp_path); monkeypatch.setattr(lab, "OUTPUT", tmp_path)
|
||||
rx = tmp_path / "rx_stream.cs8"; rx.write_bytes(b"\x00\x00")
|
||||
capture = {
|
||||
"lab": "Lab066", "rf_tx_count": 1, "automatic_tx_repeats": 0,
|
||||
"point": {}, "parameters": {}, "tx_sha256": "WRONG",
|
||||
"rx_sha256": lab.sha256(rx),
|
||||
}
|
||||
(tmp_path / "capture_result.json").write_text(json.dumps(capture))
|
||||
monkeypatch.setattr(lab, "validate_prepared", lambda: {"tx_sha256": "EXPECTED"})
|
||||
monkeypatch.setattr(lab, "decode", lambda *args, **kwargs: pytest.fail("decode"))
|
||||
with pytest.raises(RuntimeError, match="подтверждённой"):
|
||||
lab.analyze_saved_capture()
|
||||
|
||||
|
||||
def test_second_preflight_is_refused_before_hardware(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(lab, "RAW", tmp_path); monkeypatch.setattr(lab, "OUTPUT", tmp_path)
|
||||
(tmp_path / "preflight_attempt.json").write_text("{}")
|
||||
monkeypatch.setattr(lab, "validate_prepared", lambda: {"tx_sha256": "TEST"})
|
||||
(tmp_path / "software_gate.json").write_text('{"passed": true, "tx_sha256": "TEST"}')
|
||||
with pytest.raises(FileExistsError):
|
||||
lab.preflight(runner=lambda command: pytest.fail("hardware"))
|
||||
Reference in New Issue
Block a user