Repository: BehaviorTree/BehaviorTree.CPP (C++)

Issue:
BT::BehaviorTreeFactory does not fail when decorators have 0 children
The following tree is successfully parsed by `BT::BehaviorTreeFactory::createTreeFromText` when I would have expected parsing to fail.

```xml
<root main_tree_to_execute="MainTree">
  <BehaviorTree ID="MainTree">
    <ForceSuccess>
    </ForceSuccess>
  </BehaviorTree>
</root>
```

Below is a test that verifies it's parsing incorrectly.

```cpp
TEST(BehaviorTreeFactory, DecoratorWithoutChildThrows)
{
    BehaviorTreeFactory factory;
    const std::string tree_xml = R"(
<root>
    <BehaviorTree ID="Main">
        <ForceSuccess>
        </ForceSuccess>
    </BehaviorTree>
</root>
)";

    ASSERT_THROW(factory.createTreeFromText(tree_xml), BehaviorTreeException);
}
```

Output:
```
[ RUN      ] BehaviorTreeFactory.DecoratorWithoutChildThrows
/home/adam/src/BehaviorTree.CPP/tests/gtest_factory.cpp:375: Failure
Expected: factory.createTreeFromText(tree_xml) throws an exception of type BehaviorTreeException.
  Actual: it throws nothing.
[  FAILED  ] BehaviorTreeFactory.DecoratorWithoutChildThrows (2 ms)
```

---
Notes:

This caused a segmentation fault in our system as the returned `BT::Tree` has a decorator node with a `nullptr` child by default, so a `tree.rootNode()->executeTick()` crashes

https://github.com/BehaviorTree/BehaviorTree.CPP/blob/ef68cf1c2e9f30ad63e7e5f5fe4b10fe215db6a2/src/decorator_node.cpp#L19

The XML parsing logic has a validity check, but the decorator check for 1 child only works if the XML element's name `"Decorator"`. When writing a tree like above, the XML element's name is `"ForceSuccess"`

https://github.com/BehaviorTree/BehaviorTree.CPP/blob/ef68cf1c2e9f30ad63e7e5f5fe4b10fe215db6a2/src/xml_parsing.cpp#L308-L314


Discussion:

Patch:
diff --git a/include/behaviortree_cpp_v3/xml_parsing.h b/include/behaviortree_cpp_v3/xml_parsing.h
index 5ffeb5016..6537c140e 100644
--- a/include/behaviortree_cpp_v3/xml_parsing.h
+++ b/include/behaviortree_cpp_v3/xml_parsing.h
@@ -3,6 +3,8 @@
 
 #include "behaviortree_cpp_v3/bt_parser.h"
 
+#include <unordered_map>
+
 namespace BT
 {
 
@@ -38,7 +40,7 @@ class XMLParser: public Parser
 };
 
 void VerifyXML(const std::string& xml_text,
-               const std::set<std::string> &registered_nodes);
+               const std::unordered_map<std::string, NodeType> &registered_nodes);
 
 std::string writeTreeNodesModelXML(const BehaviorTreeFactory& factory,
                                    bool include_builtin = false);
diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp
index 758105c69..ce3734ecf 100644
--- a/src/xml_parsing.cpp
+++ b/src/xml_parsing.cpp
@@ -214,25 +214,25 @@ void XMLParser::Pimpl::loadDocImpl(BT_TinyXML2::XMLDocument* doc, bool add_inclu
         tree_roots.insert( {tree_name, bt_node} );
     }
 
-    std::set<std::string> registered_nodes;
+    std::unordered_map<std::string, BT::NodeType> registered_nodes;
     XMLPrinter printer;
     doc->Print(&printer);
     auto xml_text = std::string(printer.CStr(), size_t(printer.CStrSize() - 1));
 
     for( const auto& it: factory.manifests())
     {
-        registered_nodes.insert( it.first );
+        registered_nodes.insert({it.first, it.second.type});
     }
     for( const auto& it: tree_roots)
     {
-        registered_nodes.insert( it.first );
+        registered_nodes.insert({it.first, NodeType::SUBTREE});
     }
 
     VerifyXML(xml_text, registered_nodes);
 }
 
 void VerifyXML(const std::string& xml_text,
-               const std::set<std::string>& registered_nodes)
+               const std::unordered_map<std::string, BT::NodeType>& registered_nodes)
 {
 
     BT_TinyXML2::XMLDocument doc;
@@ -391,12 +391,22 @@ void VerifyXML(const std::string& xml_text,
         else
         {
             // search in the factory and the list of subtrees
-            bool found = ( registered_nodes.find(name)  != registered_nodes.end() );
+            const auto search = registered_nodes.find(name);
+            bool found = ( search != registered_nodes.end() );
             if (!found)
             {
                 ThrowError(node->GetLineNum(),
                            std::string("Node not recognized: ") + name);
             }
+
+            if (search->second == NodeType::DECORATOR)
+            {
+                if (children_count != 1)
+                {
+                    ThrowError(node->GetLineNum(),
+                            std::string("The node <") + name + "> must have exactly 1 child");
+                }
+            }
         }
         //recursion
         if (StrEqual(name, "SubTree") == false)


Test patch:
diff --git a/tests/gtest_factory.cpp b/tests/gtest_factory.cpp
index 50793acc4..60e1fe3c8 100644
--- a/tests/gtest_factory.cpp
+++ b/tests/gtest_factory.cpp
@@ -359,3 +359,35 @@ TEST(BehaviorTreeFactory, CreateTreeFromFileWhichIncludesFileFromChildDirectoryW
     ASSERT_EQ(NodeStatus::SUCCESS, tree.tickRoot());
 }
 #endif
+
+TEST(BehaviorTreeFactory, DecoratorWithoutChildThrows)
+{
+    BehaviorTreeFactory factory;
+    const std::string tree_xml = R"(
+<root>
+    <BehaviorTree ID="Main">
+        <ForceSuccess>
+        </ForceSuccess>
+    </BehaviorTree>
+</root>
+)";
+
+    ASSERT_THROW(factory.createTreeFromText(tree_xml), BehaviorTreeException);
+}
+
+TEST(BehaviorTreeFactory, DecoratorWithTwoChildrenThrows)
+{
+    BehaviorTreeFactory factory;
+    const std::string tree_xml = R"(
+<root>
+    <BehaviorTree ID="Main">
+        <ForceSuccess>
+          <AlwaysSuccess />
+          <AlwaysSuccess />
+        </ForceSuccess>
+    </BehaviorTree>
+</root>
+)";
+
+    ASSERT_THROW(factory.createTreeFromText(xml_text), BehaviorTreeException);
+}