"""Reusable Qt adapter tests, runnable with PySide2 or PySide6, without a GUI app repo.""" import os os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") import time import unittest from dataclasses import replace from unittest.mock import patch from set_devices.plot_processing import Axis, Series, Snapshot, process from set_devices.qt_ports.plot_processing import SignalProcessingPanel, PlotProcessingAttachment, QWidget try: from PySide6.QtWidgets import QApplication except ImportError: from PySide2.QtWidgets import QApplication class ProcessingQtTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.app = QApplication.instance() or QApplication([]) def setUp(self): self.panel = SignalProcessingPanel() self.snapshot = Snapshot([Series("a", "A", [(0, 0), (1, 1), (2, 0)])]) self.panel.set_snapshot(self.snapshot) def tearDown(self): self.panel.close() def calculate(self): self.panel.calculate() deadline = time.monotonic() + 5 while self.panel._job is not None and time.monotonic() < deadline: self.app.processEvents() time.sleep(.002) self.assertIsNone(self.panel._job) self.assertIsNotNone(self.panel.curve, self.panel.status.text()) def test_all_methods_accumulate_and_parameter_changes_keep_results(self): for index, method in enumerate(("polynomial", "linear", "pchip", "spline")): self.panel.method.setCurrentIndex(self.panel.method.findData(method)) self.calculate() self.assertEqual(method, self.panel.curve.request.method) self.panel.count.setValue(self.panel.count.value() + 1) self.assertEqual(index + 1, len(self.panel.curves)) self.assertTrue(self.panel.export_button.isEnabled()) specs, data = self.panel.overlay() self.assertEqual(4, len(specs)) self.assertEqual(4, len(data)) self.assertEqual(4, len({spec[3] for spec in specs})) self.panel.clear() self.assertEqual((), self.panel.curves) self.assertIsNone(self.panel.curve) def test_repeated_request_does_not_duplicate_curve(self): self.calculate() self.calculate() self.assertEqual(1, len(self.panel.curves)) def test_repeated_fft_workers_keep_gui_owned_signal_emitters(self): self.panel.set_snapshot(replace(self.snapshot, axis=Axis("Frequency", "Hz"), source="FFT")) for _ in range(5): for method in ("linear", "pchip", "spline", "polynomial"): self.panel.method.setCurrentIndex(self.panel.method.findData(method)) self.panel.calculate() self.assertIs(self.panel, self.panel._job.signals.parent()) deadline = time.monotonic() + 5 while self.panel._job is not None and time.monotonic() < deadline: self.app.processEvents() time.sleep(.002) self.assertIsNone(self.panel._job) self.assertEqual(method, self.panel.curve.request.method) self.assertEqual(4, len(self.panel.curves)) def test_switching_channel_preserves_results_and_invalidates_only_changed_source(self): snapshot = replace(self.snapshot, series=self.snapshot.series + (Series("b", "B", [(0, 2), (1, 3), (2, 2)]),)) self.panel.set_snapshot(snapshot) self.calculate() first = self.panel.curve self.panel.channel.setCurrentIndex(1) self.panel.set_snapshot(snapshot) self.assertEqual((first,), self.panel.curves) self.calculate() second = self.panel.curve self.assertEqual((first, second), self.panel.curves) second_spec = self.panel.overlay()[0][1] self.panel.set_snapshot(replace(snapshot, series=( Series("a", "A", [(0, 5), (1, 6)]), snapshot.series[1]))) self.assertEqual((second,), self.panel.curves) self.assertEqual(second_spec, self.panel.overlay()[0][0]) def test_navigation_during_second_calculation_preserves_both_results(self): self.panel.preserve_on_view_change = True self.calculate() first = self.panel.curve self.panel.method.setCurrentIndex(self.panel.method.findData("linear")) with patch("set_devices.qt_ports.plot_processing.QThreadPool"): self.panel.calculate() job = self.panel._job self.panel.set_snapshot(replace(self.snapshot, x_range=(0, 1))) self.panel._finished(job.signature, process(job.signature), "") self.assertEqual(2, len(self.panel.curves)) self.assertIs(first, self.panel.curves[0]) def test_failed_calculation_keeps_previous_curve(self): self.calculate() previous = self.panel.curve self.panel.count.setValue(123) with patch("set_devices.qt_ports.plot_processing.QThreadPool"): self.panel.calculate() self.panel._finished(self.panel._job.signature, None, "test error") self.assertEqual((previous,), self.panel.curves) def test_hidden_source_preserves_result_but_changed_samples_invalidate_it(self): self.calculate() curve = self.panel.curve hidden = replace(self.snapshot, series=(replace(self.snapshot.series[0], visible=False),)) self.panel.set_snapshot(hidden) self.assertEqual((curve,), self.panel.curves) self.assertTrue(self.panel.export_button.isEnabled()) self.panel.set_snapshot(replace(hidden, series=(replace(hidden.series[0], points=((0, 2), (1, 3))),))) self.assertEqual((), self.panel.curves) def test_source_units_and_blocking_invalidate_result(self): for changed in (replace(self.snapshot, axis=Axis("Frequency", "Hz")), replace(self.snapshot, source="new file"), replace(self.snapshot, x_range=(0, 1)), replace(self.snapshot, blocked_reason="FFT")): self.panel.set_snapshot(self.snapshot) self.calculate() self.panel.set_snapshot(changed) self.assertIsNone(self.panel.curve) self.assertFalse(self.panel.export_button.isEnabled()) self.assertFalse(self.panel.apply_button.isEnabled()) def test_late_worker_result_is_rejected_even_after_source_returns(self): # Capture, but do not schedule, the real worker. Deliver its answer after # the source changes away and back to the same numerical values. with patch("set_devices.qt_ports.plot_processing.QThreadPool"): self.panel.calculate() job = self.panel._job self.panel.set_snapshot(replace(self.snapshot, source="another")) self.panel.set_snapshot(self.snapshot) self.panel._finished(job.signature, process(job.signature), "") self.assertIsNone(self.panel.curve) self.assertFalse(self.panel.export_button.isEnabled()) def test_attachment_is_lazy_and_coalesces_source_notifications(self): widget = QWidget() calls = [] attachment = PlotProcessingAttachment(widget, lambda: calls.append(1) or self.snapshot, widget.update) try: for _ in range(10): attachment.source_changed() self.app.processEvents() self.assertEqual([], calls) self.assertIsNone(attachment.panel) attachment.open() self.assertEqual([1], calls) for _ in range(10): attachment.source_changed() self.app.processEvents() self.assertEqual([1, 1], calls) finally: widget.close() if __name__ == "__main__": unittest.main()