Rework radar system
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""Single-capture completion logic.
|
||||
|
||||
The single capture starts a one-shot pipeline and must finish on exactly the result
|
||||
that belongs to its sweep. Because collection ids restart per orchestrator run, a stale
|
||||
that belongs to its sweep. Because collection ids restart per acquisition run, a stale
|
||||
result from a previous run can carry the SAME id; the start timestamp disambiguates them.
|
||||
These tests pin that boundary (fresh vs stale same-id) and the readiness state machine —
|
||||
the path that previously hung on the simulator when start_ns was recorded too late.
|
||||
@@ -11,11 +11,12 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
from PyQt6.QtWidgets import QApplication # noqa: E402
|
||||
from PyQt6.QtWidgets import QApplication, QComboBox # noqa: E402
|
||||
|
||||
from python_app.gui.app_window import AppWindow # noqa: E402
|
||||
from python_app.tests.gui_test_support import isolated_window
|
||||
@@ -32,71 +33,71 @@ _window: AppWindow | None = None
|
||||
|
||||
def setUpModule() -> None:
|
||||
global _app, _window
|
||||
_app = QApplication.instance() or QApplication([])
|
||||
instance = QApplication.instance()
|
||||
_app = instance if isinstance(instance, QApplication) else QApplication([])
|
||||
_window = isolated_window(unittest.addModuleCleanup)
|
||||
|
||||
|
||||
def tearDownModule() -> None:
|
||||
if _window is not None:
|
||||
_window.close()
|
||||
|
||||
|
||||
def _result(collection_id: int, monotonic_ns: int) -> ResultCollection:
|
||||
def _result(collection_id: int, capture_start_ns: int) -> ResultCollection:
|
||||
payload = ResultPayload(
|
||||
processing_name="pass_through", kind=1,
|
||||
frequency_hz=np.array([1.0, 2.0], dtype=np.float32),
|
||||
trace=np.array([1 + 1j, 2 - 2j], dtype=np.complex64),
|
||||
)
|
||||
block = ResultBlock(combo=ComboKey(input=0, output=0), payloads=[payload])
|
||||
return ResultCollection(collection_id=collection_id, monotonic_ns=monotonic_ns, blocks=[block])
|
||||
return ResultCollection(collection_id=collection_id, monotonic_ns=capture_start_ns + 100, blocks=[block],
|
||||
capture_start_ns=capture_start_ns, capture_end_ns=capture_start_ns + 1)
|
||||
|
||||
|
||||
class SingleCaptureBoundaryTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
assert _window is not None
|
||||
self.w = _window
|
||||
self.w._processing_mode.setCurrentText("pass_through")
|
||||
self.w._single_capture_active = True
|
||||
self.w._single_capture_start_ns = 1000
|
||||
self.w._single_capture_seen_raw = True
|
||||
self.w._single_capture_target_collection_id = 5
|
||||
self.w._result_history.clear()
|
||||
self.pipeline = self.w._pipeline
|
||||
self.pipeline.timer.stop()
|
||||
self.w._view.processing.mode.setCurrentText("pass_through")
|
||||
self.pipeline.single_capture_active = True
|
||||
self.pipeline._single_capture_start_ns = 1000
|
||||
self.pipeline._single_capture_target_key = (5, 1500)
|
||||
self.w._history.preprocessed.clear()
|
||||
self.w._history.results.clear()
|
||||
|
||||
def test_picks_fresh_result_not_stale_same_id(self) -> None:
|
||||
self.w._result_history.append(_result(5, 500)) # stale: same id, before start_ns
|
||||
self.w._result_history.append(_result(5, 1500)) # fresh: at/after start_ns
|
||||
target = self.w._find_single_capture_target_result()
|
||||
self.assertIsNotNone(target)
|
||||
self.assertEqual(target.monotonic_ns, 1500)
|
||||
stale, fresh = _result(5, 500), _result(5, 1500)
|
||||
self.w._history.results.extend((stale, fresh))
|
||||
with mock.patch.object(self.w._plots, "draw", wraps=self.w._plots.draw) as draw:
|
||||
self.assertTrue(self.pipeline._finish_single_capture_if_ready())
|
||||
draw.assert_called_once_with(fresh)
|
||||
|
||||
def test_stale_same_id_result_is_not_matched(self) -> None:
|
||||
self.w._result_history.append(_result(5, 500)) # only a stale same-id result
|
||||
self.assertIsNone(self.w._find_single_capture_target_result())
|
||||
self.w._history.results.append(_result(5, 500))
|
||||
self.assertFalse(self.pipeline._finish_single_capture_if_ready())
|
||||
|
||||
def test_wrong_collection_id_is_not_matched(self) -> None:
|
||||
self.w._result_history.append(_result(9, 1500)) # fresh but a different sweep
|
||||
self.assertIsNone(self.w._find_single_capture_target_result())
|
||||
self.w._history.results.append(_result(9, 1500))
|
||||
self.assertFalse(self.pipeline._finish_single_capture_if_ready())
|
||||
|
||||
def test_not_ready_until_a_fresh_raw_is_seen(self) -> None:
|
||||
# The readiness guard that the timing fix makes reachable: with no raw recognized
|
||||
# as fresh yet, the capture must not complete (it must keep waiting, not hang).
|
||||
self.w._single_capture_seen_raw = False
|
||||
self.w._result_history.append(_result(5, 1500))
|
||||
self.assertFalse(self.w._finish_single_capture_if_ready())
|
||||
self.assertTrue(self.w._single_capture_active)
|
||||
self.pipeline._single_capture_target_key = None
|
||||
self.w._history.results.append(_result(5, 1500))
|
||||
self.assertFalse(self.pipeline._finish_single_capture_if_ready())
|
||||
self.assertTrue(self.pipeline.single_capture_active)
|
||||
|
||||
def test_completes_and_stops_on_the_fresh_result(self) -> None:
|
||||
self.w._result_history.append(_result(5, 1500))
|
||||
self.assertTrue(self.w._finish_single_capture_if_ready())
|
||||
self.assertFalse(self.w._single_capture_active) # _stop_run cleared the flag
|
||||
self.w._history.results.append(_result(5, 1500))
|
||||
self.assertTrue(self.pipeline._finish_single_capture_if_ready())
|
||||
self.assertFalse(self.pipeline.single_capture_active)
|
||||
|
||||
def test_empty_processed_trace_completes_capture_instead_of_waiting_for_timeout(self) -> None:
|
||||
result = _result(5, 1500)
|
||||
payload = result.blocks[0].payloads[0]
|
||||
payload.frequency_hz = np.empty(0, dtype=np.float32)
|
||||
payload.trace = np.empty(0, dtype=np.complex64)
|
||||
self.w._result_history.append(result)
|
||||
self.assertTrue(self.w._finish_single_capture_if_ready())
|
||||
self.assertFalse(self.w._single_capture_active)
|
||||
self.w._history.results.append(result)
|
||||
self.assertTrue(self.pipeline._finish_single_capture_if_ready())
|
||||
self.assertFalse(self.pipeline.single_capture_active)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user