added ruff test

This commit is contained in:
ykcheungab
2026-09-27 19:20:43 +08:00
parent bcab2790bf
commit fa2703968e
178 changed files with 3277 additions and 1034 deletions
-2
View File
@@ -95,7 +95,6 @@ def _isolate_preference_file(monkeypatch):
from orisys_app_shared import paths as paths_mod
from orisys_app_shared import prefs as prefs_pkg
from orisys_autocal import preferences as prefs_mod
scratch = _scratch_dir()
@@ -120,7 +119,6 @@ def make_window(qt_app, monkeypatch):
from orisys_app_shared import paths as paths_mod
from orisys_app_shared import prefs as prefs_pkg
from orisys_autocal import preferences as prefs_mod
from orisys_autocal.motion import DemoTransport
from orisys_autocal.ui.window import AutoCalMainWindow
@@ -21,7 +21,6 @@ import time
from pathlib import Path
import pytest
from orisys_autocal.motion import (
DEFAULT_BAS_TASK,
INIT_OK,
+38 -2
View File
@@ -16,9 +16,7 @@ What these tests protect:
from __future__ import annotations
import pytest
from orisys_app_shared.i18n import set_locale, t
from orisys_autocal.motion import ConnectionSpec
from orisys_autocal.ui.device_setup import DeviceSetupDialog
@@ -175,6 +173,44 @@ def test_the_dialog_remembers_its_size_and_tab(make_window):
window.close()
def test_the_axis_mapping_page_holds_the_mapping_and_refuses_duplicates(make_window):
"""The page and the service must never disagree about which drive moves what."""
from orisys_autocal.motion import DEFAULT_AXIS_ROLES
window = make_window()
try:
dialog = window.device_setup
assert dialog.tabs.count() == 3
page = dialog.roles_page
assert page.roles() == DEFAULT_AXIS_ROLES
assert page.problem() == ""
assert window.poller.service.axis_roles == DEFAULT_AXIS_ROLES
# Two roles on one drive: reported on the page *and* by the guard.
page.combos["pitch"].setCurrentIndex(page.combos["pitch"].findData(3))
assert page.problem() != ""
assert window.poller.service.roles_problem() != ""
assert window.poller.service.check_axes_move({"yaw": 1.0}) is not None
# Correcting it clears both.
page.combos["pitch"].setCurrentIndex(page.combos["pitch"].findData(2))
assert page.problem() == ""
assert window.poller.service.roles_problem() == ""
finally:
window.close()
def test_the_mapping_page_opens_on_the_stored_mapping(make_window):
window = make_window(
axis_roles={"x": 1, "y": 0, "pitch": 2, "yaw": 3, "z": 4}
)
try:
assert window.device_setup.roles_page.roles()["x"] == 1
assert window.poller.service.axis_roles["x"] == 1
finally:
window.close()
def test_opening_the_setup_does_not_touch_the_connection(make_window):
"""Opening a dialog is not a command: it must not connect or disconnect."""
window = make_window()
-1
View File
@@ -8,7 +8,6 @@ never invents a meaning for a code it cannot read out of that header.
from __future__ import annotations
import pytest
from orisys_autocal.motion import errors
DOCUMENTED = sorted(errors.MESSAGES)
+1 -6
View File
@@ -11,17 +11,15 @@ from pathlib import Path
import numpy as np
import pytest
from PySide6.QtCore import QPointF
from orisys_autocal.motion.kinematics import PlatformStack
from orisys_autocal.sensor import (
DEFAULT_FLAT_SIZE,
load_bundle,
rasterise,
to_platform,
to_sensor,
)
from orisys_autocal.ui.sensor_panel import FingerPanel
from PySide6.QtCore import QPointF
BUNDLE = Path(
r"D:\orisys_ws\orisys-sensor-design\build\export\finger\r01\compute"
@@ -80,7 +78,6 @@ def test_the_window_loads_the_remembered_bundle_at_startup(qt_app, monkeypatch):
from orisys_app_shared import paths as paths_mod
from orisys_app_shared import prefs as prefs_pkg
from orisys_app_shared.i18n import set_locale
from orisys_autocal import preferences as prefs_mod
from orisys_autocal.ui import window as window_mod
from orisys_autocal.ui.window import AutoCalMainWindow
@@ -152,7 +149,6 @@ def test_the_initial_view_is_the_commissioned_orientation(qt_app, bundle):
right, because getting it wrong draws the tool axis pointing down.
"""
from orisys_autocal.sensor import to_sensor
from orisys_autocal.ui.pose_preview import PosePreview
preview = PosePreview()
@@ -230,7 +226,6 @@ def test_the_motion_axes_can_be_turned_off(qt_app, bundle):
def test_the_axis_labels_project_onto_their_arrows(qt_app, bundle):
"""The 2D label and the 3D arrow must agree, or the label names nothing."""
from orisys_autocal.sensor import to_sensor
from orisys_autocal.ui.pose_preview import PosePreview
preview = PosePreview()
+1 -2
View File
@@ -12,10 +12,9 @@ from pathlib import Path
import numpy as np
import pytest
from orisys_autocal.motion.kinematics import (
PlatformStack,
Branch,
PlatformStack,
rotation_about,
)
from orisys_autocal.sensor import load_bundle
-2
View File
@@ -17,7 +17,6 @@ import dataclasses
import numpy as np
import pytest
from orisys_autocal.motion.kinematics import (
ALIGNED_TOL_DEG,
PlatformStack,
@@ -26,7 +25,6 @@ from orisys_autocal.motion.kinematics import (
rotation_about,
)
from orisys_autocal.sensor.frames import (
AXIS_MAP,
describe,
mount_rotation,
to_platform,
+35 -2
View File
@@ -11,7 +11,6 @@ import json
from pathlib import Path
import pytest
from orisys_autocal.preferences import load_preferences
LOCALES = Path(__file__).resolve().parents[1] / "config" / "locales"
@@ -44,7 +43,7 @@ def test_locales_have_identical_key_sets():
other = _flatten(_load(language))
assert other == reference, {
f"{language}_only": sorted(other - reference),
f"zh_CN_only": sorted(reference - other),
"zh_CN_only": sorted(reference - other),
}
@@ -89,6 +88,40 @@ def test_the_shipped_defaults_carry_the_controller_task_number():
assert shipped["bas_task"] == DEFAULT_BAS_TASK == 4
def test_a_stored_block_without_a_new_limit_still_gets_it():
"""The upgrade path for a real preference file, which is merged one level deep.
A stored ``safety_limits`` replaces the whole default block, so a limit added
later would read as "not set" — and motion is refused for a limit the operator
cannot see anywhere in the file. Filling the block key by key is what stops that.
"""
from orisys_app_shared.paths import preference_path
from orisys_autocal.preferences import load_preferences
path = preference_path()
path.parent.mkdir(parents=True, exist_ok=True)
# A file written before the stage limits existed, with a chosen travel limit.
path.write_text(
json.dumps(
{
"safety_limits": {"max_travel_mm": 42.0},
"sensor_setup": {"press_axis": 4},
"axis_roles": {"x": 0, "y": 1, "pitch": 2, "yaw": 3},
}
),
encoding="utf-8",
)
prefs = load_preferences()
limits = prefs["safety_limits"]
assert limits["max_travel_mm"] == 42.0, "a chosen limit was overwritten"
assert limits["max_yaw_deg"] > 0 and limits["max_pitch_deg"] > 0
assert limits["contact_search_mm"] > 0
# The old duplicate press axis moves into the mapping, and the key goes.
assert prefs["axis_roles"]["z"] == 4
assert "press_axis" not in prefs["sensor_setup"]
def _walk(node, prefix: str = ""):
for key, value in node.items():
path = f"{prefix}{key}"
+192
View File
@@ -0,0 +1,192 @@
"""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()
+6 -2
View File
@@ -11,8 +11,12 @@ import threading
import time
import pytest
from orisys_autocal.motion import ConnectionSpec, DemoTransport, MotionPoller, MotionService
from orisys_autocal.motion import (
ConnectionSpec,
DemoTransport,
MotionPoller,
MotionService,
)
class RecordingTransport(DemoTransport):
+337
View File
@@ -0,0 +1,337 @@
"""Moving the platform to a simulated pose, and stopping it.
Three things are being protected here, in the order they matter:
1. **The guards refuse before anything moves.** The service is the only place that
decides whether a pose move may run, so every refusal is tested there — and a
refusal must arrive *before* the first axis is commanded, because a sequence that
stops halfway leaves the machine in a pose nobody simulated.
2. **The order is the model's order.** The kinematics composes ``R_pitch · R_yaw``
(yaw acts on the body first), so the machine yaws before it pitches; table X/Y
last. A different order would sweep the sensor through poses the operator never
saw in the preview.
3. **Stop is a real stop.** The mover is driven by snapshots, not by a loop, so a
stop runs at the next tick and no further axis is commanded. A mover that sat in
a loop could not be interrupted at all.
"""
from __future__ import annotations
import time
import pytest
from orisys_autocal.motion import (
MotionResult,
MotionService,
Snapshot,
plan_pose_move,
plan_return_to_initial,
)
from orisys_autocal.motion.models import AxisState, AxisStatusBits, BusStatus
from orisys_autocal.motion.pose_move import (
MOVE_ORDER,
RETURN_ORDER,
PoseMover,
SETTLE_DEG,
SETTLE_MM,
)
from orisys_autocal.motion.transport import DemoTransport
from orisys_autocal.preferences import default_preferences
ROLES = {"x": 0, "y": 1, "pitch": 2, "yaw": 3, "z": 4}
LIMITS = default_preferences()["safety_limits"]
def make_service(**kwargs) -> MotionService:
"""A connected, initialised platform — the state a move is allowed in."""
service = MotionService(
DemoTransport(),
axis_roles=kwargs.pop("roles", ROLES),
limits=kwargs.pop("limits", LIMITS),
**kwargs,
)
service._spec = object() # noqa: SLF001 - connected, without opening anything
service._bus = BusStatus(bus_type="ecat", init_status=1, total_axes=5) # noqa: SLF001
return service
class FakePoller:
"""Records submissions and runs them, so a plan's commands can be inspected."""
def __init__(self, service: MotionService) -> None:
self.service = service
self.commands: list = []
def submit(self, action):
self.commands.append(action)
# Not executed here: the mover is driven by snapshots, and a real poller runs
# one command per tick. Running them synchronously would hide the ordering
# this test exists to check.
return len(self.commands)
def reading(axis: int, dpos: float, *, idle: bool = True, alarm: bool = False) -> AxisState:
return AxisState(
axis=axis,
dpos=dpos,
idle=idle,
status=AxisStatusBits(raw=512 if alarm else 0),
)
def snapshot_of(service: MotionService, positions: dict[int, float], **kwargs) -> Snapshot:
return Snapshot(
connected=True,
bus=service.bus,
axes=tuple(
reading(axis, value, **kwargs) for axis, value in positions.items()
),
)
# --------------------------------------------------------------------- planning
def test_the_plan_uses_the_roles_and_each_role_own_unit():
service = make_service()
plan = plan_pose_move(
service, {"x": 1.5, "y": -2.0, "yaw": 10.0, "pitch": -20.0}, hold=("z",)
)
by_role = {step.role: step for step in plan.steps}
assert by_role["x"].axis == 0
assert by_role["yaw"].axis == 3
assert by_role["pitch"].axis == 2
# 500 units per mm on the bench, and the measured rotary constants.
assert by_role["x"].units == pytest.approx(750.0)
assert by_role["yaw"].units == pytest.approx(10.0 * 29500.0 / 90.0)
assert by_role["pitch"].units == pytest.approx(-20.0 * 12500.0 / 90.0)
assert by_role["x"].unit == "mm"
assert by_role["yaw"].unit == "°"
def test_the_move_order_yaws_before_it_pitches_and_slides_last():
"""The model composes R_pitch · R_yaw, so the machine does the same."""
service = make_service()
plan = plan_pose_move(service, {"x": 1.0, "y": 1.0, "yaw": 1.0, "pitch": 1.0})
assert [step.role for step in plan.steps] == ["yaw", "pitch", "x", "y"]
assert MOVE_ORDER.index("yaw") < MOVE_ORDER.index("pitch")
assert MOVE_ORDER.index("pitch") < MOVE_ORDER.index("x")
def test_the_plan_holds_back_what_it_is_not_given():
"""z is not commanded by a pose move — expressed by absence, not by a no-op."""
service = make_service()
plan = plan_pose_move(service, {"x": 1.0, "y": 1.0}, hold=("z",))
assert "z" not in {step.role for step in plan.steps}
assert plan.held == ("z",)
assert "z" in plan.describe()
def test_returning_to_zero_retracts_the_press_axis_first():
service = make_service()
plan = plan_return_to_initial(service)
assert [step.role for step in plan.steps] == list(RETURN_ORDER)
assert plan.steps[0].role == "z"
assert all(step.target == 0.0 for step in plan.steps)
# ----------------------------------------------------------------------- guards
def test_a_move_is_refused_with_no_controller():
# English, so the assertion reads as the sentence the operator would see. The
# service localises its refusals; the wording is not the point, the refusal is.
service = MotionService(
DemoTransport(), axis_roles=ROLES, limits=LIMITS, language="en_US"
)
refusal = service.check_axes_move({"x": 1.0})
assert refusal is not None and "Not connected" in refusal.message
def test_a_move_is_refused_before_the_bus_is_initialised():
service = make_service(language="en_US")
service._bus = BusStatus(bus_type="unknown", init_status=0) # noqa: SLF001
refusal = service.check_axes_move({"x": 1.0})
assert refusal is not None and "initialis" in refusal.message
def test_a_target_past_the_travel_limit_is_refused_with_the_numbers():
service = make_service()
refusal = service.check_axes_move({"x": LIMITS["max_travel_mm"] + 0.5})
assert refusal is not None
assert f"{LIMITS['max_travel_mm'] + 0.5:.3f}" in refusal.message
def test_an_angle_is_checked_against_its_own_limit_not_the_millimetre_one():
"""A 400° yaw is past the stage envelope however roomy the table is."""
service = make_service()
assert service.check_axes_move({"yaw": 400.0}) is not None
assert service.check_axes_move({"yaw": 170.0}) is None
# And a millimetre target is not checked against the degree limit.
assert service.check_axes_move({"x": 200.0}) is not None
def test_a_limit_that_was_never_set_refuses_rather_than_meaning_unlimited():
service = make_service(limits={})
refusal = service.check_axes_move({"x": 1.0})
assert refusal is not None and "P0" in refusal.message
def test_a_duplicated_drive_is_refused():
"""Two roles on one axis means one of them silently does not move."""
service = make_service(roles={"x": 0, "y": 0, "pitch": 2, "yaw": 3, "z": 4})
problem = service.roles_problem()
assert problem
refusal = service.check_axes_move({"x": 1.0, "y": 1.0})
assert refusal is not None and refusal.message == problem
def test_an_incomplete_mapping_is_refused():
service = make_service(roles={"x": 0})
refusal = service.check_axes_move({"x": 1.0})
assert refusal is not None and "yaw" in refusal.message
def test_a_rotary_role_needs_a_calibrated_axis():
"""Axis 0 has no user-units-per-degree constant, so it cannot be a yaw."""
service = make_service(roles={"x": 4, "y": 1, "pitch": 2, "yaw": 0, "z": 3})
refusal = service.check_axes_move({"yaw": 1.0})
assert refusal is not None and "yaw" in refusal.message
def test_a_non_finite_target_is_refused():
service = make_service()
for value in (float("nan"), float("inf")):
refusal = service.check_axes_move({"x": value})
assert refusal is not None
def test_the_guard_names_an_unknown_direction():
service = make_service()
refusal = service.check_axes_move({"roll": 1.0})
assert refusal is not None and "roll" in refusal.message
# ------------------------------------------------------------------------ mover
def test_the_mover_commands_one_axis_and_waits_for_it_to_arrive():
service = make_service()
poller = FakePoller(service)
mover = PoseMover(service, poller.submit)
plan = plan_pose_move(service, {"x": 2.0, "y": 1.0})
assert mover.start(plan).ok
assert len(poller.commands) == 1, "more than the first axis was commanded at once"
# x is at its target and idle -> the second axis goes out.
mover.on_snapshot(snapshot_of(service, {0: 1000.0, 1: 0.0}))
assert len(poller.commands) == 2
assert mover.state.step.role == "y"
# y arrives -> the plan is done.
mover.on_snapshot(snapshot_of(service, {0: 1000.0, 1: 500.0}))
assert not mover.running
assert mover.state.finished
def test_the_mover_waits_while_an_axis_is_still_moving():
service = make_service()
poller = FakePoller(service)
mover = PoseMover(service, poller.submit)
mover.start(plan_pose_move(service, {"x": 2.0, "y": 1.0}))
mover.on_snapshot(snapshot_of(service, {0: 990.0, 1: 0.0}, idle=False))
assert len(poller.commands) == 1
assert mover.state.index == 0
def test_the_mover_fails_when_an_axis_stops_short():
"""A drive that has stopped is not going to arrive: say so, do not wait."""
service = make_service()
poller = FakePoller(service)
mover = PoseMover(service, poller.submit)
mover.start(plan_pose_move(service, {"x": 2.0, "y": 1.0}))
mover.on_snapshot(snapshot_of(service, {0: 10.0, 1: 0.0}))
assert mover.running, "gave up before the stall grace elapsed"
mover.state._idle_since = time.monotonic() - 10.0 # noqa: SLF001
mover.on_snapshot(snapshot_of(service, {0: 10.0, 1: 0.0}))
assert not mover.running
assert "stopped short" in mover.state.failure
assert len(poller.commands) == 1, "a further axis was commanded after the failure"
def test_the_mover_stops_on_an_alarm():
service = make_service()
poller = FakePoller(service)
mover = PoseMover(service, poller.submit)
mover.start(plan_pose_move(service, {"x": 2.0}))
mover.on_snapshot(snapshot_of(service, {0: 0.0}, alarm=True))
assert not mover.running
assert "alarm" in mover.state.failure
def test_a_tolerance_accepts_an_axis_that_reports_its_final_position_late():
service = make_service()
poller = FakePoller(service)
mover = PoseMover(service, poller.submit)
plan = plan_pose_move(service, {"x": 2.0, "y": 1.0})
mover.start(plan)
# 1 unit inside SETTLE_MM of 1000 units for 2 mm.
mover.on_snapshot(snapshot_of(service, {0: 1000.0 - 1.0, 1: 0.0}))
assert len(poller.commands) == 2, "a step within tolerance was treated as a miss"
assert SETTLE_MM > 0 and SETTLE_DEG > 0
def test_stopping_cancels_the_rest_of_the_plan():
"""The stop is a separate command, so the mover must stop queueing first."""
service = make_service()
poller = FakePoller(service)
mover = PoseMover(service, poller.submit)
mover.start(plan_pose_move(service, {"x": 2.0, "y": 1.0}))
assert mover.running
mover.cancel("stopped")
assert not mover.running
assert mover.state.cancelled
# Nothing further, even if the axis happens to arrive afterwards.
mover.on_snapshot(snapshot_of(service, {0: 1000.0, 1: 500.0}))
assert len(poller.commands) == 1
def test_a_second_plan_cannot_start_on_top_of_a_running_one():
service = make_service()
poller = FakePoller(service)
mover = PoseMover(service, poller.submit)
mover.start(plan_pose_move(service, {"x": 2.0, "y": 1.0}))
second = mover.start(plan_pose_move(service, {"x": 3.0}))
assert not second.ok
assert len(poller.commands) == 1
def test_the_mover_refuses_a_plan_it_would_not_be_allowed_to_run():
service = MotionService(DemoTransport(), axis_roles=ROLES, limits=LIMITS)
mover = PoseMover(service, FakePoller(service).submit)
result = mover.start(plan_pose_move(service, {"x": 1.0}))
assert not result.ok
assert not mover.running
def test_the_progress_line_names_the_step_it_is_on():
service = make_service()
mover = PoseMover(service, FakePoller(service).submit)
mover.start(plan_pose_move(service, {"x": 2.0, "y": 1.0}))
text = mover.progress_text()
assert "1/2" in text and "x" in text
def test_the_units_round_trip_through_the_service():
"""A target planned in mm must arrive at the drive as the same mm."""
service = make_service()
for role, value in (("x", 3.5), ("y", -2.25), ("yaw", 45.0), ("pitch", -30.0)):
units = service.to_units(role, value)
axis = service.role_axis(role)
if role in ("x", "y"):
assert service.to_mm(axis, units) == pytest.approx(value)
else:
from orisys_autocal.motion import user_units_to_deg
assert user_units_to_deg(axis, units) == pytest.approx(value)
-1
View File
@@ -15,7 +15,6 @@ from __future__ import annotations
import numpy as np
import pytest
from orisys_autocal.sensor.export_bundle import (
BundleError,
load_bundle,
-1
View File
@@ -12,7 +12,6 @@ import shutil
from pathlib import Path
import pytest
from orisys_autocal.sequence_io import (
DOCUMENT_KIND,
FORMAT_VERSION,
-1
View File
@@ -15,7 +15,6 @@ from __future__ import annotations
import math
import pytest
from orisys_autocal.motion.units import (
PULSES_PER_MM,
ROTARY_UNITS_PER_90_DEG,
-1
View File
@@ -12,7 +12,6 @@ from pathlib import Path
import numpy as np
import pytest
from orisys_autocal.sensor import (
DEFAULT_FLAT_SIZE,
load_bundle,