Skip to content

Commit 631e91d

Browse files
Fix incorrect registration of behavior trees containing faulty XML (BehaviorTree#438)
* fix incorrect registration of faulty trees * format * simplify XML validation * fix possible out-of-range exception in tests * Add tests * reduce scale of diffs * fix comment * add more test cases Co-authored-by: Davide Faconti <[email protected]>
1 parent 6429868 commit 631e91d

File tree

2 files changed

+114
-33
lines changed

2 files changed

+114
-33
lines changed

src/xml_parsing.cpp

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,21 @@ void XMLParser::Pimpl::loadDocImpl(tinyxml2::XMLDocument* doc, bool add_includes
199199
current_path = previous_path;
200200
}
201201

202+
// Collect the names of all nodes registered with the behavior tree factory
203+
std::unordered_map<std::string, BT::NodeType> registered_nodes;
204+
for (const auto& it : factory.manifests())
205+
{
206+
registered_nodes.insert({it.first, it.second.type});
207+
}
208+
209+
XMLPrinter printer;
210+
doc->Print(&printer);
211+
auto xml_text = std::string(printer.CStr(), size_t(printer.CStrSize() - 1));
212+
213+
// Verify the validity of the XML before adding any behavior trees to the parser's list of registered trees
214+
VerifyXML(xml_text, registered_nodes);
215+
216+
// Register each BehaviorTree within the XML
202217
for (auto bt_node = xml_root->FirstChildElement("BehaviorTree"); bt_node != nullptr;
203218
bt_node = bt_node->NextSiblingElement("BehaviorTree"))
204219
{
@@ -211,24 +226,9 @@ void XMLParser::Pimpl::loadDocImpl(tinyxml2::XMLDocument* doc, bool add_includes
211226
{
212227
tree_name = "BehaviorTree_" + std::to_string(suffix_count++);
213228
}
214-
tree_roots.insert({tree_name, bt_node});
215-
}
216-
217-
std::unordered_map<std::string, BT::NodeType> registered_nodes;
218-
XMLPrinter printer;
219-
doc->Print(&printer);
220-
auto xml_text = std::string(printer.CStr(), size_t(printer.CStrSize() - 1));
221229

222-
for (const auto& it : factory.manifests())
223-
{
224-
registered_nodes.insert({it.first, it.second.type});
225-
}
226-
for (const auto& it : tree_roots)
227-
{
228-
registered_nodes.insert({it.first, NodeType::SUBTREE});
230+
tree_roots.insert({tree_name, bt_node});
229231
}
230-
231-
VerifyXML(xml_text, registered_nodes);
232232
}
233233

234234
void VerifyXML(const std::string& xml_text,
@@ -390,6 +390,14 @@ void VerifyXML(const std::string& xml_text,
390390
"attribute [ID]");
391391
}
392392
}
393+
else if (StrEqual(name, "BehaviorTree"))
394+
{
395+
if (children_count != 1)
396+
{
397+
ThrowError(node->GetLineNum(), "The node <BehaviorTree> must have exactly 1 "
398+
"child");
399+
}
400+
}
393401
else
394402
{
395403
// search in the factory and the list of subtrees
@@ -420,26 +428,10 @@ void VerifyXML(const std::string& xml_text,
420428
}
421429
};
422430

423-
std::vector<std::string> tree_names;
424-
int tree_count = 0;
425-
426431
for (auto bt_root = xml_root->FirstChildElement("BehaviorTree"); bt_root != nullptr;
427432
bt_root = bt_root->NextSiblingElement("BehaviorTree"))
428433
{
429-
tree_count++;
430-
if (bt_root->Attribute("ID"))
431-
{
432-
tree_names.emplace_back(bt_root->Attribute("ID"));
433-
}
434-
if (ChildrenCount(bt_root) != 1)
435-
{
436-
ThrowError(bt_root->GetLineNum(), "The node <BehaviorTree> must have exactly "
437-
"1 child");
438-
}
439-
else
440-
{
441-
recursiveStep(bt_root->FirstChildElement());
442-
}
434+
recursiveStep(bt_root);
443435
}
444436
}
445437

tests/gtest_factory.cpp

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ TEST(BehaviorTreeFactory, XMLParsingOrder)
109109
XMLParser parser(factory);
110110
parser.loadFromText(xml_text_subtree);
111111
auto trees = parser.registeredBehaviorTrees();
112+
ASSERT_EQ(trees.size(), 2);
112113
ASSERT_EQ(trees[0], "CrossDoorSubtree");
113114
ASSERT_EQ(trees[1], "MainTree");
114115
}
@@ -117,6 +118,7 @@ TEST(BehaviorTreeFactory, XMLParsingOrder)
117118
parser.loadFromText(xml_text_subtree_part1);
118119
parser.loadFromText(xml_text_subtree_part2);
119120
auto trees = parser.registeredBehaviorTrees();
121+
ASSERT_EQ(trees.size(), 2);
120122
ASSERT_EQ(trees[0], "CrossDoorSubtree");
121123
ASSERT_EQ(trees[1], "MainTree");
122124
}
@@ -125,6 +127,7 @@ TEST(BehaviorTreeFactory, XMLParsingOrder)
125127
parser.loadFromText(xml_text_subtree_part2);
126128
parser.loadFromText(xml_text_subtree_part1);
127129
auto trees = parser.registeredBehaviorTrees();
130+
ASSERT_EQ(trees.size(), 2);
128131
ASSERT_EQ(trees[0], "CrossDoorSubtree");
129132
ASSERT_EQ(trees[1], "MainTree");
130133
}
@@ -217,7 +220,12 @@ TEST(BehaviorTreeFactory, Issue7)
217220
BehaviorTreeFactory factory;
218221
XMLParser parser(factory);
219222

