fix graph and white background added
This commit is contained in:
@@ -140,6 +140,25 @@ class ReplayTest(unittest.TestCase):
|
||||
self.assertIn(unrelated, self.history.results)
|
||||
self.writer_factory.assert_called_once_with(self.ring.name, self.ring.capacity, self.ring.slot_size_bytes)
|
||||
|
||||
def test_unreturned_sweeps_are_left_out_instead_of_mixing_settings(self) -> None:
|
||||
sweeps = [_sweep(cid) for cid in range(1, 7)]
|
||||
self.history.preprocessed.extend(sweeps)
|
||||
self.history.results.extend(replace(_result_of(sweep), settings_revision=3) for sweep in sweeps)
|
||||
|
||||
def push_dropping_two(payload: bytes) -> bool:
|
||||
accepted = self.processor.push(payload)
|
||||
if self.processor.received[-1].collection_id in {2, 5}:
|
||||
self.processor.published.pop()
|
||||
return accepted
|
||||
|
||||
self.writer.push = push_dropping_two
|
||||
with patch("python_app.gui.controllers.bscan_replay._REPLAY_CHUNK_TIMEOUT_S", .01):
|
||||
self.assertTrue(self.run_replay())
|
||||
source = self.history.bscan_source()
|
||||
self.assertEqual([item.collection_id for item in source], [1, 3, 4, 6])
|
||||
self.assertTrue(all(item.settings_revision == 7 for item in source))
|
||||
self.assertEqual(self.warnings[-1], "B-scan replay recovered 4 of 6 re-sent sweeps; the image leaves the others out.")
|
||||
|
||||
def test_refuses_without_reader_or_active_ring_geometry(self) -> None:
|
||||
self.history.preprocessed.append(_sweep(1))
|
||||
self.assertFalse(self.replay.run(window=1, revision=7, ring=self.ring, reader=None,
|
||||
|
||||
@@ -160,6 +160,36 @@ class CaptureWindowTest(unittest.TestCase):
|
||||
np.testing.assert_array_equal(trace.s21, np.full(5, 0.5))
|
||||
|
||||
|
||||
class CycleClosureTest(unittest.TestCase):
|
||||
def test_next_sweep_without_its_point_zero_still_closes_the_cycle(self) -> None:
|
||||
# A point of this sweep and point 0 of the next are lost: the next sweep's point 1
|
||||
# must close the cycle instead of overwriting this sweep's point 1.
|
||||
points = 40
|
||||
configuration = SweepConfiguration(1_000_000, 2_000_000, points, 1000, -10.0)
|
||||
frames = [*((point, 1 + 0j) for point in range(points) if point != 3),
|
||||
*((point, 7 + 0j) for point in range(1, points))]
|
||||
|
||||
def connection(serial: str) -> Mock:
|
||||
sequence = iter(frames)
|
||||
|
||||
def receive(*, timeout_seconds):
|
||||
point, measured = next(sequence)
|
||||
values = [(0x13, 2 + 0j), (0x01, measured), (0x02, measured),
|
||||
(0x33, 2 + 0j), (0x21, measured), (0x22, measured)]
|
||||
return 27, _datapoint_frame(1_000_000 + point * 25_000, point, values)[4:-4]
|
||||
|
||||
return Mock(serial_number=serial, receive_packet=receive)
|
||||
|
||||
result = collect_complete_running_sweep_cycle(
|
||||
master_device_connection=connection("M"), slave_device_connections=[connection("A"), connection("B")],
|
||||
active_sweep_configuration=configuration, master_stimulus_ports=(1, 2),
|
||||
)
|
||||
expected = np.full(points, .5)
|
||||
expected[3] = 0 # the lost point is zero-filled
|
||||
for trace in result.s_parameters.values():
|
||||
np.testing.assert_array_equal(trace, expected)
|
||||
|
||||
|
||||
class CrossDeviceCycleSynchronizerTest(unittest.TestCase):
|
||||
"""N1: cycle-0 anchors are accepted only when all devices saw the same wrap."""
|
||||
|
||||
|
||||
@@ -209,6 +209,44 @@ class FedBackSweepsTest(_ProcessorTestCase):
|
||||
self._live_writer.write(self._live)
|
||||
self.assertEqual(self._published_ids(_SWEEPS), expected)
|
||||
|
||||
def test_sweep_failing_to_recompute_does_not_stall_new_input(self) -> None:
|
||||
self._live.processor_mode = "gpr"
|
||||
self._live.gpr_input_positions = self._live.gpr_output_positions = [0]
|
||||
self._live.gpr_background_subtract_enabled = False
|
||||
self._live.tx_geometry = [{"output_pos": 0, "x_m": 0.0, "y_m": 0.0, "z_m": 0.0}]
|
||||
self._live.rx_geometry = [{"input_pos": 0, "x_m": 0.0, "y_m": 0.0, "z_m": 0.0}]
|
||||
self._live_writer.write(self._live)
|
||||
start = time.monotonic_ns()
|
||||
self._push([_sweep(1, start)])
|
||||
self._result_for_revision(0, 1)
|
||||
failing = _sweep(2, start + 1_000_000)
|
||||
failing.traces[0].capture_end_ns = failing.traces[0].capture_start_ns # GPR rejects an empty trace window
|
||||
self._push([failing])
|
||||
deadline = time.monotonic() + _TIMEOUT_S
|
||||
while self._input._read_u64(32) != self._input._read_u64(24): # now the newest retained sweep
|
||||
self.assertLess(time.monotonic(), deadline, self._stderr_path.read_text())
|
||||
time.sleep(.01)
|
||||
# Both arrive while the processor is stopped past its 100 ms live-file check, so on
|
||||
# resuming it recomputes that failing sweep before it takes the next one.
|
||||
self._processor.send_signal(signal.SIGSTOP)
|
||||
os.waitpid(self._processor.pid, os.WUNTRACED)
|
||||
try:
|
||||
self._live.settings_revision = 1
|
||||
self._live_writer.write(self._live)
|
||||
self._push([_sweep(3, start + 2_000_000)])
|
||||
time.sleep(.2)
|
||||
finally:
|
||||
self._processor.send_signal(signal.SIGCONT)
|
||||
self._result_for_revision(1, 3)
|
||||
|
||||
def test_bscan_band_without_two_sweep_points_gives_an_empty_profile(self) -> None:
|
||||
self._live.bscan_start_freq_mhz, self._live.bscan_stop_freq_mhz = 7000.0, 8000.0 # sweep is 1-6 GHz
|
||||
self._live.settings_revision = 1
|
||||
self._live_writer.write(self._live)
|
||||
self._push([_sweep(1, time.monotonic_ns())])
|
||||
payload = self._result_for_revision(1, 1).blocks[0].payloads[0]
|
||||
self.assertEqual((payload.processing_name, payload.frequency_hz.size, payload.trace.size), ("bscan", 0, 0))
|
||||
|
||||
def test_live_geometry_recomputes_retained_gpr_without_restarting(self) -> None:
|
||||
self._live.processor_mode = "gpr"
|
||||
self._live.gpr_input_positions = [0]
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""The display shows the newest acquisition: after polls, Stop drains and settings changes."""
|
||||
|
||||
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 python_app.gui.controllers.renderers.trace import TraceRenderer # noqa: E402
|
||||
from python_app.models.dataset_model import ComboKey, ResultBlock, ResultCollection, ResultPayload # noqa: E402
|
||||
from python_app.models.gui_profile_schema import GuiPassThroughStateModel # noqa: E402
|
||||
from python_app.tests.test_reader_recovery import pipeline_fixture # noqa: E402
|
||||
|
||||
|
||||
def _result(collection_id: int, capture_start_ns: int, revision: int = 0) -> ResultCollection:
|
||||
return ResultCollection(collection_id, capture_start_ns + 1, settings_revision=revision,
|
||||
capture_start_ns=capture_start_ns, capture_end_ns=capture_start_ns + 1)
|
||||
|
||||
|
||||
class NewestResultDisplayTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.pipeline, _supervisor = pipeline_fixture(self)
|
||||
self.pipeline.pump_events = mock.Mock()
|
||||
draw = mock.patch.object(self.pipeline.plots, "draw")
|
||||
self.draw = draw.start()
|
||||
self.addCleanup(draw.stop)
|
||||
|
||||
def read(self, *results: ResultCollection | None) -> None:
|
||||
reader = mock.Mock()
|
||||
reader.pop_result_collection.side_effect = [*results, *[None] * 10]
|
||||
self.pipeline._result_reader = reader
|
||||
|
||||
def test_poll_shows_the_newest_acquisition_rather_than_the_last_one_read(self) -> None:
|
||||
newest = _result(2, 200)
|
||||
self.read(newest, _result(1, 100)) # a re-fed earlier sweep arrives after the newest
|
||||
self.pipeline.poll()
|
||||
self.draw.assert_called_once_with(newest)
|
||||
|
||||
def test_results_drained_by_stop_are_shown_once_stopped(self) -> None:
|
||||
last = _result(2, 200)
|
||||
self.read(_result(1, 100), last)
|
||||
self.pipeline.stop()
|
||||
self.draw.assert_not_called()
|
||||
self.pipeline.poll()
|
||||
self.pipeline.poll()
|
||||
self.draw.assert_called_once_with(last)
|
||||
|
||||
def test_revision_drain_waits_past_quiet_polls_for_the_published_revision(self) -> None:
|
||||
stale, fresh = _result(1, 100, revision=1), _result(1, 100, revision=2)
|
||||
for revision, expected in ((None, stale), (2, fresh)):
|
||||
with self.subTest(revision=revision):
|
||||
self.pipeline.history.results.clear()
|
||||
self.read(stale, None, None, None, fresh)
|
||||
self.pipeline.drain_results(timeout_s=60, poll_s=0, revision=revision)
|
||||
self.assertIs(self.pipeline.history.results[-1], expected)
|
||||
|
||||
|
||||
class TraceViewTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.app = QApplication.instance() or QApplication([])
|
||||
|
||||
def test_draws_only_pass_through_payloads(self) -> None:
|
||||
renderer = TraceRenderer(mock.Mock())
|
||||
self.addCleanup(renderer.widget.close)
|
||||
payloads = [ResultPayload(name, 1, frequency_hz=np.array([1e9, 2e9]), trace=np.ones(2, np.complex64))
|
||||
for name in ("bscan", "pass_through")]
|
||||
collection = ResultCollection(1, 1, blocks=[ResultBlock(ComboKey(0, 0), payloads)])
|
||||
self.assertTrue(renderer.draw(collection, GuiPassThroughStateModel()))
|
||||
self.assertEqual([name for _combo, _index, name in renderer.magnitude.curves], ["pass_through"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -129,8 +129,9 @@ class ReviewGuiRegressions(unittest.TestCase):
|
||||
pipeline, supervisor = pipeline_fixture(self)
|
||||
pipeline.should_run = True
|
||||
supervisor.pids.return_value = dict.fromkeys(("acquisition", "data_preprocessor", "data_processor"), 1)
|
||||
with mock.patch.object(pipeline, "read_results", return_value=ResultCollection(1, 2)), \
|
||||
mock.patch.object(pipeline.plots, "draw", side_effect=ValueError("bad display")), \
|
||||
results = iter([ResultCollection(1, 2), None, ResultCollection(2, 3)])
|
||||
pipeline._result_reader = mock.Mock(pop_result_collection=lambda: next(results, None))
|
||||
with mock.patch.object(pipeline.plots, "draw", side_effect=ValueError("bad display")), \
|
||||
mock.patch.object(pipeline, "stop_all") as stop, \
|
||||
self.assertLogs("python_app.gui.controllers.pipeline_controller", "ERROR") as logs:
|
||||
pipeline.poll()
|
||||
|
||||
@@ -234,7 +234,9 @@ class Sar3dIntegrationTest(unittest.TestCase):
|
||||
(_collection(_status(2, "fewer than two frequency points")), "error"),
|
||||
(_collection(_status(1, "No signal in selected depth range")), "no signal"),
|
||||
(_projections(), "ready — CUDA")):
|
||||
self.view._pipeline.drain_results = mock.Mock(return_value=result)
|
||||
# Like the real drain, the recomputed result replaces its sweep in history.
|
||||
self.view._pipeline.drain_results = mock.Mock(
|
||||
side_effect=lambda result=result, **_timing: self.view._history.record_result(result))
|
||||
self.view._live.changed()
|
||||
self.assertIn(expected, self.renderer.status_label.text())
|
||||
self.view._log.show_exception.assert_not_called()
|
||||
|
||||
@@ -34,6 +34,7 @@ from python_app.orchestration.shm.decoder import (
|
||||
decode_result_collection,
|
||||
decode_trace_collection,
|
||||
)
|
||||
from python_app.orchestration.shm.ring_layout import HEADER
|
||||
from python_app.orchestration.shm.ring_reader import ShmRingReader
|
||||
from python_app.orchestration.shm.ring_writer import ShmRingWriter
|
||||
from python_app.storage.npz.serialize import serialize_trace_collection
|
||||
@@ -304,8 +305,18 @@ class RingOverflowTest(_RingTestCase):
|
||||
writer.push(b"new")
|
||||
|
||||
with mock.patch.object(atomics, "fence", side_effect=overwrite_after_copy):
|
||||
self.assertIsNone(reader.pop_payload())
|
||||
self.assertEqual(reader.pop_payload(), b"new")
|
||||
self.assertEqual(reader.pop_payload(), b"new") # the torn copy is never returned
|
||||
self.assertIsNone(reader.pop_payload())
|
||||
|
||||
def test_lapped_reader_continues_with_the_newest_payload(self) -> None:
|
||||
name = self._ring_name()
|
||||
writer = self._writer(name, capacity=4, slot_size=64)
|
||||
reader = self._reader(name)
|
||||
for i in range(3):
|
||||
writer.push(f"f{i}".encode())
|
||||
writer._write_u64(HEADER.size + 8, 0) # the producer is overwriting the unread f0
|
||||
self.assertEqual(reader.pop_payload(), b"f2")
|
||||
self.assertIsNone(reader.pop_payload())
|
||||
|
||||
def test_reader_cannot_rewind_cursor_after_producer_laps_during_commit(self) -> None:
|
||||
from python_app.orchestration.shm import atomics
|
||||
|
||||
@@ -1,16 +1,54 @@
|
||||
|
||||
from contextlib import ExitStack
|
||||
"""Plot capture follows client demand and actual Qt scene changes."""
|
||||
"""Plot capture follows client demand and plot changes; redraws leave no stale pixels."""
|
||||
|
||||
from collections import deque
|
||||
from contextlib import ExitStack
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
from PyQt6.QtWidgets import QApplication, QComboBox
|
||||
import pyqtgraph as pg
|
||||
from PyQt6.QtCore import QPoint, QSignalBlocker
|
||||
from PyQt6.QtGui import QImage
|
||||
from PyQt6.QtWidgets import QApplication, QCheckBox
|
||||
|
||||
from python_app.tests.gui_test_support import isolated_window
|
||||
from python_app.gui.controllers.renderers.sar3d import SAR3D_VIEWS
|
||||
from python_app.models.dataset_model import ComboKey, ResultBlock, ResultCollection, ResultPayload, TraceData
|
||||
from python_app.tests.gui_test_support import isolated_window, widget
|
||||
|
||||
|
||||
def _phase_result(phase_deg: np.ndarray) -> ResultCollection:
|
||||
trace = (0.5 * np.exp(1j * np.radians(phase_deg))).astype(np.complex64)
|
||||
payload = ResultPayload("pass_through", 1, frequency_hz=np.linspace(1e9, 2e9, phase_deg.size), trace=trace)
|
||||
return ResultCollection(1, 1, blocks=[ResultBlock(ComboKey(0, 0), [payload])])
|
||||
|
||||
|
||||
def _gpr_result(revision: int, point: tuple[float, float], size: int) -> ResultCollection:
|
||||
image = np.linspace(0, 1, size * size, dtype=np.float32).reshape(size, size)
|
||||
return ResultCollection(1, 1, settings_revision=revision, collection_payloads=[
|
||||
ResultPayload("gpr_accumulator", 3, image=image,
|
||||
image_x_axis=np.linspace(-1.5, 1.5, size), image_y_axis=np.linspace(1, 12, size)),
|
||||
ResultPayload("gpr_points", 4, table=np.array([[*point, 0.9]], dtype=np.float32)),
|
||||
])
|
||||
|
||||
|
||||
def _sar3d_result(revision: int, peak: tuple[float, float, float]) -> ResultCollection:
|
||||
x, y, z = np.linspace(-1, 1, 9), np.linspace(-.5, .5, 5), np.linspace(1, 3, 9)
|
||||
payloads = [ResultPayload(name, 3, image_x_axis=xx, image_y_axis=yy,
|
||||
image=np.full((yy.size, xx.size), .5, dtype=np.float32))
|
||||
for name, (xx, yy) in zip(SAR3D_VIEWS, ((x, y), (z, y), (x, z), (x, y)))]
|
||||
return ResultCollection(1, 1, settings_revision=revision, collection_payloads=[
|
||||
*payloads,
|
||||
ResultPayload("sar3d_status", 4, table=np.zeros((1, 1), dtype=np.float32)),
|
||||
ResultPayload("sar3d_oblique_basis", 4, table=np.array([[1, 0, 0], [0, 1, 0]], dtype=np.float32)),
|
||||
ResultPayload("sar3d_peak", 4, table=np.array([[1, *peak]], dtype=np.float32)),
|
||||
])
|
||||
|
||||
|
||||
def _bscan_result(collection_id: int, depth_m: np.ndarray) -> ResultCollection:
|
||||
trace = np.full(depth_m.size, collection_id, dtype=np.complex64)
|
||||
payload = ResultPayload("bscan", 1, frequency_hz=depth_m.astype(np.float32), trace=trace)
|
||||
return ResultCollection(collection_id, collection_id, capture_start_ns=collection_id,
|
||||
capture_end_ns=collection_id + 1, blocks=[ResultBlock(ComboKey(0, 0), [payload])])
|
||||
|
||||
|
||||
class WebPlotCaptureTest(unittest.TestCase):
|
||||
@@ -37,49 +75,53 @@ class WebPlotCaptureTest(unittest.TestCase):
|
||||
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
|
||||
def show_mode(self, name: str) -> None:
|
||||
mode = self.w._view.processing.mode
|
||||
with QSignalBlocker(mode):
|
||||
mode.setCurrentText("bscan")
|
||||
self.w._live.active_mode = "bscan"
|
||||
mode.setCurrentText(name)
|
||||
self.w._live.active_mode = name
|
||||
self.w._plots.set_mode(name)
|
||||
|
||||
def test_no_clients_never_grabs_and_first_client_gets_current_plot(self) -> None:
|
||||
trace = TraceData(ComboKey(0, 0), np.array([1., 2.]), np.array([.1 + 0j, .2 + 0j]))
|
||||
self.w._plots.draw_trace(trace, "Captured")
|
||||
self.settle()
|
||||
self.grab.assert_not_called()
|
||||
self.show_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"],
|
||||
self.assertEqual(self.web.status()["display_combo"],
|
||||
{"enabled": True, "input": 2, "output": 3, "options": [(2, 3)]})
|
||||
self.grab.assert_not_called()
|
||||
controller.set_frame_streaming(True)
|
||||
self.web.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.assertEqual(self.grab.call_count, calls) # a grab does not trigger another one
|
||||
self.web.set_frame_streaming(False)
|
||||
self.w._plots.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:
|
||||
def test_drawing_clearing_zoom_resize_and_page_switch_trigger_capture_after_idle(self) -> None:
|
||||
plots = self.w._plots
|
||||
self.show_mode("pass_through")
|
||||
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()
|
||||
view = plots.trace.magnitude.view
|
||||
actions = [
|
||||
lambda: plot.plot([0, 1, 2], [1, 4, 2]),
|
||||
lambda: view.setXRange(0.25, 1.5, padding=0),
|
||||
plot.clear,
|
||||
lambda: plots.draw(_phase_result(np.full(50, 30.0))),
|
||||
lambda: view.setXRange(1.2e9, 1.5e9, padding=0),
|
||||
plots.redraw,
|
||||
plots.clear,
|
||||
lambda: self.w.resize(self.w.width() + 50, self.w.height() + 50),
|
||||
lambda: self.w._plots.stack.setCurrentWidget(self.w._plots.sar3d.widget),
|
||||
lambda: plots.set_mode("sar3d"),
|
||||
lambda: plots.draw_trace(TraceData(ComboKey(0, 0), np.array([1., 2.]), np.array([.1 + 0j, .2 + 0j])),
|
||||
"Captured"),
|
||||
]
|
||||
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
|
||||
@@ -90,18 +132,88 @@ class WebPlotCaptureTest(unittest.TestCase):
|
||||
self.settle()
|
||||
self.assertEqual(self.grab.call_count, calls)
|
||||
|
||||
def test_throttle_keeps_dirty_and_ignores_changes_emitted_during_grab(self) -> None:
|
||||
def test_throttle_defers_a_change_and_a_grab_absorbs_changes_it_renders(self) -> None:
|
||||
plots = self.w._plots
|
||||
self.web.set_frame_streaming(True)
|
||||
self.settle()
|
||||
self.web._mark_dirty()
|
||||
self.grab.reset_mock()
|
||||
plots.clear()
|
||||
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]
|
||||
self.grab.assert_not_called()
|
||||
self.grab.side_effect = lambda: (plots.clear(), "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)
|
||||
self.assertEqual(self.web._grabbed_revision, plots.revision)
|
||||
with mock.patch("python_app.gui.controllers.web_controller.time.monotonic", return_value=100.6):
|
||||
self.assertIsNone(self.web._grab_frame_if_due())
|
||||
self.assertEqual(self.grab.call_count, 1)
|
||||
|
||||
def test_failed_grab_is_retried(self) -> None:
|
||||
plots = self.w._plots
|
||||
self.web.set_frame_streaming(True)
|
||||
self.settle()
|
||||
plots.clear()
|
||||
self.grab.reset_mock(side_effect=True)
|
||||
self.grab.side_effect = RuntimeError("render failed")
|
||||
self.web._last_grab_s = 0
|
||||
with self.assertRaises(RuntimeError):
|
||||
self.web._grab_frame_if_due()
|
||||
self.assertNotEqual(self.web._grabbed_revision, plots.revision)
|
||||
self.grab.side_effect = None
|
||||
self.settle()
|
||||
self.assertEqual(self.grab.call_count, 2)
|
||||
self.assertEqual(self.web._grabbed_revision, plots.revision)
|
||||
|
||||
def assert_window_shows_fresh_render(self) -> None:
|
||||
screen, page = QApplication.primaryScreen(), self.w._plots.stack.currentWidget()
|
||||
assert screen is not None and page is not None
|
||||
for plot in [page] if isinstance(page, pg.PlotWidget) else page.findChildren(pg.PlotWidget):
|
||||
if plot.isVisible():
|
||||
origin = plot.mapTo(self.w, QPoint())
|
||||
shown = screen.grabWindow(self.w.winId(), origin.x(), origin.y(), plot.width(), plot.height())
|
||||
self.assertEqual(shown.toImage().convertToFormat(QImage.Format.Format_RGB32),
|
||||
plot.grab().toImage().convertToFormat(QImage.Format.Format_RGB32))
|
||||
|
||||
def test_redrawn_views_leave_no_stale_pixels_on_screen(self) -> None:
|
||||
# Qt repaints only changed regions, yet the window must equal a fresh render: once
|
||||
# a phase redrawn from 100 to 0 deg left the old curve visible where none was drawn.
|
||||
plots, history = self.w._plots, self.w._history
|
||||
for plot in plots.stack.findChildren(pg.PlotWidget):
|
||||
scene = plot.scene()
|
||||
assert scene is not None
|
||||
self.assertEqual(scene.receivers(scene.changed), 0) # a receiver selects that update path
|
||||
phase = widget(self.w._view.processing, "_show_phase_checkbox", QCheckBox)
|
||||
self.addCleanup(phase.setChecked, phase.isChecked())
|
||||
phase.setChecked(True)
|
||||
self.web.set_frame_streaming(True)
|
||||
revision = plots.required_revision or 0
|
||||
later_phase = np.zeros(400)
|
||||
later_phase[200:] = np.nan
|
||||
|
||||
def bscan(count: int, depth_m: np.ndarray) -> bool:
|
||||
history.results.clear()
|
||||
history.results.extend(_bscan_result(index, depth_m) for index in range(1, count + 1))
|
||||
return plots.draw_bscan()
|
||||
|
||||
frames = {
|
||||
"pass_through": (lambda: plots.draw(_phase_result(np.full(400, 100.0))),
|
||||
lambda: plots.draw(_phase_result(later_phase))),
|
||||
"gpr": (lambda: plots.draw(_gpr_result(revision, (-1.0, 4.0), 20)),
|
||||
lambda: plots.draw(_gpr_result(revision, (1.0, 9.0), 8))),
|
||||
"sar3d": (lambda: plots.draw(_sar3d_result(revision, (.6, .3, 2.6))),
|
||||
lambda: plots.draw(_sar3d_result(revision, (-.6, -.3, 1.4)))),
|
||||
"bscan": (lambda: bscan(6, np.linspace(.5, 3., 12)), lambda: bscan(3, np.linspace(.5, 2., 4))),
|
||||
}
|
||||
self.addCleanup(history.results.clear)
|
||||
for mode, (first, second) in frames.items():
|
||||
with self.subTest(mode=mode):
|
||||
self.show_mode(mode)
|
||||
for draw in (first, second):
|
||||
self.assertTrue(draw())
|
||||
self.settle()
|
||||
self.assert_window_shows_fresh_render()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user