Skip to content

Commit cf22aa5

Browse files
forget a rejected XML document in XMLParser (#1266)
1 parent 6c448e0 commit cf22aa5

2 files changed

Lines changed: 81 additions & 3 deletions

File tree

‎src/xml_parsing.cpp‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
#include "behaviortree_cpp/xml_parsing.h"
4747

4848
#include <filesystem>
49+
#include <iterator>
4950
#include <map>
5051
#include <unordered_map>
5152

@@ -245,6 +246,25 @@ struct XMLParser::PImpl
245246

246247
void loadDocImpl(XMLDocument* doc, bool add_includes);
247248

249+
// Load the document just appended to opened_documents. A document that is
250+
// rejected (XML error, invalid tree) registers no tree, so forget it again:
251+
// it would otherwise remain the "first opened document" that
252+
// instantiateTree() reads main_tree_to_execute from, and a document that
253+
// did not parse has no root element at all.
254+
void loadLastDocImpl(bool add_includes)
255+
{
256+
const auto doc_it = std::prev(opened_documents.end());
257+
try
258+
{
259+
loadDocImpl(doc_it->get(), add_includes);
260+
}
261+
catch(...)
262+
{
263+
opened_documents.erase(doc_it);
264+
throw;
265+
}
266+
}
267+
248268
std::list<std::unique_ptr<XMLDocument> > opened_documents;
249269
std::map<std::string, const XMLElement*> tree_roots;
250270
std::unordered_map<std::string, std::string> tree_sources;
@@ -304,7 +324,7 @@ void XMLParser::loadFromFile(const std::filesystem::path& filepath, bool add_inc
304324
_p->current_path = std::filesystem::absolute(filepath.parent_path());
305325
_p->include_stack = { IncludeKey(std::filesystem::absolute(filepath)) };
306326

307-
_p->loadDocImpl(doc, add_includes);
327+
_p->loadLastDocImpl(add_includes);
308328
}
309329

310330
void XMLParser::loadFromText(const std::string& xml_text, bool add_includes)
@@ -315,7 +335,7 @@ void XMLParser::loadFromText(const std::string& xml_text, bool add_includes)
315335
doc->Parse(xml_text.c_str(), xml_text.size());
316336

317337
_p->include_stack.clear();
318-
_p->loadDocImpl(doc, add_includes);
338+
_p->loadLastDocImpl(add_includes);
319339
}
320340

321341
std::vector<std::string> XMLParser::registeredBehaviorTrees() const
@@ -448,7 +468,7 @@ void XMLParser::PImpl::loadDocImpl(XMLDocument* doc, bool add_includes)
448468

449469
include_stack.push_back(std::move(include_key));
450470
next_doc->LoadFile(file_path.string().c_str());
451-
loadDocImpl(next_doc, add_includes);
471+
loadLastDocImpl(add_includes);
452472
include_stack.pop_back();
453473

454474
// reset current path to the previous value
@@ -745,6 +765,10 @@ Tree XMLParser::instantiateTree(const Blackboard::Ptr& root_blackboard,
745765
// or the one in the FIRST document opened
746766
if(main_tree_ID.empty())
747767
{
768+
if(_p->opened_documents.empty())
769+
{
770+
throw RuntimeError("XMLParser::instantiateTree: no XML document was loaded");
771+
}
748772
XMLElement* first_xml_root = _p->opened_documents.front()->RootElement();
749773

750774
if(auto main_tree_attribute = first_xml_root->Attribute("main_tree_to_execute"))

‎tests/gtest_factory.cpp‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,60 @@ TEST(BehaviorTreeFactory, MalformedXML_UnknownNodeType)
864864
EXPECT_THROW((void)factory.createTreeFromText(xml), RuntimeError);
865865
}
866866

867+
TEST(BehaviorTreeFactory, RejectedDocumentIsForgotten)
868+
{
869+
// A document that failed to load used to stay in the parser as the "first
870+
// opened document", whose root element instantiateTree() reads
871+
// main_tree_to_execute from. For a document that did not even parse, that
872+
// root element is null.
873+
const char* valid_xml = R"(
874+
<root BTCPP_format="4">
875+
<BehaviorTree ID="Main">
876+
<AlwaysSuccess/>
877+
</BehaviorTree>
878+
</root>)";
879+
880+
BehaviorTreeFactory factory;
881+
{
882+
XMLParser parser(factory);
883+
EXPECT_THROW(parser.loadFromText("this is not XML"), RuntimeError);
884+
parser.loadFromText(valid_xml);
885+
Tree tree = parser.instantiateTree(Blackboard::create());
886+
EXPECT_EQ(tree.tickOnce(), NodeStatus::SUCCESS);
887+
}
888+
{
889+
// the main_tree_to_execute of a rejected document must not be used either
890+
const char* rejected_xml = R"(
891+
<root BTCPP_format="4" main_tree_to_execute="Rejected">
892+
<BehaviorTree ID="Rejected">
893+
<NonExistentNodeType/>
894+
</BehaviorTree>
895+
</root>)";
896+
XMLParser parser(factory);
897+
EXPECT_THROW(parser.loadFromText(rejected_xml), RuntimeError);
898+
parser.loadFromText(valid_xml);
899+
Tree tree = parser.instantiateTree(Blackboard::create());
900+
EXPECT_EQ(tree.tickOnce(), NodeStatus::SUCCESS);
901+
}
902+
{
903+
// nothing loaded: an error, not an access to the front of an empty list
904+
XMLParser parser(factory);
905+
EXPECT_THROW((void)parser.instantiateTree(Blackboard::create()), RuntimeError);
906+
}
907+
908+
// The factory keeps a single parser: a file rejected once poisoned every
909+
// later creation that has to fall back to the first document. Here two
910+
// trees and no main_tree_to_execute must be reported as such.
911+
EXPECT_THROW(factory.registerBehaviorTreeFromText("<root><BehaviorTree>"),
912+
RuntimeError);
913+
const char* two_trees_xml = R"(
914+
<root BTCPP_format="4">
915+
<BehaviorTree ID="A"><AlwaysSuccess/></BehaviorTree>
916+
<BehaviorTree ID="B"><AlwaysSuccess/></BehaviorTree>
917+
</root>)";
918+
EXPECT_THROW((void)factory.createTreeFromText(two_trees_xml), RuntimeError);
919+
}
920+
867921
TEST(BehaviorTreeFactory, TooManyNodesForUID)
868922
{
869923
// UIDs are 16 bits wide. Past 65535 nodes the counter used to wrap around

0 commit comments

Comments
 (0)