From b89132acfc1dc67f86c33ff083ac1adf864a5835 Mon Sep 17 00:00:00 2001 From: Jeremy Schoemaker Date: Fri, 18 Sep 2026 08:56:44 -0500 Subject: [PATCH] mjcf: add schema regression tests for the declarations lost in regeneration d2cf3a3d restored all three declarations this PR was opened to fix. The sensor contact element is back with the same 18 attributes, jointinparent carries reference_namespace="joint" on all nine actuators again, and custom/numeric data is an array of float again. The schema change here is therefore dropped, and what remains is the regression coverage, which nothing upstream added. The three declarations were lost once already and were restored by a regeneration rather than by a guard, so the same regeneration can drop them again. Each test asserts the behavior a user loses, not the text of the file: - a contact sensor parses and compiles, and its geom1 reference resolves - jointinparent is scoped to the child model on attach - custom/numeric accepts array data - no attribute of type="reference" omits its namespace The last one is the file-level invariant. schema.py falls back to the attribute's own name when reference_namespace is absent, so a dropped namespace is invisible wherever the two coincide and silently wrong where they do not. Asserting on the file catches the loss in both cases. Verified against the mutation each test is meant to catch. Reverting the contact element, jointinparent and numeric data on top of current main fails the first three. Stripping one reference_namespace fails the fourth with ['site'] has length of 1. dm_control/mjcf: 158 passed. --- dm_control/mjcf/schema_test.py | 59 ++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/dm_control/mjcf/schema_test.py b/dm_control/mjcf/schema_test.py index 1eda0e8c..1c1332d8 100644 --- a/dm_control/mjcf/schema_test.py +++ b/dm_control/mjcf/schema_test.py @@ -187,5 +187,64 @@ def constructible(spec_node, path): + '\n'.join(failures[:20])) +class SchemaRegressionTest(absltest.TestCase): + """Declarations that MuJoCo accepts must stay representable in PyMJCF.""" + + def test_contact_sensor_parses_and_compiles(self): + xml_string = """ + + + + + + + + + + + """ + root = mjcf.from_xml_string(xml_string) + physics = mjcf.Physics.from_mjcf_model(root) + self.assertEqual(physics.model.nsensor, 1) + self.assertEqual(root.find('sensor', 'cs').geom1.name, 'g') + + def test_jointinparent_is_scoped_on_attach(self): + child = mjcf.RootElement(model='child') + body = child.worldbody.add('body', name='b') + body.add('joint', name='j', type='hinge') + body.add('geom', name='g', size=[0.1]) + child.actuator.add('general', name='a', jointinparent='j') + parent = mjcf.RootElement(model='parent') + parent.attach(child) + self.assertIn('jointinparent="child/j"', parent.to_xml_string()) + physics = mjcf.Physics.from_mjcf_model(parent) + self.assertEqual(physics.model.nu, 1) + + def test_every_reference_declares_its_namespace(self): + """Guards the invariant a regeneration is most likely to drop. + + `schema.py` falls back to the attribute's own name when + `reference_namespace` is absent, so a missing one is invisible whenever the + two happen to coincide and silently wrong when they do not. Asserting on the + file keeps every reference explicit, which is what MuJoCo's own generated + schema does. + """ + tree = ET.parse(_SCHEMA_PATH) + bare = sorted( + attribute.get('name') + for attribute in tree.iter('attribute') + if attribute.get('type') == 'reference' + and attribute.get('reference_namespace') is None + ) + self.assertEmpty(bare) + + def test_custom_numeric_accepts_array_data(self): + root = mjcf.RootElement(model='m') + root.custom.add('numeric', name='x', data=[1, 2, 3]) + physics = mjcf.Physics.from_mjcf_model(root) + self.assertEqual(physics.model.nnumericdata, 3) + + if __name__ == '__main__': absltest.main()