Skip to content

Commit b9fbeef

Browse files
authored
fix: keep the name of SubTree nodes in WriteTreeToXML (#1262) (#1268)
addTreeToXML() wrote the [name] attribute only for non-SubTree nodes, so the instance name of a SubTree was lost when a tree was exported and loaded again. The attribute is written only when the name differs from the SubTree ID. When [name] is omitted, the parser uses the ID as instance name and generates a unique path; writing name="<ID>" for those nodes would make two SubTrees with the same ID collide ("Duplicate SubTree path") when the XML is reloaded.
1 parent 6b8e68c commit b9fbeef

2 files changed

Lines changed: 90 additions & 3 deletions

File tree

‎src/xml_parsing.cpp‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,20 +1364,25 @@ void addTreeToXML(const Tree& tree, XMLDocument& doc, XMLElement* rootXML,
13641364
{
13651365
std::function<void(const TreeNode&, XMLElement*)> addNode;
13661366
addNode = [&](const TreeNode& node, XMLElement* parent_elem) {
1367-
XMLElement* elem = nullptr;
1367+
XMLElement* elem = doc.NewElement(node.registrationName().c_str());
13681368

13691369
if(const auto* subtree = dynamic_cast<const SubTreeNode*>(&node))
13701370
{
1371-
elem = doc.NewElement(node.registrationName().c_str());
13721371
elem->SetAttribute("ID", subtree->subtreeID().c_str());
1372+
// Without [name], the parser uses the ID as instance name and generates a
1373+
// unique path. Write [name] only when it differs from the ID: writing it
1374+
// always would make SubTrees with the same ID collide when reloaded.
1375+
if(node.name() != subtree->subtreeID())
1376+
{
1377+
elem->SetAttribute("name", node.name().c_str());
1378+
}
13731379
if(add_metadata)
13741380
{
13751381
elem->SetAttribute("_fullpath", subtree->config().path.c_str());
13761382
}
13771383
}
13781384
else
13791385
{
1380-
elem = doc.NewElement(node.registrationName().c_str());
13811386
elem->SetAttribute("name", node.name().c_str());
13821387
}
13831388

‎tests/gtest_subtree.cpp‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "test_helper.hpp"
22

33
#include "behaviortree_cpp/bt_factory.h"
4+
#include "behaviortree_cpp/xml_parsing.h"
45

56
#include <set>
67

@@ -1264,3 +1265,84 @@ TEST(SubTree, DeclaredUnbindablePortLoadsWhenNotRemapped)
12641265
BehaviorTreeFactory factory;
12651266
EXPECT_NO_THROW(factory.createTreeFromText(xml_text));
12661267
}
1268+
1269+
namespace
1270+
{
1271+
std::vector<const SubTreeNode*> collectSubTreeNodes(const Tree& tree)
1272+
{
1273+
std::vector<const SubTreeNode*> nodes;
1274+
tree.applyVisitor([&](const TreeNode* node) {
1275+
if(const auto* subtree = dynamic_cast<const SubTreeNode*>(node))
1276+
{
1277+
nodes.push_back(subtree);
1278+
}
1279+
});
1280+
return nodes;
1281+
}
1282+
} // namespace
1283+
1284+
// Issue #1262: WriteTreeToXML dropped the [name] of SubTree nodes
1285+
TEST(SubTree, WriteTreeToXML_KeepsSubTreeName)
1286+
{
1287+
static const char* xml_text = R"(
1288+
<root BTCPP_format="4" main_tree_to_execute="MainTree">
1289+
<BehaviorTree ID="MainTree">
1290+
<SubTree ID="Worker" name="my_worker"/>
1291+
</BehaviorTree>
1292+
<BehaviorTree ID="Worker">
1293+
<AlwaysSuccess/>
1294+
</BehaviorTree>
1295+
</root>)";
1296+
1297+
BehaviorTreeFactory factory;
1298+
auto tree = factory.createTreeFromText(xml_text);
1299+
1300+
const auto xml = WriteTreeToXML(tree, false, false);
1301+
EXPECT_NE(xml.find(R"(name="my_worker")"), std::string::npos) << xml;
1302+
1303+
// load the exported XML again: the instance name must survive the round trip
1304+
BehaviorTreeFactory factory2;
1305+
factory2.registerBehaviorTreeFromText(xml);
1306+
auto reloaded = factory2.createTree("MainTree");
1307+
1308+
const auto subtrees = collectSubTreeNodes(reloaded);
1309+
ASSERT_EQ(subtrees.size(), 1);
1310+
EXPECT_EQ(subtrees[0]->name(), "my_worker");
1311+
EXPECT_EQ(subtrees[0]->subtreeID(), "Worker");
1312+
EXPECT_EQ(reloaded.tickWhileRunning(), NodeStatus::SUCCESS);
1313+
}
1314+
1315+
// A SubTree without [name] must not get one in the exported XML. Otherwise, two
1316+
// SubTrees with the same ID would get the same path and the XML could not be
1317+
// loaded again (see DuplicateSubTreeName_Groot2Issue56).
1318+
TEST(SubTree, WriteTreeToXML_NoNameAttributeIsNotAdded)
1319+
{
1320+
static const char* xml_text = R"(
1321+
<root BTCPP_format="4" main_tree_to_execute="MainTree">
1322+
<BehaviorTree ID="MainTree">
1323+
<Sequence>
1324+
<SubTree ID="Worker"/>
1325+
<SubTree ID="Worker"/>
1326+
</Sequence>
1327+
</BehaviorTree>
1328+
<BehaviorTree ID="Worker">
1329+
<AlwaysSuccess/>
1330+
</BehaviorTree>
1331+
</root>)";
1332+
1333+
BehaviorTreeFactory factory;
1334+
auto tree = factory.createTreeFromText(xml_text);
1335+
1336+
const auto xml = WriteTreeToXML(tree, false, false);
1337+
EXPECT_EQ(xml.find(R"(name="Worker")"), std::string::npos) << xml;
1338+
1339+
BehaviorTreeFactory factory2;
1340+
factory2.registerBehaviorTreeFromText(xml);
1341+
Tree reloaded;
1342+
ASSERT_NO_THROW(reloaded = factory2.createTree("MainTree"));
1343+
1344+
const auto subtrees = collectSubTreeNodes(reloaded);
1345+
ASSERT_EQ(subtrees.size(), 2);
1346+
EXPECT_NE(subtrees[0]->fullPath(), subtrees[1]->fullPath());
1347+
EXPECT_EQ(reloaded.tickWhileRunning(), NodeStatus::SUCCESS);
1348+
}

0 commit comments

Comments
 (0)