Files
radar_system/python_app/tests/test_bscan_history_window.py
T
2026-10-06 12:32:26 +03:00

210 lines
8.7 KiB
Python

"""Stopped B-scan windows and exact result identities through real controllers."""
from __future__ import annotations
import os
import unittest
import numpy as np
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import QApplication, QComboBox, QLineEdit, QSpinBox # noqa: E402
from python_app.gui.app_window import AppWindow # noqa: E402
from python_app.tests.gui_test_support import isolated_window, widget
from collections import deque # noqa: E402
from unittest import mock
from PyQt6.QtCore import QSignalBlocker
from python_app.gui.runtime.history import record_result_history # noqa: E402
from python_app.models.dataset_model import ( # noqa: E402
ComboKey,
ResultBlock,
ResultCollection,
ResultPayload,
SweepCollection,
)
_app: QApplication | None = None
_window: AppWindow | None = None
def setUpModule() -> None:
global _app, _window
instance = QApplication.instance()
_app = instance if isinstance(instance, QApplication) else QApplication([])
_window = isolated_window(unittest.addModuleCleanup)
def _bscan_result(collection_id: int) -> ResultCollection:
payload = ResultPayload(
processing_name="bscan",
kind=1,
frequency_hz=np.array([0.5, 1.0], dtype=np.float32),
trace=np.array([collection_id + 0j, collection_id + 0j], dtype=np.complex64),
)
block = ResultBlock(combo=ComboKey(input=0, output=0), payloads=[payload])
return ResultCollection(collection_id=collection_id, monotonic_ns=collection_id,
capture_start_ns=collection_id, capture_end_ns=collection_id + 1, blocks=[block])
class BscanDisplayWindowTest(unittest.TestCase):
def setUp(self) -> None:
assert _window is not None
self.w = _window
self.mode = self.w._view.processing.mode
self.history_window = widget(self.w._view.processing, "_bscan_history_window", QSpinBox)
with QSignalBlocker(self.mode):
self.mode.setCurrentText("bscan")
self.w._plots.set_mode("bscan")
self._original_is_running = self.w._pipeline.supervisor.is_running
self.w._history.results.clear()
self.w._plots.bscan.signature = None
def tearDown(self) -> None:
self.w._pipeline.supervisor.is_running = self._original_is_running
self.w._history.results.clear()
self.w._plots.bscan.signature = None
def _set_running(self, running: bool) -> None:
self.w._pipeline.supervisor.is_running = lambda: running
def _fill_history(self, count: int, *, id_step: int = 1) -> None:
for index in range(count):
self.w._history.results.append(_bscan_result(1 + index * id_step))
def test_running_acquisition_ignores_the_user_window(self) -> None:
self._set_running(True)
self.history_window.setValue(300)
self.assertEqual(self.w._plots.window_scans(), self.w._plots.live_window)
def test_stopped_acquisition_uses_the_user_window(self) -> None:
self._set_running(False)
self.history_window.setValue(300)
self.assertEqual(self.w._plots.window_scans(), 300)
def test_widening_the_window_brings_older_frames_back(self) -> None:
self._fill_history(300)
self._set_running(True)
self.w._plots.sync_bscan()
self.assertEqual(
len(self.w._plots.bscan.history_by_combo[(0, 0)]), self.w._plots.live_window
)
self._set_running(False)
self.history_window.setValue(300)
self.w._plots.sync_bscan()
self.assertEqual(len(self.w._plots.bscan.history_by_combo[(0, 0)]), 300)
def test_restarted_collection_ids_do_not_hide_the_fresh_run(self) -> None:
self._fill_history(300)
self._set_running(False)
self.history_window.setValue(300)
self.w._plots.sync_bscan()
self._set_running(True)
window = self.w._plots.live_window
for fresh in range(1, window + 1):
self.w._history.results.append(_bscan_result(fresh))
self.w._plots.sync_bscan()
self.assertEqual(len(self.w._plots.bscan.history_by_combo[(0, 0)]), window)
def test_window_counts_entries_not_collection_ids(self) -> None:
self._fill_history(300, id_step=3)
self._set_running(False)
self.history_window.setValue(150)
self.w._plots.sync_bscan()
self.assertEqual(len(self.w._plots.bscan.history_by_combo[(0, 0)]), 150)
def test_reprocessed_results_replace_rather_than_duplicate_columns(self) -> None:
self._set_running(False)
self.history_window.setValue(300)
self._fill_history(200)
self.w._plots.sync_bscan()
self.assertEqual(len(self.w._plots.bscan.history_by_combo[(0, 0)]), 200)
for collection in list(self.w._history.results):
record_result_history(self.w._history.results, _bscan_result(collection.collection_id))
self.assertEqual(len(self.w._history.results), 200)
self.w._history.results.append(_bscan_result(200))
self.w._plots.bscan.signature = None
self.w._plots.sync_bscan()
self.assertEqual(len(self.w._plots.bscan.history_by_combo[(0, 0)]), 200)
def test_rebuild_renders_the_full_widened_window(self) -> None:
self._fill_history(300)
self._set_running(False)
self.history_window.setValue(300)
self.w._plots.sync_bscan()
self.assertEqual(len(self.w._plots.bscan.history_by_combo[(0, 0)]), 300)
self.history_window.setValue(50)
self.w._plots.sync_bscan()
self.assertEqual(len(self.w._plots.bscan.history_by_combo[(0, 0)]), 50)
def test_replay_requires_stopped_acquisition_and_live_processor(self) -> None:
self.w._history.preprocessed.append(SweepCollection(collection_id=1, monotonic_ns=1, traces=[]))
self.addCleanup(self.w._history.preprocessed.clear)
with mock.patch.object(self.w._pipeline, "_result_reader", mock.Mock()), \
mock.patch.object(self.w._pipeline.supervisor, "is_processor_running", return_value=True):
with QSignalBlocker(self.mode):
self.mode.setCurrentText("bscan")
self._set_running(False)
self.assertTrue(self.w._pipeline.can_reprocess_history())
self.assertFalse(self.w._live.build(reprocess_current_result=False).reprocess_current_result)
self._set_running(True)
self.assertFalse(self.w._pipeline.can_reprocess_history())
self._set_running(False)
with mock.patch.object(self.w._pipeline.supervisor, "is_processor_running", return_value=False):
self.assertFalse(self.w._pipeline.can_reprocess_history())
with QSignalBlocker(self.mode):
self.mode.setCurrentText("pass_through")
self.w._plots.set_mode("pass_through")
self.assertFalse(self.w._pipeline.can_reprocess_history())
self.assertFalse(self.w._pipeline.can_reprocess_history())
def test_acquisition_combo_editor_does_not_reprocess_display(self) -> None:
with mock.patch.object(self.w._live.writer, "write") as write:
self.w._view.switches.single_output.setText("1")
write.assert_not_called()
self.assertFalse(self.w._pipeline.replay.timer.isActive())
class ResultHistoryOrderTest(unittest.TestCase):
"""The result history stays in acquisition order whatever order results arrive in.
The processor reprocessing its retained sweeps also delivers sweeps whose first
result the GUI missed (the results ring overwrites unread slots). Appended, such a
sweep showed up as the newest B-scan column. Its first arrival must still be
recorded to disk; replacing an already retained result must not duplicate it.
"""
@staticmethod
def _ids(history: deque[ResultCollection]) -> list[int]:
return [int(collection.collection_id) for collection in history]
def test_only_new_sweeps_are_recorded_to_disk(self) -> None:
window = _window
assert window is not None
arrivals = [_bscan_result(cid) for cid in (2, 3, 2, 1)] # 2 reprocessed, 1 late
reader = mock.Mock(pop_result_collection=mock.Mock(side_effect=lambda: arrivals.pop(0) if arrivals else None))
recorded: list[int] = []
window._history.results.clear()
self.addCleanup(window._history.results.clear)
with mock.patch.object(window._pipeline, "_result_reader", reader), \
mock.patch.object(window._recording, "record", side_effect=lambda stage, collection: recorded.append(collection.collection_id)):
window._pipeline.read_results()
self.assertEqual(recorded, [2, 3, 1])
self.assertEqual(self._ids(window._history.results), [1, 2, 3])
if __name__ == "__main__":
unittest.main()