Skip to content

Commit a1078ad

Browse files
authored
Report a declared port whose name cannot be bound (fixes #1215) (#1216)
A SubTree model can declare a port that nothing is able to bind, and nothing says so. `validatePortName` rejects only a leading digit, so `<input_port name="_myPort"/>` is accepted. `IsAllowedPortName` requires an alphabetic first character, and `createNodeFromXML` uses it to classify every instance attribute, so `_myPort="{outer}"` is diverted into `other_attributes`, the remapping is dropped, and the SubTree reads its declared default: Parent sets outer='FROM_PARENT'. Declared default is DEFAULT_NOT_WIRED. myPort inside SubTree -> FROM_PARENT _myPort inside SubTree -> DEFAULT_NOT_WIRED `InputPort("_myPort")` already throws "Underscore is reserved", so the C++ API and the XML declaration path disagree about the same name. Throw when an instance attribute names a port the node declares but cannot bind. Declared ports live in two places, so the check reads `manifest->ports` for a registered node and `subtree_models` for a SubTree. Rejecting the declaration in `validatePortName` would be the smaller change. It would also break XML that loads today: a port declared and never remapped is inert, and generated Objectives can carry such a declaration. This fires only where a remapping is actually being lost, so an inert declaration keeps loading.
1 parent afa1bce commit a1078ad

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

‎src/xml_parsing.cpp‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,29 @@ TreeNode::Ptr XMLParser::PImpl::createNodeFromXML(const XMLElement* element,
847847
}
848848
else if(!IsReservedAttribute(port_name))
849849
{
850+
// The name may still match a declared port. validatePortName only rejects
851+
// a leading digit, so a SubTree model can declare
852+
// <input_port name="_myPort"/>, a name IsAllowedPortName refuses and no
853+
// C++ InputPort() can create. Falling through to other_attributes would
854+
// drop the remapping without a word and leave the node on its declared
855+
// default, so report it instead.
856+
bool is_declared_port = manifest != nullptr && manifest->ports.count(port_name) > 0;
857+
if(!is_declared_port && node_type == NodeType::SUBTREE)
858+
{
859+
auto model_it = subtree_models.find(type_ID);
860+
is_declared_port = model_it != subtree_models.end() &&
861+
model_it->second.ports.count(port_name) > 0;
862+
}
863+
if(is_declared_port)
864+
{
865+
const std::string reason = " and is declared, but the name cannot be used"
866+
" as a port. A port name must begin with an"
867+
" alphabetic character. Rename the port, for"
868+
" example [_my_port] to [my_port].";
869+
throw RuntimeError(StrCat("A port with name [", port_name,
870+
"] is found in the XML (", type_ID, ", line ",
871+
std::to_string(element->GetLineNum()), ")", reason));
872+
}
850873
other_attributes[port_name] = port_value;
851874
}
852875
}

‎tests/gtest_subtree.cpp‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,3 +1132,79 @@ TEST(SubTree, LiteralNonBooleanPortsRejectLogicalNot)
11321132
ASSERT_THROW((void)tree.tickWhileRunning(), RuntimeError);
11331133
ASSERT_EQ(tree.subtrees[1]->blackboard->get<std::string>("value"), "1not_bool");
11341134
}
1135+
1136+
// A SubTree model can declare a port whose name no node can bind.
1137+
// validatePortName only rejects a leading digit, while IsAllowedPortName, which
1138+
// classifies instance attributes and which InputPort()/OutputPort() enforce,
1139+
// requires an alphabetic first character. Before the fix the remapping below was
1140+
// diverted into other_attributes and the SubTree read its declared default, with
1141+
// no exception and no log.
1142+
TEST(SubTree, DeclaredPortNameThatCannotBeBound)
1143+
{
1144+
static const char* xml_text = R"(
1145+
<root BTCPP_format="4" main_tree_to_execute="MainTree">
1146+
<BehaviorTree ID="MainTree">
1147+
<Sequence>
1148+
<Script code="outer:='from_parent'"/>
1149+
<SubTree ID="Sub" _myPort="{outer}"/>
1150+
</Sequence>
1151+
</BehaviorTree>
1152+
<BehaviorTree ID="Sub">
1153+
<AlwaysSuccess/>
1154+
</BehaviorTree>
1155+
<TreeNodesModel>
1156+
<SubTree ID="Sub">
1157+
<input_port name="_myPort" default="never_wired"/>
1158+
</SubTree>
1159+
</TreeNodesModel>
1160+
</root>)";
1161+
1162+
BehaviorTreeFactory factory;
1163+
EXPECT_THROW(factory.createTreeFromText(xml_text), RuntimeError);
1164+
}
1165+
1166+
// The same underscore attribute on a SubTree that does not declare it is not a
1167+
// port. It must keep landing in other_attributes and must not throw.
1168+
TEST(SubTree, UndeclaredUnderscoreAttributeIsNotAPort)
1169+
{
1170+
static const char* xml_text = R"(
1171+
<root BTCPP_format="4" main_tree_to_execute="MainTree">
1172+
<BehaviorTree ID="MainTree">
1173+
<SubTree ID="Sub" _my_editor_state="true"/>
1174+
</BehaviorTree>
1175+
<BehaviorTree ID="Sub">
1176+
<AlwaysSuccess/>
1177+
</BehaviorTree>
1178+
<TreeNodesModel>
1179+
<SubTree ID="Sub">
1180+
<input_port name="goal" default="g"/>
1181+
</SubTree>
1182+
</TreeNodesModel>
1183+
</root>)";
1184+
1185+
BehaviorTreeFactory factory;
1186+
EXPECT_NO_THROW(factory.createTreeFromText(xml_text));
1187+
}
1188+
1189+
// A declared port nobody remaps stays inert and keeps loading, so existing trees
1190+
// carrying such a declaration are unaffected.
1191+
TEST(SubTree, DeclaredUnbindablePortLoadsWhenNotRemapped)
1192+
{
1193+
static const char* xml_text = R"(
1194+
<root BTCPP_format="4" main_tree_to_execute="MainTree">
1195+
<BehaviorTree ID="MainTree">
1196+
<SubTree ID="Sub"/>
1197+
</BehaviorTree>
1198+
<BehaviorTree ID="Sub">
1199+
<AlwaysSuccess/>
1200+
</BehaviorTree>
1201+
<TreeNodesModel>
1202+
<SubTree ID="Sub">
1203+
<input_port name="_myPort" default="inert"/>
1204+
</SubTree>
1205+
</TreeNodesModel>
1206+
</root>)";
1207+
1208+
BehaviorTreeFactory factory;
1209+
EXPECT_NO_THROW(factory.createTreeFromText(xml_text));
1210+
}

0 commit comments

Comments
 (0)