210 lines
8.7 KiB
Python
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()
|