22658dbfc1
- 设备设置新增“单位与换算”页:显示各轴 UNITS、读回值、脉冲/mm 换算与核对状态; 默认锁定,点击“修改”后才可编辑,应用前弹窗确认,旋转轴偏离标定值时额外警告 - 轴调试页:脉冲当量改为只读并提供入口;新增报警与事件日志(报警、使能、 回零、读取失败、单位不符等)及单位核对摘要 - 新增 motion/unit_report.py、motion/events.py 及对应测试
286 lines
11 KiB
Python
286 lines
11 KiB
Python
"""The device-setup dialog: connection and bus initialisation, out of the rail.
|
|
|
|
Both groups are touched once per machine start-up. In the rail they cost ~460 px of
|
|
height above the axis readout, which is read all day — so they moved into a modeless
|
|
dialog opened from the status strip, its state chips, or the File menu.
|
|
|
|
What these tests protect:
|
|
|
|
* the rail no longer carries them, and the dialog is not open at start-up;
|
|
* every entry point works and lands on the right tab;
|
|
* the window's own handlers still receive the panels' signals, from inside the
|
|
dialog — the connect and download code paths did not move, only the widgets did;
|
|
* the dialog survives a language change and remembers its size and tab.
|
|
"""
|
|
|
|
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
|
|
|
|
|
|
def test_the_rail_no_longer_holds_the_startup_panels(make_window):
|
|
"""The point of the change: the rail is the run screen, not the setup form."""
|
|
window = make_window()
|
|
try:
|
|
rail = window.rail_content.findChildren(object)
|
|
names = {type(child).__name__ for child in rail}
|
|
assert "AxisReadoutPanel" in names
|
|
assert "ConnectionPanel" not in names
|
|
assert "BusPanel" not in names
|
|
# And they are in the dialog, once each. (The parent is Qt's internal
|
|
# stacked widget, so ask about ancestry rather than the direct parent.)
|
|
dialog = window.device_setup
|
|
assert window.connection_panel is dialog.connection_panel
|
|
assert window.bus_panel is dialog.bus_panel
|
|
assert dialog.tabs.isAncestorOf(window.connection_panel)
|
|
assert dialog.tabs.isAncestorOf(window.bus_panel)
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_the_dialog_is_hidden_until_it_is_asked_for(make_window):
|
|
window = make_window()
|
|
try:
|
|
assert not window.device_setup.isVisible()
|
|
window._show_device_setup() # noqa: SLF001
|
|
assert window.device_setup.isVisible()
|
|
# Showing it again raises rather than ignores: a second click that appears
|
|
# to do nothing reads as a broken button.
|
|
window._show_device_setup("bus") # noqa: SLF001
|
|
assert window.device_setup.isVisible()
|
|
assert window.device_setup.current_tab() == "bus"
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_the_strip_button_opens_the_setup(make_window):
|
|
window = make_window()
|
|
try:
|
|
window.status_strip.setup_button.click()
|
|
assert window.device_setup.isVisible()
|
|
assert window.status_strip.setup_button.text() == t("setup.open")
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"chip, tab",
|
|
[("chip_connection", "connection"), ("chip_bus", "bus"), ("chip_init", "bus")],
|
|
)
|
|
def test_the_state_chips_open_the_tab_they_report(make_window, chip, tab):
|
|
"""A chip that says "not connected" is the natural place to click."""
|
|
window = make_window()
|
|
try:
|
|
getattr(window.status_strip, chip).clicked.emit()
|
|
assert window.device_setup.isVisible()
|
|
assert window.device_setup.current_tab() == tab
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_the_file_menu_opens_the_setup(make_window):
|
|
window = make_window()
|
|
try:
|
|
window.act_device_setup.trigger()
|
|
assert window.device_setup.isVisible()
|
|
assert window.device_setup.current_tab() == "connection"
|
|
assert window.act_device_setup.text() == t("menu.device_setup")
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_connecting_from_the_dialog_reaches_the_window(make_window):
|
|
"""The widgets moved; the code path did not.
|
|
|
|
The connect button lives in the dialog now, so this drives the *button* rather
|
|
than emitting the signal directly: it is the signal wiring across the new
|
|
parent that could have been lost.
|
|
"""
|
|
window = make_window()
|
|
try:
|
|
seen = []
|
|
window.connection_panel.connectRequested.connect(seen.append)
|
|
|
|
panel = window.connection_panel
|
|
panel.mode_combo.setCurrentIndex(panel.mode_combo.findData("demo"))
|
|
window.device_setup.open_for("connection")
|
|
panel.open_button.click()
|
|
|
|
assert seen, "the connection request never left the dialog"
|
|
assert isinstance(seen[0], ConnectionSpec)
|
|
assert seen[0].mode == "demo"
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_downloading_the_bas_from_the_dialog_reaches_the_window(make_window):
|
|
window = make_window()
|
|
try:
|
|
# A path *must* be set first: with none, the window opens a modal file
|
|
# chooser, which offscreen is a hang rather than a test.
|
|
from orisys_autocal.ui.bus_panel import default_bas_dir
|
|
|
|
window.bus_panel.set_bas_path(str(default_bas_dir() / "ECAT.bas"))
|
|
|
|
seen = []
|
|
window.bus_panel.downloadRequested.connect(lambda: seen.append(True))
|
|
window.device_setup.open_for("bus")
|
|
window.bus_panel.download_button.click()
|
|
assert seen, "the download request never left the dialog"
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_the_dialog_survives_a_language_change(make_window):
|
|
window = make_window()
|
|
try:
|
|
window._set_language("en_US") # noqa: SLF001
|
|
assert window.device_setup.windowTitle() == t("setup.title")
|
|
assert window.device_setup.tabs.tabText(1) == t("setup.tab_bus")
|
|
assert window.status_strip.setup_button.text() == t("setup.open")
|
|
# The panels inside are retranslated too, not just the dialog's own labels.
|
|
assert window.connection_panel.open_button.text() == t("connection.open")
|
|
window._set_language("zh_CN") # noqa: SLF001
|
|
set_locale("zh_CN")
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_the_dialog_remembers_its_size_and_tab(make_window):
|
|
window = make_window()
|
|
try:
|
|
window.device_setup.select_tab("bus")
|
|
window.device_setup.resize(640, 480)
|
|
stored = window.device_setup.settings()
|
|
assert stored["tab"] == "bus"
|
|
assert stored["size"] == [640, 480]
|
|
|
|
fresh = DeviceSetupDialog(ConnectionSpec(mode="eth", ip="10.0.0.1"))
|
|
try:
|
|
fresh.apply_settings(stored)
|
|
assert fresh.size().width() == 640
|
|
assert fresh.current_tab() == "bus"
|
|
# Anything malformed is ignored, not raised on: this comes from a file
|
|
# a human may have edited.
|
|
fresh.apply_settings({"size": ["wide", 2], "tab": 7})
|
|
fresh.apply_settings(None)
|
|
finally:
|
|
fresh.deleteLater()
|
|
finally:
|
|
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() == 5
|
|
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. Yaw is
|
|
# moved onto pitch's drive, which is the duplicate this page exists to refuse.
|
|
page.combos["yaw"].setCurrentIndex(page.combos["yaw"].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["yaw"].setCurrentIndex(page.combos["yaw"].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()
|
|
try:
|
|
window.device_setup.open_for("connection")
|
|
assert not window._last_snapshot.connected # noqa: SLF001
|
|
window.device_setup.close()
|
|
assert window.connection_panel.close_button.isEnabled() is False
|
|
assert window.connection_panel.open_button.isEnabled() is True
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_the_press_page_starts_uncalibrated_and_says_so(make_window):
|
|
"""Until the zero is measured the press axis is not a coordinate anyone knows."""
|
|
window = make_window()
|
|
try:
|
|
page = window.device_setup.press_page
|
|
assert page.press()["calibrated"] is False
|
|
assert page.state_label.text() == t("setup.press_unset")
|
|
assert window.finger_panel.solved_pose # bound; the pose itself needs a bundle
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_a_press_axis_edit_reaches_the_panel_that_plans_the_move(make_window):
|
|
"""The page is a measurement, and the move has to obey it immediately.
|
|
|
|
A zero typed here but not handed to the solver's consumer would leave the
|
|
confirmation box showing a height the press axis was never told about.
|
|
"""
|
|
window = make_window()
|
|
try:
|
|
page = window.device_setup.press_page
|
|
page.zero_spin.setValue(12.5)
|
|
page.calibrated_check.setChecked(True)
|
|
assert window.prefs["press_axis"] == {
|
|
"zero_mm": 12.5,
|
|
"rising": True,
|
|
"calibrated": True,
|
|
}
|
|
assert window.finger_panel._press.calibrated is True # noqa: SLF001
|
|
assert window._press_axis.zero_mm == 12.5 # noqa: SLF001
|
|
assert page.state_label.text() == t("setup.press_set")
|
|
|
|
page.rising_check.setChecked(False)
|
|
assert window.prefs["press_axis"]["rising"] is False
|
|
assert window._press_axis.rising is False # noqa: SLF001
|
|
finally:
|
|
window.close()
|
|
|
|
|
|
def test_a_stored_press_axis_is_restored_into_the_page(make_window):
|
|
window = make_window(
|
|
press_axis={"zero_mm": -3.25, "rising": False, "calibrated": True}
|
|
)
|
|
try:
|
|
page = window.device_setup.press_page
|
|
assert page.press() == {"zero_mm": -3.25, "rising": False, "calibrated": True}
|
|
assert window.finger_panel._press.zero_mm == -3.25 # noqa: SLF001
|
|
finally:
|
|
window.close()
|
|
@pytest.mark.parametrize("settings", [
|
|
{"zero_mm": 12.5, "rising": True, "calibrated": "false"},
|
|
{"zero_mm": float("nan"), "rising": True, "calibrated": True},
|
|
{"zero_mm": 12.5, "rising": "false", "calibrated": True},
|
|
])
|
|
def test_invalid_stored_press_relation_does_not_enable_motion(make_window, settings):
|
|
window = make_window()
|
|
window.prefs["press_axis"] = settings
|
|
assert window._press_axis.target_for(19.051) is None
|