Lab056: compare 20 and 500 ksymbol/s in controlled RF session
This commit is contained in:
159
tests/test_hackrf_process_check.py
Normal file
159
tests/test_hackrf_process_check.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""Достоверность перечисления процессов и запрет аппаратных действий при FAIL."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from experiments import lab054_hardware_control as lab
|
||||
from tools import hackrf_process_check as check
|
||||
|
||||
|
||||
def snapshot(names=("python.exe",), errors=()):
|
||||
return check.classify_snapshot("fixture", [check.ProcessIdentity(index + 1, name)
|
||||
for index, name in enumerate(names)], errors)
|
||||
|
||||
|
||||
def completed(stdout="", stderr="", code=0):
|
||||
return lambda command: subprocess.CompletedProcess(command, code, stdout, stderr)
|
||||
|
||||
|
||||
def test_clear_full_snapshot():
|
||||
result = check.check_processes([lambda: snapshot()])
|
||||
assert result.state is check.ProcessState.CLEAR
|
||||
check.require_clear(result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["hackrf_transfer.exe", "hackrf_clock.exe", "HACKRF_DEBUG.EXE",
|
||||
"hackrf_info.exe", "sdrangel.exe", "SDRSharp", "gqrx"])
|
||||
def test_busy_known_sdr_clients(name):
|
||||
result = check.check_processes([lambda: snapshot((name,), errors=("partial snapshot",)),
|
||||
lambda: pytest.fail("BUSY запрещает fallback")])
|
||||
assert result.state is check.ProcessState.BUSY
|
||||
with pytest.raises(RuntimeError):
|
||||
check.require_clear(result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("names,errors", [((), ()), (("python",), ("Access denied",)), (("",), ())])
|
||||
def test_unknown_never_becomes_clear(names, errors):
|
||||
result = check.check_processes([lambda: snapshot(names, errors)])
|
||||
assert result.state is check.ProcessState.UNKNOWN
|
||||
with pytest.raises(RuntimeError):
|
||||
check.require_clear(result)
|
||||
|
||||
|
||||
def test_tasklist_access_denied_is_unknown():
|
||||
result = check.scan_tasklist(completed(stderr="ERROR: Access denied", code=1))
|
||||
assert result.state is check.ProcessState.UNKNOWN
|
||||
assert "Access denied" in str(result.errors)
|
||||
|
||||
|
||||
def test_fallback_to_powershell_after_psutil_unavailable(monkeypatch):
|
||||
powershell = check.scan_powershell
|
||||
monkeypatch.setattr(check, "scan_psutil", lambda: snapshot((), ("ImportError: psutil",)))
|
||||
monkeypatch.setattr(check, "scan_powershell", lambda: powershell(
|
||||
completed('[{"Id":12,"ProcessName":"python"}]')))
|
||||
monkeypatch.setattr(check, "scan_tasklist", lambda: pytest.fail("Полный CLEAR уже получен"))
|
||||
result = check.check_processes()
|
||||
assert result.state is check.ProcessState.CLEAR
|
||||
assert [item.state for item in result.attempts] == [check.ProcessState.UNKNOWN, check.ProcessState.CLEAR]
|
||||
|
||||
|
||||
def test_tasklist_is_last_fallback(monkeypatch):
|
||||
tasklist = check.scan_tasklist
|
||||
monkeypatch.setattr(check, "scan_psutil", lambda: snapshot((), ("unavailable",)))
|
||||
monkeypatch.setattr(check, "scan_powershell", lambda: snapshot((), ("denied",)))
|
||||
monkeypatch.setattr(check, "scan_tasklist", lambda: tasklist(
|
||||
completed('"python.exe","123","Console","1","123 K"\n')))
|
||||
result = check.check_processes()
|
||||
assert result.state is check.ProcessState.CLEAR
|
||||
assert len(result.attempts) == 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize("output", ["", "[]", "null", "not json", '[{"Id":4,"ProcessName":null}]'])
|
||||
def test_powershell_malformed_not_clear(output):
|
||||
assert check.scan_powershell(completed(output)).state is check.ProcessState.UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize("output", ["", "INFO: No tasks are running", '"python.exe","bad pid","a","b","c"'])
|
||||
def test_tasklist_malformed_not_clear(output):
|
||||
assert check.scan_tasklist(completed(output)).state is check.ProcessState.UNKNOWN
|
||||
|
||||
|
||||
def test_provider_exception_and_empty_provider_list_unknown():
|
||||
def failed():
|
||||
raise PermissionError("denied")
|
||||
assert check.check_processes([failed]).state is check.ProcessState.UNKNOWN
|
||||
assert check.check_processes([]).state is check.ProcessState.UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mechanism", ["powershell", "tasklist"])
|
||||
@pytest.mark.parametrize("malformed_first", [False, True])
|
||||
def test_busy_survives_malformed_neighbor(mechanism, malformed_first):
|
||||
if mechanism == "powershell":
|
||||
rows = [{"Id":42,"ProcessName":"hackrf_transfer"}, {}]
|
||||
if malformed_first:
|
||||
rows.reverse()
|
||||
scan = check.scan_powershell(completed(json.dumps(rows)))
|
||||
else:
|
||||
rows = ['"hackrf_transfer.exe","42","Console","1","100 K"', "BROKEN"]
|
||||
if malformed_first:
|
||||
rows.reverse()
|
||||
scan = check.scan_tasklist(completed("\n".join(rows)))
|
||||
assert scan.errors
|
||||
result = check.check_processes([lambda: scan, lambda: pytest.fail("Найденный BUSY нельзя потерять")])
|
||||
assert result.state is check.ProcessState.BUSY
|
||||
|
||||
|
||||
@pytest.mark.parametrize("disappeared", [False, True])
|
||||
def test_psutil_access_denied_or_exited_process(monkeypatch, disappeared):
|
||||
class NoSuchProcess(Exception):
|
||||
pass
|
||||
|
||||
def unreadable():
|
||||
raise NoSuchProcess() if disappeared else PermissionError("Access denied")
|
||||
|
||||
module = SimpleNamespace(NoSuchProcess=NoSuchProcess, process_iter=lambda: iter([
|
||||
SimpleNamespace(pid=1, name=lambda: "python.exe"),
|
||||
SimpleNamespace(pid=2, name=unreadable)]))
|
||||
monkeypatch.setattr(check.importlib, "import_module", lambda name: module)
|
||||
result = check.scan_psutil()
|
||||
assert result.state is (check.ProcessState.CLEAR if disappeared else check.ProcessState.UNKNOWN)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state", list(check.ProcessState))
|
||||
def test_preflight_only_clear_reaches_hardware(tmp_path, monkeypatch, state):
|
||||
actions = []
|
||||
scan = snapshot(("hackrf_transfer.exe",)) if state is check.ProcessState.BUSY else snapshot()
|
||||
if state is check.ProcessState.UNKNOWN:
|
||||
scan = snapshot((), ("tasklist Access denied",))
|
||||
result = check.ProcessCheck(state, (scan,))
|
||||
monkeypatch.setattr(lab.phy.lab045, "_verify_device_inventory", lambda runner: actions.append("inventory") or {})
|
||||
monkeypatch.setattr(lab, "clock_preflight", lambda runner, guard: actions.append("clock") or {"passed": True})
|
||||
monkeypatch.setattr(lab, "disable_clock_outputs", lambda runner: actions.append("cleanup") or [])
|
||||
if state is check.ProcessState.CLEAR:
|
||||
assert lab.preflight_only(tmp_path, runner=lambda _: None, process_checker=lambda: result)["passed"]
|
||||
assert actions == ["inventory", "clock", "cleanup"]
|
||||
else:
|
||||
with pytest.raises(RuntimeError):
|
||||
lab.preflight_only(tmp_path, runner=lambda _: None, process_checker=lambda: result)
|
||||
assert actions == []
|
||||
report = json.loads((tmp_path / "common_clock_preflight.json").read_text(encoding="utf-8"))
|
||||
assert report["process_check"]["state"] == state.value
|
||||
assert report["rf_tx_count"] == 0
|
||||
|
||||
|
||||
def test_unknown_after_cleanup_rejects_preflight(tmp_path, monkeypatch):
|
||||
states = iter([check.ProcessCheck(check.ProcessState.CLEAR, (snapshot(),)),
|
||||
check.ProcessCheck(check.ProcessState.UNKNOWN, (snapshot((), ("denied",)),))])
|
||||
monkeypatch.setattr(lab.phy.lab045, "_verify_device_inventory", lambda runner: {})
|
||||
monkeypatch.setattr(lab, "clock_preflight", lambda runner, guard: {"passed": True})
|
||||
monkeypatch.setattr(lab, "disable_clock_outputs", lambda runner: [])
|
||||
with pytest.raises(RuntimeError):
|
||||
lab.preflight_only(tmp_path, runner=lambda _: None, process_checker=lambda: next(states))
|
||||
report = json.loads((tmp_path / "common_clock_preflight.json").read_text(encoding="utf-8"))
|
||||
assert not report["passed"]
|
||||
assert report["process_check_after_cleanup"]["state"] == "UNKNOWN"
|
||||
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
|
||||
248
tests/test_lab056_preparation.py
Normal file
248
tests/test_lab056_preparation.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""Lab056: цифровые условия и отказы runner без оборудования."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from experiments import lab056_abab_energy as lab
|
||||
from experiments import lab056_capture_runner as capture
|
||||
from tools.hackrf_process_check import ProcessCheck, ProcessState
|
||||
|
||||
|
||||
def point():
|
||||
# Только синтетическая конфигурация теста; это не разрешение на эфир.
|
||||
return capture.OperatingPoint(915000000, physical_path="SIMULATED", path_confirmed=True)
|
||||
|
||||
|
||||
def test_no_authorization_has_no_side_effects(tmp_path):
|
||||
with pytest.raises(PermissionError):
|
||||
capture.capture_once(tmp_path, point(), runner=lambda _: pytest.fail("hardware"))
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("configuration", [capture.OperatingPoint(),
|
||||
capture.OperatingPoint(0), capture.OperatingPoint(True), capture.OperatingPoint(915e6)])
|
||||
def test_missing_or_invalid_frequency_precedes_io(tmp_path, configuration):
|
||||
with pytest.raises(PermissionError):
|
||||
capture.capture_once(tmp_path, configuration, authorization=True)
|
||||
with pytest.raises(PermissionError):
|
||||
capture.preflight_only(tmp_path, configuration)
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
|
||||
def test_fixed_gain_and_finite_nonrepeating_commands():
|
||||
rx = capture.command(point(), "rx.cs8", 24000000)
|
||||
tx = capture.command(point(), "tx.cs8", 1000000, transmit=True)
|
||||
assert rx[rx.index("-l") + 1] == rx[rx.index("-g") + 1] == "24"
|
||||
assert tx[tx.index("-x") + 1] == "10"
|
||||
for command in (rx, tx):
|
||||
assert "-R" not in command
|
||||
assert command[command.index("-a") + 1] == command[command.index("-p") + 1] == "0"
|
||||
assert command[command.index("-f") + 1] == "915000000"
|
||||
assert command[command.index("-s") + 1] == "8000000"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", [None, "master", "slave", "clkin", "receive"])
|
||||
def test_common_clock_logic_uses_agreed_frequency(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 == capture.previous.clock.MASTER_SERIAL) or
|
||||
(failure == "slave" and serial == capture.previous.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")
|
||||
assert command[command.index("-f") + 1] == "915000000"
|
||||
return subprocess.CompletedProcess(command, code, output, "")
|
||||
guard = capture.previous.clock.TransmitGuard()
|
||||
if failure:
|
||||
with pytest.raises(RuntimeError):
|
||||
capture.common_clock(point(), runner, guard, sleep_function=lambda _: None)
|
||||
assert not guard.tx_allowed
|
||||
else:
|
||||
assert capture.common_clock(point(), runner, guard, sleep_function=lambda _: None)["passed"]
|
||||
assert not any("-t" in command for command in commands)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", [None, "busy", "clock", "early_rx", "tx", "counters", "cleanup", "rx_role_loss", "tx_role_loss", "clipping_before", "clipping_during"])
|
||||
def test_one_shot_rx_first_and_failures(tmp_path, monkeypatch, failure):
|
||||
events = []
|
||||
monkeypatch.setattr(capture, "validated_metadata", lambda _: {"tx_sha256": "TEST", "tx_samples": 200})
|
||||
monkeypatch.setattr(capture.previous.phy.lab045, "_verify_device_inventory", lambda _: {"passed": True})
|
||||
monkeypatch.setattr(lab, "RX_SAMPLES", 65536)
|
||||
approved = {"passed": True, "rf_tx_count": 0, "point": asdict(point()), "tx_sha256": "TEST",
|
||||
"clock": {"passed": True, "master_reg29": 38, "slave_reg29": 38, "slave_clkin_status": "DETECTED"},
|
||||
"rx_level": {"saturation_detected": False}}
|
||||
lab.save_new(tmp_path / "preflight_result.json", approved)
|
||||
def clock_checker(point, runner, guard, **kwargs):
|
||||
events.append("clock")
|
||||
if failure == "clock":
|
||||
raise RuntimeError("clock")
|
||||
guard.allow()
|
||||
return {"passed": True, "master_reg29": 38, "slave_reg29": 38, "slave_clkin_status": "DETECTED"}
|
||||
class Process:
|
||||
def poll(self):
|
||||
return 1 if failure == "early_rx" else None
|
||||
|
||||
def wait(self, timeout):
|
||||
(tmp_path / "rx_stream.cs8").write_bytes(bytes([127 if failure == "clipping_during" else 1]) * 131072)
|
||||
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")
|
||||
(tmp_path / "rx_stream.cs8").write_bytes(bytes([127 if failure == "clipping_before" else 1]) * 131072)
|
||||
stdout.write_text("")
|
||||
stderr.write_text("1 RX overruns\n0 overruns" if failure == "rx_role_loss" else "0 overruns")
|
||||
return Process()
|
||||
def runner(command):
|
||||
if "-t" in command:
|
||||
events.append("tx")
|
||||
log = "" if failure == "counters" else ("1 TX underruns\n0 underruns" if failure == "tx_role_loss" else "0 underruns")
|
||||
return subprocess.CompletedProcess(command, int(failure == "tx"), "", log)
|
||||
events.append("cleanup")
|
||||
return subprocess.CompletedProcess(command, 0, "", "")
|
||||
kwargs = dict(authorization=True, runner=runner, process_starter=starter,
|
||||
process_checker=lambda: ProcessCheck(ProcessState.BUSY if failure == "busy" else ProcessState.CLEAR, ()),
|
||||
clock_checker=clock_checker, sleep_function=lambda _: events.append("lead"))
|
||||
if failure:
|
||||
with pytest.raises(RuntimeError):
|
||||
capture.capture_once(tmp_path, point(), **kwargs)
|
||||
else:
|
||||
assert capture.capture_once(tmp_path, point(), **kwargs)["passed"]
|
||||
assert events.count("tx") == (0 if failure in ("busy", "clock", "early_rx", "clipping_before") else 1)
|
||||
if "tx" in events:
|
||||
assert events.index("rx") < events.index("lead") < events.index("tx")
|
||||
before = events.copy()
|
||||
with pytest.raises(FileExistsError):
|
||||
capture.capture_once(tmp_path, point(), **kwargs)
|
||||
assert events == before
|
||||
result = json.loads((tmp_path / "capture_result.json").read_text())
|
||||
assert result["passed"] == (failure is None)
|
||||
assert result["rf_tx_count"] == events.count("tx")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", [None, "clock", "clipping", "tx_attempt", "cleanup"])
|
||||
def test_rx_only_preflight_stops_before_tx(tmp_path, monkeypatch, failure):
|
||||
calls = []
|
||||
monkeypatch.setattr(capture, "validated_metadata", lambda _: {"tx_sha256": "TEST"})
|
||||
monkeypatch.setattr(capture.previous.phy.lab045, "_verify_device_inventory", lambda _: {"passed": True})
|
||||
def runner(command):
|
||||
calls.append(command)
|
||||
assert "-t" not in command
|
||||
if "-r" in command:
|
||||
Path(command[command.index("-r") + 1]).write_bytes(bytes([127 if failure == "clipping" else 1]) * 1600000)
|
||||
if failure == "cleanup":
|
||||
raise RuntimeError("cleanup")
|
||||
return subprocess.CompletedProcess(command, 0, "0 RX overruns", "")
|
||||
def clock_checker(point, runner, guard, **kwargs):
|
||||
if failure == "tx_attempt":
|
||||
runner(["hackrf_transfer", "-t", "forbidden"])
|
||||
return {"passed": True, "master_reg29": 14 if failure == "clock" else 38,
|
||||
"slave_reg29": 38, "slave_clkin_status": "DETECTED"}
|
||||
kwargs = dict(runner=runner, clock_checker=clock_checker,
|
||||
process_checker=lambda: ProcessCheck(ProcessState.CLEAR, ()))
|
||||
if failure:
|
||||
with pytest.raises((RuntimeError, PermissionError)):
|
||||
capture.preflight_only(tmp_path, point(), **kwargs)
|
||||
else:
|
||||
assert capture.preflight_only(tmp_path, point(), **kwargs)["passed"]
|
||||
result = json.loads((tmp_path / "preflight_result.json").read_text())
|
||||
assert result["rf_tx_count"] == 0
|
||||
assert result["passed"] == (failure is None)
|
||||
assert not (tmp_path / "hardware_attempt.json").exists()
|
||||
with pytest.raises(FileExistsError):
|
||||
capture.preflight_only(tmp_path, point(), **kwargs)
|
||||
|
||||
|
||||
def test_preflight_cannot_authorize_another_frequency(tmp_path):
|
||||
approved = {"passed": True, "rf_tx_count": 0, "point": asdict(point()), "tx_sha256": "TEST",
|
||||
"clock": {"passed": True, "master_reg29": 38, "slave_reg29": 38, "slave_clkin_status": "DETECTED"},
|
||||
"rx_level": {"saturation_detected": False}}
|
||||
lab.save_new(tmp_path / "preflight_result.json", approved)
|
||||
capture.require_preflight(tmp_path, point(), {"tx_sha256": "TEST"})
|
||||
with pytest.raises(PermissionError):
|
||||
capture.require_preflight(tmp_path, capture.OperatingPoint(916000000), {"tx_sha256": "TEST"})
|
||||
|
||||
|
||||
def test_external_frequency_needs_no_country_or_band_fields():
|
||||
configuration = capture.OperatingPoint(916000000)
|
||||
command = capture.command(configuration, "NUL", 65536)
|
||||
assert command[command.index("-f") + 1] == "916000000"
|
||||
|
||||
|
||||
def test_complex_gain_and_repeatability_are_not_rx_normalization():
|
||||
random_generator = np.random.default_rng(56)
|
||||
reference = random_generator.normal(size=8000).astype(complex)
|
||||
expected = .3 + .4j
|
||||
received = 1 + 2j + expected * reference
|
||||
gain = lab.complex_gain(reference, received, 0)
|
||||
assert complex(gain["real"], gain["imag"]) == pytest.approx(expected)
|
||||
rows = []
|
||||
for label, power in (("A1", 4), ("B1", 1), ("A2", 8), ("B2", 2)):
|
||||
rows.append({"label": label, "background_power": 1, "raw_rx": {"power_code2": power + 1},
|
||||
"background_subtracted_signal_power": power, "channel_gain_power": power,
|
||||
"measurement": {"heldout_signal_power": power}})
|
||||
result = lab.comparison_summary(rows)
|
||||
assert result["received_power_difference_db"] == pytest.approx(-6.020599913)
|
||||
assert result["energy_per_symbol_difference_db"] == pytest.approx(-20)
|
||||
assert [r["channel_gain_difference_db"] for r in result["repeatability"]] == pytest.approx([3.010299957, 3.010299957])
|
||||
rows[-1]["background_subtracted_signal_power"] = None
|
||||
assert lab.comparison_summary(rows)["received_power_difference_db"] is None
|
||||
|
||||
|
||||
def test_nonpositive_excess_is_not_a_power_ratio():
|
||||
assert lab.difference_db(-1, 1) is None
|
||||
assert lab.difference_db(None, 1) is None
|
||||
assert lab.difference_db(.25, 1) == pytest.approx(-6.020599913)
|
||||
|
||||
|
||||
def test_invalid_result_does_not_leave_partial_json(tmp_path):
|
||||
path = tmp_path / "result.json"
|
||||
with pytest.raises(ValueError):
|
||||
lab.save_new(path, {"invalid": float("nan")})
|
||||
assert not path.exists()
|
||||
lab.save_new(path, {"passed": True})
|
||||
with pytest.raises(FileExistsError):
|
||||
lab.save_new(path, {"passed": False})
|
||||
assert json.loads(path.read_text())["passed"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("log,expected,passed", [
|
||||
("0 overruns", "overrun", True), ("0 RX overrun", "overrun", True),
|
||||
("0 TX underruns", "underrun", True), ("1 RX overruns\n0 overruns", "overrun", False),
|
||||
("1 TX underrun\n0 underruns", "underrun", False),
|
||||
("unknown overruns\n0 overruns", "overrun", False),
|
||||
("0 overruns\nunderrun detected", "overrun", False),
|
||||
("0.0 overruns", "overrun", False), ("-1 overruns", "overrun", False),
|
||||
("0 underruns", "overrun", False), ("", "overrun", False)])
|
||||
def test_all_loss_mentions_must_be_explicit_zero(log, expected, passed):
|
||||
assert capture.clean_counter_log(log, expected) is passed
|
||||
|
||||
|
||||
def test_deterministic_ab_rms_and_no_clipping():
|
||||
powers = []
|
||||
for rate in (20000, 500000):
|
||||
raw = lab.build_burst(rate)
|
||||
values = raw[::2].astype(float) + 1j * raw[1::2].astype(float)
|
||||
assert not np.any((raw == -128) | (raw == 127))
|
||||
powers.append(float(np.mean(abs(values)**2)))
|
||||
np.testing.assert_array_equal(raw, lab.build_burst(rate))
|
||||
assert abs(lab.difference_db(powers[1], powers[0])) < .01
|
||||
Reference in New Issue
Block a user