Files
orisys-app/apps/studio/tests/test_display_prefs.py
T
ykcheungab 92e6de6913 收紧绘图设置为两页,并把 FN 牛顿下限固定为 0。
手指 2D 方向改到 ArrowPanel 设置对话框;归零阈值不变。已用 display_prefs、finger_2d_orientation 与绘图对话框相关 smoke 测试验证。
2026-10-07 20:48:48 +08:00

397 lines
13 KiB
Python

"""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"] == 0.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": 0.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}
)
# Newton FN floor is always rewritten to 0; other stored values stay.
assert parts["host"].prefs["plot_fn_min"] == 0.0
assert parts["charts"].limits[-1]["fn_min"] == 0.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