2026-09-20 12:18:52 +08:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Checks fixed-tree expansion and generated IDL headers without claiming ROS build."""
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import re
|
|
|
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
|
|
|
|
PACKAGE = Path(__file__).resolve().parents[1]
|
|
|
|
|
REPOSITORY = PACKAGE.parents[1]
|
|
|
|
|
root = ET.parse(PACKAGE / 'trees/fixed_workflow.xml').getroot()
|
|
|
|
|
assert root.attrib['BTCPP_format'] == '4'
|
|
|
|
|
trees = {v.attrib['ID']: v for v in root.findall('BehaviorTree')}
|
|
|
|
|
assert {'NavigateSkill', 'GroundTargetSkill', 'PickSkill', 'CheckFreeSpaceSkill',
|
|
|
|
|
'PlaceSkill', 'AskUserSkill'} <= set(trees)
|
|
|
|
|
assert [v.tag for v in trees['TaskRoot'][0]] == ['ApprovedPlanGate', 'SubTree']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def expand(node, inputs=None):
|
|
|
|
|
inputs = inputs or {}
|
|
|
|
|
if node.tag == 'RunStage':
|
|
|
|
|
stage = node.attrib['stage']
|
|
|
|
|
return [inputs[stage[1:-1]] if stage.startswith('{') else stage]
|
|
|
|
|
if node.tag == 'SubTree':
|
|
|
|
|
return expand(trees[node.attrib['ID']], {**inputs, **node.attrib})
|
|
|
|
|
return [stage for child in node for stage in expand(child, inputs)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
expected = re.findall(r'case Stage::\w+:return "(\w+)"',
|
|
|
|
|
(REPOSITORY / 'core/src/workflow.cpp').read_text())
|
|
|
|
|
actual = expand(trees['TaskRoot'])
|
|
|
|
|
assert actual == expected, (actual, expected)
|
|
|
|
|
assert not any(v.tag in {'Script', 'ScriptCondition', 'RetryUntilSuccessful'} for v in root.iter())
|
|
|
|
|
assert trees['AskUserSkill'][0].tag == 'RequestClarification'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def header_name(name):
|
|
|
|
|
return re.sub(r'([a-z0-9])([A-Z])', r'\1_\2',
|
|
|
|
|
re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name)).lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for source in list((PACKAGE / 'src').glob('*.cpp')) + list((PACKAGE / 'include/bt_executor').glob('*.hpp')):
|
2026-09-22 17:40:25 +08:00
|
|
|
for package, kind, name in re.findall(r'(bt_skill_interfaces|navigation_interfaces)/(action|msg)/(\w+)\.hpp', source.read_text()):
|
|
|
|
|
candidates = list((PACKAGE.parent / package / kind).glob('*'))
|
2026-09-20 12:18:52 +08:00
|
|
|
assert any(header_name(v.stem) == name for v in candidates), (source, kind, name)
|
|
|
|
|
ET.parse(PACKAGE / 'package.xml')
|
|
|
|
|
print(f'Static checks passed: {len(actual)} ordered core stages, six skill templates, generated IDL include names, package XML.')
|
|
|
|
|
print('This does not compile C++ or validate ROS2 middleware behavior.')
|