twanghcmut's picture
download
raw
7.35 kB
"""Unit tests for scripts/fetch_robot_description.py's URDF-patching logic.
Pure XML string manipulation, deliberately: no network access happens on this
path, so these tests exercise :func:`patch_urdf_visuals` against a small
synthetic URDF rather than anything actually downloaded.
"""
from __future__ import annotations
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
SCRIPTS_DIR = Path(__file__).resolve().parents[1] / "scripts"
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
from fetch_robot_description import ( # noqa: E402
COLLISION_MESH_SCALE,
collect_referenced_meshes,
patch_urdf_visuals,
)
# Mirrors the shape of the real panda_robotiq_85.urdf closely enough to exercise
# the patch: one Robotiq link whose visual is a missing .obj (the common case),
# one Robotiq link whose visual is the pad special case (already an .stl, but at
# the wrong -- pre-collision-rescale -- scale), and one arm link that must be
# left alone entirely.
_SYNTHETIC_URDF = """<?xml version="1.0"?>
<robot name="synthetic">
<link name="robotiq_2f_85_base">
<visual>
<geometry>
<mesh filename="meshes/robotiq-2f/visual/base.obj" scale="0.1 0.1 0.1"/>
</geometry>
</visual>
<collision>
<geometry>
<mesh filename="meshes/robotiq-2f/collision/base.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="robotiq_2f_85_right_pad">
<visual>
<geometry>
<mesh filename="meshes/robotiq-2f/visual/pad.stl" scale="0.0001 0.0001 0.0001"/>
</geometry>
</visual>
<collision>
<geometry>
<mesh filename="meshes/robotiq-2f/collision/pad.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="panda_link0">
<visual>
<geometry>
<mesh filename="meshes/visual/link0.dae"/>
</geometry>
</visual>
<collision>
<geometry>
<mesh filename="meshes/collision/link0.stl"/>
</geometry>
</collision>
</link>
</robot>
"""
def _mesh_filename_and_scale(link: ET.Element, tag: str) -> tuple[str, str | None]:
mesh = link.find(f"{tag}/geometry/mesh")
assert mesh is not None, f"no <{tag}>/geometry/mesh under {link.attrib.get('name')!r}"
return mesh.attrib["filename"], mesh.attrib.get("scale")
class TestPatchUrdfVisuals:
def test_obj_visual_repointed_to_collision_stl_with_corrected_scale(self) -> None:
patched = ET.fromstring(patch_urdf_visuals(_SYNTHETIC_URDF))
base = patched.find("./link[@name='robotiq_2f_85_base']")
assert base is not None
filename, scale = _mesh_filename_and_scale(base, "visual")
assert filename == "meshes/robotiq-2f/collision/base.stl"
assert scale == COLLISION_MESH_SCALE == "0.001 0.001 0.001"
def test_collision_subtree_is_left_untouched(self) -> None:
original = ET.fromstring(_SYNTHETIC_URDF)
patched = ET.fromstring(patch_urdf_visuals(_SYNTHETIC_URDF))
base_original = original.find("./link[@name='robotiq_2f_85_base']")
base_patched = patched.find("./link[@name='robotiq_2f_85_base']")
assert base_original is not None
assert base_patched is not None
before = _mesh_filename_and_scale(base_original, "collision")
after = _mesh_filename_and_scale(base_patched, "collision")
assert before == after == ("meshes/robotiq-2f/collision/base.stl", "0.001 0.001 0.001")
def test_pad_special_case_maps_to_collision_stl_at_corrected_scale(self) -> None:
patched = ET.fromstring(patch_urdf_visuals(_SYNTHETIC_URDF))
pad = patched.find("./link[@name='robotiq_2f_85_right_pad']")
assert pad is not None
filename, scale = _mesh_filename_and_scale(pad, "visual")
assert filename == "meshes/robotiq-2f/collision/pad.stl"
# The original 0.0001 was calibrated for the missing visual mesh's own
# units, not the collision STL's -- it must be corrected, not carried over.
assert scale == "0.001 0.001 0.001"
def test_non_robotiq_visual_is_left_untouched(self) -> None:
patched = ET.fromstring(patch_urdf_visuals(_SYNTHETIC_URDF))
arm_link = patched.find("./link[@name='panda_link0']")
assert arm_link is not None
filename, scale = _mesh_filename_and_scale(arm_link, "visual")
assert filename == "meshes/visual/link0.dae"
assert scale is None
def test_patch_is_idempotent(self) -> None:
once = patch_urdf_visuals(_SYNTHETIC_URDF)
twice = patch_urdf_visuals(once)
assert once == twice
# Mirrors the shape of the real pointworld franka_panda_robotiq_2f85.urdf closely
# enough to exercise mesh collection: a <visual> mesh, a <collision> mesh referencing
# a *different* file, a mesh filename repeated across two links (must de-duplicate),
# a nested relative path (must survive unnormalised), and a <box> primitive (must be
# ignored -- it has no filename attribute and isn't a <mesh> element at all).
_SYNTHETIC_MESH_URDF = """<?xml version="1.0"?>
<robot name="synthetic">
<link name="panda_link0">
<visual>
<geometry>
<mesh filename="meshes/visual/link0.dae"/>
</geometry>
</visual>
<collision>
<geometry>
<mesh filename="meshes/collision/link0.stl"/>
</geometry>
</collision>
</link>
<link name="robotiq_85_base_link">
<visual>
<geometry>
<mesh filename="meshes/visual/robotiq_85_base_link_fine.STL"/>
</geometry>
</visual>
<collision>
<geometry>
<mesh filename="meshes/visual/robotiq_85_base_link_coarse.STL"/>
</geometry>
</collision>
</link>
<link name="left_outer_finger">
<visual>
<geometry>
<mesh filename="meshes/visual/outer_finger_fine.STL"/>
</geometry>
</visual>
</link>
<link name="right_outer_finger">
<visual>
<!-- same file as left_outer_finger, referenced from a different link -->
<mesh filename="meshes/visual/outer_finger_fine.STL"/>
</visual>
</link>
<link name="some_box_link">
<visual>
<geometry>
<box size="0.1 0.1 0.1"/>
</geometry>
</visual>
</link>
</robot>
"""
class TestCollectReferencedMeshes:
def test_returns_exactly_the_deduplicated_mesh_filenames(self) -> None:
result = collect_referenced_meshes(_SYNTHETIC_MESH_URDF)
assert result == {
"meshes/visual/link0.dae",
"meshes/collision/link0.stl",
"meshes/visual/robotiq_85_base_link_fine.STL",
"meshes/visual/robotiq_85_base_link_coarse.STL",
"meshes/visual/outer_finger_fine.STL",
}
def test_box_primitive_is_ignored(self) -> None:
result = collect_referenced_meshes(_SYNTHETIC_MESH_URDF)
assert not any("box" in path for path in result)
def test_nested_relative_paths_are_returned_unchanged(self) -> None:
result = collect_referenced_meshes(_SYNTHETIC_MESH_URDF)
# No normalisation: nested paths like "meshes/visual/link0.dae" must come
# back byte-for-byte as written in the URDF, not resolved, collapsed, or
# otherwise rewritten.
assert "meshes/visual/link0.dae" in result
assert "meshes/collision/link0.stl" in result

Xet Storage Details

Size:
7.35 kB
·
Xet hash:
09f853469173188fcf823df9d90fd8ebce91923a1be6ca5048186fee526d5dc1

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.