Lab056: compare 20 and 500 ksymbol/s in controlled RF session
This commit is contained in:
140
tests/test_lab054_hardware_control.py
Normal file
140
tests/test_lab054_hardware_control.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""Lab054: фиксированные RF-команды и отказы runner без оборудования."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from experiments import lab054_hardware_control as lab
|
||||
from tools.hackrf_process_check import ProcessCheck, ProcessState
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", [None, "master", "slave", "clkin", "receive"])
|
||||
def test_clock_gates_never_transmit(failure):
|
||||
commands = []
|
||||
|
||||
def runner(command):
|
||||
commands.append(command)
|
||||
output, code = "", 0
|
||||
if "--si5351c" in command:
|
||||
serial = command[command.index("-d") + 1]
|
||||
bad = ((failure == "master" and serial == lab.clock.MASTER_SERIAL)
|
||||
or (failure == "slave" and serial == lab.clock.SLAVE_SERIAL))
|
||||
output = "0x0e" if bad else "0x26"
|
||||
elif "-i" in command:
|
||||
output = "UNKNOWN" if failure == "clkin" else "CLKIN status: clock signal detected"
|
||||
elif "-r" in command:
|
||||
code = int(failure == "receive")
|
||||
return subprocess.CompletedProcess(command, code, output, "")
|
||||
|
||||
guard = lab.clock.TransmitGuard()
|
||||
if failure:
|
||||
with pytest.raises(RuntimeError):
|
||||
lab.clock_preflight(runner, guard, sleep_function=lambda _: None)
|
||||
assert not guard.tx_allowed
|
||||
else:
|
||||
assert lab.clock_preflight(runner, guard, sleep_function=lambda _: None)["passed"]
|
||||
assert guard.tx_allowed
|
||||
assert not any("-t" in command for command in commands)
|
||||
for command in commands:
|
||||
if Path(command[0]).name == "hackrf_transfer.exe":
|
||||
assert command[command.index("-f") + 1] == "1400000000"
|
||||
assert command[command.index("-l") + 1] == command[command.index("-g") + 1] == "24"
|
||||
|
||||
|
||||
def test_authorization_precedes_all_side_effects(tmp_path):
|
||||
with pytest.raises(PermissionError):
|
||||
lab.capture_once(tmp_path, runner=lambda _: pytest.fail("hardware"))
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", [None, "clock", "early_rx", "tx", "cleanup"])
|
||||
def test_one_shot_order_and_no_retry(tmp_path, monkeypatch, failure):
|
||||
events = []
|
||||
monkeypatch.setattr(lab, "validate_prepared", lambda directory: {"tx_samples": 123})
|
||||
monkeypatch.setattr(lab.phy.lab045, "_verify_device_inventory", lambda runner: {})
|
||||
|
||||
def preflight(runner, guard):
|
||||
if failure == "clock":
|
||||
raise RuntimeError("clock")
|
||||
guard.allow()
|
||||
return {"passed": True}
|
||||
|
||||
monkeypatch.setattr(lab, "clock_preflight", preflight)
|
||||
monkeypatch.setattr(lab, "disable_clock_outputs", lambda runner: events.append("clocks_off") or [])
|
||||
|
||||
class Process:
|
||||
def poll(self):
|
||||
return 1 if failure == "early_rx" else None
|
||||
|
||||
def wait(self, timeout):
|
||||
with (tmp_path / "rx_stream.cs8").open("wb") as stream:
|
||||
stream.truncate(lab.RX_SAMPLES * 2)
|
||||
return 0
|
||||
|
||||
def close_logs(self):
|
||||
pass
|
||||
|
||||
def terminate(self):
|
||||
events.append("terminate")
|
||||
if failure == "cleanup":
|
||||
raise RuntimeError("cleanup")
|
||||
|
||||
def starter(command, stdout, stderr):
|
||||
events.append("rx")
|
||||
stdout.write_text("")
|
||||
stderr.write_text("")
|
||||
return Process()
|
||||
|
||||
def runner(command):
|
||||
assert "-t" in command and "-R" not in command
|
||||
events.append("tx")
|
||||
return subprocess.CompletedProcess(command, int(failure == "tx"), "", "")
|
||||
|
||||
kwargs = dict(authorization=True, runner=runner, process_starter=starter,
|
||||
process_checker=lambda: ProcessCheck(ProcessState.CLEAR, ()),
|
||||
sleep_function=lambda _: events.append("lead"))
|
||||
if failure:
|
||||
with pytest.raises(RuntimeError):
|
||||
lab.capture_once(tmp_path, **kwargs)
|
||||
else:
|
||||
assert lab.capture_once(tmp_path, **kwargs)["passed"]
|
||||
assert events.count("tx") == (0 if failure in ("clock", "early_rx") else 1)
|
||||
if "tx" in events:
|
||||
assert events.index("rx") < events.index("lead") < events.index("tx")
|
||||
assert "clocks_off" in events
|
||||
assert json.loads((tmp_path / "capture_result.json").read_text())["passed"] == (failure is None)
|
||||
before = events.copy()
|
||||
with pytest.raises(FileExistsError):
|
||||
lab.capture_once(tmp_path, **kwargs)
|
||||
assert events == before
|
||||
|
||||
|
||||
def test_clock_cleanup_continues_after_timeout():
|
||||
commands = []
|
||||
|
||||
def runner(command):
|
||||
commands.append(command)
|
||||
raise subprocess.TimeoutExpired(command, 1)
|
||||
|
||||
errors = lab.disable_clock_outputs(runner)
|
||||
assert len(commands) == len(errors) == 2
|
||||
assert [command[2] for command in commands] == [lab.clock.MASTER_SERIAL, lab.clock.SLAVE_SERIAL]
|
||||
|
||||
|
||||
def test_payload_match_does_not_hide_changed_packet():
|
||||
from types import SimpleNamespace
|
||||
|
||||
plan = lab.plans()[0].plan
|
||||
packet = bytes([plan.packet[0] ^ 1]) + plan.packet[1:]
|
||||
reception = SimpleNamespace(payload_bytes=plan.payload, packet_bytes=packet)
|
||||
frame = SimpleNamespace(reception=reception,
|
||||
post_preamble_soft=lab.phy.lab045.modulate_bits_to_ideal_soft(
|
||||
__import__("numpy").concatenate((plan.phy_header_bits, plan.interleaved_bits))))
|
||||
result = lab.compare_after_verdict([(None, SimpleNamespace(accepted=True, frame=frame))])
|
||||
assert result["payload_matched_packets"] == 1
|
||||
assert result["exact_packets"] == result["unique_exact_packets"] == 0
|
||||
assert result["post_fec_errors"] == 1
|
||||
Reference in New Issue
Block a user