152 lines
5.8 KiB
Python
152 lines
5.8 KiB
Python
"""Lab065: неизменный waveform, TX VGA 20 дБ и границы классификации."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from experiments import lab065_rf_level_linearity as lab
|
|
from tools.hackrf_process_check import ProcessCheck, ProcessState
|
|
|
|
|
|
def test_only_operating_parameter_change_is_tx_vga():
|
|
old = lab.lab064.parameters()
|
|
new = lab.parameters()
|
|
assert new.pop("geometry_unchanged_from_lab064") is True
|
|
assert new.pop("waveform_source") == "Lab064 exact CS8 copy"
|
|
assert new["tx_vga_db"] == 20
|
|
new["tx_vga_db"] = 10
|
|
assert new == old
|
|
|
|
|
|
def test_tx_command_uses_20_db_and_rx_remains_24_24():
|
|
transmit = lab.transfer_command("tx.cs8", 100, transmit=True)
|
|
receive = lab.transfer_command("rx.cs8", 100)
|
|
assert transmit[transmit.index("-x") + 1] == "20"
|
|
assert receive[receive.index("-l") + 1] == "24"
|
|
assert receive[receive.index("-g") + 1] == "24"
|
|
assert transmit[-5:] == receive[-5:] == ["-a", "0", "-p", "0", "-B"]
|
|
|
|
|
|
def test_exact_lab064_waveform_is_required():
|
|
metadata = lab.validate_lab064_waveform()
|
|
assert metadata["tx_sha256"] == lab.EXPECTED_TX_SHA256
|
|
assert metadata["tx_samples"] == 6_000_000
|
|
|
|
|
|
def _result(matched_gain, residual_gain, improvements, errors=(0.0, 0.0, 0.0)):
|
|
old = {row["label"]: row for row in json.loads(
|
|
lab.LAB064_RESULT.read_text(encoding="utf-8")
|
|
)["rows"]}
|
|
rows = []
|
|
for index, label in enumerate(("L0", "L1", "L2", "L3")):
|
|
baseline = old[label]
|
|
rows.append({
|
|
"label": label,
|
|
"matched_signal_power": baseline["matched_signal_power"] * 10 ** (matched_gain[index] / 10),
|
|
"residual_power": baseline["residual_power"] * 10 ** (residual_gain[index] / 10),
|
|
"matched_to_residual_db": baseline["matched_to_residual_db"] + improvements[index],
|
|
"gain_power_abs_h_squared": baseline["gain_power_abs_h_squared"] * 10 ** (matched_gain[index] / 10),
|
|
})
|
|
return {
|
|
"capture_valid": True,
|
|
"rows": rows,
|
|
"comparisons": [{"linearity_error_db": error} for error in errors],
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("result", "expected"),
|
|
[
|
|
(_result([10] * 4, [1] * 4, [9] * 4), "LINEAR_GAIN_IMPROVEMENT"),
|
|
(_result([10] * 4, [10] * 4, [0] * 4), "RESIDUAL_RISES_WITH_SIGNAL"),
|
|
(_result([1] * 4, [0] * 4, [1] * 4), "NO_MEANINGFUL_GAIN"),
|
|
(_result([10] * 4, [1] * 4, [9] * 4, errors=(0, 0, -1.6)), "COMPRESSION_AT_HIGHER_TX_GAIN"),
|
|
],
|
|
)
|
|
def test_classification_cases(result, expected):
|
|
comparison = lab.compare_sessions(
|
|
json.loads(lab.LAB064_RESULT.read_text(encoding="utf-8")), result["rows"]
|
|
)
|
|
assert lab.classify_result(result, comparison)["case"] == expected
|
|
|
|
|
|
@pytest.mark.parametrize("failure", [None, "clock", "overrun", "clipping"])
|
|
def test_preflight_never_transmits_and_fails_closed(tmp_path, monkeypatch, failure):
|
|
raw, output = tmp_path / "raw", tmp_path / "output"
|
|
raw.mkdir()
|
|
output.mkdir()
|
|
monkeypatch.setattr(lab, "RAW", raw)
|
|
monkeypatch.setattr(lab, "OUTPUT", output)
|
|
(raw / "tx_level_staircase.cs8").write_bytes(b"test")
|
|
metadata = {"tx_sha256": "TEST"}
|
|
(output / "software_gate.json").write_text('{"passed": true, "tx_sha256": "TEST"}')
|
|
monkeypatch.setattr(lab, "validate_prepared", lambda: metadata)
|
|
monkeypatch.setattr(lab.lab054.phy.lab045, "_verify_device_inventory", lambda runner: {"passed": True})
|
|
monkeypatch.setattr(lab.lab054, "disable_clock_outputs", lambda runner: [])
|
|
monkeypatch.setattr(lab, "parameters", lambda: {})
|
|
|
|
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 1
|
|
Path(command[command.index("-r") + 1]).write_bytes(bytes([value, value]) * 800_000)
|
|
log = "1 RX overruns" if failure == "overrun" else "0 RX overruns"
|
|
return subprocess.CompletedProcess(command, 0, log, "")
|
|
|
|
options = {
|
|
"runner": runner,
|
|
"clock_checker": clock,
|
|
"process_checker": lambda: ProcessCheck(ProcessState.CLEAR, ()),
|
|
"sleep_function": lambda _: None,
|
|
}
|
|
if failure:
|
|
with pytest.raises(RuntimeError):
|
|
lab.preflight(**options)
|
|
else:
|
|
assert lab.preflight(**options)["passed"]
|
|
assert json.loads((raw / "preflight_result.json").read_text())["rf_tx_count"] == 0
|
|
|
|
|
|
def test_capture_is_unavailable_without_new_authorization():
|
|
with pytest.raises(PermissionError):
|
|
lab.capture_once(authorization=False)
|
|
|
|
|
|
def test_existing_attempt_blocks_capture_before_hardware(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(lab, "RAW", tmp_path)
|
|
monkeypatch.setattr(lab, "validate_prepared", lambda: {"tx_sha256": "TEST"})
|
|
monkeypatch.setattr(lab, "require_preflight", lambda metadata: {})
|
|
(tmp_path / "hardware_attempt.json").write_text("{}", encoding="utf-8")
|
|
with pytest.raises(FileExistsError):
|
|
lab.capture_once(
|
|
authorization=True,
|
|
runner=lambda command: pytest.fail("hardware must not run"),
|
|
process_starter=lambda *args: pytest.fail("hardware must not run"),
|
|
)
|
|
|
|
|
|
def test_cli_labels_current_rf_state_after_completed_capture(monkeypatch, capsys):
|
|
monkeypatch.setattr(
|
|
lab,
|
|
"capture_once",
|
|
lambda authorization=False: {
|
|
"stage": "SINGLE_RF_LEVEL_CAPTURE", "rf_tx_count": 1, "passed": True,
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
sys, "argv", ["lab065", "--stage", "capture", "--authorize-one-rf"]
|
|
)
|
|
lab.main()
|
|
output = capsys.readouterr().out
|
|
assert '"rf_tx_count": 1' in output
|
|
assert output.endswith("RF TX NOW = 0\n")
|