@@ -8349,6 +8349,85 @@ def test_all_custom_usd_attribute_names_in_rst(self):
83498349 missing .append (f"{ attr .name } -> { usd_name } " )
83508350 self .assertEqual (missing , [], f"Custom attr USD names missing from mujoco.rst: { missing } " )
83518351
8352+ def test_no_stale_custom_attr_names_in_rst (self ):
8353+ """Every custom attr name in the .rst custom-attributes section must
8354+ still be registered in code. Catches doc entries left behind after
8355+ code removal."""
8356+ # Collect all registered mujoco custom attribute names
8357+ registered = set ()
8358+ for _key , attr in self .builder .custom_attributes .items ():
8359+ if attr .namespace != "mujoco" :
8360+ continue
8361+ registered .add (attr .name )
8362+
8363+ # Extract custom attr names from the .rst that appear inside
8364+ # ``backticks`` within the custom-attributes-and-frequencies section
8365+ section_match = re .search (
8366+ r"Custom Attributes and Frequencies\n-+\n(.*?)(?:\n\.\. _|\Z)" ,
8367+ self .rst_content ,
8368+ re .DOTALL ,
8369+ )
8370+ if section_match is None :
8371+ self .skipTest ("Custom Attributes section not found in .rst" )
8372+
8373+ section = section_match .group (1 )
8374+ # Match backtick-quoted names that look like custom attr names
8375+ # (lowercase, underscores, no colons — distinguishes from mjc: USD names)
8376+ rst_names = set (re .findall (r"``([a-z][a-z0-9_]+)``" , section ))
8377+
8378+ # Filter to plausible custom attr names (exclude RST directives, etc.)
8379+ known_single_word = {
8380+ "condim" ,
8381+ "gravcomp" ,
8382+ "impratio" ,
8383+ "tolerance" ,
8384+ "density" ,
8385+ "viscosity" ,
8386+ "wind" ,
8387+ "magnetic" ,
8388+ "iterations" ,
8389+ "integrator" ,
8390+ "solver" ,
8391+ "cone" ,
8392+ "jacobian" ,
8393+ "autolimits" ,
8394+ "ctrl" ,
8395+ }
8396+ plausible = {n for n in rst_names if "_" in n or n in known_single_word or n in registered }
8397+
8398+ stale = plausible - registered
8399+ # Exclude known non-attr names that appear in backticks
8400+ false_positives = {"mujoco" , "model" , "state" , "control" , "finalize" }
8401+ stale -= false_positives
8402+
8403+ self .assertEqual (
8404+ stale ,
8405+ set (),
8406+ f"Custom attr names in .rst that are no longer registered: { stale } " ,
8407+ )
8408+
8409+ def test_critical_defaults_match_code (self ):
8410+ """Spot-check that key defaults documented in .rst match the code."""
8411+ spot_checks = {
8412+ "condim" : 3 ,
8413+ "iterations" : 100 ,
8414+ "impratio" : 1.0 ,
8415+ "density" : 0.0 ,
8416+ }
8417+ for attr_name , expected_default in spot_checks .items ():
8418+ key = f"mujoco:{ attr_name } "
8419+ attr = self .builder .custom_attributes .get (key )
8420+ self .assertIsNotNone (attr , f"Custom attr { key } not found" )
8421+ actual = attr .default
8422+ # Handle warp scalar types
8423+ if hasattr (actual , "numpy" ):
8424+ actual = actual .numpy ()
8425+ self .assertEqual (
8426+ actual ,
8427+ expected_default ,
8428+ f"Default for { attr_name } changed in code ({ actual } ) — update docs/integrations/mujoco.rst" ,
8429+ )
8430+
83528431 def test_no_invented_mjc_names_in_rst (self ):
83538432 """Every mjc: name mentioned in the .rst must exist in either
83548433 SchemaResolverMjc or register_custom_attributes."""
0 commit comments