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()