From ba4333cda5bb0aace6d46915a54cf4e86eb489d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20M=C3=BCller?= Date: Thu, 12 Feb 2026 15:15:00 +0100 Subject: [PATCH 1/4] Add iterator operator to NodeElement and extend getFirstNode and removeFirstNode with an option to set a flag for enabling deep search --- auto_apms_behavior_tree_core/CMakeLists.txt | 1 + .../node/node_model_type.hpp | 35 +- .../tree/tree_document.hpp | 118 ++- .../src/tree/tree_document.cpp | 63 +- .../test/unit/testable_tree_document.hpp | 54 ++ .../test/unit/tree_document_node_element.cpp | 681 ++++++++++++++++++ .../unit/tree_document_tree_generation.cpp | 40 +- 7 files changed, 906 insertions(+), 86 deletions(-) create mode 100644 auto_apms_behavior_tree_core/test/unit/testable_tree_document.hpp create mode 100644 auto_apms_behavior_tree_core/test/unit/tree_document_node_element.cpp diff --git a/auto_apms_behavior_tree_core/CMakeLists.txt b/auto_apms_behavior_tree_core/CMakeLists.txt index 88a0237..8abc6b3 100644 --- a/auto_apms_behavior_tree_core/CMakeLists.txt +++ b/auto_apms_behavior_tree_core/CMakeLists.txt @@ -153,6 +153,7 @@ if(BUILD_TESTING) ament_add_gtest(${PROJECT_NAME}_unit_tests "test/unit/node_manifest.cpp" "test/unit/ros_subscriber_node.cpp" + "test/unit/tree_document_node_element.cpp" "test/unit/tree_document_node_model.cpp" "test/unit/tree_document_recursive_include.cpp" "test/unit/tree_document_tree_generation.cpp" diff --git a/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/node/node_model_type.hpp b/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/node/node_model_type.hpp index fb6e7f2..c85ff25 100644 --- a/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/node/node_model_type.hpp +++ b/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/node/node_model_type.hpp @@ -22,23 +22,24 @@ /// @cond INTERNAL -#define AUTO_APMS_BEHAVIOR_TREE_CORE_DEFINE_NON_LEAF_THISREF_METHODS(ClassType) \ - ClassType & removeFirstChild(const std::string & registration_name = "", const std::string & instance_name = "") \ - { \ - NodeElement::removeFirstChild(registration_name, instance_name); \ - return *this; \ - } \ - template \ - typename std::enable_if_t, ClassType &> removeFirstChild( \ - const std::string & instance_name = "") \ - { \ - NodeElement::removeFirstChild(instance_name); \ - return *this; \ - } \ - ClassType & removeChildren() \ - { \ - NodeElement::removeChildren(); \ - return *this; \ +#define AUTO_APMS_BEHAVIOR_TREE_CORE_DEFINE_NON_LEAF_THISREF_METHODS(ClassType) \ + ClassType & removeFirstChild( \ + const std::string & registration_name = "", const std::string & instance_name = "", bool deep_search = false) \ + { \ + NodeElement::removeFirstChild(registration_name, instance_name, deep_search); \ + return *this; \ + } \ + template \ + typename std::enable_if_t, ClassType &> removeFirstChild( \ + const std::string & instance_name = "", bool deep_search = false) \ + { \ + NodeElement::removeFirstChild(instance_name, deep_search); \ + return *this; \ + } \ + ClassType & removeChildren() \ + { \ + NodeElement::removeChildren(); \ + return *this; \ } #define AUTO_APMS_BEHAVIOR_TREE_CORE_DEFINE_LEAF_THISREF_METHODS(ClassType) \ diff --git a/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/tree/tree_document.hpp b/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/tree/tree_document.hpp index 7584d3d..4463b0d 100644 --- a/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/tree/tree_document.hpp +++ b/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/tree/tree_document.hpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -549,8 +550,7 @@ class TreeDocument : private tinyxml2::XMLDocument // clang-format off /** - * @brief Recursively visit this node's children in execution order and get the first node with a particular - * registration and instance name. + * @brief Get the first node with a particular registration and instance name. * * This method determines which node element to return by evaluating its arguments as follows: * @@ -559,17 +559,23 @@ class TreeDocument : private tinyxml2::XMLDocument * | `registration_name` **empty** | Simply return the first child node without inspecting it further | Return the first child node that has the given instance name regardless its registration name | * | `registration_name` **non-empty** | Return the first child node that has the given registration name regardless its instance name | Return the first child node that has both the given registration *AND* instance name | * + * Additionally, the user may specify whether to recursively visit all descendants of this node in execution order by setting @p deep_search to `true`. By default, only direct children of this node are considered for the search. + * * @param registration_name Registration name of the node that is searched for. * @param instance_name Name of the specific node instance that is searched for. + * @param deep_search If `true`, recursively visit all descendants in execution order. If `false` (default), only consider + * direct children of this node. * @return First child node element that meets the search criterion defined according to the above. * @throw auto_apms_behavior_tree::exceptions::TreeDocumentError if no node matching the search criterion could be * found. */ - NodeElement getFirstNode(const std::string & registration_name = "", const std::string & instance_name = "") const; + NodeElement getFirstNode( + const std::string & registration_name = "", const std::string & instance_name = "", + bool deep_search = false) const; + // clang-format off /** - * @brief Recursively visit this node's children in execution order and get the first node with a particular - * instance name. + * @brief Get the first node with a particular instance name. * * The kind of node to search for is specified using a node model type passed as the template argument @p T. * Additionally, the user may provide an instance name. According to @p instance_name, the node element to be @@ -579,19 +585,23 @@ class TreeDocument : private tinyxml2::XMLDocument * | --- | | * | Return the first child node that has the registration name provided by the model regardless its instance name | Return the first child node that has both the registration name of the model *AND* the given instance name | * + * Additionally, the user may specify whether to recursively visit all descendants of this node in execution order by setting @p deep_search to `true`. By default, only direct children of this node are considered for the search. + * * @tparam T Node model type. * @param instance_name Name of the specific node instance that is searched for. + * @param deep_search If `true`, recursively visit all descendants in execution order. If `false`, only consider + * direct children of this node. * @return First child node element that meets the search criterion defined according to the above. * @throw auto_apms_behavior_tree::exceptions::TreeDocumentError if no node matching the search criterion could be * found. */ template typename std::enable_if_t, T> getFirstNode( - const std::string & instance_name = "") const; + const std::string & instance_name = "", bool deep_search = false) const; + // clang-format off /** - * @brief Recursively visit this node's children in execution order and remove the first node with a particular - * registration and instance name. + * @brief Remove the first node with a particular registration and instance name. * * This method determines which node element to remove by evaluating its arguments as follows: * @@ -600,17 +610,23 @@ class TreeDocument : private tinyxml2::XMLDocument * | `registration_name` **empty** | Simply remove the first child node without inspecting it further | Remove the first child node that has the given instance name regardless its registration name | * | `registration_name` **non-empty** | Remove the first child node that has the given registration name regardless its instance name | Remove the first child node that has both the given registration *AND* instance name | * + * Additionally, the user may specify whether to recursively visit all descendants of this node in execution order by setting @p deep_search to `true`. By default, only direct children of this node are considered for the removal. + * * @param registration_name Registration name of the node to be removed. * @param instance_name Name of the specific node instance to be removed. + * @param deep_search If `true`, recursively visit all descendants in execution order. If `false` (default), only consider + * direct children of this node. * @return Modified node element. * @throw auto_apms_behavior_tree::exceptions::TreeDocumentError if no node matching the search criterion could be * found. */ - NodeElement & removeFirstChild(const std::string & registration_name = "", const std::string & instance_name = ""); + NodeElement & removeFirstChild( + const std::string & registration_name = "", const std::string & instance_name = "", + bool deep_search = false); + // clang-format off /** - * @brief Recursively visit this node's children in execution order and remove the first node with a particular - * instance name. + * @brief Remove the first node with a particular instance name. * * The kind of node to remove is specified using a node model type passed as the template argument @p T. * Additionally, the user may provide an instance name. According to @p instance_name, the child node to be removed @@ -620,15 +636,19 @@ class TreeDocument : private tinyxml2::XMLDocument * | --- | | * | Remove the first child node that has the registration name provided by the model regardless its instance name | Remove the first child node that has both the registration name of the model *AND* the given instance name | * + * Additionally, the user may specify whether to recursively visit all descendants of this node in execution order by setting @p deep_search to `true`. By default, only direct children of this node are considered for the removal. + * * @tparam T Node model type. * @param instance_name Name of the specific node instance to be removed. + * @param deep_search If `true`, recursively visit all descendants in execution order. If `false` (default), only consider + * direct children of this node. * @return Modified node element. * @throw auto_apms_behavior_tree::exceptions::TreeDocumentError if no node matching the search criterion could be * found. */ template typename std::enable_if_t, NodeElement &> removeFirstChild( - const std::string & instance_name = ""); + const std::string & instance_name = "", bool deep_search = false); // clang-format on /** @@ -760,6 +780,57 @@ class TreeDocument : private tinyxml2::XMLDocument */ std::vector deepApply(DeepApplyCallback apply_callback); + /** + * @brief Forward iterator for traversing the first-level children of a node. + * + * This iterator yields NodeElement handles for each direct child element. It does **not** recurse into children of + * children. For recursive traversal, use NodeElement::deepApply or NodeElement::deepApplyConst instead. + */ + class ChildIterator + { + friend class NodeElement; + + public: + using iterator_category = std::forward_iterator_tag; + using value_type = NodeElement; + using difference_type = std::ptrdiff_t; + using pointer = value_type *; + using reference = value_type; + + /// Create a past-the-end iterator. + ChildIterator(); + + /// Dereference the iterator to obtain a NodeElement handle for the current child. + value_type operator*() const; + + /// Pre-increment: advance to the next sibling element. + ChildIterator & operator++(); + + /// Post-increment: advance to the next sibling element, returning the previous state. + ChildIterator operator++(int); + + bool operator==(const ChildIterator & other) const; + bool operator!=(const ChildIterator & other) const; + + private: + ChildIterator(TreeDocument * doc_ptr, tinyxml2::XMLElement * current); + + TreeDocument * doc_ptr_; + tinyxml2::XMLElement * current_; + }; + + /** + * @brief Get an iterator to the first child of this node. + * @return Iterator pointing to the first child, or a past-the-end iterator if there are no children. + */ + ChildIterator begin() const; + + /** + * @brief Get a past-the-end iterator for the children of this node. + * @return Past-the-end iterator. + */ + ChildIterator end() const; + private: NodeElement insertBeforeImpl(const NodeElement * before_this, XMLElement * add_this); @@ -869,17 +940,18 @@ class TreeDocument : private tinyxml2::XMLDocument /** * This function is an exact reimplementation of NodeElement::removeFirstChild(const std::string &, const - * std::string &) only that it returns a tree element instead of a node element to support method chaining. + * std::string &, bool) only that it returns a tree element instead of a node element to support method chaining. */ - TreeElement & removeFirstChild(const std::string & registration_name = "", const std::string & instance_name = ""); + TreeElement & removeFirstChild( + const std::string & registration_name = "", const std::string & instance_name = "", bool deep_search = false); /** - * This function is an exact reimplementation of NodeElement::removeFirstChild(const std::string &) only that it - * returns a tree element instead of a node element to support method chaining. + * This function is an exact reimplementation of NodeElement::removeFirstChild(const std::string &, bool) only + * that it returns a tree element instead of a node element to support method chaining. */ template typename std::enable_if_t, TreeElement &> removeFirstChild( - const std::string & instance_name = ""); + const std::string & instance_name = "", bool deep_search = false); /** * This function is an exact reimplementation of NodeElement::removeChildren only that it @@ -1435,24 +1507,24 @@ TreeDocument::NodeElement::insertNode(const TreeElement & tree, const NodeElemen template inline typename std::enable_if_t, T> core::TreeDocument::NodeElement::getFirstNode( - const std::string & instance_name) const + const std::string & instance_name, bool deep_search) const { - const NodeElement ele = getFirstNode(T::name(), instance_name); + const NodeElement ele = getFirstNode(T::name(), instance_name, deep_search); return T(ele.doc_ptr_, ele.ele_ptr_); } template inline typename std::enable_if_t, TreeDocument::NodeElement &> -core::TreeDocument::NodeElement::removeFirstChild(const std::string & instance_name) +core::TreeDocument::NodeElement::removeFirstChild(const std::string & instance_name, bool deep_search) { - return removeFirstChild(T::name(), instance_name); + return removeFirstChild(T::name(), instance_name, deep_search); } template inline typename std::enable_if_t, TreeDocument::TreeElement &> -TreeDocument::TreeElement::removeFirstChild(const std::string & instance_name) +TreeDocument::TreeElement::removeFirstChild(const std::string & instance_name, bool deep_search) { - NodeElement::removeFirstChild(instance_name); + NodeElement::removeFirstChild(instance_name, deep_search); return *this; } diff --git a/auto_apms_behavior_tree_core/src/tree/tree_document.cpp b/auto_apms_behavior_tree_core/src/tree/tree_document.cpp index 934df7f..946d116 100644 --- a/auto_apms_behavior_tree_core/src/tree/tree_document.cpp +++ b/auto_apms_behavior_tree_core/src/tree/tree_document.cpp @@ -336,17 +336,23 @@ TreeDocument::NodeElement TreeDocument::NodeElement::insertTreeFromResource( bool TreeDocument::NodeElement::hasChildren() const { return ele_ptr_->FirstChild() == nullptr ? false : true; } TreeDocument::NodeElement TreeDocument::NodeElement::getFirstNode( - const std::string & registration_name, const std::string & instance_name) const + const std::string & registration_name, const std::string & instance_name, bool deep_search) const { if (registration_name.empty() && instance_name.empty()) return NodeElement(doc_ptr_, ele_ptr_->FirstChildElement()); - // If name is given, recursively search for the first node with this name ConstDeepApplyCallback apply = [®istration_name, &instance_name](const NodeElement & ele) { if (registration_name.empty()) return ele.getName() == instance_name; if (instance_name.empty()) return ele.getRegistrationName() == registration_name; return ele.getRegistrationName() == registration_name && ele.getName() == instance_name; }; - if (const std::vector found = deepApplyConst(apply); !found.empty()) return found[0]; + + if (deep_search) { + if (const std::vector found = deepApplyConst(apply); !found.empty()) return found[0]; + } else { + for (auto child : *this) { + if (apply(child)) return child; + } + } // Cannot find node in children of this throw exceptions::TreeDocumentError( @@ -355,9 +361,10 @@ TreeDocument::NodeElement TreeDocument::NodeElement::getFirstNode( } TreeDocument::NodeElement & TreeDocument::NodeElement::removeFirstChild( - const std::string & registration_name, const std::string & instance_name) + const std::string & registration_name, const std::string & instance_name, bool deep_search) { - ele_ptr_->DeleteChild(getFirstNode(registration_name, instance_name).ele_ptr_); + XMLElement * found = getFirstNode(registration_name, instance_name, deep_search).ele_ptr_; + found->Parent()->DeleteChild(found); return *this; } @@ -529,6 +536,48 @@ void TreeDocument::NodeElement::deepApplyImpl( } } +TreeDocument::NodeElement::ChildIterator::ChildIterator() : doc_ptr_(nullptr), current_(nullptr) {} + +TreeDocument::NodeElement::ChildIterator::ChildIterator(TreeDocument * doc_ptr, tinyxml2::XMLElement * current) +: doc_ptr_(doc_ptr), current_(current) +{ +} + +TreeDocument::NodeElement::ChildIterator::value_type TreeDocument::NodeElement::ChildIterator::operator*() const +{ + return NodeElement(doc_ptr_, current_); +} + +TreeDocument::NodeElement::ChildIterator & TreeDocument::NodeElement::ChildIterator::operator++() +{ + if (current_) current_ = current_->NextSiblingElement(); + return *this; +} + +TreeDocument::NodeElement::ChildIterator TreeDocument::NodeElement::ChildIterator::operator++(int) +{ + ChildIterator tmp = *this; + ++(*this); + return tmp; +} + +bool TreeDocument::NodeElement::ChildIterator::operator==(const ChildIterator & other) const +{ + return current_ == other.current_; +} + +bool TreeDocument::NodeElement::ChildIterator::operator!=(const ChildIterator & other) const +{ + return current_ != other.current_; +} + +TreeDocument::NodeElement::ChildIterator TreeDocument::NodeElement::begin() const +{ + return ChildIterator(doc_ptr_, ele_ptr_->FirstChildElement()); +} + +TreeDocument::NodeElement::ChildIterator TreeDocument::NodeElement::end() const { return ChildIterator(); } + TreeDocument::TreeElement::TreeElement(TreeDocument * doc_ptr, XMLElement * ele_ptr) : NodeElement(doc_ptr, ele_ptr) { if (!ele_ptr->Attribute(TREE_NAME_ATTRIBUTE_NAME)) { @@ -603,9 +652,9 @@ std::string TreeDocument::TreeElement::writeToString() const } TreeDocument::TreeElement & TreeDocument::TreeElement::removeFirstChild( - const std::string & registration_name, const std::string & instance_name) + const std::string & registration_name, const std::string & instance_name, bool deep_search) { - NodeElement::removeFirstChild(registration_name, instance_name); + NodeElement::removeFirstChild(registration_name, instance_name, deep_search); return *this; } diff --git a/auto_apms_behavior_tree_core/test/unit/testable_tree_document.hpp b/auto_apms_behavior_tree_core/test/unit/testable_tree_document.hpp new file mode 100644 index 0000000..a7e1d6f --- /dev/null +++ b/auto_apms_behavior_tree_core/test/unit/testable_tree_document.hpp @@ -0,0 +1,54 @@ +// Copyright 2026 Robin Müller +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "auto_apms_behavior_tree_core/tree/tree_document.hpp" + +/** + * @brief A testable TreeDocument that allows directly adding nodes to the internal manifest + * without going through the plugin loader validation. + */ +class TestableTreeDocument : public auto_apms_behavior_tree::core::TreeDocument +{ +public: + using TreeDocument::TreeDocument; + + /** + * @brief Add a test node to the internal manifest and factory. + * This bypasses the plugin loader check that registerNodes performs. + */ + TestableTreeDocument & addTestNode(const std::string & node_name, const std::string & class_name = "test::TestClass") + { + // Add to internal manifest (accessible since it's now protected) + auto_apms_behavior_tree::core::NodeManifest::RegistrationOptions opts; + opts.class_name = class_name; + registered_nodes_manifest_.add(node_name, opts); + + // Also register with factory so the document can work with these nodes + if (factory_.builtinNodes().count(node_name) == 0) { + factory_.registerSimpleAction(node_name, [](BT::TreeNode &) { return BT::NodeStatus::SUCCESS; }); + } + + return *this; + } + + /** + * @brief Get the set of registered node names (non-native only) + */ + std::set getTestNodeNames() const { return getRegisteredNodeNames(false); } +}; diff --git a/auto_apms_behavior_tree_core/test/unit/tree_document_node_element.cpp b/auto_apms_behavior_tree_core/test/unit/tree_document_node_element.cpp new file mode 100644 index 0000000..07d8594 --- /dev/null +++ b/auto_apms_behavior_tree_core/test/unit/tree_document_node_element.cpp @@ -0,0 +1,681 @@ +// Copyright 2026 Robin Müller +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include + +#include "auto_apms_behavior_tree_core/exceptions.hpp" +#include "testable_tree_document.hpp" + +using namespace auto_apms_behavior_tree::core; +using namespace auto_apms_behavior_tree; + +// ============================================================================= +// Test Fixture +// ============================================================================= + +class NodeElementChildIteratorTest : public ::testing::Test +{ +protected: + void SetUp() override + { + doc_ = std::make_unique(); + doc_->addTestNode("TestAction1"); + doc_->addTestNode("TestAction2"); + doc_->addTestNode("TestAction3"); + } + + std::unique_ptr doc_; +}; + +// ============================================================================= +// ChildIterator Tests +// ============================================================================= + +TEST_F(NodeElementChildIteratorTest, EmptyNodeBeginEqualsEnd) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + + // A node with no children should have begin() == end() + EXPECT_EQ(sequence.begin(), sequence.end()); +} + +TEST_F(NodeElementChildIteratorTest, EmptyTreeBeginEqualsEnd) +{ + auto tree = doc_->newTree("TestTree"); + + // An empty tree element should have begin() == end() + EXPECT_EQ(tree.begin(), tree.end()); +} + +TEST_F(NodeElementChildIteratorTest, NonEmptyNodeBeginNotEqualsEnd) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + + EXPECT_NE(sequence.begin(), sequence.end()); +} + +TEST_F(NodeElementChildIteratorTest, IterateSingleChild) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + + std::vector names; + for (auto child : sequence) { + names.push_back(child.getRegistrationName()); + } + + ASSERT_EQ(names.size(), 1u); + EXPECT_EQ(names[0], "TestAction1"); +} + +TEST_F(NodeElementChildIteratorTest, IterateMultipleChildren) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + sequence.insertNode("TestAction2"); + sequence.insertNode("TestAction3"); + + std::vector names; + for (auto child : sequence) { + names.push_back(child.getRegistrationName()); + } + + ASSERT_EQ(names.size(), 3u); + EXPECT_EQ(names[0], "TestAction1"); + EXPECT_EQ(names[1], "TestAction2"); + EXPECT_EQ(names[2], "TestAction3"); +} + +TEST_F(NodeElementChildIteratorTest, IterateFirstLevelOnly) +{ + auto tree = doc_->newTree("TestTree"); + auto outer_seq = tree.insertNode("Sequence"); + auto inner_seq = outer_seq.insertNode("Sequence"); + inner_seq.insertNode("TestAction1"); + inner_seq.insertNode("TestAction2"); + outer_seq.insertNode("TestAction3"); + + // Iterating over outer_seq should only yield the inner Sequence and TestAction3, + // NOT the children of inner Sequence + std::vector names; + for (auto child : outer_seq) { + names.push_back(child.getRegistrationName()); + } + + ASSERT_EQ(names.size(), 2u); + EXPECT_EQ(names[0], "Sequence"); + EXPECT_EQ(names[1], "TestAction3"); +} + +TEST_F(NodeElementChildIteratorTest, IterateTreeElement) +{ + auto tree = doc_->newTree("TestTree"); + tree.insertNode("Sequence"); + + // TreeElement inherits from NodeElement, so begin()/end() should work + std::vector names; + for (auto child : tree) { + names.push_back(child.getRegistrationName()); + } + + ASSERT_EQ(names.size(), 1u); + EXPECT_EQ(names[0], "Sequence"); +} + +TEST_F(NodeElementChildIteratorTest, PreIncrementOperator) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + sequence.insertNode("TestAction2"); + + auto it = sequence.begin(); + EXPECT_EQ((*it).getRegistrationName(), "TestAction1"); + + auto & ref = ++it; + EXPECT_EQ((*it).getRegistrationName(), "TestAction2"); + // Pre-increment returns reference to self + EXPECT_EQ(&ref, &it); + + ++it; + EXPECT_EQ(it, sequence.end()); +} + +TEST_F(NodeElementChildIteratorTest, PostIncrementOperator) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + sequence.insertNode("TestAction2"); + + auto it = sequence.begin(); + auto prev = it++; + + // prev should still point to the first child + EXPECT_EQ((*prev).getRegistrationName(), "TestAction1"); + // it should have advanced + EXPECT_EQ((*it).getRegistrationName(), "TestAction2"); +} + +TEST_F(NodeElementChildIteratorTest, EqualityBetweenIterators) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + + auto it1 = sequence.begin(); + auto it2 = sequence.begin(); + + EXPECT_EQ(it1, it2); + EXPECT_FALSE(it1 != it2); +} + +TEST_F(NodeElementChildIteratorTest, InequalityBetweenIterators) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + sequence.insertNode("TestAction2"); + + auto it1 = sequence.begin(); + auto it2 = sequence.begin(); + ++it2; + + EXPECT_NE(it1, it2); + EXPECT_FALSE(it1 == it2); +} + +TEST_F(NodeElementChildIteratorTest, DefaultConstructedIteratorsAreEqual) +{ + TreeDocument::NodeElement::ChildIterator it1; + TreeDocument::NodeElement::ChildIterator it2; + + EXPECT_EQ(it1, it2); +} + +TEST_F(NodeElementChildIteratorTest, DefaultConstructedIteratorEqualsEnd) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + + TreeDocument::NodeElement::ChildIterator default_it; + EXPECT_EQ(default_it, sequence.end()); +} + +TEST_F(NodeElementChildIteratorTest, IteratorPreservesChildIdentity) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + auto child = sequence.insertNode("TestAction1"); + child.setName("my_instance"); + + auto it = sequence.begin(); + auto iterated_child = *it; + + EXPECT_EQ(iterated_child.getRegistrationName(), child.getRegistrationName()); + EXPECT_EQ(iterated_child.getName(), "my_instance"); + EXPECT_EQ(iterated_child, child); +} + +TEST_F(NodeElementChildIteratorTest, IterateConstNodeElement) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + sequence.insertNode("TestAction2"); + + // Obtain a const reference + const TreeDocument::NodeElement & const_seq = sequence; + + std::vector names; + for (auto child : const_seq) { + names.push_back(child.getRegistrationName()); + } + + ASSERT_EQ(names.size(), 2u); + EXPECT_EQ(names[0], "TestAction1"); + EXPECT_EQ(names[1], "TestAction2"); +} + +TEST_F(NodeElementChildIteratorTest, ManualIteratorLoop) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + sequence.insertNode("TestAction2"); + sequence.insertNode("TestAction3"); + + // Classic iterator loop + int count = 0; + for (auto it = sequence.begin(); it != sequence.end(); ++it) { + ++count; + } + + EXPECT_EQ(count, 3); +} + +// ============================================================================= +// getFirstNode Tests +// ============================================================================= + +class NodeElementGetFirstNodeTest : public ::testing::Test +{ +protected: + void SetUp() override + { + doc_ = std::make_unique(); + doc_->addTestNode("TestAction1"); + doc_->addTestNode("TestAction2"); + doc_->addTestNode("TestAction3"); + } + + std::unique_ptr doc_; +}; + +TEST_F(NodeElementGetFirstNodeTest, DefaultSearchFindsDirectChild) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + sequence.insertNode("TestAction2"); + + auto found = sequence.getFirstNode("TestAction2"); + EXPECT_EQ(found.getRegistrationName(), "TestAction2"); +} + +TEST_F(NodeElementGetFirstNodeTest, DefaultSearchDoesNotFindGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode("Sequence"); + auto inner = outer.insertNode("Sequence"); + inner.insertNode("TestAction1"); + + // TestAction1 is a grandchild of outer — default (shallow) search should not find it + EXPECT_THROW(outer.getFirstNode("TestAction1"), exceptions::TreeDocumentError); +} + +TEST_F(NodeElementGetFirstNodeTest, DeepSearchFindsGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode("Sequence"); + auto inner = outer.insertNode("Sequence"); + inner.insertNode("TestAction1"); + + // With deep_search=true, grandchild should be found + auto found = outer.getFirstNode("TestAction1", "", true); + EXPECT_EQ(found.getRegistrationName(), "TestAction1"); +} + +TEST_F(NodeElementGetFirstNodeTest, DeepSearchFindsDirectChildToo) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + + auto found = sequence.getFirstNode("TestAction1", "", true); + EXPECT_EQ(found.getRegistrationName(), "TestAction1"); +} + +TEST_F(NodeElementGetFirstNodeTest, DefaultSearchByInstanceName) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + auto child = sequence.insertNode("TestAction1"); + child.setName("my_node"); + + auto found = sequence.getFirstNode("", "my_node"); + EXPECT_EQ(found.getName(), "my_node"); +} + +TEST_F(NodeElementGetFirstNodeTest, DefaultSearchByInstanceNameDoesNotFindGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode("Sequence"); + auto inner = outer.insertNode("Sequence"); + auto grandchild = inner.insertNode("TestAction1"); + grandchild.setName("deep_node"); + + EXPECT_THROW(outer.getFirstNode("", "deep_node"), exceptions::TreeDocumentError); +} + +TEST_F(NodeElementGetFirstNodeTest, DeepSearchByInstanceNameFindsGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode("Sequence"); + auto inner = outer.insertNode("Sequence"); + auto grandchild = inner.insertNode("TestAction1"); + grandchild.setName("deep_node"); + + auto found = outer.getFirstNode("", "deep_node", true); + EXPECT_EQ(found.getName(), "deep_node"); +} + +TEST_F(NodeElementGetFirstNodeTest, DefaultSearchByRegistrationAndInstanceName) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + auto child1 = sequence.insertNode("TestAction1"); + child1.setName("first"); + auto child2 = sequence.insertNode("TestAction1"); + child2.setName("second"); + + auto found = sequence.getFirstNode("TestAction1", "second"); + EXPECT_EQ(found.getName(), "second"); +} + +TEST_F(NodeElementGetFirstNodeTest, DefaultSearchNoArgsReturnsFirstChild) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + sequence.insertNode("TestAction2"); + + // Both names empty: returns the first child regardless + auto found = sequence.getFirstNode(); + EXPECT_EQ(found.getRegistrationName(), "TestAction1"); +} + +TEST_F(NodeElementGetFirstNodeTest, DefaultSearchThrowsWhenNotFound) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + + EXPECT_THROW(sequence.getFirstNode("TestAction2"), exceptions::TreeDocumentError); +} + +TEST_F(NodeElementGetFirstNodeTest, DeepSearchThrowsWhenNotFound) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + + EXPECT_THROW(sequence.getFirstNode("TestAction2", "", true), exceptions::TreeDocumentError); +} + +TEST_F(NodeElementGetFirstNodeTest, DeepSearchReturnsFirstInExecutionOrder) +{ + // Build a tree: + // outer_seq + // inner_seq + // TestAction1 (name="deep") + // TestAction1 (name="shallow") + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode("Sequence"); + auto inner = outer.insertNode("Sequence"); + auto deep = inner.insertNode("TestAction1"); + deep.setName("deep"); + auto shallow = outer.insertNode("TestAction1"); + shallow.setName("shallow"); + + // Deep search follows execution order (depth-first), so "deep" comes first + auto found = outer.getFirstNode("TestAction1", "", true); + EXPECT_EQ(found.getName(), "deep"); + + // Shallow search should find "shallow" since it's the only direct child with that registration name + // (the inner Sequence is a direct child, but not a TestAction1) + // Wait - actually inner is Sequence, and shallow is TestAction1. So shallow search finds shallow. + auto found_shallow = outer.getFirstNode("TestAction1"); + EXPECT_EQ(found_shallow.getName(), "shallow"); +} + +TEST_F(NodeElementGetFirstNodeTest, DefaultSearchOnTreeElement) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + + auto found = tree.getFirstNode("Sequence"); + EXPECT_EQ(found.getRegistrationName(), "Sequence"); +} + +TEST_F(NodeElementGetFirstNodeTest, DefaultSearchOnTreeElementDoesNotFindGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + + // TestAction1 is a grandchild of the tree element — shallow search should not find it + EXPECT_THROW(tree.getFirstNode("TestAction1"), exceptions::TreeDocumentError); +} + +TEST_F(NodeElementGetFirstNodeTest, DeepSearchOnTreeElementFindsGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + + auto found = tree.getFirstNode("TestAction1", "", true); + EXPECT_EQ(found.getRegistrationName(), "TestAction1"); +} + +// ============================================================================= +// removeFirstChild Tests +// ============================================================================= + +class NodeElementRemoveFirstChildTest : public ::testing::Test +{ +protected: + void SetUp() override + { + doc_ = std::make_unique(); + doc_->addTestNode("TestAction1"); + doc_->addTestNode("TestAction2"); + doc_->addTestNode("TestAction3"); + } + + std::unique_ptr doc_; +}; + +TEST_F(NodeElementRemoveFirstChildTest, DefaultRemovesDirectChild) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + sequence.insertNode("TestAction2"); + + sequence.removeFirstChild("TestAction1"); + + // Only TestAction2 should remain + std::vector names; + for (auto child : sequence) { + names.push_back(child.getRegistrationName()); + } + ASSERT_EQ(names.size(), 1u); + EXPECT_EQ(names[0], "TestAction2"); +} + +TEST_F(NodeElementRemoveFirstChildTest, DefaultDoesNotRemoveGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode("Sequence"); + auto inner = outer.insertNode("Sequence"); + inner.insertNode("TestAction1"); + + // TestAction1 is a grandchild — shallow remove should throw + EXPECT_THROW(outer.removeFirstChild("TestAction1"), exceptions::TreeDocumentError); + + // Verify nothing was removed + EXPECT_TRUE(inner.hasChildren()); +} + +TEST_F(NodeElementRemoveFirstChildTest, DeepSearchRemovesGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode("Sequence"); + auto inner = outer.insertNode("Sequence"); + inner.insertNode("TestAction1"); + + outer.removeFirstChild("TestAction1", "", true); + + // inner should now have no children + EXPECT_FALSE(inner.hasChildren()); + // outer should still have inner + EXPECT_TRUE(outer.hasChildren()); +} + +TEST_F(NodeElementRemoveFirstChildTest, DeepSearchRemovesFirstInExecutionOrder) +{ + // Build: + // outer + // inner + // TestAction1 (name="deep") + // TestAction1 (name="shallow") + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode("Sequence"); + auto inner = outer.insertNode("Sequence"); + auto deep = inner.insertNode("TestAction1"); + deep.setName("deep"); + auto shallow = outer.insertNode("TestAction1"); + shallow.setName("shallow"); + + // Deep remove should remove "deep" (depth-first order) + outer.removeFirstChild("TestAction1", "", true); + + EXPECT_FALSE(inner.hasChildren()); + // "shallow" should still be there + auto remaining = outer.getFirstNode("TestAction1"); + EXPECT_EQ(remaining.getName(), "shallow"); +} + +TEST_F(NodeElementRemoveFirstChildTest, DefaultRemovesByInstanceName) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + auto child1 = sequence.insertNode("TestAction1"); + child1.setName("keep"); + auto child2 = sequence.insertNode("TestAction2"); + child2.setName("remove_me"); + + sequence.removeFirstChild("", "remove_me"); + + std::vector names; + for (auto child : sequence) { + names.push_back(child.getName()); + } + ASSERT_EQ(names.size(), 1u); + EXPECT_EQ(names[0], "keep"); +} + +TEST_F(NodeElementRemoveFirstChildTest, DefaultRemovesByRegistrationAndInstanceName) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + auto child1 = sequence.insertNode("TestAction1"); + child1.setName("first"); + auto child2 = sequence.insertNode("TestAction1"); + child2.setName("second"); + + sequence.removeFirstChild("TestAction1", "second"); + + std::vector names; + for (auto child : sequence) { + names.push_back(child.getName()); + } + ASSERT_EQ(names.size(), 1u); + EXPECT_EQ(names[0], "first"); +} + +TEST_F(NodeElementRemoveFirstChildTest, DefaultThrowsWhenNotFound) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + + EXPECT_THROW(sequence.removeFirstChild("TestAction2"), exceptions::TreeDocumentError); +} + +TEST_F(NodeElementRemoveFirstChildTest, DeepSearchThrowsWhenNotFound) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + + EXPECT_THROW(sequence.removeFirstChild("TestAction2", "", true), exceptions::TreeDocumentError); +} + +TEST_F(NodeElementRemoveFirstChildTest, DefaultRemoveNoArgsRemovesFirstChild) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + sequence.insertNode("TestAction2"); + + sequence.removeFirstChild(); + + std::vector names; + for (auto child : sequence) { + names.push_back(child.getRegistrationName()); + } + ASSERT_EQ(names.size(), 1u); + EXPECT_EQ(names[0], "TestAction2"); +} + +TEST_F(NodeElementRemoveFirstChildTest, ReturnsSelfForChaining) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + sequence.insertNode("TestAction2"); + + auto & ref = sequence.removeFirstChild("TestAction1"); + EXPECT_EQ(&ref, &sequence); +} + +TEST_F(NodeElementRemoveFirstChildTest, TreeElementRemoveFirstChildDefaultShallow) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + + // Sequence is a direct child of tree — removable with shallow search + tree.removeFirstChild("Sequence"); + + EXPECT_FALSE(tree.hasChildren()); +} + +TEST_F(NodeElementRemoveFirstChildTest, TreeElementRemoveFirstChildDoesNotFindGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + + // TestAction1 is a grandchild of tree — shallow remove should throw + EXPECT_THROW(tree.removeFirstChild("TestAction1"), exceptions::TreeDocumentError); +} + +TEST_F(NodeElementRemoveFirstChildTest, TreeElementRemoveFirstChildDeepSearch) +{ + auto tree = doc_->newTree("TestTree"); + auto sequence = tree.insertNode("Sequence"); + sequence.insertNode("TestAction1"); + sequence.insertNode("TestAction2"); + + tree.removeFirstChild("TestAction1", "", true); + + // TestAction1 should be gone, TestAction2 and the Sequence should remain + EXPECT_TRUE(tree.hasChildren()); + std::vector names; + for (auto child : sequence) { + names.push_back(child.getRegistrationName()); + } + ASSERT_EQ(names.size(), 1u); + EXPECT_EQ(names[0], "TestAction2"); +} diff --git a/auto_apms_behavior_tree_core/test/unit/tree_document_tree_generation.cpp b/auto_apms_behavior_tree_core/test/unit/tree_document_tree_generation.cpp index 5176bf9..30ed4cd 100644 --- a/auto_apms_behavior_tree_core/test/unit/tree_document_tree_generation.cpp +++ b/auto_apms_behavior_tree_core/test/unit/tree_document_tree_generation.cpp @@ -16,49 +16,11 @@ #include "auto_apms_behavior_tree_core/exceptions.hpp" #include "auto_apms_behavior_tree_core/node/node_model_type.hpp" -#include "auto_apms_behavior_tree_core/tree/tree_document.hpp" +#include "testable_tree_document.hpp" using namespace auto_apms_behavior_tree::core; using namespace auto_apms_behavior_tree; -// ============================================================================= -// Testable TreeDocument subclass for testing internal behavior -// ============================================================================= - -/** - * @brief A testable TreeDocument that allows directly adding nodes to the internal manifest - * without going through the plugin loader validation. - */ -class TestableTreeDocument : public TreeDocument -{ -public: - using TreeDocument::TreeDocument; - - /** - * @brief Add a test node to the internal manifest and factory. - * This bypasses the plugin loader check that registerNodes performs. - */ - TestableTreeDocument & addTestNode(const std::string & node_name, const std::string & class_name = "test::TestClass") - { - // Add to internal manifest (accessible since it's now protected) - NodeManifest::RegistrationOptions opts; - opts.class_name = class_name; - registered_nodes_manifest_.add(node_name, opts); - - // Also register with factory so the document can work with these nodes - if (factory_.builtinNodes().count(node_name) == 0) { - factory_.registerSimpleAction(node_name, [](BT::TreeNode &) { return BT::NodeStatus::SUCCESS; }); - } - - return *this; - } - - /** - * @brief Get the set of registered node names (non-native only) - */ - std::set getTestNodeNames() const { return getRegisteredNodeNames(false); } -}; - // ============================================================================= // Test Fixture // ============================================================================= From 111bbc171e6ec51713e3239988a88927565f7984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20M=C3=BCller?= Date: Thu, 12 Feb 2026 15:56:06 +0100 Subject: [PATCH 2/4] Add tests for node model types passed as template argument --- auto_apms_behavior_tree/CMakeLists.txt | 1 + .../unit/tree_document_model_templates.cpp | 626 ++++++++++++++++++ .../node/node_model_type.hpp | 6 + 3 files changed, 633 insertions(+) create mode 100644 auto_apms_behavior_tree/test/unit/tree_document_model_templates.cpp diff --git a/auto_apms_behavior_tree/CMakeLists.txt b/auto_apms_behavior_tree/CMakeLists.txt index 751b3ad..7ecf2c5 100644 --- a/auto_apms_behavior_tree/CMakeLists.txt +++ b/auto_apms_behavior_tree/CMakeLists.txt @@ -318,6 +318,7 @@ if(BUILD_TESTING) ament_add_gtest(${PROJECT_NAME}_unit_tests "test/unit/node_registration.cpp" + "test/unit/tree_document_model_templates.cpp" ) target_link_libraries(${PROJECT_NAME}_unit_tests ${PROJECT_NAME}) endif() diff --git a/auto_apms_behavior_tree/test/unit/tree_document_model_templates.cpp b/auto_apms_behavior_tree/test/unit/tree_document_model_templates.cpp new file mode 100644 index 0000000..3786117 --- /dev/null +++ b/auto_apms_behavior_tree/test/unit/tree_document_model_templates.cpp @@ -0,0 +1,626 @@ +// Copyright 2026 Robin Müller +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include + +#include "auto_apms_behavior_tree/behavior_tree_nodes.hpp" +#include "auto_apms_behavior_tree_core/exceptions.hpp" +#include "auto_apms_behavior_tree_core/tree/tree_document.hpp" + +using namespace auto_apms_behavior_tree::core; +using namespace auto_apms_behavior_tree; + +// ============================================================================= +// Test Fixture +// ============================================================================= + +class TreeDocumentModelTemplateTest : public ::testing::Test +{ +protected: + void SetUp() override { doc_ = std::make_unique(); } + + std::unique_ptr doc_; +}; + +// ============================================================================= +// insertNode Tests +// ============================================================================= + +TEST_F(TreeDocumentModelTemplateTest, InsertControlNode) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + + EXPECT_EQ(seq.getRegistrationName(), "Sequence"); +} + +TEST_F(TreeDocumentModelTemplateTest, InsertDecoratorNode) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto inv = seq.insertNode(); + + EXPECT_EQ(inv.getRegistrationName(), "Inverter"); +} + +TEST_F(TreeDocumentModelTemplateTest, InsertLeafNode) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto success = seq.insertNode(); + + EXPECT_EQ(success.getRegistrationName(), "AlwaysSuccess"); +} + +TEST_F(TreeDocumentModelTemplateTest, InsertNodeWithRegistrationOptions) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto logger = seq.insertNode(); + + // Logger has registrationOptions(), so it should auto-register + EXPECT_EQ(logger.getRegistrationName(), "Logger"); +} + +TEST_F(TreeDocumentModelTemplateTest, InsertedNodeRetainsModelType) +{ + auto tree = doc_->newTree("TestTree"); + model::Sequence seq = tree.insertNode(); + model::AlwaysSuccess success = seq.insertNode(); + + // Verify the exact model type is returned (not just NodeElement) + EXPECT_EQ(success.getRegistrationName(), "AlwaysSuccess"); +} + +TEST_F(TreeDocumentModelTemplateTest, InsertMultipleChildrenWithModels) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + seq.insertNode(); + seq.insertNode(); + + std::vector names; + for (auto child : seq) { + names.push_back(child.getRegistrationName()); + } + + ASSERT_EQ(names.size(), 2u); + EXPECT_EQ(names[0], "AlwaysSuccess"); + EXPECT_EQ(names[1], "AlwaysFailure"); +} + +TEST_F(TreeDocumentModelTemplateTest, InsertNodeBeforeAnother) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto second = seq.insertNode(); + + // Insert AlwaysSuccess before AlwaysFailure + seq.insertNode(&second); + + std::vector names; + for (auto child : seq) { + names.push_back(child.getRegistrationName()); + } + + ASSERT_EQ(names.size(), 2u); + EXPECT_EQ(names[0], "AlwaysSuccess"); + EXPECT_EQ(names[1], "AlwaysFailure"); +} + +TEST_F(TreeDocumentModelTemplateTest, InsertNestedControlNodes) +{ + auto tree = doc_->newTree("TestTree"); + auto fallback = tree.insertNode(); + auto seq = fallback.insertNode(); + seq.insertNode(); + + EXPECT_EQ(fallback.getRegistrationName(), "Fallback"); + auto first = fallback.getFirstNode(); + EXPECT_EQ(first.getRegistrationName(), "Sequence"); +} + +// ============================================================================= +// insertNode Tests +// ============================================================================= + +TEST_F(TreeDocumentModelTemplateTest, InsertSubTreeNodeByName) +{ + auto main_tree = doc_->newTree("MainTree"); + auto sub_tree = doc_->newTree("SubTree"); + sub_tree.insertNode(); + + auto seq = main_tree.insertNode(); + auto subtree_node = seq.insertNode("SubTree"); + + EXPECT_EQ(subtree_node.getRegistrationName(), "SubTree"); +} + +TEST_F(TreeDocumentModelTemplateTest, InsertSubTreeNodeByTreeElement) +{ + auto sub_tree = doc_->newTree("SubTree"); + sub_tree.insertNode(); + + auto main_tree = doc_->newTree("MainTree"); + auto seq = main_tree.insertNode(); + auto subtree_node = seq.insertNode(sub_tree); + + EXPECT_EQ(subtree_node.getRegistrationName(), "SubTree"); +} + +// ============================================================================= +// Port Methods via Model Types Tests +// ============================================================================= + +TEST_F(TreeDocumentModelTemplateTest, SetPortsViaModelSetters) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto repeat = seq.insertNode(); + repeat.set_num_cycles(5); + + EXPECT_EQ(repeat.get_num_cycles(), 5); + EXPECT_EQ(repeat.get_num_cycles_str(), "5"); +} + +TEST_F(TreeDocumentModelTemplateTest, SetPortsViaStringSetters) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto repeat = seq.insertNode(); + repeat.set_num_cycles("10"); + + EXPECT_EQ(repeat.get_num_cycles_str(), "10"); + EXPECT_EQ(repeat.get_num_cycles(), 10); +} + +TEST_F(TreeDocumentModelTemplateTest, ModelMethodChaining) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + + // Model setters return *this for chaining + auto repeat = seq.insertNode(); + auto & ref = repeat.set_num_cycles(3); + + EXPECT_EQ(ref.get_num_cycles(), 3); + EXPECT_EQ(&ref, &repeat); +} + +TEST_F(TreeDocumentModelTemplateTest, SetAndGetMultiplePorts) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto parallel = seq.insertNode(); + parallel.set_failure_count(2); + parallel.set_success_count(3); + + EXPECT_EQ(parallel.get_failure_count(), 2); + EXPECT_EQ(parallel.get_success_count(), 3); +} + +TEST_F(TreeDocumentModelTemplateTest, SetNameViaModel) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + seq.setName("my_sequence"); + + EXPECT_EQ(seq.getName(), "my_sequence"); +} + +TEST_F(TreeDocumentModelTemplateTest, SetPortsViaBaseMethod) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto repeat = seq.insertNode(); + repeat.setPorts({{"num_cycles", "7"}}); + + EXPECT_EQ(repeat.get_num_cycles(), 7); +} + +TEST_F(TreeDocumentModelTemplateTest, ResetPortsViaModel) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto repeat = seq.insertNode(); + repeat.set_num_cycles(5); + + repeat.resetPorts(); + + // After reset, the port should have its default value (or be empty) + auto ports = repeat.getPorts(); + EXPECT_TRUE(ports.find("num_cycles") == ports.end() || ports.at("num_cycles").empty()); +} + +// ============================================================================= +// getFirstNode Tests +// ============================================================================= + +TEST_F(TreeDocumentModelTemplateTest, GetFirstNodeByModelType) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + seq.insertNode(); + seq.insertNode(); + + auto found = seq.getFirstNode(); + EXPECT_EQ(found.getRegistrationName(), "AlwaysSuccess"); +} + +TEST_F(TreeDocumentModelTemplateTest, GetFirstNodeByModelTypeReturnsFirst) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto first = seq.insertNode(); + first.setName("first_instance"); + auto second = seq.insertNode(); + second.setName("second_instance"); + + auto found = seq.getFirstNode(); + EXPECT_EQ(found.getName(), "first_instance"); +} + +TEST_F(TreeDocumentModelTemplateTest, GetFirstNodeByModelTypeAndInstanceName) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto first = seq.insertNode(); + first.setName("first_instance"); + auto second = seq.insertNode(); + second.setName("second_instance"); + + auto found = seq.getFirstNode("second_instance"); + EXPECT_EQ(found.getName(), "second_instance"); +} + +TEST_F(TreeDocumentModelTemplateTest, GetFirstNodeByModelTypeThrowsWhenNotFound) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + seq.insertNode(); + + EXPECT_THROW(seq.getFirstNode(), exceptions::TreeDocumentError); +} + +TEST_F(TreeDocumentModelTemplateTest, GetFirstNodeByModelTypeShallowDoesNotFindGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode(); + auto inner = outer.insertNode(); + inner.insertNode(); + + // AlwaysSuccess is a grandchild — shallow search should not find it + EXPECT_THROW(outer.getFirstNode(), exceptions::TreeDocumentError); +} + +TEST_F(TreeDocumentModelTemplateTest, GetFirstNodeByModelTypeDeepSearchFindsGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode(); + auto inner = outer.insertNode(); + inner.insertNode(); + + auto found = outer.getFirstNode("", true); + EXPECT_EQ(found.getRegistrationName(), "AlwaysSuccess"); +} + +TEST_F(TreeDocumentModelTemplateTest, GetFirstNodeByModelTypeDeepSearchReturnsFirstInOrder) +{ + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode(); + auto inner = outer.insertNode(); + auto deep = inner.insertNode(); + deep.setName("deep"); + auto shallow = outer.insertNode(); + shallow.setName("shallow"); + + auto found = outer.getFirstNode("", true); + EXPECT_EQ(found.getName(), "deep"); +} + +TEST_F(TreeDocumentModelTemplateTest, GetFirstNodeReturnsCorrectModelType) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto repeat = seq.insertNode(); + repeat.set_num_cycles(42); + + // getFirstNode should return a model::Repeat, not just NodeElement + model::Repeat found = seq.getFirstNode(); + EXPECT_EQ(found.get_num_cycles(), 42); +} + +TEST_F(TreeDocumentModelTemplateTest, GetFirstNodeOnTreeElement) +{ + auto tree = doc_->newTree("TestTree"); + tree.insertNode(); + + auto found = tree.getFirstNode(); + EXPECT_EQ(found.getRegistrationName(), "Sequence"); +} + +// ============================================================================= +// removeFirstChild Tests +// ============================================================================= + +TEST_F(TreeDocumentModelTemplateTest, RemoveFirstChildByModelType) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + seq.insertNode(); + seq.insertNode(); + + seq.removeFirstChild(); + + std::vector names; + for (auto child : seq) { + names.push_back(child.getRegistrationName()); + } + ASSERT_EQ(names.size(), 1u); + EXPECT_EQ(names[0], "AlwaysFailure"); +} + +TEST_F(TreeDocumentModelTemplateTest, RemoveFirstChildByModelTypeRemovesFirst) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto first = seq.insertNode(); + first.setName("first"); + auto second = seq.insertNode(); + second.setName("second"); + + seq.removeFirstChild(); + + auto remaining = seq.getFirstNode(); + EXPECT_EQ(remaining.getName(), "second"); +} + +TEST_F(TreeDocumentModelTemplateTest, RemoveFirstChildByModelTypeAndInstanceName) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto first = seq.insertNode(); + first.setName("keep"); + auto second = seq.insertNode(); + second.setName("remove_me"); + + seq.removeFirstChild("remove_me"); + + auto remaining = seq.getFirstNode(); + EXPECT_EQ(remaining.getName(), "keep"); +} + +TEST_F(TreeDocumentModelTemplateTest, RemoveFirstChildByModelTypeThrowsWhenNotFound) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + seq.insertNode(); + + EXPECT_THROW(seq.removeFirstChild(), exceptions::TreeDocumentError); +} + +TEST_F(TreeDocumentModelTemplateTest, RemoveFirstChildByModelTypeShallowDoesNotRemoveGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode(); + auto inner = outer.insertNode(); + inner.insertNode(); + + EXPECT_THROW(outer.removeFirstChild(), exceptions::TreeDocumentError); + EXPECT_TRUE(inner.hasChildren()); +} + +TEST_F(TreeDocumentModelTemplateTest, RemoveFirstChildByModelTypeDeepSearchRemovesGrandchild) +{ + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode(); + auto inner = outer.insertNode(); + inner.insertNode(); + + outer.removeFirstChild("", true); + + EXPECT_FALSE(inner.hasChildren()); + EXPECT_TRUE(outer.hasChildren()); // inner Fallback is still there +} + +TEST_F(TreeDocumentModelTemplateTest, RemoveFirstChildByModelTypeDeepSearchRemovesFirstInOrder) +{ + auto tree = doc_->newTree("TestTree"); + auto outer = tree.insertNode(); + auto inner = outer.insertNode(); + auto deep = inner.insertNode(); + deep.setName("deep"); + auto shallow = outer.insertNode(); + shallow.setName("shallow"); + + outer.removeFirstChild("", true); + + EXPECT_FALSE(inner.hasChildren()); + auto remaining = outer.getFirstNode(); + EXPECT_EQ(remaining.getName(), "shallow"); +} + +TEST_F(TreeDocumentModelTemplateTest, RemoveFirstChildReturnsSelfForChaining) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + seq.insertNode(); + seq.insertNode(); + + auto & ref = seq.removeFirstChild(); + EXPECT_EQ(&ref, &seq); +} + +TEST_F(TreeDocumentModelTemplateTest, TreeElementRemoveFirstChildByModelType) +{ + auto tree = doc_->newTree("TestTree"); + tree.insertNode(); + + tree.removeFirstChild(); + + EXPECT_FALSE(tree.hasChildren()); +} + +TEST_F(TreeDocumentModelTemplateTest, TreeElementRemoveFirstChildByModelTypeDeepSearch) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + seq.insertNode(); + seq.insertNode(); + + tree.removeFirstChild("", true); + + std::vector names; + for (auto child : seq) { + names.push_back(child.getRegistrationName()); + } + ASSERT_EQ(names.size(), 1u); + EXPECT_EQ(names[0], "AlwaysFailure"); +} + +TEST_F(TreeDocumentModelTemplateTest, TreeElementRemoveFirstChildReturnsSelf) +{ + auto tree = doc_->newTree("TestTree"); + tree.insertNode(); + + auto & ref = tree.removeFirstChild(); + EXPECT_EQ(&ref, &tree); +} + +// ============================================================================= +// toNodeElement Tests +// ============================================================================= + +TEST_F(TreeDocumentModelTemplateTest, ModelToNodeElement) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + + TreeDocument::NodeElement ele = seq.toNodeElement(); + EXPECT_EQ(ele.getRegistrationName(), "Sequence"); +} + +TEST_F(TreeDocumentModelTemplateTest, ModelToNodeElementWithPorts) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto repeat = seq.insertNode(); + repeat.set_num_cycles(5); + + TreeDocument::NodeElement ele = repeat.toNodeElement(); + auto ports = ele.getPorts(); + EXPECT_EQ(ports.at("num_cycles"), "5"); +} + +// ============================================================================= +// Static Method Tests +// ============================================================================= + +TEST_F(TreeDocumentModelTemplateTest, ModelStaticNameMethod) +{ + EXPECT_EQ(model::Sequence::name(), "Sequence"); + EXPECT_EQ(model::Fallback::name(), "Fallback"); + EXPECT_EQ(model::Inverter::name(), "Inverter"); + EXPECT_EQ(model::AlwaysSuccess::name(), "AlwaysSuccess"); + EXPECT_EQ(model::AlwaysFailure::name(), "AlwaysFailure"); + EXPECT_EQ(model::Repeat::name(), "Repeat"); +} + +TEST_F(TreeDocumentModelTemplateTest, ModelStaticTypeMethod) +{ + EXPECT_EQ(model::Sequence::type(), BT::NodeType::CONTROL); + EXPECT_EQ(model::Fallback::type(), BT::NodeType::CONTROL); + EXPECT_EQ(model::Inverter::type(), BT::NodeType::DECORATOR); + EXPECT_EQ(model::AlwaysSuccess::type(), BT::NodeType::ACTION); + EXPECT_EQ(model::AlwaysFailure::type(), BT::NodeType::ACTION); + EXPECT_EQ(model::Repeat::type(), BT::NodeType::DECORATOR); +} + +// ============================================================================= +// Conditional Script via Model Tests +// ============================================================================= + +TEST_F(TreeDocumentModelTemplateTest, SetConditionalScriptViaModel) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + auto success = seq.insertNode(); + + success.setConditionalScript(BT::PreCond::FAILURE_IF, Script("port_val == true")); + + // Verify the node is still valid after setting the script + EXPECT_EQ(success.getRegistrationName(), "AlwaysSuccess"); +} + +// ============================================================================= +// Mixed Template and Non-Template API Tests +// ============================================================================= + +TEST_F(TreeDocumentModelTemplateTest, InsertWithTemplateGetWithString) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + seq.insertNode(); + + // Use non-template getFirstNode with string name + auto found = seq.getFirstNode("AlwaysSuccess"); + EXPECT_EQ(found.getRegistrationName(), "AlwaysSuccess"); +} + +TEST_F(TreeDocumentModelTemplateTest, InsertWithStringGetWithTemplate) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + seq.insertNode("AlwaysSuccess"); + + // Use template getFirstNode + auto found = seq.getFirstNode(); + EXPECT_EQ(found.getRegistrationName(), "AlwaysSuccess"); +} + +TEST_F(TreeDocumentModelTemplateTest, InsertWithTemplateRemoveWithString) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + seq.insertNode(); + seq.insertNode(); + + seq.removeFirstChild("AlwaysSuccess"); + + std::vector names; + for (auto child : seq) { + names.push_back(child.getRegistrationName()); + } + ASSERT_EQ(names.size(), 1u); + EXPECT_EQ(names[0], "AlwaysFailure"); +} + +TEST_F(TreeDocumentModelTemplateTest, InsertWithStringRemoveWithTemplate) +{ + auto tree = doc_->newTree("TestTree"); + auto seq = tree.insertNode(); + seq.insertNode("AlwaysSuccess"); + seq.insertNode("AlwaysFailure"); + + seq.removeFirstChild(); + + std::vector names; + for (auto child : seq) { + names.push_back(child.getRegistrationName()); + } + ASSERT_EQ(names.size(), 1u); + EXPECT_EQ(names[0], "AlwaysFailure"); +} diff --git a/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/node/node_model_type.hpp b/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/node/node_model_type.hpp index c85ff25..3a8e82a 100644 --- a/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/node/node_model_type.hpp +++ b/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/node/node_model_type.hpp @@ -121,6 +121,12 @@ class LeafNodeModelType : public NodeModelType LeafNodeModelType insertTreeFromResource() = delete; LeafNodeModelType & removeFirstChild() = delete; LeafNodeModelType & removeChildren() = delete; + bool hasChildren() const = delete; + NodeElement getFirstNode() const = delete; + ChildIterator begin() const = delete; + ChildIterator end() const = delete; + std::vector deepApplyConst() = delete; + std::vector deepApply() = delete; }; } // namespace core From a26a726bafe1f3175d5a7170d6348ac92b6a3c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20M=C3=BCller?= Date: Thu, 12 Feb 2026 16:04:19 +0100 Subject: [PATCH 3/4] Add getBlackboardRemapping to SubTree model --- .../node/node_model_type.hpp | 8 +++++++- .../src/node/node_model_type.cpp | 13 ++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/node/node_model_type.hpp b/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/node/node_model_type.hpp index 3a8e82a..ab500b2 100644 --- a/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/node/node_model_type.hpp +++ b/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/node/node_model_type.hpp @@ -193,7 +193,13 @@ class SubTree : public core::LeafNodeModelType * @param remapping Mapping of blackboard entry names in the format {subtree_entry_name: original_tree_entry_name} * @return Modified subtree model. */ - SubTree & setBlackboardRemapping(const PortValues & remapping); + SubTree & setBlackboardRemapping(const std::map & remapping); + + /** + * @brief Get the currently configured blackboard remapping. + * @return Mapping of blackboard entry names in the format {subtree_entry_name: original_tree_entry_name} + */ + std::map getBlackboardRemapping() const; /** * @brief Set automatic blackboard remapping. diff --git a/auto_apms_behavior_tree_core/src/node/node_model_type.cpp b/auto_apms_behavior_tree_core/src/node/node_model_type.cpp index 8bf497b..4c2df55 100644 --- a/auto_apms_behavior_tree_core/src/node/node_model_type.cpp +++ b/auto_apms_behavior_tree_core/src/node/node_model_type.cpp @@ -36,7 +36,7 @@ std::string SubTree::name() { return core::TreeDocument::SUBTREE_ELEMENT_NAME; } std::string SubTree::getRegistrationName() const { return name(); } -SubTree & SubTree::setBlackboardRemapping(const PortValues & remapping) +SubTree & SubTree::setBlackboardRemapping(const std::map & remapping) { for (const auto & [key, val] : remapping) { if (!BT::TreeNode::isBlackboardPointer(val)) { @@ -50,6 +50,17 @@ SubTree & SubTree::setBlackboardRemapping(const PortValues & remapping) return *this; } +std::map SubTree::getBlackboardRemapping() const +{ + std::map remapping; + for (const tinyxml2::XMLAttribute * attr = ele_ptr_->FirstAttribute(); attr != nullptr; attr = attr->Next()) { + if (BT::TreeNode::isBlackboardPointer(attr->Value())) { + remapping[attr->Name()] = attr->Value(); + } + } + return remapping; +} + SubTree & SubTree::set_auto_remap(bool val) { return setPorts({{"_autoremap", BT::toStr(val)}}); } bool SubTree::get_auto_remap() const { return BT::convertFromString(getPorts().at("_autoremap")); } From 6bf515cb470b5d766f8bd2dfb404be2dc18961cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20M=C3=BCller?= Date: Thu, 12 Feb 2026 16:22:53 +0100 Subject: [PATCH 4/4] Add getXMLElement method to NodeElement for special use cases --- .../auto_apms_behavior_tree_core/tree/tree_document.hpp | 9 +++++++++ auto_apms_behavior_tree_core/src/tree/tree_document.cpp | 2 ++ 2 files changed, 11 insertions(+) diff --git a/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/tree/tree_document.hpp b/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/tree/tree_document.hpp index 4463b0d..58a70b8 100644 --- a/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/tree/tree_document.hpp +++ b/auto_apms_behavior_tree_core/include/auto_apms_behavior_tree_core/tree/tree_document.hpp @@ -744,6 +744,15 @@ class TreeDocument : private tinyxml2::XMLDocument */ const TreeDocument & getParentDocument() const; + /** + * @brief Get a pointer to the underlying `tinyxml2::XMLElement` of this node. + * + * @warning This method allows direct access to the underlying XML element for advanced use cases. It should be + * used with caution, as modifying the XML structure directly can lead to inconsistencies if not done carefully. + * @return Pointer to the underlying `tinyxml2::XMLElement` of this node. + */ + XMLElement * getXMLElement(); + /** * @brief Recursively apply a callback to this node's children. * diff --git a/auto_apms_behavior_tree_core/src/tree/tree_document.cpp b/auto_apms_behavior_tree_core/src/tree/tree_document.cpp index 946d116..b01f03f 100644 --- a/auto_apms_behavior_tree_core/src/tree/tree_document.cpp +++ b/auto_apms_behavior_tree_core/src/tree/tree_document.cpp @@ -456,6 +456,8 @@ std::string TreeDocument::NodeElement::getFullyQualifiedName() const const TreeDocument & TreeDocument::NodeElement::getParentDocument() const { return *doc_ptr_; } +TreeDocument::XMLElement * TreeDocument::NodeElement::getXMLElement() { return ele_ptr_; } + const std::vector TreeDocument::NodeElement::deepApplyConst( ConstDeepApplyCallback apply_callback) const {