From e2abd4bf62fc7b0c37683441c89c9d548ee6abf7 Mon Sep 17 00:00:00 2001 From: Kenji Miyake <31987104+kenji-miyake@users.noreply.github.com> Date: Wed, 9 Jun 2021 05:22:33 +0900 Subject: [PATCH] Support container in frontend (#235) Signed-off-by: Kenji Miyake --- .../actions/composable_node_container.py | 20 +++ .../actions/load_composable_nodes.py | 21 +++ .../descriptions/composable_node.py | 48 ++++++ .../frontend/test_component_container.py | 161 ++++++++++++++++++ 4 files changed, 250 insertions(+) create mode 100644 test_launch_ros/test/test_launch_ros/frontend/test_component_container.py diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index bbdb00ee..d054d85b 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -18,6 +18,9 @@ from typing import Optional from launch.action import Action +from launch.frontend import Entity +from launch.frontend import expose_action +from launch.frontend import Parser from launch.launch_context import LaunchContext from launch.some_substitutions_type import SomeSubstitutionsType @@ -26,6 +29,7 @@ from ..descriptions import ComposableNode +@expose_action('node_container') class ComposableNodeContainer(Node): """Action that executes a container ROS node for composable ROS nodes.""" @@ -51,6 +55,22 @@ def __init__( super().__init__(name=name, namespace=namespace, **kwargs) self.__composable_node_descriptions = composable_node_descriptions + @classmethod + def parse(cls, entity: Entity, parser: Parser): + """Parse node_container.""" + _, kwargs = super().parse(entity, parser) + + composable_nodes = entity.get_attr( + 'composable_node', data_type=List[Entity], optional=True) + if composable_nodes is not None: + kwargs['composable_node_descriptions'] = [] + for entity in composable_nodes: + composable_node_cls, composable_node_kwargs = ComposableNode.parse(parser, entity) + kwargs['composable_node_descriptions'].append( + composable_node_cls(**composable_node_kwargs)) + + return cls, kwargs + def execute(self, context: LaunchContext) -> Optional[List[Action]]: """ Execute the action. diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index 9f8990bb..fe6241e0 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -24,6 +24,9 @@ import composition_interfaces.srv from launch.action import Action +from launch.frontend import Entity +from launch.frontend import expose_action +from launch.frontend import Parser from launch.launch_context import LaunchContext import launch.logging from launch.some_substitutions_type import SomeSubstitutionsType @@ -46,6 +49,7 @@ from ..utilities.normalize_parameters import normalize_parameter_dict +@expose_action('load_composable_node') class LoadComposableNodes(Action): """Action that loads composable ROS nodes into a running container.""" @@ -82,6 +86,23 @@ def __init__( self.__final_target_container_name = None # type: Optional[Text] self.__logger = launch.logging.get_logger(__name__) + @classmethod + def parse(cls, entity: Entity, parser: Parser): + """Parse load_composable_node.""" + _, kwargs = super().parse(entity, parser) + + kwargs['target_container'] = parser.parse_substitution( + entity.get_attr('target', data_type=str)) + + composable_nodes = entity.get_attr('composable_node', data_type=List[Entity]) + kwargs['composable_node_descriptions'] = [] + for entity in composable_nodes: + composable_node_cls, composable_node_kwargs = ComposableNode.parse(parser, entity) + kwargs['composable_node_descriptions'].append( + composable_node_cls(**composable_node_kwargs)) + + return cls, kwargs + def _load_node( self, request: composition_interfaces.srv.LoadNode.Request, diff --git a/launch_ros/launch_ros/descriptions/composable_node.py b/launch_ros/launch_ros/descriptions/composable_node.py index c2fa4abc..7f1bcc61 100644 --- a/launch_ros/launch_ros/descriptions/composable_node.py +++ b/launch_ros/launch_ros/descriptions/composable_node.py @@ -17,6 +17,8 @@ from typing import List from typing import Optional +from launch.frontend import Entity +from launch.frontend import Parser from launch.some_substitutions_type import SomeSubstitutionsType from launch.substitution import Substitution # from launch.utilities import ensure_argument_type @@ -76,6 +78,52 @@ def __init__( if extra_arguments: self.__extra_arguments = normalize_parameters(extra_arguments) + @classmethod + def parse(cls, parser: Parser, entity: Entity): + """Parse composable_node.""" + from launch_ros.actions import Node + kwargs = {} + + kwargs['package'] = parser.parse_substitution(entity.get_attr('pkg')) + kwargs['plugin'] = parser.parse_substitution(entity.get_attr('plugin')) + kwargs['name'] = parser.parse_substitution(entity.get_attr('name')) + + namespace = entity.get_attr('namespace', optional=True) + if namespace is not None: + kwargs['namespace'] = parser.parse_substitution(namespace) + + parameters = entity.get_attr('param', data_type=List[Entity], optional=True) + if parameters is not None: + kwargs['parameters'] = Node.parse_nested_parameters(parameters, parser) + + remappings = entity.get_attr('remap', data_type=List[Entity], optional=True) + if remappings is not None: + kwargs['remappings'] = [ + ( + parser.parse_substitution(remap.get_attr('from')), + parser.parse_substitution(remap.get_attr('to')) + ) for remap in remappings + ] + + for remap in remappings: + remap.assert_entity_completely_parsed() + + extra_arguments = entity.get_attr('extra_arg', data_type=List[Entity], optional=True) + if extra_arguments is not None: + kwargs['extra_arguments'] = [ + { + tuple(parser.parse_substitution(extra_arg.get_attr('name'))): + parser.parse_substitution(extra_arg.get_attr('value')) + } for extra_arg in extra_arguments + ] + + for extra_arg in extra_arguments: + extra_arg.assert_entity_completely_parsed() + + entity.assert_entity_completely_parsed() + + return cls, kwargs + @property def package(self) -> List[Substitution]: """Get node package name as a sequence of substitutions to be performed.""" diff --git a/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py b/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py new file mode 100644 index 00000000..0ed30485 --- /dev/null +++ b/test_launch_ros/test/test_launch_ros/frontend/test_component_container.py @@ -0,0 +1,161 @@ +# Copyright 2019 Open Source Robotics Foundation, Inc. +# Copyright 2020 Open Avatar Inc. +# +# 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 asyncio +import io +import textwrap + +from launch import LaunchService +from launch.frontend import Parser +from launch.utilities import perform_substitutions +from launch_ros.utilities import evaluate_parameters +import osrf_pycommon.process_utils + + +def test_launch_component_container_yaml(): + yaml_file = textwrap.dedent( + r""" + launch: + - node_container: + pkg: rclcpp_components + exec: component_container + name: my_container + namespace: '' + args: 'test_args' + composable_node: + - pkg: composition + plugin: composition::Talker + name: talker + namespace: test_namespace + remap: + - from: chatter + to: /remap/chatter + param: + - name: use_sim_time + value: true + extra_arg: + - name: use_intra_process_comms + value: 'true' + + - load_composable_node: + target: my_container + composable_node: + - pkg: composition + plugin: composition::Listener + name: listener + namespace: test_namespace + remap: + - from: chatter + to: /remap/chatter + param: + - name: use_sim_time + value: true + extra_arg: + - name: use_intra_process_comms + value: 'true' + """ + ) + with io.StringIO(yaml_file) as f: + check_launch_component_container(f) + + +def test_launch_component_container_xml(): + xml_file = textwrap.dedent( + r""" + + + + + + + + + + + + + + + + + + """ # noqa: E501 + ) + with io.StringIO(xml_file) as f: + check_launch_component_container(f) + + +def check_launch_component_container(file): + root_entity, parser = Parser.load(file) + ld = parser.parse_description(root_entity) + ls = LaunchService() + ls.include_launch_description(ld) + + loop = osrf_pycommon.process_utils.get_loop() + launch_task = loop.create_task(ls.run_async()) + + node_container, load_composable_node = ld.describe_sub_entities() + talker = node_container._ComposableNodeContainer__composable_node_descriptions[0] + listener = load_composable_node._LoadComposableNodes__composable_node_descriptions[0] + + def perform(substitution): + return perform_substitutions(ls.context, substitution) + + # Check container params + assert perform(node_container._Node__package) == 'rclcpp_components' + assert perform(node_container._Node__node_executable) == 'component_container' + assert perform(node_container._Node__node_name) == 'my_container' + assert perform(node_container._Node__node_namespace) == '' + assert perform(node_container._Node__arguments[0]) == 'test_args' + + assert perform(load_composable_node._LoadComposableNodes__target_container) == 'my_container' + + # Check node parameters + talker_remappings = list(talker._ComposableNode__remappings) + listener_remappings = list(listener._ComposableNode__remappings) + + talker_params = evaluate_parameters(ls.context, talker._ComposableNode__parameters) + listener_params = evaluate_parameters(ls.context, listener._ComposableNode__parameters) + + talker_extra_args = evaluate_parameters(ls.context, talker._ComposableNode__extra_arguments) + listener_extra_args = evaluate_parameters( + ls.context, listener._ComposableNode__extra_arguments) + + assert perform(talker._ComposableNode__package) == 'composition' + assert perform(talker._ComposableNode__node_plugin) == 'composition::Talker' + assert perform(talker._ComposableNode__node_name) == 'talker' + assert perform(talker._ComposableNode__node_namespace) == 'test_namespace' + assert (perform(talker_remappings[0][0]), + perform(talker_remappings[0][1])) == ('chatter', '/remap/chatter') + assert talker_params[0]['use_sim_time'] is True + + assert perform(listener._ComposableNode__package) == 'composition' + assert perform(listener._ComposableNode__node_plugin) == 'composition::Listener' + assert perform(listener._ComposableNode__node_name) == 'listener' + assert perform(listener._ComposableNode__node_namespace) == 'test_namespace' + assert (perform(listener_remappings[0][0]), + perform(listener_remappings[0][1])) == ('chatter', '/remap/chatter') + assert listener_params[0]['use_sim_time'] is True + + # Check extra arguments + assert talker_extra_args[0]['use_intra_process_comms'] is True + assert listener_extra_args[0]['use_intra_process_comms'] is True + + timeout_sec = 5 + loop.run_until_complete(asyncio.sleep(timeout_sec)) + if not launch_task.done(): + loop.create_task(ls.shutdown()) + loop.run_until_complete(launch_task) + assert 0 == launch_task.result()