#!/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')): for kind, name in re.findall(r'bt_skill_interfaces/(action|msg)/(\w+)\.hpp', source.read_text()): candidates = list((PACKAGE.parent / 'bt_skill_interfaces' / kind).glob('*')) 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.')