Files
dora-ulite6/tests/test_config.py
T
cristhian eccd9e3c24 add xArm driver layer ported from littlehand
Brings across the robot layer from monolitic-littlehand-python, which was
written against this same controller. The point of it is that a command
either moved the arm or raised: the SDK returns a code for a motion the
controller quietly refused, wait_move calls that refusal finished, and
arm.error_code is the report stream's stale copy.

So errors are read live, arrival is confirmed rather than assumed, and a
move whose straight-line path leaves the joint envelope is refused before
it is commanded. Failures are typed, because the distinctions decide the
response: an unreachable pose is not an unavailable arm, and a joint past
its limit needs a motion nobody should start without warning the room.

read_report is the deliberate exception to all of it: a report-cache read
that costs no round trip and cannot block, which is the only read that
works while a motion is in flight.
2026-09-12 11:46:54 -03:00

111 lines
3.2 KiB
Python

"""Reading the environment, and refusing a setting the node cannot use."""
import pytest
from dora_ulite6.config import Config, env_float, env_int
from dora_ulite6.effectors import (
EFFECTORS,
GripperEffector,
NoEffector,
VacuumEffector,
get_effector,
)
from dora_ulite6.errors import EffectorUnsupportedError
@pytest.fixture(autouse=True)
def clean_env(monkeypatch):
"""Run every test against a bare environment, whatever the shell set."""
for name in ("ROBOT_IP", "TOOL_TYPE", "TOOL_OFFSET_MM", "INIT_MODE", "INIT_Z", "DEFAULT_SPEED"):
monkeypatch.delenv(name, raising=False)
def test_the_defaults_need_no_environment():
config = Config.from_env()
assert config.robot_ip == "192.168.1.192"
assert isinstance(config.effector, NoEffector)
assert config.tool_offset_mm == 0.0
assert config.init_mode == "none"
def test_the_tool_offset_is_the_tools_own_plus_the_extra(monkeypatch):
monkeypatch.setenv("TOOL_TYPE", "vacuum")
monkeypatch.setenv("TOOL_OFFSET_MM", "4.5")
config = Config.from_env()
assert isinstance(config.effector, VacuumEffector)
assert config.tool_offset_mm == pytest.approx(VacuumEffector.offset_mm + 4.5)
def test_an_unknown_tool_is_refused_at_startup(monkeypatch):
monkeypatch.setenv("TOOL_TYPE", "magnet")
with pytest.raises(ValueError, match="vacuum"):
Config.from_env()
def test_an_unknown_init_mode_is_refused_at_startup(monkeypatch):
# Caught here rather than at the first motion, so the node never joins the
# dataflow with a startup move nobody can name.
monkeypatch.setenv("INIT_MODE", "somewhere")
with pytest.raises(ValueError, match="INIT_MODE"):
Config.from_env()
def test_init_pose_components_are_read(monkeypatch):
monkeypatch.setenv("INIT_MODE", "pose")
monkeypatch.setenv("INIT_Z", "180")
assert Config.from_env().init_pose.z == 180.0
@pytest.mark.parametrize("value", ["", " "])
def test_a_blank_variable_falls_back_to_the_default(monkeypatch, value):
monkeypatch.setenv("DEFAULT_SPEED", value)
assert env_float("DEFAULT_SPEED", 30.0) == 30.0
def test_a_number_that_is_not_one_names_itself(monkeypatch):
monkeypatch.setenv("PATH_SAMPLES", "four")
with pytest.raises(ValueError, match="PATH_SAMPLES"):
env_int("PATH_SAMPLES", 4)
def test_a_bare_flange_refuses_to_grasp():
with pytest.raises(EffectorUnsupportedError):
NoEffector().attach(object())
def test_an_effector_is_refused_by_a_driver_that_cannot_switch_it():
class ArmWithNoTools:
pass
with pytest.raises(EffectorUnsupportedError, match="set_vacuum"):
VacuumEffector().verify(ArmWithNoTools())
def test_each_effector_switches_the_driver_method_it_declares():
switched = []
class Arm:
def set_gripper(self, closed):
switched.append(closed)
arm = Arm()
effector = get_effector("gripper")
effector.verify(arm)
effector.attach(arm)
effector.detach(arm)
assert isinstance(effector, GripperEffector)
assert switched == [True, False]
def test_every_registered_effector_declares_a_reach():
assert all(effector.offset_mm >= 0 for effector in EFFECTORS.values())