Skip to content

Commit 80c7541

Browse files
committed
[ports] Add tests for including subtrees in other files
1 parent c2b25a2 commit 80c7541

5 files changed

Lines changed: 110 additions & 1 deletion

File tree

docs/ports.rst

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,67 @@ Example::
227227
<Greeting name="hello_node" name_key="{target}" prefix="Howdy"/>
228228
<!-- ^^^ behaviour name ^^^ port remap ^^^ ctor kwarg -->
229229
230+
.. _ports-xml-includes-label:
231+
232+
Composing trees from multiple files
233+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
234+
235+
Subtrees do not have to live in the same file as the tree that uses them.
236+
A top-level ``<Include>`` (or ``<Import>``) element pulls all ``<BehaviorTree>``
237+
definitions from another XML file into the current document before parsing,
238+
so you can maintain a library of reusable subtrees in separate files:
239+
240+
.. code-block:: xml
241+
242+
<!-- subtree_library.xml -->
243+
<root>
244+
<BehaviorTree ID="LibraryTree">
245+
<Sequence>
246+
<Producer name="LibProducer" output="{lib_out}"/>
247+
</Sequence>
248+
</BehaviorTree>
249+
</root>
250+
251+
.. code-block:: xml
252+
253+
<!-- main_tree.xml -->
254+
<root main_tree_to_execute="MainTree">
255+
<Include src="subtree_library.xml"/>
256+
<BehaviorTree ID="MainTree">
257+
<Sequence>
258+
<SubTree ID="LibraryTree" name="Library" lib_out="{final}"/>
259+
<Consumer name="FinalConsumer" input="{final}"/>
260+
</Sequence>
261+
</BehaviorTree>
262+
</root>
263+
264+
The path in ``src=`` (``file=`` is accepted as an alias) is resolved in order:
265+
266+
1. As an absolute path, if it is one.
267+
2. Relative to the directory of the *including* XML file.
268+
3. Against each directory in the ``search_paths`` argument of
269+
:func:`~py_trees.parsers.behaviour_tree_xml.parse_behaviour_tree_xml`.
270+
271+
So if the library file lives somewhere other than next to the main file --- say a
272+
shared ``subtrees/`` directory --- pass that directory via ``search_paths``:
273+
274+
.. code-block:: python
275+
276+
root = parse_behaviour_tree_xml(
277+
"main_tree.xml",
278+
search_paths=["/path/to/subtrees"],
279+
)
280+
281+
A few more rules to be aware of:
282+
283+
* Includes are only recognised as **top-level** children of ``<root>``; an
284+
``<Include>`` nested inside a ``<BehaviorTree>`` is an error.
285+
* Includes are processed **recursively** --- an included file may itself include
286+
further files --- and cycles are detected and skipped.
287+
* All BehaviorTree IDs must be unique across the main file and everything it
288+
includes; a collision raises ``ValueError``.
289+
* ``main_tree_to_execute`` may refer to a tree defined in an included file.
290+
230291
Scope, limitations, and how to extend
231292
-------------------------------------
232293

tests/test_ports_xml_parser.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def test_xml_parser_remapping(self) -> None:
181181

182182
def test_grandparent_xml(self) -> None:
183183
"""Test XML parser with grandparent-child relationships and correct value propagation."""
184-
xml_path = os.path.join(os.path.dirname(__file__), "grandparent_test.xml")
184+
xml_path = os.path.join(os.path.dirname(__file__), "xml", "grandparent_test.xml")
185185
root_node = parse_behaviour_tree_xml(xml_path, logger=StdoutLogger())
186186
btree = py_trees.trees.BehaviourTree(root_node)
187187
btree.tick()
@@ -888,6 +888,33 @@ def test_import_with_search_paths(self) -> None:
888888
assert isinstance(c, Consumer)
889889
self.assertEqual(c.consumed_value, "Producer[/L:L.MySeq.P]")
890890

891+
def test_include_subtree_file_with_search_paths(self) -> None:
892+
"""A subtree <Include>d from a sibling directory resolves via 'search_paths'.
893+
894+
Uses the on-disk fixtures in tests/xml/: the main tree includes
895+
'subtree_library.xml', which lives in tests/xml/subtrees/ and is therefore
896+
only found when that directory is passed via 'search_paths'.
897+
"""
898+
xml_dir = os.path.join(os.path.dirname(__file__), "xml")
899+
main_path = os.path.join(xml_dir, "main_tree_with_include.xml")
900+
root = parse_behaviour_tree_xml(
901+
main_path,
902+
logger=StdoutLogger(),
903+
search_paths=[os.path.join(xml_dir, "subtrees")],
904+
)
905+
tree = py_trees.trees.BehaviourTree(root)
906+
tree.tick()
907+
consumer = find_node_by_name(root, "FinalConsumer", strip_prefix=True)
908+
assert isinstance(consumer, Consumer)
909+
self.assertEqual(consumer.consumed_value, "Producer[/Library:Library.LibSeq.LibProducer]")
910+
911+
def test_include_subtree_file_without_search_paths_fails(self) -> None:
912+
"""Without 'search_paths', the include in tests/xml/ cannot be resolved."""
913+
xml_dir = os.path.join(os.path.dirname(__file__), "xml")
914+
main_path = os.path.join(xml_dir, "main_tree_with_include.xml")
915+
with self.assertRaises(FileNotFoundError):
916+
parse_behaviour_tree_xml(main_path, logger=StdoutLogger())
917+
891918
def test_imported_bt_missing_id(self) -> None:
892919
"""Imported file containing a <BehaviorTree> without an ID raises ValueError."""
893920
lib_path = self._write_temp_xml("""<root><BehaviorTree><Sequence/></BehaviorTree></root>""")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<root main_tree_to_execute="MainTree">
2+
<!--
3+
The included file lives in the sibling 'subtrees' directory, so it can only
4+
be resolved by passing search_paths=[.../tests/xml/subtrees] to
5+
parse_behaviour_tree_xml(). Without it, parsing raises FileNotFoundError.
6+
-->
7+
<Include src="subtree_library.xml"/>
8+
<BehaviorTree ID="MainTree">
9+
<Sequence>
10+
<SubTree ID="LibraryTree" name="Library" lib_out="{final}"/>
11+
<Consumer name="FinalConsumer" input="{final}"/>
12+
</Sequence>
13+
</BehaviorTree>
14+
</root>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<root>
2+
<BehaviorTree ID="LibraryTree">
3+
<Sequence name="LibSeq">
4+
<Producer name="LibProducer" output="{lib_out}"/>
5+
</Sequence>
6+
</BehaviorTree>
7+
</root>

0 commit comments

Comments
 (0)