diff --git a/rclpy/CMakeLists.txt b/rclpy/CMakeLists.txt index d91c6b091..6862715cc 100644 --- a/rclpy/CMakeLists.txt +++ b/rclpy/CMakeLists.txt @@ -195,6 +195,7 @@ if(BUILD_TESTING) test/test_rate.py test/test_rosout_subscription.py test/test_serialization.py + test/test_service.py test/test_service_introspection.py test/test_subscription.py test/test_task.py diff --git a/rclpy/rclpy/client.py b/rclpy/rclpy/client.py index 7ae2350d4..66c44c984 100644 --- a/rclpy/rclpy/client.py +++ b/rclpy/rclpy/client.py @@ -204,5 +204,10 @@ def configure_introspection( def handle(self): return self.__client + @property + def service_name(self) -> str: + with self.handle: + return self.__client.service_name + def destroy(self): self.__client.destroy_when_not_in_use() diff --git a/rclpy/rclpy/service.py b/rclpy/rclpy/service.py index 377996b26..c375e4bd6 100644 --- a/rclpy/rclpy/service.py +++ b/rclpy/rclpy/service.py @@ -102,5 +102,10 @@ def configure_introspection( def handle(self): return self.__service + @property + def service_name(self) -> str: + with self.handle: + return self.__service.name + def destroy(self): self.__service.destroy_when_not_in_use() diff --git a/rclpy/src/rclpy/client.cpp b/rclpy/src/rclpy/client.cpp index 1376ec91a..c25a70a55 100644 --- a/rclpy/src/rclpy/client.cpp +++ b/rclpy/src/rclpy/client.cpp @@ -164,11 +164,20 @@ Client::configure_introspection( } } +const char * +Client::get_service_name() +{ + return rcl_client_get_service_name(rcl_client_.get()); +} + void define_client(py::object module) { py::class_>(module, "Client") .def(py::init()) + .def_property_readonly( + "service_name", &Client::get_service_name, + "Get the name of the service") .def_property_readonly( "pointer", [](const Client & client) { return reinterpret_cast(client.rcl_ptr()); diff --git a/rclpy/src/rclpy/client.hpp b/rclpy/src/rclpy/client.hpp index 90088afce..9cef3400c 100644 --- a/rclpy/src/rclpy/client.hpp +++ b/rclpy/src/rclpy/client.hpp @@ -105,6 +105,10 @@ class Client : public Destroyable, public std::enable_shared_from_this void destroy() override; + /// Get the service name. + const char * + get_service_name(); + private: Node node_; std::shared_ptr rcl_client_; diff --git a/rclpy/test/test_client.py b/rclpy/test/test_client.py index 6903fc78b..69cfd3b68 100644 --- a/rclpy/test/test_client.py +++ b/rclpy/test/test_client.py @@ -20,6 +20,7 @@ import rclpy import rclpy.executors from rclpy.utilities import get_rmw_implementation_identifier +from test_msgs.srv import Empty # TODO(sloretz) Reduce fudge once wait_for_service uses node graph events TIME_FUDGE = 0.3 @@ -38,6 +39,23 @@ def tearDownClass(cls): cls.node.destroy_node() rclpy.shutdown(context=cls.context) + @classmethod + def do_test_service_name(cls, test_service_name_list): + for service_name, ns, cli_args, target_service_name in test_service_name_list: + node = rclpy.create_node( + node_name='node_name', + context=cls.context, + namespace=ns, + cli_args=cli_args, + start_parameter_services=False) + client = node.create_client( + srv_type=Empty, + srv_name=service_name + ) + assert client.service_name == target_service_name + client.destroy() + node.destroy_node() + def test_wait_for_service_5sec(self): cli = self.node.create_client(GetParameters, 'get/parameters') try: @@ -136,6 +154,35 @@ def test_different_type_raises(self): self.node.destroy_client(cli) self.node.destroy_service(srv) + def test_get_service_name(self): + test_service_name_list = [ + # test_service_name, namespace, cli_args for remap, expected service name + # No namespaces + ('service', None, None, '/service'), + ('example/service', None, None, '/example/service'), + # Using service names with namespaces + ('service', 'ns', None, '/ns/service'), + ('example/service', 'ns', None, '/ns/example/service'), + ('example/service', 'my/ns', None, '/my/ns/example/service'), + ('example/service', '/my/ns', None, '/my/ns/example/service'), + # Global service name + ('/service', 'ns', None, '/service'), + ('/example/service', 'ns', None, '/example/service') + ] + TestClient.do_test_service_name(test_service_name_list) + + def test_get_service_name_after_remapping(self): + test_service_name_list = [ + ('service', None, ['--ros-args', '--remap', 'service:=new_service'], '/new_service'), + ('service', 'ns', ['--ros-args', '--remap', 'service:=new_service'], + '/ns/new_service'), + ('service', 'ns', ['--ros-args', '--remap', 'service:=example/new_service'], + '/ns/example/new_service'), + ('example/service', 'ns', ['--ros-args', '--remap', 'example/service:=new_service'], + '/ns/new_service') + ] + TestClient.do_test_service_name(test_service_name_list) + if __name__ == '__main__': unittest.main() diff --git a/rclpy/test/test_service.py b/rclpy/test/test_service.py new file mode 100644 index 000000000..c620afb11 --- /dev/null +++ b/rclpy/test/test_service.py @@ -0,0 +1,80 @@ +# Copyright 2023 Sony Group Corporation. +# +# 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 pytest + +import rclpy +from rclpy.node import Node + +from test_msgs.srv import Empty + + +@pytest.fixture(autouse=True) +def default_context(): + rclpy.init() + yield + rclpy.shutdown() + + +@pytest.mark.parametrize('service_name, namespace, expected', [ + # No namespaces + ('service', None, '/service'), + ('example/service', None, '/example/service'), + # Using service names with namespaces + ('service', 'ns', '/ns/service'), + ('example/service', 'ns', '/ns/example/service'), + ('example/service', 'my/ns', '/my/ns/example/service'), + ('example/service', '/my/ns', '/my/ns/example/service'), + # Global service name + ('/service', 'ns', '/service'), + ('/example/service', 'ns', '/example/service'), +]) +def test_get_service_name(service_name, namespace, expected): + node = Node('node_name', namespace=namespace, cli_args=None, start_parameter_services=False) + srv = node.create_service( + srv_type=Empty, + srv_name=service_name, + callback=lambda _: None + ) + + assert srv.service_name == expected + + srv.destroy() + node.destroy_node() + + +@pytest.mark.parametrize('service_name, namespace, cli_args, expected', [ + ('service', None, ['--ros-args', '--remap', 'service:=new_service'], '/new_service'), + ('service', 'ns', ['--ros-args', '--remap', 'service:=new_service'], '/ns/new_service'), + ('service', 'ns', ['--ros-args', '--remap', 'service:=example/new_service'], + '/ns/example/new_service'), + ('example/service', 'ns', ['--ros-args', '--remap', 'example/service:=new_service'], + '/ns/new_service'), +]) +def test_get_service_name_after_remapping(service_name, namespace, cli_args, expected): + node = Node( + 'node_name', + namespace=namespace, + cli_args=cli_args, + start_parameter_services=False) + srv = node.create_service( + srv_type=Empty, + srv_name=service_name, + callback=lambda _: None + ) + + assert srv.service_name == expected + + srv.destroy() + node.destroy_node()