import tempfile
import unittest
from pathlib import Path
from sdf.validation import validate_sdf_xml
class SdfValidationTests(unittest.TestCase):
def setUp(self) -> None:
self._tempdir = tempfile.TemporaryDirectory(prefix="tmp-sdf-validation-")
self.temp_root = Path(self._tempdir.name)
def tearDown(self) -> None:
self._tempdir.cleanup()
def _validate(self, body: str):
return validate_sdf_xml(
body.strip(),
source_path=self.temp_root / "generated.sdf",
base_dir=self.temp_root,
)
def _error_codes(self, body: str) -> set[str]:
return {finding.code for finding in self._validate(body).errors}
def _warning_codes(self, body: str) -> set[str]:
return {finding.code for finding in self._validate(body).warnings}
def test_valid_minimal_model_passes(self) -> None:
result = self._validate(
"""
"""
)
self.assertEqual([], result.errors)
def test_valid_world_only_scene_passes(self) -> None:
result = self._validate(
"""
model://sun
"""
)
self.assertEqual([], result.errors)
def test_malformed_pose_length_fails(self) -> None:
self.assertIn(
"invalid_numeric_vector",
self._error_codes(
"""
1 2 3
"""
),
)
def test_quaternion_pose_zero_norm_fails_and_non_unit_warns(self) -> None:
self.assertIn(
"zero_quaternion",
self._error_codes(
"""
0 0 0 0 0 0 0
"""
),
)
self.assertIn(
"non_unit_quaternion",
self._warning_codes(
"""
0 0 0 0 0 0 2
"""
),
)
def test_nontrivial_pose_without_relative_to_warns(self) -> None:
self.assertIn(
"pose_missing_relative_to",
self._warning_codes(
"""
1 0 0 0 0 0
"""
),
)
def test_frame_cycle_fails(self) -> None:
self.assertIn(
"frame_cycle",
self._error_codes(
"""
"""
),
)
def test_joint_type_and_world_child_are_checked(self) -> None:
illegal_type_errors = self._error_codes(
"""
base_link
arm_link
"""
)
self.assertIn("unknown_joint_type", illegal_type_errors)
world_child_errors = self._error_codes(
"""
base_link
world
"""
)
self.assertIn("invalid_joint_child", world_child_errors)
def test_axis_zero_fails_and_non_unit_warns(self) -> None:
zero_errors = self._error_codes(
"""
base_link
arm_link
0 0 0
"""
)
self.assertIn("zero_axis", zero_errors)
warnings = self._warning_codes(
"""
base_link
arm_link
0 0 2
"""
)
self.assertIn("non_unit_axis", warnings)
def test_invalid_primitive_dimensions_fail(self) -> None:
errors = self._error_codes(
"""
1 0 1
"""
)
self.assertIn("invalid_dimension", errors)
def test_visual_without_geometry_fails(self) -> None:
self.assertIn(
"invalid_geometry_count",
self._error_codes(
"""
"""
),
)
def test_mesh_uri_resolution_distinguishes_local_and_external(self) -> None:
local_errors = self._error_codes(
"""
meshes/missing.stl
"""
)
self.assertIn("missing_mesh_file", local_errors)
external_result = self._validate(
"""
package://sample/meshes/base.dae
"""
)
self.assertEqual([], external_result.errors)
def test_invalid_inertial_values_fail(self) -> None:
mass_errors = self._error_codes(
"""
0
"""
)
self.assertIn("invalid_mass", mass_errors)
inertia_errors = self._error_codes(
"""
1
11-1
000
"""
)
self.assertIn("invalid_inertia_matrix", inertia_errors)
def test_sensor_plugin_and_include_required_fields(self) -> None:
errors = self._error_codes(
"""
"""
)
self.assertIn("missing_sensor_type", errors)
self.assertIn("missing_plugin_filename", errors)
self.assertIn("missing_child_text", errors)
def test_plugin_contents_are_static_metadata_not_motion_contracts(self) -> None:
result = self._validate(
"""
base_link
door_link
opaque
"""
)
self.assertEqual([], result.errors)
if __name__ == "__main__":
unittest.main()