added ruff test
This commit is contained in:
@@ -76,16 +76,14 @@ def _visible_descendants(widget):
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from PySide6 import __version__ as pyside_version
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import set_locale, t
|
||||
from orisys_app_shared.theme import build_qss, load_colors
|
||||
|
||||
from orisys_autocal.motion import ConnectionSpec
|
||||
from orisys_autocal.ui.axis_panel import COLUMNS
|
||||
from orisys_autocal.ui.bus_panel import default_bas_dir
|
||||
from orisys_autocal.ui.window import AutoCalMainWindow
|
||||
from PySide6 import __version__ as pyside_version
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
checker = Checker()
|
||||
app = QApplication.instance() or QApplication([])
|
||||
@@ -115,7 +113,14 @@ def main() -> int:
|
||||
set_locale("zh_CN")
|
||||
|
||||
# ---- window ----------------------------------------------------------
|
||||
from orisys_autocal.motion import DemoTransport
|
||||
import math
|
||||
|
||||
from orisys_autocal.motion import (
|
||||
DemoTransport,
|
||||
plan_pose_move,
|
||||
plan_return_to_initial,
|
||||
user_units_to_deg,
|
||||
)
|
||||
|
||||
for theme in ("dark", "light"):
|
||||
window = AutoCalMainWindow(prefs={**prefs, "theme": theme})
|
||||
@@ -267,6 +272,50 @@ def main() -> int:
|
||||
not window.device_setup.isVisible(),
|
||||
"the device setup is open before anyone asked for it",
|
||||
)
|
||||
# Where the three move controls live, and why. Execute is about the selected
|
||||
# point, so it belongs to the finger screen; the stop is repeated next to it
|
||||
# *and* stays in the strip, because an emergency stop has to be where the
|
||||
# operator's eyes are; returning to zero is a machine action wanted after any
|
||||
# tab's work, so it lives in the always-visible rail and is not duplicated.
|
||||
checker.that(
|
||||
window.finger_panel.execute_button.parent() is not None
|
||||
and window.finger_panel.stop_button.parent() is not None,
|
||||
"the finger screen is missing an execute or stop button",
|
||||
)
|
||||
checker.that(
|
||||
window.status_strip.stop_button.isEnabled() is not None,
|
||||
"the strip has no stop button",
|
||||
)
|
||||
checker.that(
|
||||
window.axis_readout.return_button.text() == t("move.return_initial"),
|
||||
f"the rail return button says "
|
||||
f"{window.axis_readout.return_button.text()!r}",
|
||||
)
|
||||
checker.that(
|
||||
window.axis_readout.return_button.parent() is window.axis_readout,
|
||||
"the return button is not in the always-visible rail",
|
||||
)
|
||||
# The mapping page is the third tab of the device setup, and the service
|
||||
# already holds the mapping it shows.
|
||||
checker.that(
|
||||
window.device_setup.tabs.count() == 3,
|
||||
f"the device setup has {window.device_setup.tabs.count()} tabs, expected 3",
|
||||
)
|
||||
checker.that(
|
||||
window.device_setup.tabs.tabText(2) == t("setup.tab_axes"),
|
||||
f"the third setup tab is {window.device_setup.tabs.tabText(2)!r}",
|
||||
)
|
||||
# With no controller, a move must be refused with a reason and the execute
|
||||
# button must say so rather than appearing to work.
|
||||
disconnected = window.poller.service.check_axes_move({"x": 1.0})
|
||||
checker.that(
|
||||
disconnected is not None,
|
||||
"a move was allowed with no controller connected",
|
||||
)
|
||||
checker.that(
|
||||
not window.finger_panel.execute_button.isEnabled(),
|
||||
"execute is enabled with no controller connected",
|
||||
)
|
||||
checker.that(
|
||||
window.device_setup.connection_panel is window.connection_panel
|
||||
and window.device_setup.bus_panel is window.bus_panel,
|
||||
@@ -401,14 +450,14 @@ def main() -> int:
|
||||
|
||||
# A full simulated session: connect, initialise from the shipped BAS.
|
||||
window.connect_controller(ConnectionSpec(mode="demo"), label="connect")
|
||||
_wait_for(app, lambda: window._last_snapshot.connected, 6.0)
|
||||
_wait_for(app, lambda w=window: w._last_snapshot.connected, 6.0)
|
||||
checker.that(
|
||||
window._last_snapshot.connected, "the simulated controller did not connect"
|
||||
)
|
||||
|
||||
window.bus_panel.set_bas_path(str(default_bas_dir() / "ECAT.bas"))
|
||||
window._on_download_bas()
|
||||
ok = _wait_for(app, lambda: window._last_snapshot.bus.init_status == 1, 8.0)
|
||||
ok = _wait_for(app, lambda w=window: w._last_snapshot.bus.init_status == 1, 8.0)
|
||||
checker.that(ok, "the simulated bus did not report initialised")
|
||||
if ok:
|
||||
checker.that(
|
||||
@@ -453,12 +502,167 @@ def main() -> int:
|
||||
or "5" in strip.chip_axes.text(),
|
||||
f"strip axis chip says {strip.chip_axes.text()!r}",
|
||||
)
|
||||
checker.that(
|
||||
window.axis_readout._rows[0]["position"].text().startswith("0.000"),
|
||||
f"readout row 0 shows {window.axis_readout._rows[0]['position'].text()!r}",
|
||||
)
|
||||
window.workspace.set_current_key("axes")
|
||||
|
||||
# ---- the pose move ------------------------------------------------
|
||||
# The whole path end to end, on the simulated controller: solve a pose
|
||||
# from a picked point, plan it, run it, and check the drives arrived.
|
||||
# Nothing else exercises the order, the units and the guards together.
|
||||
window.workspace.set_current_key("finger")
|
||||
app.processEvents()
|
||||
finger = window.finger_panel
|
||||
checker.that(
|
||||
finger.execute_button.isEnabled(),
|
||||
"execute is disabled on a connected, initialised platform",
|
||||
)
|
||||
checker.that(
|
||||
finger.stop_button.isEnabled(),
|
||||
"the local stop is disabled while the platform may move",
|
||||
)
|
||||
|
||||
# A point to solve: pick the centre of the flat image, which is the
|
||||
# operator's own gesture, so the pose the check plans is a real one.
|
||||
if not finger.points():
|
||||
view = finger.view
|
||||
window._on_pick_point(view.width() // 2, view.height() // 2) # noqa: SLF001
|
||||
app.processEvents()
|
||||
checker.that(
|
||||
bool(finger.points()),
|
||||
"picking the middle of the patch produced no point",
|
||||
)
|
||||
|
||||
solve = finger.solved_pose()
|
||||
checker.that(
|
||||
solve is not None,
|
||||
"the finger screen solved no pose from its point list",
|
||||
)
|
||||
if solve is not None:
|
||||
targets, held = solve
|
||||
checker.that(
|
||||
"z" in held and "z" not in targets,
|
||||
f"execute does not hold the press axis back: "
|
||||
f"{sorted(targets)} held={held}",
|
||||
)
|
||||
plan = plan_pose_move(window.poller.service, targets, hold=held)
|
||||
checker.that(
|
||||
[step.role for step in plan.steps] == ["yaw", "pitch", "x", "y"],
|
||||
f"the move order is {[step.role for step in plan.steps]}",
|
||||
)
|
||||
checker.that(
|
||||
all(math.isfinite(step.units) for step in plan.steps),
|
||||
"a planned step does not convert to user units",
|
||||
)
|
||||
# Straight to the mover: the confirmation box is the operator's, and
|
||||
# a modal dialog in a headless check is a hang (it has been one).
|
||||
result = window.mover.start(plan)
|
||||
checker.that(result.ok, f"the mover refused the plan: {result.message}")
|
||||
moved = _wait_for(app, lambda w=window: not w.mover.running, 25.0)
|
||||
checker.that(moved, "the pose move did not finish")
|
||||
checker.that(
|
||||
not window.mover.state.failure,
|
||||
f"the pose move failed: {window.mover.state.failure}",
|
||||
)
|
||||
for step in plan.steps:
|
||||
state = window._last_snapshot.axis(step.axis)
|
||||
if state is None:
|
||||
continue
|
||||
if step.unit == "mm":
|
||||
actual = window.poller.service.to_mm(step.axis, state.dpos)
|
||||
else:
|
||||
actual = user_units_to_deg(step.axis, state.dpos)
|
||||
checker.that(
|
||||
abs(actual - step.target) <= 0.05,
|
||||
f"{step.role} ended at {actual:.4f}{step.unit}, "
|
||||
f"target {step.target:.4f}{step.unit}",
|
||||
)
|
||||
print(
|
||||
" move: "
|
||||
+ ", ".join(f"{s.role}={s.target:+.3f}{s.unit}" for s in plan.steps)
|
||||
+ " reached"
|
||||
)
|
||||
|
||||
# Returning to the initial state, press axis first.
|
||||
back = plan_return_to_initial(window.poller.service)
|
||||
checker.that(
|
||||
[step.role for step in back.steps]
|
||||
== ["z", "pitch", "yaw", "x", "y"],
|
||||
f"the return order is {[step.role for step in back.steps]}",
|
||||
)
|
||||
result = window.mover.start(back)
|
||||
checker.that(result.ok, f"the return was refused: {result.message}")
|
||||
returned = _wait_for(app, lambda w=window: not w.mover.running, 30.0)
|
||||
checker.that(
|
||||
returned and not window.mover.state.failure,
|
||||
f"the return did not finish: {window.mover.state.failure}",
|
||||
)
|
||||
zeroed = [
|
||||
window._last_snapshot.axis(step.axis).dpos
|
||||
for step in back.steps
|
||||
if window._last_snapshot.axis(step.axis) is not None
|
||||
]
|
||||
checker.that(
|
||||
all(abs(value) <= 1.0 for value in zeroed),
|
||||
f"the axes did not return to zero: {zeroed}",
|
||||
)
|
||||
print(" move: every axis returned to zero")
|
||||
|
||||
# Stop cancels the queue: no further axis may be commanded.
|
||||
window.mover.start(plan)
|
||||
window._on_stop_all()
|
||||
app.processEvents()
|
||||
checker.that(not window.mover.running, "stop left the move queued")
|
||||
checker.that(
|
||||
window.mover.state.cancelled,
|
||||
"stop did not mark the move cancelled",
|
||||
)
|
||||
print(" move: stop cancels the queue")
|
||||
|
||||
# The guards, on the same connected session: a mapping that claims one
|
||||
# drive twice must refuse motion rather than quietly pick a role.
|
||||
service = window.poller.service
|
||||
saved = service.axis_roles
|
||||
service.set_axis_roles({"x": 0, "y": 0, "pitch": 2, "yaw": 3, "z": 4})
|
||||
refusal = service.check_axes_move({"x": 1.0, "y": 1.0})
|
||||
checker.that(
|
||||
refusal is not None,
|
||||
f"a duplicated drive was not refused: {refusal}",
|
||||
)
|
||||
service.set_axis_roles(saved)
|
||||
refusal = service.check_axes_move({"x": 500.0})
|
||||
checker.that(
|
||||
refusal is not None and "500" in refusal.message,
|
||||
f"a target past the travel limit was not refused: {refusal}",
|
||||
)
|
||||
refusal = service.check_axes_move({"yaw": 400.0})
|
||||
checker.that(
|
||||
refusal is not None,
|
||||
f"a yaw past the stage envelope was not refused: {refusal}",
|
||||
)
|
||||
# The mapping page must agree with the service it feeds.
|
||||
page = window.device_setup.roles_page
|
||||
checker.that(
|
||||
page.roles() == saved,
|
||||
f"the mapping page shows {page.roles()}, service holds {saved}",
|
||||
)
|
||||
page.combos["y"].setCurrentIndex(page.combos["y"].findData(0))
|
||||
app.processEvents()
|
||||
checker.that(
|
||||
bool(page.problem()), "the mapping page accepted two roles on one drive"
|
||||
)
|
||||
checker.that(
|
||||
service.roles_problem() != "",
|
||||
"the service did not pick up the duplicated mapping",
|
||||
)
|
||||
page.combos["y"].setCurrentIndex(page.combos["y"].findData(1))
|
||||
app.processEvents()
|
||||
checker.that(
|
||||
page.problem() == "" and service.roles_problem() == "",
|
||||
"correcting the mapping did not clear the refusal",
|
||||
)
|
||||
print(" move: guards refuse a duplicate mapping and an out-of-range target")
|
||||
window.workspace.set_current_key("axes")
|
||||
app.processEvents()
|
||||
|
||||
try:
|
||||
window.poller.shutdown()
|
||||
except Exception: # noqa: BLE001 - a shutdown must not mask the result
|
||||
|
||||
Reference in New Issue
Block a user