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()