Skip to content

Commit b4e217e

Browse files
facontidavideclaude
andcommitted
Detect recursive subtree cycles at parse time (fixes #979)
Use an ancestor set passed through recursivelyCreateSubtree to detect cycles, avoiding the substring-matching false positives of a prefix path check. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e0684b0 commit b4e217e

2 files changed

Lines changed: 93 additions & 8 deletions

File tree

src/xml_parsing.cpp

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <string>
2222
#include <tuple>
2323
#include <typeindex>
24+
#include <unordered_set>
2425

2526
#if defined(_MSVC_LANG) && !defined(__clang__)
2627
#define __bt_cplusplus (_MSC_VER == 1900 ? 201103L : _MSVC_LANG)
@@ -196,7 +197,8 @@ struct XMLParser::PImpl
196197
void recursivelyCreateSubtree(const std::string& tree_ID, const std::string& tree_path,
197198
const std::string& prefix_path, Tree& output_tree,
198199
Blackboard::Ptr blackboard,
199-
const TreeNode::Ptr& root_node);
200+
const TreeNode::Ptr& root_node,
201+
std::unordered_set<std::string> ancestors = {});
200202

201203
void getPortsRecursively(const XMLElement* element,
202204
std::vector<std::string>& output_ports);
@@ -1000,13 +1002,16 @@ TreeNode::Ptr XMLParser::PImpl::createNodeFromXML(const XMLElement* element,
10001002
return new_node;
10011003
}
10021004

1003-
void BT::XMLParser::PImpl::recursivelyCreateSubtree(const std::string& tree_ID,
1004-
const std::string& tree_path,
1005-
const std::string& prefix_path,
1006-
Tree& output_tree,
1007-
Blackboard::Ptr blackboard,
1008-
const TreeNode::Ptr& root_node)
1005+
void BT::XMLParser::PImpl::recursivelyCreateSubtree(
1006+
const std::string& tree_ID, const std::string& tree_path,
1007+
const std::string& prefix_path, Tree& output_tree, Blackboard::Ptr blackboard,
1008+
const TreeNode::Ptr& root_node, std::unordered_set<std::string> ancestors)
10091009
{
1010+
if(!ancestors.insert(tree_ID).second)
1011+
{
1012+
throw RuntimeError("Recursive behavior tree cycle detected: tree '", tree_ID,
1013+
"' references itself (directly or indirectly)");
1014+
}
10101015
std::function<void(const TreeNode::Ptr&, Tree::Subtree::Ptr, std::string,
10111016
const XMLElement*)>
10121017
recursiveStep;
@@ -1135,7 +1140,7 @@ void BT::XMLParser::PImpl::recursivelyCreateSubtree(const std::string& tree_ID,
11351140
recursivelyCreateSubtree(subtree_ID,
11361141
subtree_path, // name
11371142
subtree_path + "/", //prefix
1138-
output_tree, new_bb, node);
1143+
output_tree, new_bb, node, ancestors);
11391144
}
11401145
};
11411146

tests/gtest_subtree.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,86 @@ TEST(SubTree, SubtreeNameNotRegistered)
732732
ASSERT_ANY_THROW(factory.registerBehaviorTreeFromText(xml_text));
733733
}
734734

735+
TEST(SubTree, RecursiveSubtree)
736+
{
737+
// clang-format off
738+
739+
static const char* xml_text = R"(
740+
<root BTCPP_format="4" >
741+
<BehaviorTree ID="MainTree">
742+
<Sequence name="root">
743+
<AlwaysSuccess/>
744+
<SubTree ID="MainTree" />
745+
</Sequence>
746+
</BehaviorTree>
747+
</root>
748+
)";
749+
750+
// clang-format on
751+
BehaviorTreeFactory factory;
752+
753+
ASSERT_ANY_THROW(auto tree = factory.createTreeFromText(xml_text));
754+
}
755+
756+
TEST(SubTree, RecursiveCycle)
757+
{
758+
// clang-format off
759+
760+
static const char* xml_text = R"(
761+
<root BTCPP_format="4" main_tree_to_execute="MainTree">
762+
<BehaviorTree ID="MainTree">
763+
<Sequence name="root">
764+
<AlwaysSuccess/>
765+
<SubTree ID="TreeA" />
766+
</Sequence>
767+
</BehaviorTree>
768+
769+
<BehaviorTree ID="TreeA">
770+
<Sequence name="root">
771+
<AlwaysSuccess/>
772+
<SubTree ID="TreeB" />
773+
</Sequence>
774+
</BehaviorTree>
775+
776+
<BehaviorTree ID="TreeB">
777+
<Sequence name="root">
778+
<AlwaysSuccess/>
779+
<SubTree ID="MainTree" />
780+
</Sequence>
781+
</BehaviorTree>
782+
</root>
783+
)";
784+
785+
// clang-format on
786+
BehaviorTreeFactory factory;
787+
788+
ASSERT_ANY_THROW(auto tree = factory.createTreeFromText(xml_text));
789+
}
790+
791+
TEST(SubTree, SubstringTreeIDsAreNotRecursive)
792+
{
793+
// Verify that tree IDs which are substrings of each other do NOT
794+
// incorrectly trigger the recursive cycle detection.
795+
// clang-format off
796+
797+
static const char* xml_text = R"(
798+
<root BTCPP_format="4" main_tree_to_execute="Tree">
799+
<BehaviorTree ID="Tree">
800+
<SubTree ID="TreeABC" />
801+
</BehaviorTree>
802+
803+
<BehaviorTree ID="TreeABC">
804+
<AlwaysSuccess/>
805+
</BehaviorTree>
806+
</root>
807+
)";
808+
809+
// clang-format on
810+
BehaviorTreeFactory factory;
811+
812+
ASSERT_NO_THROW(auto tree = factory.createTreeFromText(xml_text));
813+
}
814+
735815
// Test for Groot2 issue #56: duplicate _fullpath when multiple subtrees have the same name
736816
// https://github.com/BehaviorTree/Groot2/issues/56
737817
//

0 commit comments

Comments
 (0)