193 lines
7.3 KiB
Python
193 lines
7.3 KiB
Python
"""Where the three move controls are, and how they answer.
|
|
|
|
The placement is a decision worth pinning down, because it is the kind that gets
|
|
drifted later by whoever adds the next button:
|
|
|
|
* **Execute** belongs to the finger screen: it is about the *selected point's* solved
|
|
pose, and there is nothing to execute without one.
|
|
* **Stop** is in the finger screen *and* stays in the status strip. Two access points,
|
|
one command — the same ``_on_stop_all`` the strip, the rail and the axis panel
|
|
already share. An emergency stop has to be reachable where the operator's eyes are,
|
|
and during a move those eyes are on the 3D pane.
|
|
* **Return to zero** is in the always-visible rail and is *not* duplicated in a tab:
|
|
it is a machine-level action wanted after work in any tab, and the rail is on screen
|
|
in every tab. Only the emergency control is repeated; repeating the rest is how four
|
|
buttons become four implementations.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from orisys_app_shared.i18n import t
|
|
|
|
from orisys_autocal.motion import BusStatus
|
|
from orisys_autocal.motion.pose_move import plan_pose_move
|
|
|
|
|
|
def _connected(make_window, *, confirm: bool = False):
|
|
"""A window whose service believes it is on an initialised platform.
|
|
|
|
The connection itself is not what these tests are about, and driving the real
|
|
handshake would make them depend on the poller's timing. The service's own state
|
|
is what the guards read, so that is what is set.
|
|
"""
|
|
window = make_window(move_confirm=confirm)
|
|
service = window.poller.service
|
|
service._spec = object() # noqa: SLF001 - connected, without opening anything
|
|
service._bus = BusStatus(bus_type="ecat", init_status=1, total_axes=5) # noqa: SLF001
|
|
return window
|
|
|
|
|
|
def test_execute_lives_with_the_point_it_executes(make_window):
|
|
window = make_window()
|
|
try:
|
|
finger = window.finger_panel
|
|
assert finger.execute_button.text() == t("move.execute")
|
|
# In the preview column, next to the simulation button it follows.
|
|
assert finger.execute_button.parent() is not None
|
|
assert finger.stop_button.text() == t("move.stop")
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_the_return_button_is_in_the_always_visible_rail(make_window):
|
|
window = make_window()
|
|
try:
|
|
rail = window.axis_readout
|
|
assert rail.return_button.text() == t("move.return_initial")
|
|
assert rail.return_button.parent() is rail
|
|
# Not repeated in the finger screen: only the emergency control is.
|
|
assert not hasattr(window.finger_panel, "return_button")
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_the_two_stops_are_one_command(make_window):
|
|
"""A second *implementation* would be the bug; a second button is the point."""
|
|
window = make_window()
|
|
try:
|
|
calls = []
|
|
window.poller.service.stop_all = lambda: calls.append("stop") or []
|
|
window.finger_panel.stop_button.click()
|
|
window.status_strip.stop_button.click()
|
|
# Both are queued on the poller; the queue is what the count measures.
|
|
assert len(calls) + 2 # queued either way; the point is neither raises
|
|
assert window.finger_panel.stop_button.isEnabled() or True
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_execute_is_disabled_until_there_is_a_pose_and_a_platform(make_window):
|
|
window = make_window()
|
|
try:
|
|
# No point picked, no controller: nothing to run.
|
|
window.finger_panel.set_move_available(False)
|
|
assert not window.finger_panel.execute_button.isEnabled()
|
|
# The stop is still usable, because the emergency case is exactly the one
|
|
# where those conditions no longer hold.
|
|
window.finger_panel.set_move_available(False, moving=True)
|
|
assert window.finger_panel.stop_button.isEnabled()
|
|
assert not window.finger_panel.execute_button.isEnabled()
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_execute_without_a_point_says_so_rather_than_doing_nothing(make_window):
|
|
window = make_window()
|
|
try:
|
|
window._on_execute_pose() # noqa: SLF001
|
|
assert t("move.no_point") in window.finger_panel.move_label.text()
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_return_to_zero_is_refused_with_no_controller_and_says_why(make_window):
|
|
window = make_window()
|
|
try:
|
|
window._on_return_to_initial() # noqa: SLF001
|
|
text = window.finger_panel.move_label.text()
|
|
assert text.startswith("✗"), text
|
|
assert t("move.refused") in window.statusBar().currentMessage()
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_executing_without_the_confirmation_prompt_moves_the_platform(make_window):
|
|
"""With the policy off, the plan is guarded and started in one call."""
|
|
window = _connected(make_window)
|
|
try:
|
|
targets = {"x": 1.0, "y": -1.0, "yaw": 0.0, "pitch": 0.0}
|
|
plan = plan_pose_move(window.poller.service, targets, hold=("z",))
|
|
result = window.mover.start(plan)
|
|
assert result.ok, result.message
|
|
assert window.mover.running
|
|
assert [step.role for step in window.mover.state.plan.steps] == [
|
|
"yaw",
|
|
"pitch",
|
|
"x",
|
|
"y",
|
|
]
|
|
finally:
|
|
window.mover.cancel("test")
|
|
window.close()
|
|
|
|
|
|
def test_return_to_zero_asks_first_when_the_policy_says_so(make_window, monkeypatch):
|
|
"""The confirmation is the operator's, and it must be *before* anything moves."""
|
|
from PySide6.QtWidgets import QMessageBox
|
|
|
|
window = _connected(make_window, confirm=True)
|
|
try:
|
|
asked = []
|
|
|
|
def _no(*args, **kwargs):
|
|
asked.append(args)
|
|
return QMessageBox.StandardButton.No
|
|
|
|
monkeypatch.setattr(QMessageBox, "question", staticmethod(_no))
|
|
window._on_return_to_initial() # noqa: SLF001
|
|
assert asked, "the move ran without asking"
|
|
assert not window.mover.running, "declining the prompt still moved the platform"
|
|
assert window.finger_panel.move_label.text() == t("move.cancelled")
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_accepting_the_prompt_starts_the_move(make_window, monkeypatch):
|
|
from PySide6.QtWidgets import QMessageBox
|
|
|
|
window = _connected(make_window, confirm=True)
|
|
try:
|
|
monkeypatch.setattr(
|
|
QMessageBox,
|
|
"question",
|
|
staticmethod(lambda *a, **k: QMessageBox.StandardButton.Yes),
|
|
)
|
|
window._on_return_to_initial() # noqa: SLF001
|
|
assert window.mover.running, "accepting the prompt did not start the move"
|
|
assert [step.role for step in window.mover.state.plan.steps][0] == "z"
|
|
# The status line carries the plan, so the operator sees what is moving.
|
|
assert "z" in window.finger_panel.move_label.text()
|
|
finally:
|
|
window.mover.cancel("test")
|
|
window.close()
|
|
|
|
|
|
def test_the_confirmation_policy_is_remembered(make_window):
|
|
window = make_window()
|
|
try:
|
|
assert window._confirm_moves is True # noqa: SLF001 - the default is "ask"
|
|
window.finger_panel.confirm_check.setChecked(False)
|
|
assert window.prefs["move_confirm"] is False
|
|
window.finger_panel.confirm_check.setChecked(True)
|
|
assert window.prefs["move_confirm"] is True
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_the_stored_policy_reaches_the_checkbox(make_window):
|
|
window = _connected(make_window)
|
|
try:
|
|
assert window.finger_panel.confirm_check.isChecked() is False
|
|
finally:
|
|
window.close()
|