Refactor radar system
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""Desktop channel navigation and fixed-size manual saves."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
from PyQt6.QtCore import QSignalBlocker # noqa: E402
|
||||
from PyQt6.QtWidgets import QApplication # noqa: E402
|
||||
|
||||
from python_app.tests.gui_test_support import isolated_window
|
||||
from python_app.gui.controllers.app_window_snapshot_mixin import AppWindowSnapshotMixin # noqa: E402
|
||||
from python_app.models.dataset_model import ComboKey, ResultBlock, ResultCollection, ResultPayload # noqa: E402
|
||||
from python_app.models.run_config_model import RunConfigModel # noqa: E402
|
||||
|
||||
|
||||
class ComboNavigationTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.app = QApplication.instance() or QApplication([])
|
||||
cls.window = isolated_window(cls.addClassCleanup)
|
||||
cls.window._timer.stop()
|
||||
cls.window._bscan_reprocess_timer.stop()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
cls.window.close()
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.w = self.window
|
||||
config = RunConfigModel()
|
||||
config.radar.model = "librevna_multi"
|
||||
config.apply_device_model_constraints()
|
||||
self.w._defaults_config = config
|
||||
self.w._set_combo_selection_mode("text")
|
||||
with QSignalBlocker(self.w._processing_mode):
|
||||
self.w._processing_mode.setCurrentText("bscan")
|
||||
self.w._result_history.clear()
|
||||
self.w._bscan_replay_results = []
|
||||
self.w._bscan_render_signature = None
|
||||
self.w._single_combo_input.setText("0")
|
||||
self.w._single_combo_output.setText("0")
|
||||
|
||||
def test_next_button_draws_the_next_cached_combo_without_reprocessing(self) -> None:
|
||||
self.w._result_history.append(ResultCollection(
|
||||
collection_id=1,
|
||||
monotonic_ns=1,
|
||||
blocks=[
|
||||
ResultBlock(combo=ComboKey(input=0, output=output), payloads=[ResultPayload(
|
||||
processing_name="bscan", kind=1,
|
||||
frequency_hz=np.array([0.0, 1.0], dtype=np.float32),
|
||||
trace=np.array([output + 1, output + 2], dtype=np.complex64),
|
||||
)])
|
||||
for output in (0, 1)
|
||||
],
|
||||
))
|
||||
with mock.patch.object(self.w, "_write_live_processing_config") as write_config:
|
||||
self.w._next_combo_button.click()
|
||||
|
||||
self.assertEqual(self.w._single_combo_input.text(), "0")
|
||||
self.assertEqual(self.w._single_combo_output.text(), "1")
|
||||
self.assertIn("in0/out1", self.w._bscan_plot.getPlotItem().titleLabel.text)
|
||||
write_config.assert_not_called()
|
||||
self.assertFalse(self.w._bscan_reprocess_timer.isActive())
|
||||
|
||||
def test_navigation_wraps_with_output_changing_first(self) -> None:
|
||||
keys = self.w._available_display_combo_keys()
|
||||
visited = [(0, 0)]
|
||||
for _ in keys:
|
||||
self.w._next_combo_button.click()
|
||||
visited.append((int(self.w._single_combo_input.text()), int(self.w._single_combo_output.text())))
|
||||
self.assertEqual(visited, keys + [keys[0]])
|
||||
|
||||
def test_invalid_current_pair_recovers_to_the_first_pair(self) -> None:
|
||||
self.w._single_combo_input.setText("invalid")
|
||||
self.w._single_combo_output.setText("99")
|
||||
self.w._next_combo_button.click()
|
||||
self.assertEqual(self.w._single_combo_input.text(), "0")
|
||||
self.assertEqual(self.w._single_combo_output.text(), "0")
|
||||
|
||||
def test_unknown_selection_keeps_the_previous_fields(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "Unknown channel combination"):
|
||||
self.w._select_display_combo(999, 999)
|
||||
self.assertEqual(self.w._single_combo_input.text(), "0")
|
||||
self.assertEqual(self.w._single_combo_output.text(), "0")
|
||||
|
||||
|
||||
class ManualSaveLimitTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.w = AppWindowSnapshotMixin()
|
||||
self.w._raw_history = ["raw"]
|
||||
self.w._pre_history = ["pre"]
|
||||
self.w._result_history = ["result"]
|
||||
self.w._drain_runtime_rings_for_snapshot = mock.Mock()
|
||||
self.w._show_exception = mock.Mock()
|
||||
|
||||
def test_snapshot_always_saves_1000_and_preserves_unaligned_stages(self) -> None:
|
||||
self.w._write_snapshot_dataset = mock.Mock()
|
||||
self.w._save_snapshot()
|
||||
self.w._write_snapshot_dataset.assert_called_once_with(
|
||||
["raw"], ["pre"], ["result"], 1000, include_unaligned=True,
|
||||
)
|
||||
|
||||
def test_json_export_uses_the_same_fixed_limit(self) -> None:
|
||||
self.w._save_path_input = SimpleNamespace(text=lambda: "/tmp/measurements")
|
||||
self.w._save_name_input = SimpleNamespace(text=lambda: "scene")
|
||||
self.w._radar_config_name_prefix = lambda: "radar"
|
||||
save_json = mock.Mock(return_value=([Path("/tmp/measurements/scene/trace.json")], {
|
||||
"output_stem": "scene", "output_dir": "/tmp/measurements/scene",
|
||||
}))
|
||||
self.w._store = SimpleNamespace(save_runtime_vna_history_json_batch=save_json)
|
||||
self.w._write_gui_profile_to_path = mock.Mock()
|
||||
self.w._log = mock.Mock()
|
||||
|
||||
self.w._save_vna_history_json()
|
||||
|
||||
self.w._show_exception.assert_not_called()
|
||||
save_json.assert_called_once_with(
|
||||
Path("/tmp/measurements"), "scene", ["raw"], ["pre"], ["result"], 1000,
|
||||
channel="s21", primary_stage="preprocessed", name_prefix="radar", include_unaligned=True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user