Rework radar system
This commit is contained in:
@@ -8,17 +8,16 @@ from pathlib import Path
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from PyQt6.QtWidgets import QApplication, QComboBox, QDoubleSpinBox
|
||||
|
||||
from python_app.gui.antenna_geometry_editor import AntennaGeometryEditor
|
||||
from python_app.tests.gui_test_support import isolated_window
|
||||
from python_app.models.dataset_model import ResultCollection, ResultPayload
|
||||
from python_app.models.gui_profile_model import GuiProfileModel
|
||||
from python_app.models.run_config_model import GprTxGeometryModel, GprRxGeometryModel
|
||||
from python_app.tests.gui_test_support import isolated_window, widget
|
||||
from python_app.models.dataset_model import ResultCollection
|
||||
from python_app.models.gui_profile_schema import GuiProfileModel
|
||||
from python_app.models.run_config_schema import GprTxGeometryModel, GprRxGeometryModel
|
||||
|
||||
|
||||
class AntennaGeometryTest(unittest.TestCase):
|
||||
@@ -38,7 +37,9 @@ class AntennaGeometryTest(unittest.TestCase):
|
||||
changed = mock.Mock()
|
||||
editor.changed.connect(changed)
|
||||
with mock.patch("python_app.gui.antenna_geometry_editor.QMessageBox.warning") as warning:
|
||||
editor._table.item(0, 1).setText("nan")
|
||||
item = editor._table.item(0, 1)
|
||||
assert item is not None
|
||||
item.setText("nan")
|
||||
warning.assert_called_once()
|
||||
changed.assert_not_called()
|
||||
self.assertEqual(editor.records()[0]["x_m"], 1.0)
|
||||
@@ -52,13 +53,24 @@ class AntennaGeometryTest(unittest.TestCase):
|
||||
editor._remove_button.click()
|
||||
self.assertEqual(editor.records(), [])
|
||||
self.assertTrue(editor._add_button.isEnabled())
|
||||
changed = mock.Mock()
|
||||
editor.changed.connect(changed)
|
||||
editor._add_button.click()
|
||||
self.assertEqual(editor.records()[0]["input_pos"], 0)
|
||||
self.assertEqual(editor.records(), [])
|
||||
changed.assert_not_called()
|
||||
item = editor._table.item(0, 1)
|
||||
assert item is not None
|
||||
item.setText("1.25")
|
||||
self.assertEqual(editor.records()[0], {"input_pos": 0, "x_m": 1.25, "y_m": 0.0, "z_m": 0.0})
|
||||
changed.assert_called_once()
|
||||
|
||||
def test_web_geometry_rejects_duplicates_and_out_of_range_before_mutating(self) -> None:
|
||||
def test_invalid_web_geometry_is_rejected_before_mutating(self) -> None:
|
||||
editor = AntennaGeometryEditor("output_pos", [], position_count=2)
|
||||
for records in ([{"output_pos": 2}], [{"output_pos": 0}, {"output_pos": 0}],
|
||||
[{"output_pos": True}], [{"output_pos": 0, "x_m": True}]):
|
||||
[{"output_pos": True}], [{"output_pos": 0.5}], [{"output_pos": "0"}],
|
||||
[{}], [None], [{"output_pos": 0, "x_m": None}],
|
||||
[{"output_pos": 0, "x_m": True}], [{"output_pos": 0, "y_m": float("nan")}],
|
||||
[{"output_pos": 0, "z_m": float("inf")}], [{"output_pos": 0, "x_m": 10**400}]):
|
||||
with self.subTest(records=records), self.assertRaises(ValueError):
|
||||
editor.set_records(records)
|
||||
self.assertEqual(editor.records(), [])
|
||||
@@ -69,63 +81,75 @@ class WindowGeometryTest(unittest.TestCase):
|
||||
def setUpClass(cls) -> None:
|
||||
cls.app = QApplication.instance() or QApplication([])
|
||||
cls.window = isolated_window(cls.addClassCleanup)
|
||||
cls.window._timer.stop()
|
||||
cls.window._shutdown_web_ui()
|
||||
cls.initial_config = cls.window._build_config()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
cls.window.close()
|
||||
cls.window._pipeline.timer.stop()
|
||||
cls.initial_config = cls.window._profile.build_config()
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.w = self.window
|
||||
self.w._apply_loaded_profile(GuiProfileModel(run_config=self.initial_config.clone()), Path("run_config.json"))
|
||||
self.mode = self.w._view.processing.mode
|
||||
self.gain = widget(self.w._view.processing, "_bscan_gain", QDoubleSpinBox)
|
||||
self.w._profile.apply(GuiProfileModel(run_config=self.initial_config.clone()), Path("run_config.json"))
|
||||
|
||||
def test_profile_and_live_config_preserve_all_coordinates(self) -> None:
|
||||
config = self.initial_config.clone()
|
||||
config.gpr.tx_geometry = [GprTxGeometryModel(output_pos=0, x_m=1.25, y_m=-2.0, z_m=3.0)]
|
||||
config.gpr.rx_geometry = [GprRxGeometryModel(input_pos=0, x_m=-1.25, y_m=4.0, z_m=-5.0)]
|
||||
self.w._apply_loaded_profile(GuiProfileModel(run_config=config), Path("run_config.json"))
|
||||
rebuilt = self.w._build_config()
|
||||
self.w._profile.apply(GuiProfileModel(run_config=config), Path("run_config.json"))
|
||||
rebuilt = self.w._profile.build_config()
|
||||
self.assertEqual(rebuilt.gpr, config.gpr)
|
||||
live = self.w._live_processing_config().to_dict()
|
||||
live = self.w._live.build().to_dict()
|
||||
self.assertEqual(live["tx_geometry"], [asdict(entry) for entry in config.gpr.tx_geometry])
|
||||
self.assertEqual(live["rx_geometry"], [asdict(entry) for entry in config.gpr.rx_geometry])
|
||||
schema = self.w._web_settings_schema()
|
||||
schema = self.w._config.web_schema()
|
||||
self.assertFalse(any(entry.get("applies_on_start") for entry in schema))
|
||||
|
||||
def test_malformed_web_batch_restores_every_changed_widget(self) -> None:
|
||||
original_gain = self.w._bscan_gain.value()
|
||||
original_mode = self.w._processing_mode.currentText()
|
||||
with mock.patch.object(self.w, "_show_exception") as error, mock.patch.object(self.w._live_config_writer, "write") as write:
|
||||
self.w._apply_web_live_settings({
|
||||
original_gain = self.gain.value()
|
||||
original_mode = self.mode.currentText()
|
||||
with mock.patch.object(self.w._log, "show_exception") as error, mock.patch.object(self.w._live.writer, "write") as write:
|
||||
self.w._live.apply_web({
|
||||
"processor_mode": "bscan", "bscan_gain": original_gain + 1,
|
||||
"tx_geometry": [{"output_pos": 0}, {"output_pos": 0}],
|
||||
})
|
||||
error.assert_called_once()
|
||||
write.assert_not_called()
|
||||
self.assertEqual(self.w._bscan_gain.value(), original_gain)
|
||||
self.assertEqual(self.w._processing_mode.currentText(), original_mode)
|
||||
self.assertEqual(self.gain.value(), original_gain)
|
||||
self.assertEqual(self.mode.currentText(), original_mode)
|
||||
|
||||
def test_stale_results_cannot_draw_over_new_geometry(self) -> None:
|
||||
revision = (1 << 63) + 123456789
|
||||
self.w._required_processing_revision = revision
|
||||
with QSignalBlocker(self.w._processing_mode):
|
||||
self.w._processing_mode.setCurrentText("gpr")
|
||||
words = [(revision >> shift) & 0xffff for shift in range(0, 64, 16)]
|
||||
fresh = ResultCollection(collection_id=1, monotonic_ns=1, collection_payloads=[ResultPayload(
|
||||
processing_name="processing_revision", kind=4, table=np.asarray([words], dtype=np.float32),
|
||||
)])
|
||||
with mock.patch.object(self.w, "_draw_gpr_map", return_value=True) as draw:
|
||||
self.assertFalse(self.w._draw_results(ResultCollection(collection_id=1, monotonic_ns=1)))
|
||||
self.assertTrue(self.w._draw_results(fresh))
|
||||
draw.assert_called_once_with(fresh)
|
||||
self.assertFalse(self.w._result_collection_has_trace(fresh))
|
||||
self.w._plots.required_revision = revision
|
||||
with QSignalBlocker(self.mode):
|
||||
self.mode.setCurrentText("gpr")
|
||||
fresh = ResultCollection(collection_id=1, monotonic_ns=1, settings_revision=revision)
|
||||
with mock.patch.object(self.w._plots.gpr, "draw", return_value=True) as draw:
|
||||
self.assertFalse(self.w._plots.draw(ResultCollection(collection_id=1, monotonic_ns=1)))
|
||||
self.assertTrue(self.w._plots.draw(fresh))
|
||||
draw.assert_called_once()
|
||||
self.assertIs(draw.call_args.args[0], fresh)
|
||||
self.assertFalse(self.w._plots.has_output(fresh))
|
||||
|
||||
def test_desktop_and_web_reject_empty_active_geometry(self) -> None:
|
||||
with QSignalBlocker(self.mode):
|
||||
self.mode.setCurrentText("gpr")
|
||||
editor = self.w._view.processing.geometries[0]
|
||||
original = editor.records()
|
||||
editor._table.selectAll()
|
||||
with mock.patch("python_app.gui.antenna_geometry_editor.QMessageBox.warning") as warning, \
|
||||
mock.patch.object(self.w._live.writer, "write") as write:
|
||||
editor._remove_rows()
|
||||
warning.assert_called_once()
|
||||
write.assert_not_called()
|
||||
self.assertEqual(editor.records(), original)
|
||||
with mock.patch.object(self.w._log, "show_exception") as error:
|
||||
self.w._live.apply_web({"tx_geometry": []})
|
||||
error.assert_called_once()
|
||||
self.assertEqual(editor.records(), original)
|
||||
|
||||
def test_invalid_or_mixed_history_commands_do_not_clear_data(self) -> None:
|
||||
with mock.patch.object(self.w, "_show_exception") as error, mock.patch.object(self.w, "_clear_all_runtime_history") as clear:
|
||||
self.w._apply_web_live_settings({"history_command": "bogus"})
|
||||
self.w._apply_web_live_settings({"history_command": "clear_all", "bscan_gain": 2})
|
||||
with mock.patch.object(self.w._log, "show_exception") as error, mock.patch.object(self.w._snapshots, "delete_history") as clear:
|
||||
self.w._live.apply_web({"history_command": "bogus"})
|
||||
self.w._live.apply_web({"history_command": "clear_all", "bscan_gain": 2})
|
||||
self.assertEqual(error.call_count, 2)
|
||||
clear.assert_not_called()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user