"""Acceptance for the display preferences, on their own. ``DisplayPrefsController`` decides how a measurement is *presented*: newtons or raw counts, which chart range, which arrow style, whether the rail shows the frame rate. It drives the panels through their own interfaces and borrows a few window-level operations through ``DisplayHost``, so both can be stood in for — which these tests do. """ from __future__ import annotations from orisys_studio.display_prefs import DisplayPrefsController PLOT_DEFAULTS = { "window_sec": 10.0, "fn_min": 0.0, "fn_max": 5.0, "shear_max": 5.0, "fn_raw_min": 0.0, "fn_raw_max": 1e5, "shear_raw_max": 1e5, } class _Action: def __init__(self) -> None: self.enabled: bool | None = None def setEnabled(self, value: bool) -> None: # noqa: N802 self.enabled = bool(value) class _Toolbar: def __init__(self, units: str = "newton") -> None: self._units = units self.set_units_calls: list[str] = [] def units(self) -> str: return self._units def set_units(self, units: str) -> None: self.set_units_calls.append(str(units)) self._units = str(units) class _Charts: def __init__(self, unit: str = "N") -> None: self._unit = unit self.fixed_newton: list[bool] = [] self.units: list[str] = [] self.window_sec: list[float] = [] self.limits: list[dict] = [] self.deadband_floors: list[dict] = [] self.presentation_enabled: list[bool] = [] self.presentation: list[dict] = [] def unit(self) -> str: return self._unit def set_fixed_newton(self, value: bool) -> None: # noqa: N802 self.fixed_newton.append(bool(value)) def set_units(self, unit: str) -> None: self.units.append(str(unit)) self._unit = str(unit) def set_window_sec(self, value: float) -> None: # noqa: N802 self.window_sec.append(float(value)) def set_axis_limits(self, **values) -> None: # noqa: N802 self.limits.append({key: float(value) for key, value in values.items()}) def set_deadband_floors(self, *, newton: float, raw: float) -> None: # noqa: N802 self.deadband_floors.append({"newton": float(newton), "raw": float(raw)}) def configure_presentation(self, **values) -> None: # noqa: N802 self.presentation.append(dict(values)) def set_presentation_enabled(self, enabled: bool) -> None: # noqa: N802 self.presentation_enabled.append(bool(enabled)) class _ArrowPanel: def __init__(self) -> None: self.rotation_enabled: list[bool] = [] self.scales: list[float] = [] self.rotations = 0 def set_rotation_enabled(self, enabled: bool) -> None: self.rotation_enabled.append(bool(enabled)) def set_arrow_scale(self, scale: float) -> None: self.scales.append(float(scale)) def rotate_90_ccw(self) -> int: self.rotations += 1 return 180 class _Engine: def __init__(self, loaded: bool = False) -> None: self._loaded = loaded self.units: list[str] = [] def is_loaded(self) -> bool: return self._loaded def set_unit(self, unit: str) -> None: self.units.append(str(unit)) class _Picker: def __init__(self, sensor_type: str = "box") -> None: self._sensor = sensor_type def sensor_type(self) -> str: return self._sensor class _Host: def __init__(self, prefs: dict, latest_fps=None) -> None: self.prefs = dict(prefs) self.persisted = 0 self.rail_updates = 0 self.markers = 0 self.repaints = 0 self._latest_fps_value = latest_fps @property def latest_fps(self): return self._latest_fps_value @latest_fps.setter def latest_fps(self, value) -> None: self._latest_fps_value = value # compatibility for tests that still assign ``_latest_fps`` @property def _latest_fps(self): return self._latest_fps_value @_latest_fps.setter def _latest_fps(self, value) -> None: self._latest_fps_value = value def persist_prefs(self) -> None: self.persisted += 1 def refresh_status_rail(self) -> None: self.rail_updates += 1 def drop_render_marker(self) -> None: self.markers += 1 def repaint_live_frame(self) -> None: self.repaints += 1 def _controller( *, prefs=None, sensor_type="box", units="newton", loaded=False, latest_fps=None, arrow_panel=None, mesh_panel=None, ): host = _Host(prefs or {}, latest_fps=latest_fps) toolbar = _Toolbar(units) charts = _Charts("N" if units == "newton" else "raw") arrows = arrow_panel if arrow_panel is not None else _ArrowPanel() engine = _Engine(loaded) action = _Action() controller = DisplayPrefsController( host=host, toolbar=toolbar, charts=charts, arrow_panel=arrows, mesh_panel=mesh_panel, engine=engine, sensor_picker=_Picker(sensor_type), rotate_arrows_action=action, plot_defaults=PLOT_DEFAULTS, default_sample_rate_hz=30.0, arrow_rotate_types=frozenset({"box", "mini"}), ) return controller, { "host": host, "toolbar": toolbar, "charts": charts, "arrows": arrows, "engine": engine, "action": action, } # --------------------------------------------------------------------- tests def test_the_unit_reaches_the_toolbar_the_charts_and_the_replay(): controller, parts = _controller(prefs={}, units="newton", loaded=True) controller.set_display_unit("raw") assert parts["toolbar"].set_units_calls == ["raw"] assert parts["engine"].units == ["raw"], "an open recording was not told" assert parts["charts"].fixed_newton == [False] assert parts["charts"].units == ["raw"] assert parts["host"].prefs["units"] == "raw" assert parts["host"].persisted == 1 def test_a_unit_change_from_the_toolbar_does_not_talk_back_to_it(): """The combo already *is* on that unit; writing it again is how a loop starts.""" controller, parts = _controller(prefs={}, units="raw") controller.on_units_changed("newton") assert parts["toolbar"].set_units_calls == [], parts["toolbar"].set_units_calls assert parts["charts"].units == ["N"] assert parts["charts"].fixed_newton == [True] def test_the_chart_chip_flips_which_unit_the_charts_use(): controller, parts = _controller(prefs={}, units="newton") controller.on_chart_unit_toggle() assert parts["charts"].units == ["raw"] controller.on_chart_unit_toggle() assert parts["charts"].units == ["raw", "N"] def test_the_plot_dialog_values_come_from_the_preferences_with_defaults(): controller, parts = _controller(prefs={"plot_window_sec": 3.0}) settings = controller.plot_settings() assert settings["window_sec"] == 3.0 assert settings["fn_max"] == PLOT_DEFAULTS["fn_max"], "a missing range used no default" assert settings["fn_min"] == 0.0, "FN has no lower bound by default" assert settings["deadband_n"] == 0.0, "the deadband default is 'auto'" assert settings["units"] == "newton" def test_a_stored_zero_is_a_value_and_not_a_missing_preference(): """0 means "auto" on the raw bounds and the deadband, so it must survive.""" controller, parts = _controller(prefs={"plot_fn_raw_max": 0.0}) settings = controller.plot_settings() assert settings["fn_raw_max"] == 0.0, "a stored 0 became the default" def test_applying_plot_settings_writes_the_preferences_and_the_panel(): controller, parts = _controller(prefs={}) controller.apply_plot_settings( { "window_sec": 20.0, "fn_min": -2.0, "fn_max": 8.0, "shear_max": 9.0, "fn_raw_min": -1000.0, "fn_raw_max": 2e5, "shear_raw_max": 3e5, "deadband_n": 0.25, "deadband_raw": 0.0, } ) assert parts["host"].prefs["plot_window_sec"] == 20.0 assert parts["host"].prefs["plot_fn_min"] == -2.0 assert parts["host"].prefs["plot_shear_raw_max"] == 3e5 assert parts["host"].prefs["plot_deadband_n"] == 0.25 assert parts["charts"].window_sec == [20.0] assert parts["charts"].limits[-1] == { "fn_min": -2.0, "fn_max": 8.0, "shear_max": 9.0, "fn_raw_min": -1000.0, "fn_raw_max": 2e5, "shear_raw_max": 3e5, } # 0 on a deadband floor means "auto", and the charts are told exactly that. assert parts["charts"].deadband_floors[-1] == {"newton": 0.25, "raw": 0.0} assert parts["host"].persisted == 1 def test_a_partial_plot_settings_mapping_leaves_the_rest_as_stored(): """The older five-value shape must not silently reset a range.""" controller, parts = _controller(prefs={"plot_fn_min": -5.0, "plot_deadband_raw": 900.0}) controller.apply_plot_settings( {"window_sec": 6.0, "fn_max": 12.0, "shear_max": 3.0, "fn_raw_max": 4e5, "shear_raw_max": 5e5} ) assert parts["host"].prefs["plot_fn_min"] == -5.0 assert parts["charts"].limits[-1]["fn_min"] == -5.0 assert parts["charts"].deadband_floors[-1]["raw"] == 900.0 def test_force_threshold_policy_is_available_for_each_sensor_and_unit(): for sensor, units in (("box", "newton"), ("box", "raw"), ("finger", "newton"), ("finger", "raw")): controller, parts = _controller(sensor_type=sensor, units=units) assert parts["charts"].presentation_enabled[-1] is True, (sensor, units) assert parts["charts"].presentation[-1]["unit"] == ( "N" if units == "newton" else "raw" ) def test_the_deadband_floors_reach_the_charts_from_the_preferences(): controller, parts = _controller(prefs={"plot_deadband_n": 0.5, "plot_deadband_raw": 2000.0}) assert parts["charts"].deadband_floors[-1] == {"newton": 0.5, "raw": 2000.0} assert parts["charts"].presentation_enabled[-1] is True def test_the_sample_rate_prefers_the_measured_rate_over_the_stored_one(): controller, parts = _controller(prefs={"measured_fps": 12.0}, latest_fps=48.5) assert controller.device_sample_rate() == 48.5 parts["host"]._latest_fps = None assert controller.device_sample_rate() == 12.0 parts["host"].prefs.pop("measured_fps") assert controller.device_sample_rate() == 30.0, "the default did not apply" def test_rotation_is_offered_only_where_the_sensor_can_be_rotated(): controller, parts = _controller(sensor_type="finger") controller.apply_arrow_rotation_for_sensor() assert parts["arrows"].rotation_enabled[-1] is False assert parts["action"].enabled is False parts["arrows"].rotations = 0 controller.on_rotate_arrows() assert parts["arrows"].rotations == 0, "a finger sensor was rotated anyway" assert "arrow_rotation_deg" not in parts["host"].prefs def test_rotating_remembers_the_angle_and_a_box_sensor_may_rotate(): controller, parts = _controller(sensor_type="box") controller.apply_arrow_rotation_for_sensor() assert parts["arrows"].rotation_enabled[-1] is True assert parts["action"].enabled is True controller.on_rotate_arrows() assert parts["host"].prefs["arrow_rotation_deg"] == 180 assert parts["host"].persisted == 1 def test_the_arrow_legend_describes_the_configured_sensor(): controller, parts = _controller(sensor_type="finger") controller.apply_arrow_legend() finger_scale = parts["arrows"].scales[-1] controller, parts = _controller(sensor_type="box") controller.apply_arrow_legend() box_scale = parts["arrows"].scales[-1] assert finger_scale != box_scale, "the two draw styles share one magnification" def test_a_new_2d_view_mode_repaints_the_panel_once(): controller, parts = _controller(prefs={}) controller.on_image_view_mode_changed("depth") assert parts["host"].prefs["image_view_mode"] == "depth" assert parts["host"].markers == 1, "the stale frame was kept" assert parts["host"].repaints == 1 assert parts["host"].persisted == 1 def test_the_fps_readout_is_a_preference_like_any_other(): controller, parts = _controller(prefs={}) assert controller.show_fps is False controller.on_show_fps_toggled(True) assert controller.show_fps is True assert parts["host"].prefs["show_fps"] is True assert parts["host"].rail_updates == 1, "the rail was not told to show it" assert parts["host"].persisted == 1 def test_the_diagnostics_switch_reaches_both_panels(): """One preference decides how much the panels explain around their pictures.""" class _Panel: def __init__(self) -> None: self.visible: list[bool] = [] def set_diagnostics_visible(self, visible: bool) -> None: self.visible.append(bool(visible)) arrows = _Panel() mesh = _Panel() controller, parts = _controller(prefs={}, arrow_panel=arrows, mesh_panel=mesh) # The constructor applies the stored preference to whatever panels exist. assert arrows.visible == [False] and mesh.visible == [False] controller.on_show_diagnostics_toggled(True) assert arrows.visible[-1] is True and mesh.visible[-1] is True assert controller.show_diagnostics is True assert parts["host"].prefs["show_diagnostics"] is True assert parts["host"].persisted == 1 controller.on_show_diagnostics_toggled(False) assert arrows.visible[-1] is False and mesh.visible[-1] is False assert parts["host"].prefs["show_diagnostics"] is False