some fixes

This commit is contained in:
Ayzen
2026-06-06 02:07:46 +03:00
parent b3a8cd834f
commit c12ae05148
2 changed files with 109 additions and 6 deletions
@@ -142,6 +142,14 @@ class AppWindowPipelineMixin:
self._log("History reset because run settings changed") self._log("History reset because run settings changed")
self._history_run_signature = run_signature self._history_run_signature = run_signature
# Open the single-capture acquisition window BEFORE the producer can emit:
# any collection the one-shot orchestrator publishes is then strictly newer
# than this timestamp and is recognized as the target. Reading the clock
# AFTER start() raced a fast (simulator) orchestrator that had already
# published — its collection.monotonic_ns < start_ns, so the capture never
# completed and the GUI hung.
single_capture_start_ns = time.monotonic_ns() if single_capture else None
self._supervisor.start(config_path, allow_clean_orchestrator_exit=single_capture) self._supervisor.start(config_path, allow_clean_orchestrator_exit=single_capture)
self._close_readers() self._close_readers()
self._raw_reader = ShmRingReader(config.rings.raw_tap.name) self._raw_reader = ShmRingReader(config.rings.raw_tap.name)
@@ -149,12 +157,15 @@ class AppWindowPipelineMixin:
self._result_reader = ShmRingReader(config.rings.results.name) self._result_reader = ShmRingReader(config.rings.results.name)
self._processor_run_signature = processor_signature self._processor_run_signature = processor_signature
self._single_capture_active = single_capture self._single_capture_active = single_capture
self._single_capture_start_ns = None self._single_capture_start_ns = single_capture_start_ns
self._single_capture_seen_raw = False self._single_capture_seen_raw = False
self._single_capture_target_collection_id = None self._single_capture_target_collection_id = None
# Always drop unread payloads for all stages so single-capture starts if not single_capture:
# from a clean boundary and does not retain stale results-only tail. # Continuous start: discard any stale tail so rendering begins fresh.
# Single capture must NOT drop here — the drop races with (and can
# discard) the one-shot result; it instead filters stale data by the
# start timestamp recorded above.
self._drop_pending_ring_payloads(include_results=True) self._drop_pending_ring_payloads(include_results=True)
self._last_reader_error_signature = None self._last_reader_error_signature = None
self._reader_error_repeat_count = 0 self._reader_error_repeat_count = 0
@@ -164,8 +175,6 @@ class AppWindowPipelineMixin:
self._active_run_config_path = config_path self._active_run_config_path = config_path
self._pipeline_should_run = not single_capture self._pipeline_should_run = not single_capture
self._pipeline_restart_count = 0 self._pipeline_restart_count = 0
if single_capture:
self._single_capture_start_ns = time.monotonic_ns()
pid_map = self._supervisor.pids() pid_map = self._supervisor.pids()
pid_text = ", ".join(f"{name}={pid}" for name, pid in sorted(pid_map.items())) or "none" pid_text = ", ".join(f"{name}={pid}" for name, pid in sorted(pid_map.items())) or "none"
+94
View File
@@ -0,0 +1,94 @@
"""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
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.
"""
from __future__ import annotations
import os
import unittest
from pathlib import Path
import numpy as np
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import QApplication # noqa: E402
from python_app.gui.app_window import AppWindow # noqa: E402
from python_app.models.dataset_model import ( # noqa: E402
ComboKey,
ResultBlock,
ResultCollection,
ResultPayload,
)
_app: QApplication | None = None
_window: AppWindow | None = None
def setUpModule() -> None:
global _app, _window
_app = QApplication.instance() or QApplication([])
_window = AppWindow(Path("."))
def tearDownModule() -> None:
if _window is not None:
_window.close()
def _result(collection_id: int, monotonic_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])
class SingleCaptureBoundaryTest(unittest.TestCase):
def setUp(self) -> 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()
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)
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())
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())
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)
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
if __name__ == "__main__":
unittest.main()