223+
// We expect that an incorrectly-constructed behavior tree will fail to load
220224
EXPECT_THROW(parser.loadFromText(xml_text_issue), RuntimeError);
225+
226+
// We expect that no behavior trees will be registered after we unsuccessfully attempt to load a single tree
227+
auto trees = parser.registeredBehaviorTrees();
228+
EXPECT_TRUE( trees.empty() );
221229
}
222230

223231
// clang-format off
@@ -408,6 +416,87 @@ TEST(BehaviorTreeFactory, DecoratorWithTwoChildrenThrows)
408416
ASSERT_THROW(factory.createTreeFromText(xml_text), BehaviorTreeException);
409417
}
410418

419+
420+
TEST(BehaviorTreeFactory, RegisterValidAndInvalidTrees)
421+
{
422+
const std::string xml_text_ok = R"(
423+
<root>
424+
<BehaviorTree ID="ValidTree">
425+
<Sequence name="door_open_sequence">
426+
<Action ID="AlwaysSuccess" />
427+
</Sequence>
428+
</BehaviorTree>
429+
</root> )";
430+
431+
const std::string xml_text_invalid = R"(
432+
<root>
433+
<BehaviorTree ID="InvalidTreeWithNoChildren">
434+
</BehaviorTree>
435+
</root> )";
436+
437+
BehaviorTreeFactory factory;
438+
XMLParser parser(factory);
439+
440+
// GIVEN that a valid tree has been loaded
441+
ASSERT_NO_THROW(parser.loadFromText(xml_text_ok));
442+
443+
// WHEN we attempt to load an invalid tree
444+
ASSERT_THROW(parser.loadFromText(xml_text_invalid), RuntimeError);
445+
446+
// THEN the valid tree is still registered
447+
auto trees = parser.registeredBehaviorTrees();
448+
ASSERT_EQ(trees.size(), 1);
449+
EXPECT_EQ(trees[0], "ValidTree");
450+
}
451+
452+
TEST(BehaviorTreeFactory, RegisterInvalidXMLBadActionNodeThrows)
453+
{
454+
// GIVEN an invalid tree
455+
// This tree contains invalid XML because the action node is missing a trailing `/`.
456+
// A valid line would read: <Action ID="AlwaysSuccess" />
457+
const std::string xml_text_invalid = R"(
458+
<root>
459+
<BehaviorTree ID="InvalidTreeWithBadChild">
460+
<Sequence name="seq">
461+
<Action ID="AlwaysSuccess" >
462+
</Sequence>
463+
</BehaviorTree>
464+
</root> )";
465+
466+
BehaviorTreeFactory factory;
467+
XMLParser parser(factory);
468+
469+
// WHEN we attempt to load an invalid tree
470+
// THEN a RuntimeError exception is thrown
471+
EXPECT_THROW(parser.loadFromText(xml_text_invalid), RuntimeError);
472+
473+
// THEN no tree is registered
474+
auto trees = parser.registeredBehaviorTrees();
475+
EXPECT_TRUE(trees.empty());
476+
}
477+
478+
TEST(BehaviorTreeFactory, RegisterInvalidXMLNoRootThrows)
479+
{
480+
// GIVEN an invalid tree
481+
// This tree contains invalid XML because it does not have a root node
482+
const std::string xml_text_invalid = R"(
483+
<BehaviorTree ID="InvalidTreeNoRoot">
484+
<Sequence name="seq">
485+
<Action ID="AlwaysSuccess" />
486+
</Sequence>
487+
</BehaviorTree> )";
488+
489+
BehaviorTreeFactory factory;
490+
XMLParser parser(factory);
491+
492+
// WHEN we attempt to load an invalid tree
493+
// THEN a RuntimeError exception is thrown
494+
EXPECT_THROW(parser.loadFromText(xml_text_invalid), RuntimeError);
495+
496+
// THEN no tree is registered
497+
auto trees = parser.registeredBehaviorTrees();
498+
EXPECT_TRUE(trees.empty());
499+
411500
TEST(BehaviorTreeFactory, ParserClearRegisteredBehaviorTrees)
412501
{
413502
const std::string tree_xml = R"(

0 commit comments

Comments
 (0)