diff --git a/.github/workflows/reusable-ubuntu-ci.yml b/.github/workflows/reusable-ubuntu-ci.yml index 11209aea0eb..7a00db03702 100644 --- a/.github/workflows/reusable-ubuntu-ci.yml +++ b/.github/workflows/reusable-ubuntu-ci.yml @@ -463,7 +463,7 @@ jobs: - name: Install Python dependencies uses: eProsima/eProsima-CI/ubuntu/install_python_packages@v0 with: - packages: vcstool xmlschema + packages: vcstool xmlschema psutil - name: Setup CCache uses: eProsima/eProsima-CI/external/setup-ccache-action@v0 @@ -650,7 +650,7 @@ jobs: - name: Install Python dependencies uses: eProsima/eProsima-CI/ubuntu/install_python_packages@v0 with: - packages: vcstool xmlschema xmltodict==0.13.0 jsondiff==2.0.0 pandas==1.5.2 + packages: vcstool xmlschema xmltodict==0.13.0 jsondiff==2.0.0 pandas==1.5.2 psutil - name: Setup CCache uses: eProsima/eProsima-CI/external/setup-ccache-action@v0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 887daa78ca8..823d11f94af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -603,7 +603,7 @@ option(INSTALL_TOOLS "Install tools" OFF) if(INSTALL_TOOLS) # Install tools - install(DIRECTORY ${PROJECT_SOURCE_DIR}/tools + install(DIRECTORY ${PROJECT_SOURCE_DIR}/tools/ DESTINATION tools COMPONENT tools PATTERN "tools/CMakeLists.txt" EXCLUDE diff --git a/include/fastdds/rtps/attributes/BuiltinTransports.hpp b/include/fastdds/rtps/attributes/BuiltinTransports.hpp index cc5addafc3c..c7e424282f3 100644 --- a/include/fastdds/rtps/attributes/BuiltinTransports.hpp +++ b/include/fastdds/rtps/attributes/BuiltinTransports.hpp @@ -104,14 +104,15 @@ inline bool operator ==( */ enum class BuiltinTransports : uint16_t { - NONE = 0, //< No transport will be instantiated - DEFAULT = 1, //< Default value that will instantiate UDPv4 and SHM transports + NONE = 0, //< No transport will be instantiated + DEFAULT = 1, //< Default value that will instantiate UDPv4 and SHM transports DEFAULTv6 = 2, //< Instantiate UDPv6 and SHM transports SHM = 3, //< Instantiate SHM transport only UDPv4 = 4, //< Instantiate UDPv4 transport only UDPv6 = 5, //< Instantiate UDPv6 transport only LARGE_DATA = 6, //< Instantiate SHM, UDPv4 and TCPv4 transports, but UDPv4 is only used for bootstrapping discovery - LARGE_DATAv6 = 7 //< Instantiate SHM, UDPv6 and TCPv6 transports, but UDPv6 is only used for bootstrapping discovery + LARGE_DATAv6 = 7, //< Instantiate SHM, UDPv6 and TCPv6 transports, but UDPv6 is only used for bootstrapping discovery + P2P = 8 //< Instantiate SHM, UDPv4 (unicast) and TCPv4 transports, shall only be used along with ROS2_EASY_MODE= }; inline std::ostream& operator <<( @@ -144,6 +145,9 @@ inline std::ostream& operator <<( case BuiltinTransports::LARGE_DATAv6: output << "LARGE_DATAv6"; break; + case BuiltinTransports::P2P: + output << "P2P"; + break; default: output << "UNKNOWN"; break; diff --git a/include/fastdds/rtps/common/PortParameters.hpp b/include/fastdds/rtps/common/PortParameters.hpp index 449d2400fd2..2a05ca7191f 100644 --- a/include/fastdds/rtps/common/PortParameters.hpp +++ b/include/fastdds/rtps/common/PortParameters.hpp @@ -42,6 +42,7 @@ class PortParameters , offsetd1(10) , offsetd2(1) , offsetd3(11) + , offsetd4(2) { } @@ -58,7 +59,8 @@ class PortParameters (this->offsetd0 == b.offsetd0) && (this->offsetd1 == b.offsetd1) && (this->offsetd2 == b.offsetd2) && - (this->offsetd3 == b.offsetd3); + (this->offsetd3 == b.offsetd3) && + (this->offsetd4 == b.offsetd4); } /** @@ -111,6 +113,30 @@ class PortParameters return port; } + /** + * Get a discovery server port based on the domain ID. + * + * @param domainId Domain ID. + * @return Discovery server port + */ + inline uint16_t get_discovery_server_port( + uint32_t domainId) const + { + uint32_t port = portBase + domainIDGain * domainId + offsetd4; + + if (port > 65535) + { + EPROSIMA_LOG_ERROR(RTPS, "Calculated port number is too high. Probably the domainId is over 232 " + << "or portBase is too high."); + std::cout << "Calculated port number is too high. Probably the domainId is over 232 " + << "or portBase is too high." << std::endl; + std::cout.flush(); + exit(EXIT_FAILURE); + } + + return static_cast(port); + } + public: //!PortBase, default value 7400. @@ -127,6 +153,8 @@ class PortParameters uint16_t offsetd2; //!Offset d3, default value 11. uint16_t offsetd3; + //!Offset d4, default value 2. + uint16_t offsetd4; }; } // namespace rtps diff --git a/resources/images/fastdds_github_banner.png b/resources/images/fastdds_github_banner.png index 284e377e2d6..5eb782133e4 100644 Binary files a/resources/images/fastdds_github_banner.png and b/resources/images/fastdds_github_banner.png differ diff --git a/resources/xsd/fastdds_profiles.xsd b/resources/xsd/fastdds_profiles.xsd index c74b52d3283..56395e79518 100644 --- a/resources/xsd/fastdds_profiles.xsd +++ b/resources/xsd/fastdds_profiles.xsd @@ -816,7 +816,8 @@ ├ offsetd0 [uint16], ├ offsetd1 [uint16], ├ offsetd2 [uint16], - └ offsetd3 [uint16] --> + ├ offsetd3 [uint16], + └ offsetd4 [uint16] --> @@ -826,6 +827,7 @@ + @@ -1961,6 +1963,7 @@ + diff --git a/src/cpp/rtps/RTPSDomain.cpp b/src/cpp/rtps/RTPSDomain.cpp index 843dd0d01fd..0e04082d7f1 100644 --- a/src/cpp/rtps/RTPSDomain.cpp +++ b/src/cpp/rtps/RTPSDomain.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include @@ -518,59 +519,128 @@ RTPSParticipant* RTPSDomainImpl::clientServerEnvironmentCreationOverride( // Is up to the caller guarantee the att argument is not modified during the call RTPSParticipantAttributes client_att(att); - // Retrieve the info from the environment variable - LocatorList_t& server_list = client_att.builtin.discovery_config.m_DiscoveryServers; - if (load_environment_server_info(server_list) && server_list.empty()) - { - // It's not an error, the environment variable may not be set. Any issue with environment - // variable syntax is EPROSIMA_LOG_ERROR already - return nullptr; - } + // Check whether we need to initialize in easy mode + const std::string& easy_mode_env_value = easy_mode_env(); - // Check if some address requires the UDPv6, TCPv4 or TCPv6 transport - if (server_list.has_kind() && - !has_user_transport(client_att)) + if (easy_mode_env_value.empty()) { - // Extend builtin transports with the UDPv6 transport - auto descriptor = std::make_shared(); - descriptor->sendBufferSize = client_att.sendSocketBufferSize; - descriptor->receiveBufferSize = client_att.listenSocketBufferSize; - client_att.userTransports.push_back(std::move(descriptor)); - } - if (server_list.has_kind() && - !has_user_transport(client_att)) - { - // Extend builtin transports with the TCPv4 transport - auto descriptor = std::make_shared(); - // Add automatic port - descriptor->add_listener_port(0); - descriptor->sendBufferSize = client_att.sendSocketBufferSize; - descriptor->receiveBufferSize = client_att.listenSocketBufferSize; - client_att.userTransports.push_back(std::move(descriptor)); - } - if (server_list.has_kind() && - !has_user_transport(client_att)) - { - // Extend builtin transports with the TCPv6 transport - auto descriptor = std::make_shared(); - // Add automatic port - descriptor->add_listener_port(0); - descriptor->sendBufferSize = client_att.sendSocketBufferSize; - descriptor->receiveBufferSize = client_att.listenSocketBufferSize; - client_att.userTransports.push_back(std::move(descriptor)); - } + // Retrieve the info from the environment variable + LocatorList_t& server_list = client_att.builtin.discovery_config.m_DiscoveryServers; + if (load_environment_server_info(server_list) && server_list.empty()) + { + // It's not an error, the environment variable may not be set. Any issue with environment + // variable syntax is EPROSIMA_LOG_ERROR already + return nullptr; + } - EPROSIMA_LOG_INFO(DOMAIN, "Detected auto client-server environment variable." - << "Trying to create client with the default server setup: " - << client_att.builtin.discovery_config.m_DiscoveryServers); + // Check if some address requires the UDPv6, TCPv4 or TCPv6 transport + if (server_list.has_kind() && + !has_user_transport(client_att)) + { + // Extend builtin transports with the UDPv6 transport + auto descriptor = std::make_shared(); + descriptor->sendBufferSize = client_att.sendSocketBufferSize; + descriptor->receiveBufferSize = client_att.listenSocketBufferSize; + client_att.userTransports.push_back(std::move(descriptor)); + } + if (server_list.has_kind() && + !has_user_transport(client_att)) + { + // Extend builtin transports with the TCPv4 transport + auto descriptor = std::make_shared(); + // Add automatic port + descriptor->add_listener_port(0); + descriptor->sendBufferSize = client_att.sendSocketBufferSize; + descriptor->receiveBufferSize = client_att.listenSocketBufferSize; + client_att.userTransports.push_back(std::move(descriptor)); + } + if (server_list.has_kind() && + !has_user_transport(client_att)) + { + // Extend builtin transports with the TCPv6 transport + auto descriptor = std::make_shared(); + // Add automatic port + descriptor->add_listener_port(0); + descriptor->sendBufferSize = client_att.sendSocketBufferSize; + descriptor->receiveBufferSize = client_att.listenSocketBufferSize; + client_att.userTransports.push_back(std::move(descriptor)); + } + + EPROSIMA_LOG_INFO(DOMAIN, "Detected auto client-server environment variable." + << "Trying to create client with the default server setup: " + << client_att.builtin.discovery_config.m_DiscoveryServers); - client_att.builtin.discovery_config.discoveryProtocol = DiscoveryProtocol::CLIENT; - // RemoteServerAttributes already fill in above + client_att.builtin.discovery_config.discoveryProtocol = DiscoveryProtocol::CLIENT; + // RemoteServerAttributes already fill in above - // Check if the client must become a super client - if (ros_super_client_env()) + // Check if the client must become a super client + if (ros_super_client_env()) + { + client_att.builtin.discovery_config.discoveryProtocol = DiscoveryProtocol::SUPER_CLIENT; + } + } + else { + // SUPER_CLIENT client_att.builtin.discovery_config.discoveryProtocol = DiscoveryProtocol::SUPER_CLIENT; + + // P2P transport. Similar to LARGE_DATA, but with UDPv4 unicast + client_att.useBuiltinTransports = false; + client_att.setup_transports(BuiltinTransports::P2P); + + // Ignore initialpeers + client_att.builtin.initialPeersList = LocatorList(); + + eprosima::fastdds::rtps::PortParameters port_params; + + auto domain_port = port_params.get_discovery_server_port(domain_id); + + // Add user traffic TCP + eprosima::fastdds::rtps::Locator_t locator_tcp; + locator_tcp.kind = LOCATOR_KIND_TCPv4; + + IPLocator::setPhysicalPort(locator_tcp, 0); + IPLocator::setLogicalPort(locator_tcp, 0); + // Initialize to the wan interface + IPLocator::setIPv4(locator_tcp, "0.0.0.0"); + + client_att.defaultUnicastLocatorList.push_back(locator_tcp); + + // Add remote DS based on port + eprosima::fastdds::rtps::Locator_t locator_udp; + locator_udp.kind = LOCATOR_KIND_UDPv4; + + locator_udp.port = domain_port; + IPLocator::setIPv4(locator_udp, 127, 0, 0, 1); + + // Point to the well known DS port in the corresponding domain + client_att.builtin.discovery_config.m_DiscoveryServers.push_back(locator_udp); + + SystemCommandBuilder sys_command; + int res = sys_command.executable(FAST_DDS_DEFAULT_CLI_SCRIPT_NAME) + .verb(FAST_DDS_DEFAULT_CLI_DISCOVERY_VERB) + .verb(FAST_DDS_DEFAULT_CLI_AUTO_VERB) + .arg("-d") + .value(std::to_string(domain_id)) + .value(easy_mode_env_value + ":" + std::to_string(domain_id)) + .build_and_call(); +#ifndef _WIN32 + // Adecuate Python subprocess return + res = WEXITSTATUS(res); +#endif // _WIN32 + + if (res != SystemCommandBuilder::SystemCommandResult::SUCCESS) + { + if (res == SystemCommandBuilder::SystemCommandResult::BAD_PARAM) + { + EPROSIMA_LOG_ERROR("DOMAIN", "ROS2_EASY_MODE IP connection conflicts with a previous one."); + } + else + { + EPROSIMA_LOG_ERROR(DOMAIN, "Auto discovery server client setup. Unable to spawn daemon."); + } + return nullptr; + } } RTPSParticipant* part = createParticipant(domain_id, enabled, client_att, listen); diff --git a/src/cpp/rtps/attributes/RTPSParticipantAttributes.cpp b/src/cpp/rtps/attributes/RTPSParticipantAttributes.cpp index 74140cf2896..a1ae827210a 100644 --- a/src/cpp/rtps/attributes/RTPSParticipantAttributes.cpp +++ b/src/cpp/rtps/attributes/RTPSParticipantAttributes.cpp @@ -301,6 +301,38 @@ static void setup_transports_large_datav6( } } +static void setup_transports_p2p( + RTPSParticipantAttributes& att, + bool intraprocess_only, + const fastdds::rtps::BuiltinTransportsOptions& options) +{ + if (!intraprocess_only) + { + setup_large_data_shm_transport(att, options); + + auto tcp_transport = create_tcpv4_transport(att, options); + att.userTransports.push_back(tcp_transport); + + Locator_t tcp_loc; + tcp_loc.kind = LOCATOR_KIND_TCPv4; + IPLocator::setIPv4(tcp_loc, "0.0.0.0"); + IPLocator::setPhysicalPort(tcp_loc, 0); + IPLocator::setLogicalPort(tcp_loc, 0); + att.defaultUnicastLocatorList.push_back(tcp_loc); + } + + auto udp_descriptor = create_udpv4_transport(att, intraprocess_only, options); + att.userTransports.push_back(udp_descriptor); + + if (!intraprocess_only) + { + Locator_t udp_locator; + udp_locator.kind = LOCATOR_KIND_UDPv4; + IPLocator::setIPv4(udp_locator, "127.0.0.1"); + att.builtin.metatrafficUnicastLocatorList.push_back(udp_locator); + } +} + void RTPSParticipantAttributes::setup_transports( fastdds::rtps::BuiltinTransports transports, const fastdds::rtps::BuiltinTransportsOptions& options) @@ -309,7 +341,8 @@ void RTPSParticipantAttributes::setup_transports( (transports != fastdds::rtps::BuiltinTransports::NONE && transports != fastdds::rtps::BuiltinTransports::SHM && transports != fastdds::rtps::BuiltinTransports::LARGE_DATA && - transports != fastdds::rtps::BuiltinTransports::LARGE_DATAv6)) + transports != fastdds::rtps::BuiltinTransports::LARGE_DATAv6 && + transports != fastdds::rtps::BuiltinTransports::P2P)) { EPROSIMA_LOG_ERROR(RTPS_PARTICIPANT, "Max message size of UDP cannot be greater than " << std::to_string( @@ -347,17 +380,23 @@ void RTPSParticipantAttributes::setup_transports( break; case fastdds::rtps::BuiltinTransports::LARGE_DATA: - // This parameter will allow allow the initialization of UDP transports with maxMessageSize > 65500 KB (s_maximumMessageSize) + // This parameter will allow the initialization of UDP transports with maxMessageSize > 65500 KB (s_maximumMessageSize) max_msg_size_no_frag = options.maxMessageSize; setup_transports_large_data(*this, intraprocess_only, options); break; case fastdds::rtps::BuiltinTransports::LARGE_DATAv6: - // This parameter will allow allow the initialization of UDP transports with maxMessageSize > 65500 KB (s_maximumMessageSize) + // This parameter will allow the initialization of UDP transports with maxMessageSize > 65500 KB (s_maximumMessageSize) max_msg_size_no_frag = options.maxMessageSize; setup_transports_large_datav6(*this, intraprocess_only, options); break; + case fastdds::rtps::BuiltinTransports::P2P: + // This parameter will allow the initialization of UDP transports with maxMessageSize > 65500 KB (s_maximumMessageSize) + max_msg_size_no_frag = options.maxMessageSize; + setup_transports_p2p(*this, intraprocess_only, options); + break; + default: EPROSIMA_LOG_ERROR(RTPS_PARTICIPANT, "Setup for '" << transports << "' transport configuration not yet supported."); diff --git a/src/cpp/rtps/attributes/ServerAttributes.cpp b/src/cpp/rtps/attributes/ServerAttributes.cpp index f77ec978c0b..9caf73fceef 100644 --- a/src/cpp/rtps/attributes/ServerAttributes.cpp +++ b/src/cpp/rtps/attributes/ServerAttributes.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -68,6 +69,13 @@ const std::string& ros_discovery_server_env() return servers; } +const std::string& easy_mode_env() +{ + static std::string ip_value; + SystemInfo::get_env(EASY_MODE_URI, ip_value); + return ip_value; +} + bool load_environment_server_info( LocatorList_t& servers_list) { @@ -106,6 +114,14 @@ bool load_environment_server_info( throw std::out_of_range("Too large port passed into the server's list"); } + if (port < 420) + { + // This is a domain id, not a port. Translate it to a port + PortParameters port_params; + uint16_t port_from_domain = port_params.get_discovery_server_port(port); + port = port_from_domain; + } + if (!IPLocator::setPhysicalPort(server, static_cast(port))) { std::stringstream ss; diff --git a/src/cpp/rtps/attributes/ServerAttributes.hpp b/src/cpp/rtps/attributes/ServerAttributes.hpp index bb4b20c4a8a..92d0b72d6c4 100644 --- a/src/cpp/rtps/attributes/ServerAttributes.hpp +++ b/src/cpp/rtps/attributes/ServerAttributes.hpp @@ -136,16 +136,26 @@ std::basic_ostream& operator <<( // Default server base guidPrefix const char* const DEFAULT_ROS2_SERVER_GUIDPREFIX = "44.53.00.5f.45.50.52.4f.53.49.4d.41"; -/* Environment variable to specify a semicolon-separated list of locators ([transport]ip:port) that define remote server - * locators. The [transport] specification is optional. The default transport is UDPv4. - * For the variable to take any effect, the following pre-condition must be met: - * - The discovery protocol must be either SIMPLE or SERVER. - * a. In the case of SIMPLE, the participant is created as a CLIENT instead. - * b. In the case of SERVER, the participant is created as a SERVER, using the DEFAULT_ROS2_MASTER_URI list to - * expand the list of remote servers. +/* Environment variable that can either serve to: + * - Specify the Discovery Server auto mode by setting its value to AUTO. + * - Specify a semicolon-separated list of locators ([transport]ip:port) that define remote server + * locators. The [transport] specification is optional. The default transport is UDPv4. + * For the variable to take any effect, the following pre-condition must be met: + * - The discovery protocol must be either SIMPLE or SERVER. + * a. In the case of SIMPLE, the participant is created as a CLIENT instead. + * b. In the case of SERVER, the participant is created as a SERVER, using the DEFAULT_ROS2_MASTER_URI list to + * expand the list of remote servers. */ const char* const DEFAULT_ROS2_MASTER_URI = "ROS_DISCOVERY_SERVER"; +/* Environment variable that: + * - Will spawn a background Discovery Server in the current domain (if there were not). + * - Specify an external ip address to connect the background Discovery Server (the port is deduced from the domain). + * - Set the transports to TCP and SHM. + * - Make the participant a SUPER_CLIENT. + */ +const char* const EASY_MODE_URI = "ROS2_EASY_MODE"; + /* Environment variable to transform a SIMPLE participant in a SUPER CLIENT. * If the participant is not SIMPLE, the variable doesn't have any effects. * The variable can assume the following values: @@ -186,6 +196,12 @@ bool load_environment_server_info( */ const std::string& ros_discovery_server_env(); +/** + * Get the value of environment variable EASY_MODE_URI + * @return The value of environment variable EASY_MODE_URI. Empty string if the variable is not defined. + */ +const std::string& easy_mode_env(); + /** * Get the value of environment variable ROS_SUPER_CLIENT * @return The value of environment variable ROS_SUPER_CLIENT. False if the variable is not defined. diff --git a/src/cpp/rtps/builtin/discovery/participant/PDPServer.cpp b/src/cpp/rtps/builtin/discovery/participant/PDPServer.cpp index 1cf9622ce34..ea0981ceecb 100644 --- a/src/cpp/rtps/builtin/discovery/participant/PDPServer.cpp +++ b/src/cpp/rtps/builtin/discovery/participant/PDPServer.cpp @@ -68,7 +68,6 @@ PDPServer::PDPServer( LocatorList_t env_servers; { std::lock_guard lock(*getMutex()); - if (load_environment_server_info(env_servers)) { for (auto server : env_servers) diff --git a/src/cpp/rtps/participant/RTPSParticipantImpl.cpp b/src/cpp/rtps/participant/RTPSParticipantImpl.cpp index 179dc1d2e67..9bdc3175d90 100644 --- a/src/cpp/rtps/participant/RTPSParticipantImpl.cpp +++ b/src/cpp/rtps/participant/RTPSParticipantImpl.cpp @@ -115,7 +115,8 @@ static void set_builtin_transports_from_env_var( "UDPv4", BuiltinTransports::UDPv4, "UDPv6", BuiltinTransports::UDPv6, "LARGE_DATA", BuiltinTransports::LARGE_DATA, - "LARGE_DATAv6", BuiltinTransports::LARGE_DATAv6)) + "LARGE_DATAv6", BuiltinTransports::LARGE_DATAv6, + "P2P", BuiltinTransports::P2P)) { EPROSIMA_LOG_ERROR(RTPS_PARTICIPANT, "Wrong value '" << env_value << "' for environment variable '" << env_var_name << "'. Leaving as DEFAULT"); @@ -141,7 +142,8 @@ static void set_builtin_transports_from_env_var( "UDPv4", BuiltinTransports::UDPv4, "UDPv6", BuiltinTransports::UDPv6, "LARGE_DATA", BuiltinTransports::LARGE_DATA, - "LARGE_DATAv6", BuiltinTransports::LARGE_DATAv6)) + "LARGE_DATAv6", BuiltinTransports::LARGE_DATAv6, + "P2P", BuiltinTransports::P2P)) { EPROSIMA_LOG_ERROR(RTPS_PARTICIPANT, "Wrong value '" << env_value << "' for environment variable '" << env_var_name << "'. Leaving as DEFAULT"); diff --git a/src/cpp/rtps/transport/TCPTransportInterface.cpp b/src/cpp/rtps/transport/TCPTransportInterface.cpp index 5a8693c2049..946656ddbf7 100644 --- a/src/cpp/rtps/transport/TCPTransportInterface.cpp +++ b/src/cpp/rtps/transport/TCPTransportInterface.cpp @@ -307,8 +307,15 @@ ResponseCode TCPTransportInterface::bind_socket( decltype(channel_resources_)::value_type{channel->locator(), channel}); if (false == insert_ret.second) { - // There is an existing channel that can be used. Force the Client to close unnecessary socket - ret = RETCODE_SERVER_ERROR; + if (insert_ret.first->second->connection_established()) + { + // There is an existing channel that can be used. Force the Client to close unnecessary socket + ret = RETCODE_SERVER_ERROR; + } + else + { + insert_ret.first->second = channel; + } } std::vector local_interfaces; @@ -320,7 +327,12 @@ ResponseCode TCPTransportInterface::bind_socket( for (auto& interface_it : local_interfaces) { IPLocator::setIPv4(local_locator, interface_it.locator); - channel_resources_.insert(decltype(channel_resources_)::value_type{local_locator, channel}); + const auto insert_ret_local = channel_resources_.insert( + decltype(channel_resources_)::value_type{local_locator, channel}); + if (!insert_ret_local.first->second->connection_established()) + { + insert_ret_local.first->second = channel; + } } } return ret; diff --git a/src/cpp/utils/SystemCommandBuilder.hpp b/src/cpp/utils/SystemCommandBuilder.hpp new file mode 100644 index 00000000000..982ab5b5656 --- /dev/null +++ b/src/cpp/utils/SystemCommandBuilder.hpp @@ -0,0 +1,87 @@ +// Copyright 2024 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// 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. + +#ifndef UTILS__SYSTEMCOMMANDBUILDER_HPP_ +#define UTILS__SYSTEMCOMMANDBUILDER_HPP_ + +#include +#include + +namespace eprosima { + +namespace fastdds { + +static constexpr const char* FAST_DDS_DEFAULT_CLI_SCRIPT_NAME = "fastdds"; +static constexpr const char* FAST_DDS_DEFAULT_CLI_DISCOVERY_VERB = "discovery"; +static constexpr const char* FAST_DDS_DEFAULT_CLI_AUTO_VERB = "auto"; + +/** + * @brief Class to build and execute system commands. + */ +class SystemCommandBuilder +{ +public: + + enum SystemCommandResult + { + SUCCESS = 0, + FAILURE, + BAD_PARAM, + INVALID + }; + + SystemCommandBuilder() = default; + + SystemCommandBuilder& executable( + const std::string& executable) + { + command_ << executable; + return *this; + } + + SystemCommandBuilder& verb( + const std::string& verb) + { + command_ << " " << verb; + return *this; + } + + SystemCommandBuilder& arg( + const std::string& arg) + { + command_ << " " << arg; + return *this; + } + + SystemCommandBuilder& value( + const std::string& value) + { + command_ << " " << value; + return *this; + } + + int build_and_call() + { + return std::system(command_.str().c_str()); + } + +private: + + std::stringstream command_; +}; + +} // namespace fastdds +} // namespace eprosima + +#endif // UTILS__SYSTEMCOMMANDBUILDER_HPP_ diff --git a/src/cpp/xmlparser/XMLElementParser.cpp b/src/cpp/xmlparser/XMLElementParser.cpp index 3f49d0741a1..b1ecf8ffc82 100644 --- a/src/cpp/xmlparser/XMLElementParser.cpp +++ b/src/cpp/xmlparser/XMLElementParser.cpp @@ -777,6 +777,7 @@ XMLP_ret XMLParser::getXMLPortParameters( + */ @@ -842,6 +843,14 @@ XMLP_ret XMLParser::getXMLPortParameters( return XMLP_ret::XML_ERROR; } } + else if (strcmp(name, OFFSETD4) == 0) + { + // offsetd4 - uint16Type + if (XMLP_ret::XML_OK != getXMLUint(p_aux0, &port.offsetd4, ident)) + { + return XMLP_ret::XML_ERROR; + } + } else { EPROSIMA_LOG_ERROR(XMLPARSER, "Invalid element found into 'portType'. Name: " << name); @@ -4664,6 +4673,7 @@ XMLP_ret XMLParser::getXMLBuiltinTransports( + @@ -4825,7 +4835,8 @@ XMLP_ret XMLParser::getXMLBuiltinTransports( UDPv4, eprosima::fastdds::rtps::BuiltinTransports::UDPv4, UDPv6, eprosima::fastdds::rtps::BuiltinTransports::UDPv6, LARGE_DATA, eprosima::fastdds::rtps::BuiltinTransports::LARGE_DATA, - LARGE_DATAv6, eprosima::fastdds::rtps::BuiltinTransports::LARGE_DATAv6)) + LARGE_DATAv6, eprosima::fastdds::rtps::BuiltinTransports::LARGE_DATAv6, + P2P, eprosima::fastdds::rtps::BuiltinTransports::P2P)) { EPROSIMA_LOG_ERROR(XMLPARSER, "Node '" << KIND << "' bad content"); ret = XMLP_ret::XML_ERROR; diff --git a/src/cpp/xmlparser/XMLParserCommon.cpp b/src/cpp/xmlparser/XMLParserCommon.cpp index 04d05a6562c..237e774b24d 100644 --- a/src/cpp/xmlparser/XMLParserCommon.cpp +++ b/src/cpp/xmlparser/XMLParserCommon.cpp @@ -199,6 +199,7 @@ const char* DEFAULT_C = "DEFAULT"; const char* DEFAULTv6 = "DEFAULTv6"; const char* LARGE_DATA = "LARGE_DATA"; const char* LARGE_DATAv6 = "LARGE_DATAv6"; +const char* P2P = "P2P"; const char* INIT_ACKNACK_DELAY = "initial_acknack_delay"; const char* HEARTB_RESP_DELAY = "heartbeat_response_delay"; const char* INIT_HEARTB_DELAY = "initial_heartbeat_delay"; @@ -298,6 +299,7 @@ const char* OFFSETD0 = "offsetd0"; const char* OFFSETD1 = "offsetd1"; const char* OFFSETD2 = "offsetd2"; const char* OFFSETD3 = "offsetd3"; +const char* OFFSETD4 = "offsetd4"; const char* RTPS_PDP_TYPE = "discoveryProtocol"; const char* NONE = "NONE"; const char* CLIENT = "CLIENT"; diff --git a/src/cpp/xmlparser/XMLParserCommon.h b/src/cpp/xmlparser/XMLParserCommon.h index 296b15b4cb2..1d33dc58299 100644 --- a/src/cpp/xmlparser/XMLParserCommon.h +++ b/src/cpp/xmlparser/XMLParserCommon.h @@ -215,6 +215,7 @@ extern const char* DEFAULT_C; extern const char* DEFAULTv6; extern const char* LARGE_DATA; extern const char* LARGE_DATAv6; +extern const char* P2P; extern const char* INIT_ACKNACK_DELAY; extern const char* HEARTB_RESP_DELAY; extern const char* INIT_HEARTB_DELAY; @@ -311,6 +312,7 @@ extern const char* OFFSETD0; extern const char* OFFSETD1; extern const char* OFFSETD2; extern const char* OFFSETD3; +extern const char* OFFSETD4; extern const char* RTPS_PDP_TYPE; extern const char* NONE; extern const char* CLIENT; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 427936f315a..fc85b29a599 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -93,6 +93,9 @@ endif() # System tests ############################################################################### if(SYSTEM_TESTS) + find_package(GTest CONFIG REQUIRED) + include(${PROJECT_SOURCE_DIR}/cmake/testing/GoogleTest.cmake) + add_subdirectory(system/tools/fastdds) add_subdirectory(system/tools/fds) endif() diff --git a/test/blackbox/api/dds-pim/PubSubReader.hpp b/test/blackbox/api/dds-pim/PubSubReader.hpp index d3069adce6c..32a121b888e 100644 --- a/test/blackbox/api/dds-pim/PubSubReader.hpp +++ b/test/blackbox/api/dds-pim/PubSubReader.hpp @@ -336,6 +336,8 @@ class PubSubReader , message_receive_count_(0) , filter_expression_("") , expression_parameters_({}) + , use_preferred_domain_id_(false) + , preferred_domain_id_(0) { // Load default QoS to permit testing with external XML profile files. DomainParticipantFactory::get_instance()->load_profiles(); @@ -427,10 +429,11 @@ class PubSubReader ASSERT_TRUE(participant_->is_enabled()); } } + if (participant_ == nullptr) { participant_ = DomainParticipantFactory::get_instance()->create_participant( - (uint32_t)GET_PID() % 230, + (use_preferred_domain_id_ ? preferred_domain_id_ : (uint32_t)GET_PID() % 230), participant_qos_, &participant_listener_, eprosima::fastdds::dds::StatusMask::none()); @@ -952,6 +955,14 @@ class PubSubReader return *this; } + PubSubReader& set_domain_id( + const uint32_t& domain_id) + { + use_preferred_domain_id_ = true; + preferred_domain_id_ = domain_id; + return *this; + } + /*** Function to change QoS ***/ PubSubReader& reliability( const eprosima::fastdds::dds::ReliabilityQosPolicyKind kind) @@ -1725,7 +1736,7 @@ class PubSubReader std::cout << "Reader gets discovery result..." << std::endl; } - void setOnDiscoveryFunction( + void set_on_discovery_function( std::function f) { @@ -1861,6 +1872,11 @@ class PubSubReader return matched_; } + unsigned int get_participants_matched() const + { + return participant_matched_; + } + void set_xml_filename( const std::string& name) { @@ -2223,6 +2239,10 @@ class PubSubReader std::string filter_expression_; //! Parameters for CFT expression std::vector expression_parameters_; + + //! Preferred domain ID + bool use_preferred_domain_id_; + uint32_t preferred_domain_id_; }; template diff --git a/test/blackbox/api/dds-pim/PubSubWriter.hpp b/test/blackbox/api/dds-pim/PubSubWriter.hpp index 2ea4a818b66..469c494691d 100644 --- a/test/blackbox/api/dds-pim/PubSubWriter.hpp +++ b/test/blackbox/api/dds-pim/PubSubWriter.hpp @@ -88,7 +88,7 @@ class PubSubWriter static_cast(should_be_ignored); if (writer_.onDiscovery_ != nullptr) { - writer_.discovery_result_ = writer_.onDiscovery_(info); + writer_.discovery_result_ = writer_.onDiscovery_(info, status); } if (status == eprosima::fastdds::rtps::ParticipantDiscoveryStatus::DISCOVERED_PARTICIPANT) @@ -303,7 +303,8 @@ class PubSubWriter , times_liveliness_lost_(0) , times_incompatible_qos_(0) , last_incompatible_qos_(eprosima::fastdds::dds::INVALID_QOS_POLICY_ID) - + , use_preferred_domain_id_(false) + , preferred_domain_id_(0) #if HAVE_SECURITY , authorized_(0) , unauthorized_(0) @@ -392,7 +393,7 @@ class PubSubWriter if (participant_ == nullptr) { participant_ = DomainParticipantFactory::get_instance()->create_participant( - (uint32_t)GET_PID() % 230, + (use_preferred_domain_id_ ? preferred_domain_id_ : (uint32_t)GET_PID() % 230), participant_qos_, &participant_listener_, eprosima::fastdds::dds::StatusMask::none()); @@ -868,6 +869,21 @@ class PubSubWriter return *this; } + PubSubWriter& set_domain_id( + const uint32_t& domain_id) + { + use_preferred_domain_id_ = true; + preferred_domain_id_ = domain_id; + return *this; + } + + void set_on_discovery_function( + std::function f) + { + onDiscovery_ = f; + } + /*** Function to change QoS ***/ PubSubWriter& reliability( const eprosima::fastdds::dds::ReliabilityQosPolicyKind kind) @@ -1078,6 +1094,12 @@ class PubSubWriter return *this; } + PubSubWriter& setup_p2p_transports() + { + participant_qos_.setup_transports(eprosima::fastdds::rtps::BuiltinTransports::P2P); + return *this; + } + PubSubWriter& disable_builtin_transport() { participant_qos_.transport().use_builtin_transports = false; @@ -1653,6 +1675,11 @@ class PubSubWriter return matched_; } + unsigned int get_participants_matched() const + { + return participant_matched_; + } + unsigned int missed_deadlines() const { return listener_.missed_deadlines(); @@ -2056,7 +2083,8 @@ class PubSubWriter std::string participant_profile_ = ""; std::string datawriter_profile_ = ""; - std::function onDiscovery_; + std::function onDiscovery_; //! A mutex for liveliness std::mutex liveliness_mutex_; @@ -2073,6 +2101,9 @@ class PubSubWriter unsigned int times_incompatible_qos_; //! Latest conflicting PolicyId eprosima::fastdds::dds::QosPolicyId_t last_incompatible_qos_; + //! Preferred domain ID + bool use_preferred_domain_id_; + uint32_t preferred_domain_id_; #if HAVE_SECURITY std::mutex mutexAuthentication_; diff --git a/test/blackbox/builtin_transports_profile.xml b/test/blackbox/builtin_transports_profile.xml index 6ee611b4b9f..767b5dfe814 100644 --- a/test/blackbox/builtin_transports_profile.xml +++ b/test/blackbox/builtin_transports_profile.xml @@ -111,5 +111,13 @@ + + 0 + + Participant.builtin_transports_p2p + P2P + + + diff --git a/test/blackbox/common/BlackboxTestsDiscovery.cpp b/test/blackbox/common/BlackboxTestsDiscovery.cpp index 1123d338a9d..16c1f724e29 100644 --- a/test/blackbox/common/BlackboxTestsDiscovery.cpp +++ b/test/blackbox/common/BlackboxTestsDiscovery.cpp @@ -891,7 +891,7 @@ TEST_P(Discovery, PubSubAsReliableHelloworldParticipantDiscovery) ASSERT_TRUE(writer.isInitialized()); int count = 0; - reader.setOnDiscoveryFunction([&writer, &count](const ParticipantBuiltinTopicData& info, + reader.set_on_discovery_function([&writer, &count](const ParticipantBuiltinTopicData& info, ParticipantDiscoveryStatus status) -> bool { if (info.guid == writer.participant_guid()) @@ -937,7 +937,7 @@ TEST_P(Discovery, PubSubAsReliableHelloworldUserData) ASSERT_TRUE(writer.isInitialized()); - reader.setOnDiscoveryFunction([&writer](const ParticipantBuiltinTopicData& info, + reader.set_on_discovery_function([&writer](const ParticipantBuiltinTopicData& info, ParticipantDiscoveryStatus /*status*/) -> bool { if (info.guid == writer.participant_guid()) diff --git a/test/blackbox/common/BlackboxTestsSecurity.cpp b/test/blackbox/common/BlackboxTestsSecurity.cpp index 4b582667a03..e97eacac854 100644 --- a/test/blackbox/common/BlackboxTestsSecurity.cpp +++ b/test/blackbox/common/BlackboxTestsSecurity.cpp @@ -2250,7 +2250,7 @@ TEST_P(Security, BuiltinAuthenticationAndCryptoPlugin_user_data) sub_property_policy.properties().emplace_back("rtps.endpoint.submessage_protection_kind", "ENCRYPT"); sub_property_policy.properties().emplace_back("rtps.endpoint.payload_protection_kind", "ENCRYPT"); - reader.setOnDiscoveryFunction([&writer](const ParticipantBuiltinTopicData& info, + reader.set_on_discovery_function([&writer](const ParticipantBuiltinTopicData& info, ParticipantDiscoveryStatus /*status*/) -> bool { if (info.guid == writer.participant_guid()) diff --git a/test/blackbox/common/BlackboxTestsTransportCustom.cpp b/test/blackbox/common/BlackboxTestsTransportCustom.cpp index 020f11ace1b..3615ba7b3ac 100644 --- a/test/blackbox/common/BlackboxTestsTransportCustom.cpp +++ b/test/blackbox/common/BlackboxTestsTransportCustom.cpp @@ -612,6 +612,20 @@ TEST(ChainingTransportTests, builtin_transports_env_large_data) BuiltinTransportsTest::test_env("LARGE_DATA"); } +/** + * DS Auto transport shall always be used along with ROS_DISCOVERY_SERVER=AUTO. + * This is due to the working principle of the mode. If it is not specified, + * the background discovery server will not be launched and the test will never + * finish since both clients will keep waiting for it. + */ +TEST(ChainingTransportTests, builtin_transports_env_p2p) +{ +#ifndef _WIN32 // ROS2_EASY_MODE not available on Windows yet + setenv("ROS2_EASY_MODE", "127.0.0.1", 1); + BuiltinTransportsTest::test_env("P2P"); +#endif // _WIN32 +} + TEST(ChainingTransportTests, builtin_transports_env_large_data_with_max_msg_size) { BuiltinTransportsTest::test_env("LARGE_DATA?max_msg_size=70KB&sockets_size=70KB"); @@ -675,6 +689,22 @@ TEST(ChainingTransportTests, builtin_transports_xml_large_data) BuiltinTransportsTest::test_xml("builtin_transports_profile.xml", "participant_largedata"); } +/** + * DS Auto transport shall always be used along with ROS2_EASY_MODE=. + * This is due to the working principle of the mode. If it is not specified, + * the background discovery server will not be launched and the test will never + * finish since both clients will keep waiting for it. + * On the other hand, defining the environment variable somehow shadows the + * xml parsing, but it is assumed in this case. + */ +TEST(ChainingTransportTests, builtin_transports_xml_p2p) +{ +#ifndef _WIN32 // ROS2_EASY_MODE not available on Windows yet + setenv("ROS2_EASY_MODE", "127.0.0.1", 1); + BuiltinTransportsTest::test_xml("builtin_transports_profile.xml", "participant_p2p"); +#endif // _WIN32 +} + TEST(ChainingTransportTests, builtin_transports_xml_large_data_with_max_msg_size) { BuiltinTransportsTest::test_xml("builtin_transports_profile.xml", "participant_largedata_max_msg_size"); diff --git a/test/blackbox/common/DDSBlackboxTestsDSEasyMode.cpp b/test/blackbox/common/DDSBlackboxTestsDSEasyMode.cpp new file mode 100644 index 00000000000..7dd41224ac4 --- /dev/null +++ b/test/blackbox/common/DDSBlackboxTestsDSEasyMode.cpp @@ -0,0 +1,364 @@ +// Copyright 2024 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// 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 +#include +#include +#include + +#include "BlackboxTests.hpp" +#include "PubSubReader.hpp" +#include "PubSubWriter.hpp" + + +void set_easy_discovery_mode_env( + const std::string& ip = "127.0.0.1") +{ + /* Set environment variable which will contain the ip of an external point of discovery*/ +#ifdef _WIN32 + _putenv_s("ROS2_EASY_MODE", ip.c_str()); +#else + setenv("ROS2_EASY_MODE", ip.c_str(), 1); +#endif // _WIN32 + +} + +void stop_background_servers() +{ + // Stop server(s) + int res = std::system("fastdds discovery stop"); + ASSERT_EQ(res, 0); +} + +/** + * Refers to FASTDDS-EASYMODE-TEST:01 from the test plan. + * + * Launching a participant client with the environment variable ROS2_EASY_MODE + * correctly spawns and discovers a Discovery Server in the expected + * domain. + * + */ +TEST(DSEasyMode, easy_discovery_mode_env_correctly_launches) +{ +#ifndef _WIN32 // The feature is not supported on Windows yet + PubSubWriter writer(TEST_TOPIC_NAME); + PubSubReader reader(TEST_TOPIC_NAME); + + // Setting ROS_DISCOVERY_SERVER to AUTO + // Configures as SUPER_CLIENT SHM and TCP + set_easy_discovery_mode_env(); + + std::atomic writer_background_ds_discovered(false); + std::atomic reader_background_ds_discovered(false); + + writer.set_on_discovery_function( + [&writer_background_ds_discovered]( + const eprosima::fastdds::rtps::ParticipantBuiltinTopicData& data, + eprosima::fastdds::rtps::ParticipantDiscoveryStatus) + { + if (data.participant_name == "DiscoveryServerAuto") + { + writer_background_ds_discovered.store(true); + } + return true; + }); + writer.init(); + + reader.set_on_discovery_function( + [&reader_background_ds_discovered](const eprosima::fastdds::rtps::ParticipantBuiltinTopicData& data, + eprosima::fastdds::rtps::ParticipantDiscoveryStatus) + { + if (data.participant_name == "DiscoveryServerAuto") + { + reader_background_ds_discovered.store(true); + } + return true; + }); + reader.init(); + + ASSERT_TRUE(writer.isInitialized()); + ASSERT_TRUE(reader.isInitialized()); + + // Wait for endpoint discovery first + writer.wait_discovery(); + reader.wait_discovery(); + + // Two participants are expected to have been discovered: + // Backgroud DS and the other reader/writer + ASSERT_GE(writer.get_participants_matched(), 2u); + ASSERT_GE(reader.get_participants_matched(), 2u); + ASSERT_TRUE(writer_background_ds_discovered.load()); + ASSERT_TRUE(reader_background_ds_discovered.load()); + + auto data = default_helloworld_data_generator(); + + reader.startReception(data); + writer.send(data); + ASSERT_TRUE(data.empty()); + + reader.block_for_all(); + + // Stop server + stop_background_servers(); +#endif // _WIN32 +} + +/** + * Refers to FASTDDS-EASYMODE-TEST:02 from the test plan. + * + * TCP and SHM are the transports used in ROS2_EASY_MODE. + */ +TEST(DSEasyMode, easy_discovery_mode_env_correct_transports_are_used) +{ +#ifndef _WIN32 // The feature is not supported on Windows yet + PubSubWriter writer_udp(TEST_TOPIC_NAME), writer_auto(TEST_TOPIC_NAME); + PubSubReader reader_auto(TEST_TOPIC_NAME); + + auto udpv4_transport = std::make_shared(); + + writer_udp.disable_builtin_transport() + .add_user_transport_to_pparams(udpv4_transport) + .init(); + + ASSERT_TRUE(writer_udp.isInitialized()); + + // Setting ROS_DISCOVERY_SERVER to AUTO + // Configures as SUPER_CLIENT SHM and TCP + set_easy_discovery_mode_env(); + + std::atomic locators_match_p2p_transport(true); + + reader_auto.set_on_discovery_function( + [&locators_match_p2p_transport](const eprosima::fastdds::rtps::ParticipantBuiltinTopicData& data, + eprosima::fastdds::rtps::ParticipantDiscoveryStatus) + { + for (auto locator : data.metatraffic_locators.unicast) + { + locators_match_p2p_transport.store( locators_match_p2p_transport && + (locator.kind == LOCATOR_KIND_UDPv4 || locator.kind == LOCATOR_KIND_SHM)); + } + + if (!data.metatraffic_locators.multicast.empty()) + { + locators_match_p2p_transport.store(false); + } + + return true; + }); + reader_auto.init(); + + ASSERT_TRUE(reader_auto.isInitialized()); + + // Discovery shall not happen + writer_udp.wait_discovery(std::chrono::seconds(1)); + reader_auto.wait_discovery(std::chrono::seconds(1)); + + ASSERT_FALSE(writer_udp.is_matched()); + ASSERT_FALSE(reader_auto.is_matched()); + + // Now launch another DS AUTO participant writer + writer_auto.init(); + + writer_auto.wait_discovery(); + reader_auto.wait_discovery(); + + ASSERT_TRUE(locators_match_p2p_transport.load()); + + // Stop server + stop_background_servers(); +#endif // _WIN32 +} + +/** + * Refers to FASTDDS-EASYMODE-TEST:03 from the test plan. + * + * Client participants are aware of the discovery + * information of the rest of participants in the same domain. + */ +TEST(DSEasyMode, easy_discovery_mode_env_discovery_info) +{ +#ifndef _WIN32 // The feature is not supported on Windows yet + unsigned int num_writers = 3; + std::vector>> writers; + writers.reserve(num_writers); + PubSubReader reader_auto(TEST_TOPIC_NAME + "_auto"); + + for (std::size_t i = 0; i < num_writers; ++i) + { + writers.emplace_back(std::make_shared>(TEST_TOPIC_NAME + std::to_string(i))); + + eprosima::fastdds::dds::WireProtocolConfigQos wire_protocol_qos; + + wire_protocol_qos.builtin.discovery_config.discoveryProtocol = + eprosima::fastdds::rtps::DiscoveryProtocol::CLIENT; + + eprosima::fastdds::rtps::Locator_t locator; + locator.kind = LOCATOR_KIND_UDPv4; + + eprosima::fastdds::rtps::PortParameters port_params; + + auto domain_port = port_params.get_discovery_server_port((uint32_t)GET_PID() % 230); + + locator.port = domain_port; + IPLocator::setIPv4(locator, "127.0.0.1"); + + // Point to the well known DS port in the corresponding domain + wire_protocol_qos.builtin.discovery_config.m_DiscoveryServers.push_back(locator); + + writers.back()->set_wire_protocol_qos(wire_protocol_qos) + .setup_p2p_transports() + .init(); + + ASSERT_TRUE(writers.back()->isInitialized()); + } + + // Setting ROS_DISCOVERY_SERVER to AUTO + // Configures as SUPER_CLIENT SHM and TCP + set_easy_discovery_mode_env(); + + reader_auto.init(); + ASSERT_TRUE(reader_auto.isInitialized()); + + // Discovery DS in Domain 0 + num_writers + reader_auto.wait_participant_discovery(num_writers + 1); + + // This participant shall discover all the other participants + // Despite not sharing a common topic with them (SUPER_CLIENT) + ASSERT_EQ(reader_auto.get_participants_matched(), num_writers + 1u); + + for (auto& writer : writers) + { + // Writers shall discover SERVER participant only + ASSERT_LE(writer->get_participants_matched(), 1u); + } + + // Stop server + stop_background_servers(); +#endif // _WIN32 +} + +/** + * Refers to FASTDDS-EASYMODE-TEST:04 from the test plan. + * + * Launching participant clients in different domains with + * ROS_DISCOVERY_SERVER set to AUTO correctly + * launches and discovers the Discovery Server in its domain. + */ +TEST(DSEasyMode, easy_discovery_mode_env_multiple_clients_multiple_domains) +{ +#ifndef _WIN32 // The feature is not supported on Windows yet + unsigned int num_writer_reader_pairs = 5; + + std::vector>> writers; + std::vector>> readers; + + // Setting ROS_DISCOVERY_SERVER to AUTO + // Configures as SUPER_CLIENT SHM and TCP + set_easy_discovery_mode_env(); + + for (std::size_t i = 10; i < 10 + num_writer_reader_pairs; ++i) + { + writers.emplace_back(std::make_shared>(TEST_TOPIC_NAME + "_domain_" + + std::to_string(i))); + readers.emplace_back(std::make_shared>(TEST_TOPIC_NAME + "_domain_" + + std::to_string(i))); + + writers.back()->set_domain_id((uint32_t)i) + .init(); + readers.back()->set_domain_id((uint32_t)i) + .init(); + } + + for (std::size_t i = 0; i < num_writer_reader_pairs; ++i) + { + writers[i]->wait_discovery(); + readers[i]->wait_discovery(); + + ASSERT_EQ(writers[i]->get_matched(), 1u); + ASSERT_EQ(readers[i]->get_matched(), 1u); + + auto data = default_helloworld_data_generator(); + + readers[i]->startReception(data); + writers[i]->send(data); + ASSERT_TRUE(data.empty()); + + readers[i]->block_for_all(); + } + + // Stop servers + stop_background_servers(); +#endif // _WIN32 +} + +/** + * Launching a second participant in the same domain with different + * ROS2_EASY_MODE ip value shall log an error. + */ +TEST(DSEasyMode, easy_discovery_mode_env_inconsistent_ip) +{ +#ifndef _WIN32 // The feature is not supported on Windows yet + + using Log = eprosima::fastdds::dds::Log; + using LogConsumer = eprosima::fastdds::dds::LogConsumer; + + // A LogConsumer accounting for any log errors + struct TestConsumer : public LogConsumer + { + TestConsumer( + std::atomic_size_t& n_logs_ref) + : n_logs_(n_logs_ref) + { + } + + void Consume( + const Log::Entry&) override + { + ++n_logs_; + } + + private: + + std::atomic_size_t& n_logs_; + }; + + // Counter for log entries + std::atomicn_logs{}; + + // Prepare Log module to check that at least one DOMAIN error is produced + Log::SetCategoryFilter(std::regex("DOMAIN")); + Log::SetVerbosity(Log::Kind::Error); + Log::RegisterConsumer(std::unique_ptr(new TestConsumer(n_logs))); + + // Set ROS2_EASY_MODE to localhost + set_easy_discovery_mode_env(); + PubSubWriter writer(TEST_TOPIC_NAME); + + writer.init(); + ASSERT_TRUE(writer.isInitialized()); + + // Set ROS2_EASY_MODE to another address in the same domain + set_easy_discovery_mode_env("192.168.1.100"); + PubSubWriter reader(TEST_TOPIC_NAME); + + reader.init(); + ASSERT_TRUE(n_logs.load() > 0); + + // Stop servers + stop_background_servers(); +#endif // _WIN32 +} diff --git a/test/blackbox/common/DDSBlackboxTestsTransportSHMUDP.cpp b/test/blackbox/common/DDSBlackboxTestsTransportSHMUDP.cpp index 1f004f0f673..ba009a8a280 100644 --- a/test/blackbox/common/DDSBlackboxTestsTransportSHMUDP.cpp +++ b/test/blackbox/common/DDSBlackboxTestsTransportSHMUDP.cpp @@ -214,7 +214,7 @@ static void shm_metatraffic_test( check_shm_locators(info, unicast, multicast); return true; }; - reader.setOnDiscoveryFunction(discovery_checker); + reader.set_on_discovery_function(discovery_checker); reader.max_multicast_locators_number(2); reader.init(); ASSERT_TRUE(reader.isInitialized()); diff --git a/test/dds/communication/security/multiple_secure_ds_pubsub_secure_crypto_communication.py b/test/dds/communication/security/multiple_secure_ds_pubsub_secure_crypto_communication.py index aaa709786cc..ed57e317452 100644 --- a/test/dds/communication/security/multiple_secure_ds_pubsub_secure_crypto_communication.py +++ b/test/dds/communication/security/multiple_secure_ds_pubsub_secure_crypto_communication.py @@ -132,7 +132,8 @@ def launch_discovery_server_processes(servers, xml_servers): f'{servers[i]}') sys.exit(1) - server_cmd.append(servers[i]) + # Call tool with the SERVER option + server_cmd.extend([servers[i], '42']) server_cmd.extend(['--xml-file', xml_servers[i]]) server_cmd.extend(['--server-id', str(i)]) diff --git a/test/dds/communication/security/secure_ds_pubsub_secure_crypto_communication.py b/test/dds/communication/security/secure_ds_pubsub_secure_crypto_communication.py index d28a673deca..7da5b62b79e 100644 --- a/test/dds/communication/security/secure_ds_pubsub_secure_crypto_communication.py +++ b/test/dds/communication/security/secure_ds_pubsub_secure_crypto_communication.py @@ -150,7 +150,8 @@ def run(args): f'{args.ds_server}') sys.exit(1) - ds_command.append(args.ds_server) + # Call tool with the SERVER option + ds_command.extend([args.ds_server, '42']) if args.xml_pub and args.xml_sub and args.xml_ds: if args.xml_pub: diff --git a/test/system/tools/fastdds/CMakeLists.txt b/test/system/tools/fastdds/CMakeLists.txt index c086215fea1..c2c8f1cc767 100644 --- a/test/system/tools/fastdds/CMakeLists.txt +++ b/test/system/tools/fastdds/CMakeLists.txt @@ -29,6 +29,8 @@ if(Python3_Interpreter_FOUND) test_fastdds_version test_fastdds_discovery test_fastdds_discovery_run + test_fastdds_discovery_help + test_fastdds_discovery_examples test_ros_discovery test_fastdds_shm test_fastdds_shm_force @@ -58,6 +60,46 @@ if(Python3_Interpreter_FOUND) endforeach() + if (INSTALL_TOOLS AND NOT WIN32) + set(DISCOVERY_PARSER_TESTS + TestDiscoveryParser.test_parser_shutdown_when_on + TestDiscoveryParser.test_parser_shutdown_when_off + TestDiscoveryParser.test_parser_auto + TestDiscoveryParser.test_parser_auto_domain_arg + TestDiscoveryParser.test_parser_auto_domain_env + TestDiscoveryParser.test_parser_auto_easy_mode_domain_env + TestDiscoveryParser.test_parser_auto_ros_static_peers + TestDiscoveryParser.test_parser_start + TestDiscoveryParser.test_parser_start_ros_static_peers + TestDiscoveryParser.test_parser_start_with_arg + TestDiscoveryParser.test_parser_stop_when_off + TestDiscoveryParser.test_parser_stop_when_on + TestDiscoveryParser.test_parser_stop_whith_unknown_args + TestDiscoveryParser.test_parser_list_when_off + TestDiscoveryParser.test_parser_list_when_on + TestDiscoveryParser.test_parser_add_when_off + TestDiscoveryParser.test_parser_add_when_on + TestDiscoveryParser.test_parser_set_when_off + TestDiscoveryParser.test_parser_set_when_on + ) + + foreach(TEST ${DISCOVERY_PARSER_TESTS}) + add_test( + NAME system.tools.fastdds.${TEST} + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_discovery_parser.py ${TEST} + ) + # Set environment + set(TEST_ENVIRONMENT + "PYTHON_VERSION=${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}" + "TOOL_PATH=${CMAKE_INSTALL_PREFIX}" + ) + set_property( + TEST system.tools.fastdds.${TEST} + PROPERTY ENVIRONMENT ${TEST_ENVIRONMENT} + ) + endforeach() + endif() + ############################################################################### # XML GENERAL validation ############################################################################### diff --git a/test/system/tools/fastdds/test_discovery_parser.py b/test/system/tools/fastdds/test_discovery_parser.py new file mode 100644 index 00000000000..a20255130af --- /dev/null +++ b/test/system/tools/fastdds/test_discovery_parser.py @@ -0,0 +1,572 @@ +# Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +# +# 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. + +import unittest +from unittest.mock import patch, MagicMock +import sys +import os +from pathlib import Path + +# Add the path to the parser module and the cpp tool to sys.path +sys.path.insert(0, str(Path(os.getenv('TOOL_PATH'), 'tools/fastdds/discovery'))) +sys.path.insert(0, str(Path(os.getenv('TOOL_PATH'), 'tools/fastdds'))) +sys.path.insert(0, str(Path(os.getenv('TOOL_PATH'), 'bin'))) +# Import the Parser class from the parser module +from parser import Parser, command_to_int, Command + + +class TestDiscoveryParser(unittest.TestCase): + def __init__(self, methodName = "test_fastdds_daemon"): + super().__init__(methodName) + # Attribute to check the command sent to the RPC server + self.check_command = '' + # Attribute to check the domain sent to the RPC server to act as index + self.domain = 0 + # Attribute to check the value of the third attr (easy_mode or check_server) sent to the RPC server + self.third_attr = '' + + def side_effect_rpc(self, *args, **kwargs): + domain = args[0] + used_cmd = args[1] + third_attr = args[2] + assert(domain == self.domain) + assert(len(used_cmd) == len(self.check_command)) + assert(third_attr == self.third_attr) + for i in range(len(self.check_command)): + assert(used_cmd[i] == self.check_command[i]) + return 'Mocked request' + + def set_env_values(self, env_var_name, value): + os.environ[env_var_name] = value + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.shutdown_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_shutdown_when_on(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_shutdown, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_shutdown.return_value = True + mock_is_running.return_value = True + + argv = ['stop'] + parser = Parser(argv) + + mock_is_running.assert_called_once() + mock_shutdown.assert_called_once() + mock_rpc_stopall.assert_called_once() + mock_rpc_nbrequest.assert_not_called() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_not_called() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.shutdown_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_shutdown_when_off(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_shutdown, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_shutdown.return_value = False + mock_is_running.return_value = False + + argv = ['stop'] + try: + parser = Parser(argv) + except SystemExit as e: + # Expecting to fail with exit code 0 + self.assertEqual(e.code, 0) + + mock_is_running.assert_called_once() + mock_shutdown.assert_not_called() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_not_called() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_called_once() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_auto(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_spawn.return_value = True + + self.check_command = [str(command_to_int[Command.AUTO]), '-d', '0'] + mock_rpc_nbrequest.side_effect = self.side_effect_rpc + + argv = ['auto'] + parser = Parser(argv) + + mock_is_running.assert_not_called() + mock_spawn.assert_called_once() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_called_once() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_not_called() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_auto_domain_arg(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_spawn.return_value = True + + self.domain = 7 + self.check_command = [str(command_to_int[Command.AUTO]), '-d', '7'] + mock_rpc_nbrequest.side_effect = self.side_effect_rpc + + argv = ['auto', '-d', '7'] + parser = Parser(argv) + print('End of test') + + mock_is_running.assert_not_called() + mock_spawn.assert_called_once() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_called_once() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_not_called() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_auto_domain_env(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_spawn.return_value = True + + self.set_env_values('ROS_DOMAIN_ID', '42') + self.domain = 42 + self.check_command = [str(command_to_int[Command.AUTO]), '-d', '42'] + mock_rpc_nbrequest.side_effect = self.side_effect_rpc + + argv = ['auto'] + parser = Parser(argv) + + mock_is_running.assert_not_called() + mock_spawn.assert_called_once() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_called_once() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_not_called() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_auto_easy_mode_domain_env(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_spawn.return_value = True + + self.set_env_values('ROS2_EASY_MODE', '127.0.0.1') + self.domain = 42 + self.third_attr = '127.0.0.1' + self.check_command = [str(command_to_int[Command.AUTO]), '-d', '42', '127.0.0.1:42'] + mock_rpc_nbrequest.side_effect = self.side_effect_rpc + + # The parser is not responsible of adding the ROS2_EASY_MODE argument to the command. This is done in Fast DDS. + # The parser only checks if the ROS2_EASY_MODE variable is set to pass it to the RPC server as third argument + argv = ['auto', '-d', '42', '127.0.0.1:42'] + parser = Parser(argv) + + mock_is_running.assert_not_called() + mock_spawn.assert_called_once() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_called_once() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_not_called() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_auto_ros_static_peers(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_spawn.return_value = True + + self.set_env_values('ROS_STATIC_PEERS', '127.0.0.1:1') + self.check_command = [str(command_to_int[Command.AUTO]), '-d', '0', '127.0.0.1:1'] + mock_rpc_nbrequest.side_effect = self.side_effect_rpc + + argv = ['auto'] + parser = Parser(argv) + + mock_is_running.assert_not_called() + mock_spawn.assert_called_once() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_called_once() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_not_called() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_start(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_spawn.return_value = True + + self.check_command = [str(command_to_int[Command.START]), '-d', '0'] + mock_rpc_nbrequest.side_effect = self.side_effect_rpc + + argv = ['start'] + parser = Parser(argv) + + mock_is_running.assert_not_called() + mock_spawn.assert_called_once() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_called_once() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_not_called() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_start_ros_static_peers(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_spawn.return_value = True + + self.set_env_values('ROS_STATIC_PEERS', '127.0.0.1:1;127.0.0.1:2') + self.check_command = [str(command_to_int[Command.START]), '-d', '0', '127.0.0.1:1;127.0.0.1:2'] + mock_rpc_nbrequest.side_effect = self.side_effect_rpc + + argv = ['start'] + parser = Parser(argv) + + mock_is_running.assert_not_called() + mock_spawn.assert_called_once() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_called_once() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_not_called() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_start_with_arg(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_spawn.return_value = True + + self.check_command = [str(command_to_int[Command.START]), '-d', '0', '127.0.0.1:4;127.0.0.1:2'] + mock_rpc_nbrequest.side_effect = self.side_effect_rpc + + argv = ['start', '127.0.0.1:4;127.0.0.1:2'] + parser = Parser(argv) + + mock_is_running.assert_not_called() + mock_spawn.assert_called_once() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_called_once() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_not_called() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.shutdown_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_request') + def test_parser_stop_when_off(self, mock_rpc_stop_once, mock_rpc_nbrequest, mock_rpc_brequest, mock_shutdown, mock_is_running, mock_exit): + mock_rpc_stop_once.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_is_running.return_value = False + mock_shutdown.return_value = True + + self.check_command = [str(command_to_int[Command.STOP]), '-d', '0'] + mock_rpc_brequest.side_effect = self.side_effect_rpc + + argv = ['stop', '-d', '0'] + try: + parser = Parser(argv) + except SystemExit as e: + # Expecting to fail with exit code 0 + self.assertEqual(e.code, 0) + + mock_is_running.assert_called_once() + mock_shutdown.assert_not_called() + mock_rpc_stop_once.assert_not_called() + mock_rpc_nbrequest.assert_not_called() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_called_once() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.shutdown_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_request') + def test_parser_stop_when_on(self, mock_rpc_stop_once, mock_rpc_nbrequest, mock_rpc_brequest, mock_shutdown, mock_is_running, mock_exit): + mock_rpc_stop_once.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_is_running.return_value = True + mock_shutdown.return_value = True + + self.check_command = [str(command_to_int[Command.STOP]), '-d', '0'] + mock_rpc_brequest.side_effect = self.side_effect_rpc + + argv = ['stop', '-d', '0'] + parser = Parser(argv) + + mock_is_running.assert_called_once() + mock_shutdown.assert_not_called() + mock_rpc_stop_once.assert_called_once() + mock_rpc_nbrequest.assert_not_called() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_not_called() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.shutdown_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_request') + def test_parser_stop_whith_unknown_args(self, mock_rpc_stop_once, mock_rpc_nbrequest, mock_rpc_brequest, mock_shutdown, mock_is_running, mock_exit): + mock_rpc_stop_once.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_is_running.return_value = True + mock_shutdown.return_value = True + + self.check_command = [str(command_to_int[Command.STOP]), '-d', '0'] + mock_rpc_brequest.side_effect = self.side_effect_rpc + + argv = ['stop', '-d', '0', 'extra_arg'] + parser = Parser(argv) + + mock_is_running.assert_called_once() + mock_shutdown.assert_not_called() + mock_rpc_stop_once.assert_not_called() + mock_rpc_nbrequest.assert_not_called() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_called_once() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_list_when_off(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_is_running.return_value = False + mock_spawn.return_value = True + + self.third_attr = False + self.check_command = [str(command_to_int[Command.LIST]), '-d', '0'] + mock_rpc_brequest.side_effect = self.side_effect_rpc + + argv = ['list'] + try: + parser = Parser(argv) + except SystemExit as e: + # Expecting to fail with exit code 0 + self.assertEqual(e.code, 0) + + mock_is_running.assert_called_once() + mock_spawn.assert_not_called() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_not_called() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_called_once() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_list_when_on(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_is_running.return_value = True + mock_spawn.return_value = True + + self.third_attr = False + self.check_command = [str(command_to_int[Command.LIST]), '-d', '0'] + mock_rpc_brequest.side_effect = self.side_effect_rpc + + argv = ['list'] + parser = Parser(argv) + + mock_is_running.assert_called_once() + mock_spawn.assert_not_called() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_not_called() + mock_rpc_brequest.assert_called_once() + mock_exit.assert_not_called() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_add_when_off(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_is_running.return_value = False + mock_spawn.return_value = True + + self.third_attr = True + self.check_command = [str(command_to_int[Command.ADD]), '-d', '0'] + mock_rpc_brequest.side_effect = self.side_effect_rpc + + argv = ['add'] + try: + parser = Parser(argv) + except SystemExit as e: + # Expecting to fail with exit code 0 + self.assertEqual(e.code, 0) + + mock_is_running.assert_called_once() + mock_spawn.assert_not_called() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_not_called() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_called_once() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_add_when_on(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_is_running.return_value = True + mock_spawn.return_value = True + + self.third_attr = True + self.check_command = [str(command_to_int[Command.ADD]), '-d', '0'] + mock_rpc_brequest.side_effect = self.side_effect_rpc + + argv = ['add'] + parser = Parser(argv) + + mock_is_running.assert_called_once() + mock_spawn.assert_not_called() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_not_called() + mock_rpc_brequest.assert_called_once() + mock_exit.assert_not_called() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_set_when_off(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_is_running.return_value = False + mock_spawn.return_value = True + + self.third_attr = True + self.check_command = [str(command_to_int[Command.SET]), '-d', '0'] + mock_rpc_brequest.side_effect = self.side_effect_rpc + + argv = ['set'] + try: + parser = Parser(argv) + except SystemExit as e: + # Expecting to fail with exit code 0 + self.assertEqual(e.code, 0) + + mock_is_running.assert_called_once() + mock_spawn.assert_not_called() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_not_called() + mock_rpc_brequest.assert_not_called() + mock_exit.assert_called_once() + + @patch('parser.sys.exit') + @patch('parser.is_daemon_running') + @patch('parser.spawn_daemon') + @patch('parser.client_cli.run_request_b') + @patch('parser.client_cli.run_request_nb') + @patch('parser.client_cli.stop_all_request') + def test_parser_set_when_on(self, mock_rpc_stopall, mock_rpc_nbrequest, mock_rpc_brequest, mock_spawn, mock_is_running, mock_exit): + mock_rpc_stopall.return_value = 'Mocked request' + mock_rpc_nbrequest.return_value = 'Mocked request' + mock_rpc_brequest.return_value = 'Mocked request' + mock_is_running.return_value = True + mock_spawn.return_value = True + + self.third_attr = True + self.check_command = [str(command_to_int[Command.SET]), '-d', '0'] + mock_rpc_brequest.side_effect = self.side_effect_rpc + + argv = ['set'] + parser = Parser(argv) + + mock_is_running.assert_called_once() + mock_spawn.assert_not_called() + mock_rpc_stopall.assert_not_called() + mock_rpc_nbrequest.assert_not_called() + mock_rpc_brequest.assert_called_once() + mock_exit.assert_not_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/test/system/tools/fastdds/tests.py b/test/system/tools/fastdds/tests.py index ad79c8cc330..446bc6c138c 100644 --- a/test/system/tools/fastdds/tests.py +++ b/test/system/tools/fastdds/tests.py @@ -29,14 +29,16 @@ test_fastdds_version test_fastdds_discovery test_fastdds_discovery_run + test_fastdds_discovery_help + test_fastdds_discovery_examples test_fastdds_shm test_fastdds_xml_validate test_ros_discovery """ - import argparse import os +import signal import subprocess import signal import sys @@ -49,7 +51,6 @@ 'Try to install running "pip install psutil"') sys.exit(1) - def setup_script_name(): """ Test script for POSIX os. @@ -111,10 +112,24 @@ def test_fastdds_installed(install_path): def test_fastdds_version(install_path): """Test that fastdds version is printed correctly.""" args = '-v' - ret = subprocess.call(cmd(install_path, args=args), shell=True) - if 0 != ret: - print('test_fastdds_version FAILED') - sys.exit(ret) + try: + ret = subprocess.run(cmd(install_path, args=args), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + shell=True, + timeout=5) + except subprocess.TimeoutExpired: + print('test_fastdds_version FAILED due to timeout') + sys.exit(1) + + if 0 != ret.returncode: + print('test_fastdds_version FAILED due to return code') + sys.exit(ret.returncode) + + if 'Fast DDS version:' not in ret.stdout: + print('test_fastdds_version FAILED due to unexpected output') + sys.exit(ret.returncode) def test_fastdds_shm(install_path): """Test that shm command runs.""" @@ -155,6 +170,161 @@ def test_fastdds_discovery(install_path, setup_script_path): print('test_fastdds_discovery FAILED') sys.exit(ret) +def test_fastdds_discovery_help(install_path, setup_script_path): + """Test that discovery help command is displayed if present in command.""" + args = ' discovery -l 127.0.0.1 -p 11811 -h' + test_timeout = 5 + try: + if os.name == "nt": + # Windows: use CREATE_NEW_PROCESS_GROUP + process = subprocess.Popen( + cmd(install_path=install_path, + setup_script_path=setup_script_path, + args=args), + shell=True, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP + ) + else: + # POSIX-specific: use os.setsid + process = subprocess.Popen( + cmd(install_path=install_path, + setup_script_path=setup_script_path, + args=args), + shell=True, + preexec_fn=os.setsid) + ret = process.wait(timeout=test_timeout) + except subprocess.TimeoutExpired: + print(f'Timeout {test_timeout} expired.') + if os.name == "nt": + # Use psutil to end all child processes in Windows + parent = psutil.Process(process.pid) + for child in parent.children(recursive=True): + child.terminate() + parent.terminate() + else: + # Use os.killpg to end all child processes in POSIX + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + ret = -1 + + if 0 != ret: + print('test_fastdds_discovery_help_short FAILED with ret_code: ', ret) + sys.exit(ret) + + args = ' discovery -l 127.0.0.1 -p 11811 --help' + try: + if os.name == "nt": + # Windows: use CREATE_NEW_PROCESS_GROUP + process = subprocess.Popen( + cmd(install_path=install_path, + setup_script_path=setup_script_path, + args=args), + shell=True, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP + ) + else: + # POSIX-specific: use os.setsid + process = subprocess.Popen( + cmd(install_path=install_path, + setup_script_path=setup_script_path, + args=args), + shell=True, + preexec_fn=os.setsid) + ret = process.wait(timeout=test_timeout) + except subprocess.TimeoutExpired: + print(f'Timeout {test_timeout} expired.') + if os.name == "nt": + # Use psutil to end all child processes in Windows + parent = psutil.Process(process.pid) + for child in parent.children(recursive=True): + child.terminate() + parent.terminate() + else: + # Use os.killpg to end all child processes in POSIX + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + ret = -1 + + if 0 != ret: + print('test_fastdds_discovery_help_short FAILED with ret_code: ', ret) + sys.exit(ret) + + +def test_fastdds_discovery_examples(install_path, setup_script_path): + """Test that discovery examples command is displayed if present in command.""" + args = ' discovery -l 127.0.0.1 -p 11811 -e' + test_timeout = 5 + try: + if os.name == "nt": + # Windows: use CREATE_NEW_PROCESS_GROUP + process = subprocess.Popen( + cmd(install_path=install_path, + setup_script_path=setup_script_path, + args=args), + shell=True, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP + ) + else: + # POSIX-specific: use os.setsid + process = subprocess.Popen( + cmd(install_path=install_path, + setup_script_path=setup_script_path, + args=args), + shell=True, + preexec_fn=os.setsid) + ret = process.wait(timeout=test_timeout) + except subprocess.TimeoutExpired: + print(f'Timeout {test_timeout} expired.') + if os.name == "nt": + # Use psutil to end all child processes in Windows + parent = psutil.Process(process.pid) + for child in parent.children(recursive=True): + child.terminate() + parent.terminate() + else: + # Use os.killpg to end all child processes in POSIX + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + ret = -1 + + if 0 != ret: + print('test_fastdds_discovery_examples_short FAILED with ret_code: ', ret) + sys.exit(ret) + + args = ' discovery -l 127.0.0.1 -p 11811 --examples' + try: + if os.name == "nt": + # Windows: use CREATE_NEW_PROCESS_GROUP + process = subprocess.Popen( + cmd(install_path=install_path, + setup_script_path=setup_script_path, + args=args), + shell=True, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP + ) + else: + # POSIX-specific: use os.setsid + process = subprocess.Popen( + cmd(install_path=install_path, + setup_script_path=setup_script_path, + args=args), + shell=True, + preexec_fn=os.setsid) + ret = process.wait(timeout=test_timeout) + except subprocess.TimeoutExpired: + print(f'Timeout {test_timeout} expired.') + if os.name == "nt": + # Use psutil to end all child processes in Windows + parent = psutil.Process(process.pid) + for child in parent.children(recursive=True): + child.terminate() + parent.terminate() + else: + # Use os.killpg to end all child processes in POSIX + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + ret = -1 + + if 0 != ret: + print('test_fastdds_discovery_examples_long FAILED with ret_code: ', ret) + sys.exit(ret) + def test_fastdds_discovery_run(install_path, setup_script_path): """Test that discovery command runs.""" @@ -269,6 +439,10 @@ def get_paths(install_path): fastdds_tool_path, setup_script_path), 'test_fastdds_discovery_run': lambda: test_fastdds_discovery_run( fastdds_tool_path, setup_script_path), + 'test_fastdds_discovery_help': lambda: test_fastdds_discovery_help( + fastdds_tool_path, setup_script_path), + 'test_fastdds_discovery_examples': lambda: test_fastdds_discovery_examples( + fastdds_tool_path, setup_script_path), 'test_ros_discovery': lambda: test_ros_discovery(ros_disc_tool_path, setup_script_path), 'test_fastdds_shm': lambda: test_fastdds_shm(fastdds_tool_path), diff --git a/test/system/tools/fds/CMakeLists.txt b/test/system/tools/fds/CMakeLists.txt index 94eb117d662..64d17bdbd4f 100644 --- a/test/system/tools/fds/CMakeLists.txt +++ b/test/system/tools/fds/CMakeLists.txt @@ -127,3 +127,33 @@ if(Python3_Interpreter_FOUND) unset(TEST_ENVIRONMENT) endif() + +# C++ Unittests +set(CLI_MANAGER_TESTS_EXEC CliDiscoveryManagerTests) + +set(CLI_MANAGER_TESTS_SOURCE + CliDiscoveryManagerTests.cpp + ${PROJECT_SOURCE_DIR}/tools/fds/CliDiscoveryManager.cpp + ) + +add_executable(${CLI_MANAGER_TESTS_EXEC} ${CLI_MANAGER_TESTS_SOURCE}) + +target_compile_definitions(${CLI_MANAGER_TESTS_EXEC} + PRIVATE FAST_SERVER_VERSION=\"${PROJECT_VERSION}\" + PRIVATE FAST_SERVER_BINARY=\"$\" ) + +target_include_directories( ${CLI_MANAGER_TESTS_EXEC} PRIVATE + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_BINARY_DIR}/include + ${PROJECT_SOURCE_DIR}/tools/fds + ) + +target_link_libraries( + ${CLI_MANAGER_TESTS_EXEC} + fastdds + fastcdr + fastdds::optionparser + GTest::gtest + ${CMAKE_DL_LIBS}) + +gtest_discover_tests(${CLI_MANAGER_TESTS_EXEC}) diff --git a/test/system/tools/fds/CliDiscoveryManagerTests.cpp b/test/system/tools/fds/CliDiscoveryManagerTests.cpp new file mode 100644 index 00000000000..c922df98179 --- /dev/null +++ b/test/system/tools/fds/CliDiscoveryManagerTests.cpp @@ -0,0 +1,825 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// 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 "MockCliDiscoveryManager.hpp" +#include "CliDiscoveryParser.hpp" + +#include +#include +#include +#include +#include +#if __APPLE__ +#include +#endif // if __APPLE__ + +#include +#include +#include +#include +#include +#include + + +using namespace eprosima::fastdds::dds; +using namespace eprosima::fastdds::rtps; +using namespace eprosima::option; + +// TestCase uses the following format: +// {argv, udp_ips, udp_ports, tcp_ips, tcp_ports} +using TestCase = std::tuple, std::vector, std::vector, + std::vector, std::vector>; + +class CliDiscoveryManagerTest : public ::testing::Test +{ +public: + + void createOptionsAndParser( + int argc, + const char* argv[]) + { + Stats stats(usage, argc, argv); + options = std::vector