diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index b0f57d999..535af9ff9 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -1309,8 +1309,24 @@ void addNodeModelToXML(const TreeNodeManifest& model, XMLDocument& doc, XMLElement* element = doc.NewElement(toStr(model.type).c_str()); element->SetAttribute("ID", model.registration_ID.c_str()); - for(const auto& [port_name, port_info] : model.ports) + // PortsList is unordered: sort by direction, then by name, so that the + // output (and the order shown in Groot2) is deterministic. + std::vector sorted_ports; + sorted_ports.reserve(model.ports.size()); + for(const auto& port : model.ports) { + sorted_ports.push_back(&port); + } + std::sort(sorted_ports.begin(), sorted_ports.end(), + [](const auto* lhs, const auto* rhs) { + const auto lhs_dir = lhs->second.direction(); + const auto rhs_dir = rhs->second.direction(); + return lhs_dir != rhs_dir ? lhs_dir < rhs_dir : lhs->first < rhs->first; + }); + + for(const auto* port : sorted_ports) + { + const auto& [port_name, port_info] = *port; XMLElement* port_element = nullptr; switch(port_info.direction()) { diff --git a/tests/gtest_factory.cpp b/tests/gtest_factory.cpp index 2f1559681..96150d8a7 100644 --- a/tests/gtest_factory.cpp +++ b/tests/gtest_factory.cpp @@ -505,6 +505,49 @@ TEST(BehaviorTreeFactory, WrittenXMLHasLibraryVersion) EXPECT_EQ(reloaded.tickWhileRunning(), NodeStatus::SUCCESS); } +class ActionWithManyPorts : public SyncActionNode +{ +public: + ActionWithManyPorts(const std::string& name, const NodeConfig& config) + : SyncActionNode(name, config) + {} + + BT::NodeStatus tick() override + { + return NodeStatus::SUCCESS; + } + + static PortsList providedPorts() + { + return { OutputPort("out_b"), InputPort("in_c"), + BidirectionalPort("io_a"), InputPort("in_a"), + OutputPort("out_a"), InputPort("in_b") }; + } +}; + +// Issue #851: PortsList is an unordered_map, so the model must sort the ports +// itself, otherwise Groot2 shows them in a different order on every build. +TEST(BehaviorTreeFactory, ModelPortsAreSorted) +{ + BehaviorTreeFactory factory; + factory.registerNodeType("ActionWithManyPorts"); + + const auto xml = writeTreeNodesModelXML(factory, false); + const std::vector expected_order = { + R"(