diff --git a/launch_ros/launch_ros/actions/load_composable_nodes.py b/launch_ros/launch_ros/actions/load_composable_nodes.py index 0d24a459..fda356a2 100644 --- a/launch_ros/launch_ros/actions/load_composable_nodes.py +++ b/launch_ros/launch_ros/actions/load_composable_nodes.py @@ -229,16 +229,19 @@ def execute( # Generate load requests before execute() exits to avoid race with context changing # due to scope change (e.g. if loading nodes from within a GroupAction). - load_node_requests = [ - get_composable_node_load_request(node_description, context) - for node_description in self.__composable_node_descriptions - ] - - context.add_completion_future( - context.asyncio_loop.run_in_executor( - None, self._load_in_sequence, load_node_requests, context + load_node_requests = [] + for node_description in self.__composable_node_descriptions: + request = get_composable_node_load_request(node_description, context) + # The request can be None if the node description's condition evaluates to False + if request is not None: + load_node_requests.append(request) + + if load_node_requests: + context.add_completion_future( + context.asyncio_loop.run_in_executor( + None, self._load_in_sequence, load_node_requests, context + ) ) - ) def get_composable_node_load_request( @@ -246,6 +249,11 @@ def get_composable_node_load_request( context: LaunchContext ): """Get the request that will be sent to the composable node container.""" + if composable_node_description.condition() is not None: + if not composable_node_description.condition().evaluate(context): + # Return no request if the node description's condition evaluates to False + return None + request = composition_interfaces.srv.LoadNode.Request() request.package_name = perform_substitutions( context, composable_node_description.package diff --git a/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py b/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py index 5026130c..76ca7afd 100644 --- a/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py +++ b/test_launch_ros/test/test_launch_ros/actions/test_load_composable_nodes.py @@ -22,6 +22,7 @@ from launch import LaunchDescription from launch import LaunchService from launch.actions import GroupAction +from launch.conditions import IfCondition from launch_ros.actions import LoadComposableNodes from launch_ros.actions import PushROSNamespace from launch_ros.actions import SetRemap @@ -99,6 +100,7 @@ def _load_composable_node( plugin, name, namespace='', + condition=None, parameters=None, remappings=None, target_container=f'/{TEST_CONTAINER_NAME}' @@ -107,6 +109,7 @@ def _load_composable_node( target_container=target_container, composable_node_descriptions=[ ComposableNode( + condition=condition, package=package, plugin=plugin, name=name, @@ -150,6 +153,41 @@ def test_load_node(mock_component_container): assert len(request.extra_arguments) == 0 +def test_load_node_with_conditions(mock_component_container): + """Test loading nodes with conditions scoped to a group.""" + context = _assert_launch_no_errors([ + _load_composable_node( + package='foo_package', + plugin='bar_plugin', + name='test_node_name_true', + namespace='test_node_namespace', + condition=IfCondition('True') + ), + _load_composable_node( + package='foo_package', + plugin='bar_plugin', + name='test_node_name_false', + namespace='test_node_namespace', + condition=IfCondition('False') + ) + ]) + + # Check that launch is aware of loaded component + assert get_node_name_count(context, '/test_node_namespace/test_node_name_true') == 1 + assert get_node_name_count(context, '/test_node_namespace/test_node_name_false') == 0 + + # Check that container recieved correct request + assert len(mock_component_container.requests) == 1 + request = mock_component_container.requests[0] + assert request.package_name == 'foo_package' + assert request.plugin_name == 'bar_plugin' + assert request.node_name == 'test_node_name_true' + assert request.node_namespace == '/test_node_namespace' + assert len(request.remap_rules) == 0 + assert len(request.parameters) == 0 + assert len(request.extra_arguments) == 0 + + def test_load_node_with_remaps(mock_component_container): """Test loading a node with remappings.""" context = _assert_launch_no_errors([ @@ -515,3 +553,44 @@ def test_load_node_with_namespace_in_group(mock_component_container): assert len(request.remap_rules) == 0 assert len(request.parameters) == 0 assert len(request.extra_arguments) == 0 + + +def test_load_node_with_condition_in_group(mock_component_container): + """Test loading nodes with conditions scoped to a group.""" + context = _assert_launch_no_errors([ + GroupAction( + [ + PushROSNamespace('foo'), + _load_composable_node( + package='foo_package', + plugin='bar_plugin', + name='test_node_name_true', + namespace='test_node_namespace', + condition=IfCondition('True') + ), + _load_composable_node( + package='foo_package', + plugin='bar_plugin', + name='test_node_name_false', + namespace='test_node_namespace', + condition=IfCondition('False') + ), + ], + scoped=True, + ), + ]) + + # Check that launch is aware of loaded component + assert get_node_name_count(context, '/foo/test_node_namespace/test_node_name_true') == 1 + assert get_node_name_count(context, '/foo/test_node_namespace/test_node_name_false') == 0 + + # Check that container recieved correct request + assert len(mock_component_container.requests) == 1 + request = mock_component_container.requests[0] + assert request.package_name == 'foo_package' + assert request.plugin_name == 'bar_plugin' + assert request.node_name == 'test_node_name_true' + assert request.node_namespace == '/foo/test_node_namespace' + assert len(request.remap_rules) == 0 + assert len(request.parameters) == 0 + assert len(request.extra_arguments) == 0