Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
mjcf: add schema regression tests for the declarations lost in regene…
…ration

d2cf3a3 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.
  • Loading branch information
shoemoney committed Sep 18, 2026
commit b89132acfc1dc67f86c33ff083ac1adf864a5835
59 changes: 59 additions & 0 deletions dm_control/mjcf/schema_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
<mujoco>
<worldbody>
<body name="b">
<joint name="j" type="slide"/>
<geom name="g" size="0.1"/>
</body>
</worldbody>
<sensor>
<contact name="cs" geom1="g" num="1" data="found"
reduce="netforce"/>
</sensor>
</mujoco>"""
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()
Loading