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

109 lines
4.3 KiB
Python

from contextlib import ExitStack
"""Plot capture follows client demand and actual Qt scene changes."""
from collections import deque
import unittest
from unittest import mock
import numpy as np
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWidgets import QApplication, QComboBox
from python_app.tests.gui_test_support import isolated_window
class WebPlotCaptureTest(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.app = QApplication.instance() or QApplication([])
cls.window = isolated_window(cls.addClassCleanup)
cls.window._pipeline.timer.stop()
cls.window._pipeline.replay.timer.stop()
cls.window.show()
def setUp(self) -> None:
cleanup = ExitStack()
self.addCleanup(cleanup.close)
self.w = self.window
self.web = self.w._web
self.web.set_frame_streaming(False)
self.grab = cleanup.enter_context(mock.patch.object(self.web, "_grab_png", wraps=self.web._grab_png))
def settle(self) -> None:
# Initial paints can settle the axis layout over several Qt event turns.
for _ in range(10):
self.app.processEvents()
self.web._last_grab_s = 0
self.web._grab_frame_if_due()
def test_no_clients_never_grabs_and_first_client_gets_current_plot(self) -> None:
self.w._plots.bscan.widget.plot([0, 1], [1, 2])
self.settle()
self.grab.assert_not_called()
controller = self.web
mode = self.w._view.processing.mode
with QSignalBlocker(mode):
mode.setCurrentText("bscan")
self.w._live.active_mode = "bscan"
self.w._plots.bscan.history_by_combo = {(2, 3): deque([np.array([1., 2.])])}
self.w._plots.sync_display_combos()
self.web.refresh_snapshot()
self.assertEqual(controller.status()["display_combo"],
{"enabled": True, "input": 2, "output": 3, "options": [(2, 3)]})
self.grab.assert_not_called()
controller.set_frame_streaming(True)
self.settle()
self.assertGreater(self.grab.call_count, 0)
self.assertIsNotNone(self.web._last_png)
calls = self.grab.call_count
self.settle()
self.assertEqual(self.grab.call_count, calls) # grab does not keep itself dirty
controller.set_frame_streaming(False)
self.w._plots.bscan.widget.clear()
self.settle()
self.assertEqual(self.grab.call_count, calls)
def test_data_clear_zoom_resize_and_sar_grid_trigger_capture_after_idle(self) -> None:
self.web.set_frame_streaming(True)
plot = self.w._plots.bscan.widget
item = plot.getPlotItem()
assert item is not None
view = item.getViewBox()
self.w._plots.stack.setCurrentWidget(plot)
self.settle()
actions = [
lambda: plot.plot([0, 1, 2], [1, 4, 2]),
lambda: view.setXRange(0.25, 1.5, padding=0),
plot.clear,
lambda: self.w.resize(self.w.width() + 50, self.w.height() + 50),
lambda: self.w._plots.stack.setCurrentWidget(self.w._plots.sar3d.widget),
]
actions.extend(lambda plot=plot: plot.plot([0, 1], [1, 3]) for plot in self.w._plots.sar3d.plots.values())
for action in actions:
with self.subTest(action=action):
calls = self.grab.call_count
action()
self.settle()
self.assertGreater(self.grab.call_count, calls)
calls = self.grab.call_count
self.settle()
self.assertEqual(self.grab.call_count, calls)
def test_throttle_keeps_dirty_and_ignores_changes_emitted_during_grab(self) -> None:
self.web.set_frame_streaming(True)
self.settle()
self.web._mark_dirty()
self.web._last_grab_s = 100
with mock.patch("python_app.gui.controllers.web_controller.time.monotonic", return_value=100.1):
self.assertIsNone(self.web._grab_frame_if_due())
self.assertTrue(self.web._dirty)
self.grab.side_effect = lambda: (self.web._mark_dirty(), "changed-png")[1]
with mock.patch("python_app.gui.controllers.web_controller.time.monotonic", return_value=100.3):
self.assertIsNotNone(self.web._grab_frame_if_due())
self.assertFalse(self.web._dirty)
if __name__ == "__main__":
unittest.main()