@@ -757,3 +757,283 @@ TEST(SubTree, SubtreeNameNotRegistered)
757757 ASSERT_ANY_THROW (auto tree = factory.createTreeFromText (xml_text));
758758 ASSERT_ANY_THROW (factory.registerBehaviorTreeFromText (xml_text));
759759}
760+ TEST (SubTree, RecursiveSubtree)
761+ {
762+ // clang-format off
763+
764+ static const char * xml_text = R"(
765+ <root BTCPP_format="4" >
766+ <BehaviorTree ID="MainTree">
767+ <Sequence name="root">
768+ <AlwaysSuccess/>
769+ <SubTree ID="MainTree" />
770+ </Sequence>
771+ </BehaviorTree>
772+ </root>
773+ )" ;
774+
775+ // clang-format on
776+ BehaviorTreeFactory factory;
777+
778+ ASSERT_ANY_THROW (auto tree = factory.createTreeFromText (xml_text));
779+ }
780+
781+ TEST (SubTree, RecursiveCycle)
782+ {
783+ // clang-format off
784+
785+ static const char * xml_text = R"(
786+ <root BTCPP_format="4" main_tree_to_execute="MainTree">
787+ <BehaviorTree ID="MainTree">
788+ <Sequence name="root">
789+ <AlwaysSuccess/>
790+ <SubTree ID="TreeA" />
791+ </Sequence>
792+ </BehaviorTree>
793+
794+ <BehaviorTree ID="TreeA">
795+ <Sequence name="root">
796+ <AlwaysSuccess/>
797+ <SubTree ID="TreeB" />
798+ </Sequence>
799+ </BehaviorTree>
800+
801+ <BehaviorTree ID="TreeB">
802+ <Sequence name="root">
803+ <AlwaysSuccess/>
804+ <SubTree ID="MainTree" />
805+ </Sequence>
806+ </BehaviorTree>
807+ </root>
808+ )" ;
809+
810+ // clang-format on
811+ BehaviorTreeFactory factory;
812+
813+ ASSERT_ANY_THROW (auto tree = factory.createTreeFromText (xml_text));
814+ }
815+
816+ TEST (SubTree, SubstringTreeIDsAreNotRecursive)
817+ {
818+ // Verify that tree IDs which are substrings of each other do NOT
819+ // incorrectly trigger the recursive cycle detection.
820+ // clang-format off
821+
822+ static const char * xml_text = R"(
823+ <root BTCPP_format="4" main_tree_to_execute="Tree">
824+ <BehaviorTree ID="Tree">
825+ <SubTree ID="TreeABC" />
826+ </BehaviorTree>
827+
828+ <BehaviorTree ID="TreeABC">
829+ <AlwaysSuccess/>
830+ </BehaviorTree>
831+ </root>
832+ )" ;
833+
834+ // clang-format on
835+ BehaviorTreeFactory factory;
836+
837+ ASSERT_NO_THROW (auto tree = factory.createTreeFromText (xml_text));
838+ }
839+
840+ // Test for Groot2 issue #56: duplicate _fullpath when multiple subtrees have the same name
841+ // https://github.com/BehaviorTree/Groot2/issues/56
842+ //
843+ // When two SubTree nodes under the same parent have the same "name" attribute,
844+ // tree creation should fail with a clear error message.
845+ TEST (SubTree, DuplicateSubTreeName_Groot2Issue56)
846+ {
847+ // clang-format off
848+ static const char * xml_text = R"(
849+ <root BTCPP_format="4" main_tree_to_execute="MainTree">
850+ <BehaviorTree ID="MainTree">
851+ <ParallelAll>
852+ <SubTree ID="Worker" name="my_worker"/>
853+ <SubTree ID="Worker" name="my_worker"/>
854+ </ParallelAll>
855+ </BehaviorTree>
856+
857+ <BehaviorTree ID="Worker">
858+ <AlwaysSuccess name="do_work"/>
859+ </BehaviorTree>
860+ </root>
861+ )" ;
862+ // clang-format on
863+
864+ BehaviorTreeFactory factory;
865+
866+ // Should throw RuntimeError because of duplicate SubTree names
867+ ASSERT_THROW ((void )factory.createTreeFromText (xml_text), RuntimeError);
868+ }
869+
870+ // Additional test to verify the error message content
871+ TEST (SubTree, DuplicateSubTreeName_ErrorMessage)
872+ {
873+ // clang-format off
874+ static const char * xml_text = R"(
875+ <root BTCPP_format="4" main_tree_to_execute="MainTree">
876+ <BehaviorTree ID="MainTree">
877+ <Sequence>
878+ <SubTree ID="Task" name="my_task"/>
879+ <SubTree ID="Task" name="my_task"/>
880+ </Sequence>
881+ </BehaviorTree>
882+
883+ <BehaviorTree ID="Task">
884+ <AlwaysSuccess/>
885+ </BehaviorTree>
886+ </root>
887+ )" ;
888+ // clang-format on
889+
890+ BehaviorTreeFactory factory;
891+
892+ try
893+ {
894+ (void )factory.createTreeFromText (xml_text);
895+ FAIL () << " Expected RuntimeError to be thrown" ;
896+ }
897+ catch (const RuntimeError& e)
898+ {
899+ std::string msg = e.what ();
900+ EXPECT_TRUE (msg.find (" Duplicate SubTree path" ) != std::string::npos)
901+ << " Error message should mention 'Duplicate SubTree path'. Got: " << msg;
902+ EXPECT_TRUE (msg.find (" my_task" ) != std::string::npos)
903+ << " Error message should mention the duplicate path 'my_task'. Got: " << msg;
904+ }
905+ }
906+
907+ // Test that unique names under the same parent work correctly
908+ TEST (SubTree, UniqueSubTreeNames_WorksCorrectly)
909+ {
910+ // clang-format off
911+ static const char * xml_text = R"(
912+ <root BTCPP_format="4" main_tree_to_execute="MainTree">
913+ <BehaviorTree ID="MainTree">
914+ <ParallelAll>
915+ <SubTree ID="Worker" name="worker_1"/>
916+ <SubTree ID="Worker" name="worker_2"/>
917+ </ParallelAll>
918+ </BehaviorTree>
919+
920+ <BehaviorTree ID="Worker">
921+ <AlwaysSuccess name="do_work"/>
922+ </BehaviorTree>
923+ </root>
924+ )" ;
925+ // clang-format on
926+
927+ BehaviorTreeFactory factory;
928+ Tree tree = factory.createTreeFromText (xml_text);
929+
930+ // Verify paths are unique
931+ std::set<std::string> all_paths;
932+ tree.applyVisitor ([&](TreeNode* node) {
933+ EXPECT_EQ (all_paths.count (node->fullPath ()), 0 );
934+ all_paths.insert (node->fullPath ());
935+ });
936+
937+ ASSERT_EQ (tree.subtrees .size (), 3 );
938+ auto status = tree.tickWhileRunning ();
939+ ASSERT_EQ (status, NodeStatus::SUCCESS);
940+ }
941+
942+ // Test that omitting name attribute auto-generates unique paths
943+ TEST (SubTree, NoNameAttribute_AutoGeneratesUniquePaths)
944+ {
945+ // clang-format off
946+ static const char * xml_text = R"(
947+ <root BTCPP_format="4" main_tree_to_execute="MainTree">
948+ <BehaviorTree ID="MainTree">
949+ <ParallelAll>
950+ <SubTree ID="Worker"/>
951+ <SubTree ID="Worker"/>
952+ </ParallelAll>
953+ </BehaviorTree>
954+
955+ <BehaviorTree ID="Worker">
956+ <AlwaysSuccess name="do_work"/>
957+ </BehaviorTree>
958+ </root>
959+ )" ;
960+ // clang-format on
961+
962+ BehaviorTreeFactory factory;
963+ Tree tree = factory.createTreeFromText (xml_text);
964+
965+ // Verify paths are unique (auto-generated with UID)
966+ std::set<std::string> all_paths;
967+ tree.applyVisitor ([&](TreeNode* node) {
968+ EXPECT_EQ (all_paths.count (node->fullPath ()), 0 );
969+ all_paths.insert (node->fullPath ());
970+ });
971+
972+ ASSERT_EQ (tree.subtrees .size (), 3 );
973+ auto status = tree.tickWhileRunning ();
974+ ASSERT_EQ (status, NodeStatus::SUCCESS);
975+ }
976+
977+ // Test nested subtrees - duplicate names at the same level should fail
978+ TEST (SubTree, NestedDuplicateNames_ShouldFail)
979+ {
980+ // clang-format off
981+ static const char * xml_text = R"(
982+ <root BTCPP_format="4" main_tree_to_execute="MainTree">
983+ <BehaviorTree ID="MainTree">
984+ <Sequence>
985+ <SubTree ID="Level1" name="task"/>
986+ <SubTree ID="Level1" name="task"/>
987+ </Sequence>
988+ </BehaviorTree>
989+
990+ <BehaviorTree ID="Level1">
991+ <AlwaysSuccess name="work"/>
992+ </BehaviorTree>
993+ </root>
994+ )" ;
995+ // clang-format on
996+
997+ BehaviorTreeFactory factory;
998+
999+ // Should throw RuntimeError because of duplicate SubTree names
1000+ ASSERT_THROW ((void )factory.createTreeFromText (xml_text), RuntimeError);
1001+ }
1002+
1003+ // Regression test: literal numeric values passed to subtrees should preserve
1004+ // their numeric type so that Script expressions can do arithmetic.
1005+ TEST (SubTree, LiteralNumericPortsPreserveType)
1006+ {
1007+ // clang-format off
1008+ static const char * xml_text = R"(
1009+ <root BTCPP_format="4" main_tree_to_execute="MainTree">
1010+
1011+ <BehaviorTree ID="MainTree">
1012+ <Sequence>
1013+ <SubTree ID="DoMath" int_val="42" dbl_val="3.14" str_val="hello"
1014+ remapped_val="{from_parent}" />
1015+ </Sequence>
1016+ </BehaviorTree>
1017+
1018+ <BehaviorTree ID="DoMath">
1019+ <Sequence>
1020+ <ScriptCondition code=" int_val + 1 == 43 " />
1021+ <ScriptCondition code=" dbl_val > 3.0 " />
1022+ <ScriptCondition code=" remapped_val + 1 == 101 " />
1023+ </Sequence>
1024+ </BehaviorTree>
1025+
1026+ </root>
1027+ )" ;
1028+ // clang-format on
1029+
1030+ BehaviorTreeFactory factory;
1031+
1032+ auto tree = factory.createTreeFromText (xml_text);
1033+
1034+ // Set the remapped parent value as an integer
1035+ tree.rootBlackboard ()->set (" from_parent" , 100 );
1036+
1037+ const auto status = tree.tickWhileRunning ();
1038+ ASSERT_EQ (status, NodeStatus::SUCCESS);
1039+ }
0 commit comments