def test_unallowed_yaml_types_in_substitutions(): with pytest.raises(TypeError) as exc: orig = [{'foo': 1, 'fiz': TextSubstitution(text="{'asd': 3}")}] norm = normalize_parameters(orig) evaluate_parameters(LaunchContext(), norm) assert 'Allowed value types' in str(exc.value) assert 'dict' in str(exc.value) with pytest.raises(TypeError) as exc: orig = [{'foo': 1, 'fiz': TextSubstitution(text='[1, 2.0, 3]')}] norm = normalize_parameters(orig) evaluate_parameters(LaunchContext(), norm) assert 'Expected a non-empty sequence' in str(exc.value) with pytest.raises(TypeError) as exc: orig = [{'foo': 1, 'fiz': TextSubstitution(text='[[2, 3], [2, 3], [2, 3]]')}] norm = normalize_parameters(orig) evaluate_parameters(LaunchContext(), norm) assert 'Expected a non-empty sequence' in str(exc.value) with pytest.raises(TypeError) as exc: orig = [{'foo': 1, 'fiz': TextSubstitution(text='[]')}] norm = normalize_parameters(orig) evaluate_parameters(LaunchContext(), norm) assert 'Expected a non-empty sequence' in str(exc.value) with pytest.raises(TypeError) as exc: orig = [{ 'foo': 1, 'fiz': [ [TextSubstitution(text="['asd', 'bsd']")], [TextSubstitution(text="['asd', 'csd']")] ] }] norm = normalize_parameters(orig) evaluate_parameters(LaunchContext(), norm) assert 'Expected a non-empty sequence' in str(exc.value) with pytest.raises(TypeError) as exc: orig = [{'foo': 1, 'fiz': TextSubstitution(text='Text That : Cannot Be Parsed As : Yaml')}] norm = normalize_parameters(orig) evaluate_parameters(LaunchContext(), norm) assert 'Unable to parse' in str(exc.value)
def test_nested_dictionaries(): orig = [{'foo': {'bar': 'baz'}, 'fiz': {'buz': 3}}] norm = normalize_parameters(orig) expected = ({'foo.bar': 'baz', 'fiz.buz': 3},) assert evaluate_parameters(LaunchContext(), norm) == expected
def test_mixed_path_dicts(): orig = ['/foo/bar', {'fiz': {'buz': 3}}, pathlib.Path('/tmp/baz')] norm = normalize_parameters(orig) expected = (pathlib.Path('/foo/bar'), {'fiz.buz': 3}, pathlib.Path('/tmp/baz')) assert evaluate_parameters(LaunchContext(), norm) == expected
def test_dictionary_with_bytes(): orig = [{'foo': 1, 'fiz': bytes([0xff, 0x5c, 0xaa])}] norm = normalize_parameters(orig) expected = ({'foo': 1, 'fiz': bytes([0xff, 0x5c, 0xaa])},) assert evaluate_parameters(LaunchContext(), norm) == expected
def test_dictionary_with_dissimilar_array(): with pytest.raises(TypeError) as exc: orig = [{'foo': 1, 'fiz': [True, 2.0, 3]}] norm = normalize_parameters(orig) evaluate_parameters(LaunchContext(), norm) assert 'Expected a non-empty' in str(exc.value) with pytest.raises(TypeError) as exc: orig = [{'foo': 1, 'fiz': [True, 1, TextSubstitution(text='foo')]}] norm = normalize_parameters(orig) evaluate_parameters(LaunchContext(), norm) assert 'Expected a non-empty' in str(exc.value) with pytest.raises(TypeError) as exc: orig = [{'foo': 1, 'fiz': [TextSubstitution(text='foo'), True, 1]}] norm = normalize_parameters(orig) evaluate_parameters(LaunchContext(), norm) assert 'Expected a non-empty' in str(exc.value) with pytest.raises(TypeError) as exc: orig = [{'foo': 1, 'fiz': [True, 1, [TextSubstitution(text='foo')]]}] norm = normalize_parameters(orig) evaluate_parameters(LaunchContext(), norm) assert 'Expected a non-empty' in str(exc.value) with pytest.raises(TypeError) as exc: orig = [{'foo': 1, 'fiz': [[TextSubstitution(text='foo')], True, 1]}] norm = normalize_parameters(orig) evaluate_parameters(LaunchContext(), norm) assert 'Expected a non-empty' in str(exc.value) with pytest.raises(TypeError) as exc: orig = [{'foo': [ [TextSubstitution(text='True')], [TextSubstitution(text='2.0')], [TextSubstitution(text='3')], ]}] norm = normalize_parameters(orig) evaluate_parameters(LaunchContext(), norm) assert 'Expected a non-empty' in str(exc.value)
def test_dictionary_with_float(): orig = [{'foo': 1.2, 'fiz': [2.3, 3.4, 4.5]}] norm = normalize_parameters(orig) expected = ({'foo': 1.2, 'fiz': (2.3, 3.4, 4.5)},) assert evaluate_parameters(LaunchContext(), norm) == expected
def test_dictionary_with_int(): orig = [{'foo': 1, 'fiz': [2, 3, 4]}] norm = normalize_parameters(orig) expected = ({'foo': 1, 'fiz': (2, 3, 4)},) assert evaluate_parameters(LaunchContext(), norm) == expected
def test_dictionary_with_bool(): orig = [{'foo': False, 'fiz': [True, False, True]}] norm = normalize_parameters(orig) expected = ({'foo': False, 'fiz': (True, False, True)},) assert evaluate_parameters(LaunchContext(), norm) == expected
def _perform_substitutions(self, context: LaunchContext) -> None: try: if self.__substitutions_performed: # This function may have already been called by a subclass' `execute`, for example. return self.__substitutions_performed = True if self.__node_name is not None: self.__expanded_node_name = perform_substitutions( context, normalize_to_list_of_substitutions(self.__node_name)) validate_node_name(self.__expanded_node_name) self.__expanded_node_name.lstrip('/') expanded_node_namespace = None if self.__node_namespace is not None: expanded_node_namespace = perform_substitutions( context, normalize_to_list_of_substitutions(self.__node_namespace)) base_ns = context.launch_configurations.get('ros_namespace', None) if base_ns is not None or expanded_node_namespace is not None: if expanded_node_namespace is None: expanded_node_namespace = '' if base_ns is None: base_ns = '' if not expanded_node_namespace.startswith('/'): expanded_node_namespace = ( base_ns + '/' + expanded_node_namespace).rstrip('/') if not expanded_node_namespace.startswith('/'): expanded_node_namespace = '/' + expanded_node_namespace self.__expanded_node_namespace = expanded_node_namespace if expanded_node_namespace is not None: cmd_extension = [ '-r', LocalSubstitution("ros_specific_arguments['ns']") ] self.cmd.extend([ normalize_to_list_of_substitutions(x) for x in cmd_extension ]) validate_namespace(self.__expanded_node_namespace) except Exception: self.__logger.error( "Error while expanding or validating node name or namespace for '{}':" .format( 'package={}, executable={}, name={}, namespace={}'.format( self.__package, self.__node_executable, self.__node_name, self.__node_namespace, ))) raise self.__final_node_name = '' if self.__expanded_node_namespace != '/': self.__final_node_name += self.__expanded_node_namespace self.__final_node_name += '/' + self.__expanded_node_name # expand global parameters first, # so they can be overriden with specific parameters of this Node global_params = context.launch_configurations.get('ros_params', None) if global_params is not None or self.__parameters is not None: self.__expanded_parameter_files = [] if global_params is not None: param_file_path = self._create_params_file_from_dict(global_params) self.__expanded_parameter_files.append(param_file_path) cmd_extension = ['--params-file', f'{param_file_path}'] self.cmd.extend( [normalize_to_list_of_substitutions(x) for x in cmd_extension]) assert os.path.isfile(param_file_path) # expand parameters too if self.__parameters is not None: evaluated_parameters = evaluate_parameters(context, self.__parameters) for i, params in enumerate(evaluated_parameters): if isinstance(params, dict): param_file_path = self._create_params_file_from_dict( params) assert os.path.isfile(param_file_path) elif isinstance(params, pathlib.Path): param_file_path = str(params) else: raise RuntimeError( 'invalid normalized parameters {}'.format( repr(params))) if not os.path.isfile(param_file_path): self.__logger.warning( 'Parameter file path is not a file: {}'.format( param_file_path), ) continue self.__expanded_parameter_files.append(param_file_path) cmd_extension = ['--params-file', f'{param_file_path}'] self.cmd.extend([ normalize_to_list_of_substitutions(x) for x in cmd_extension ]) # expand remappings too if self.__remappings is not None: self.__expanded_remappings = [] for k, v in self.__remappings: key = perform_substitutions( context, normalize_to_list_of_substitutions(k)) value = perform_substitutions( context, normalize_to_list_of_substitutions(v)) self.__expanded_remappings.append((key, value))
def _perform_substitutions(self, context: LaunchContext) -> None: # Here to avoid cyclic import from ..descriptions import Parameter try: if self.__substitutions_performed: # This function may have already been called by a subclass' `execute`, for example. return self.__substitutions_performed = True if self.__node_name is not None: self.__expanded_node_name = perform_substitutions( context, normalize_to_list_of_substitutions(self.__node_name)) validate_node_name(self.__expanded_node_name) self.__expanded_node_name.lstrip('/') expanded_node_namespace: Optional[Text] = None if self.__node_namespace is not None: expanded_node_namespace = perform_substitutions( context, normalize_to_list_of_substitutions(self.__node_namespace)) base_ns = context.launch_configurations.get('ros_namespace', None) expanded_node_namespace = make_namespace_absolute( prefix_namespace(base_ns, expanded_node_namespace)) if expanded_node_namespace is not None: self.__expanded_node_namespace = expanded_node_namespace cmd_extension = ['-r', LocalSubstitution("ros_specific_arguments['ns']")] self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) validate_namespace(self.__expanded_node_namespace) except Exception: self.__logger.error( "Error while expanding or validating node name or namespace for '{}':" .format('package={}, executable={}, name={}, namespace={}'.format( self.__package, self.__node_executable, self.__node_name, self.__node_namespace, )) ) raise self.__final_node_name = prefix_namespace( self.__expanded_node_namespace, self.__expanded_node_name) # Expand global parameters first, # so they can be overridden with specific parameters of this Node # The params_container list is expected to contain name-value pairs (tuples) # and/or strings representing paths to parameter files. params_container = context.launch_configurations.get('global_params', None) if any(x is not None for x in (params_container, self.__parameters)): self.__expanded_parameter_arguments = [] if params_container is not None: for param in params_container: if isinstance(param, tuple): name, value = param cmd_extension = ['-p', f'{name}:={value}'] self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) else: param_file_path = os.path.abspath(param) self.__expanded_parameter_arguments.append((param_file_path, True)) cmd_extension = ['--params-file', f'{param_file_path}'] assert os.path.isfile(param_file_path) self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) # expand parameters too if self.__parameters is not None: evaluated_parameters = evaluate_parameters(context, self.__parameters) for params in evaluated_parameters: is_file = False if isinstance(params, dict): param_argument = self._create_params_file_from_dict(params) is_file = True assert os.path.isfile(param_argument) elif isinstance(params, pathlib.Path): param_argument = str(params) is_file = True elif isinstance(params, Parameter): param_argument = self._get_parameter_rule(params, context) else: raise RuntimeError('invalid normalized parameters {}'.format(repr(params))) if is_file and not os.path.isfile(param_argument): self.__logger.warning( 'Parameter file path is not a file: {}'.format(param_argument), ) continue self.__expanded_parameter_arguments.append((param_argument, is_file)) cmd_extension = ['--params-file' if is_file else '-p', f'{param_argument}'] self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) # expand remappings too global_remaps = context.launch_configurations.get('ros_remaps', None) if global_remaps or self.__remappings: self.__expanded_remappings = [] if global_remaps: self.__expanded_remappings.extend(global_remaps) if self.__remappings: self.__expanded_remappings.extend([ (perform_substitutions(context, src), perform_substitutions(context, dst)) for src, dst in self.__remappings ]) if self.__expanded_remappings: cmd_extension = [] for src, dst in self.__expanded_remappings: cmd_extension.extend(['-r', f'{src}:={dst}']) self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension])
def _perform_substitutions(self, context: LaunchContext) -> None: try: if self.__substitutions_performed: # This function may have already been called by a subclass' `execute`, for example. return self.__substitutions_performed = True if self.__node_name is not None: self.__expanded_node_name = perform_substitutions( context, normalize_to_list_of_substitutions(self.__node_name)) validate_node_name(self.__expanded_node_name) self.__expanded_node_name.lstrip('/') self.__expanded_node_namespace = perform_substitutions( context, normalize_to_list_of_substitutions(self.__node_namespace)) if not self.__expanded_node_namespace.startswith('/'): base_ns = context.launch_configurations.get( 'ros_namespace', '') self.__expanded_node_namespace = ( base_ns + '/' + self.__expanded_node_namespace).rstrip('/') if (self.__expanded_node_namespace != '' and not self.__expanded_node_namespace.startswith('/')): self.__expanded_node_namespace = '/' + self.__expanded_node_namespace if self.__expanded_node_namespace != '': cmd_extension = [ '-r', LocalSubstitution("ros_specific_arguments['ns']") ] self.cmd.extend([ normalize_to_list_of_substitutions(x) for x in cmd_extension ]) validate_namespace(self.__expanded_node_namespace) except Exception: self.__logger.error( "Error while expanding or validating node name or namespace for '{}':" .format( 'package={}, node_executable={}, name={}, namespace={}'. format( self.__package, self.__node_executable, self.__node_name, self.__node_namespace, ))) raise self.__final_node_name = '' if self.__expanded_node_namespace not in ['', '/']: self.__final_node_name += self.__expanded_node_namespace self.__final_node_name += '/' + self.__expanded_node_name # expand parameters too if self.__parameters is not None: self.__expanded_parameter_files = [] evaluated_parameters = evaluate_parameters(context, self.__parameters) for params in evaluated_parameters: if isinstance(params, dict): param_file_path = self._create_params_file_from_dict( params) elif isinstance(params, pathlib.Path): param_file_path = str(params) else: raise RuntimeError( 'invalid normalized parameters {}'.format( repr(params))) if not os.path.isfile(param_file_path): self.__logger.warning( 'Parameter file path is not a file: {}'.format( param_file_path), ) # Don't skip adding the file to the parameter list since space has been # reserved for it in the ros_specific_arguments. self.__expanded_parameter_files.append(param_file_path) # expand remappings too if self.__remappings is not None: self.__expanded_remappings = [] for k, v in self.__remappings: key = perform_substitutions( context, normalize_to_list_of_substitutions(k)) value = perform_substitutions( context, normalize_to_list_of_substitutions(v)) self.__expanded_remappings.append((key, value))
def check_launch_node(file): root_entity, parser = Parser.load(file) ld = parser.parse_description(root_entity) ls = LaunchService() ls.include_launch_description(ld) assert (0 == ls.run()) evaluated_parameters = evaluate_parameters( ls.context, ld.describe_sub_entities()[3]._Node__parameters) assert len(evaluated_parameters) == 3 assert isinstance(evaluated_parameters[0], dict) assert isinstance(evaluated_parameters[1], dict) assert isinstance(evaluated_parameters[2], pathlib.Path) assert 'param1' in evaluated_parameters[0] assert evaluated_parameters[0]['param1'] == 'ads' param_dict = evaluated_parameters[1] assert 'param_group1.param_group2.param2' in param_dict assert 'param_group1.param3' in param_dict assert 'param_group1.param4' in param_dict assert 'param_group1.param5' in param_dict assert 'param_group1.param6' in param_dict assert 'param_group1.param7' in param_dict assert 'param_group1.param8' in param_dict assert 'param_group1.param9' in param_dict assert 'param_group1.param10' in param_dict assert 'param_group1.param11' in param_dict assert 'param_group1.param12' in param_dict assert 'param_group1.param13' in param_dict assert 'param_group1.param14' in param_dict assert 'param_group1.param15' in param_dict assert param_dict['param_group1.param_group2.param2'] == 2 assert param_dict['param_group1.param3'] == [2, 5, 8] assert param_dict['param_group1.param4'] == [2, 5, 8] assert param_dict['param_group1.param5'] == '[2, 5, 8]' assert param_dict['param_group1.param6'] == [2., 5., 8.] assert param_dict['param_group1.param7'] == ['2', '5', '8'] assert param_dict['param_group1.param8'] == ["'2'", "'5'", "'8'"] assert param_dict['param_group1.param9'] == ["'2'", "'5'", "'8'"] assert param_dict['param_group1.param10'] == ["'asd'", "'bsd'", "'csd'"] assert param_dict['param_group1.param11'] == ['asd', 'bsd', 'csd'] assert param_dict['param_group1.param12'] == '' assert param_dict['param_group1.param13'] == '100' assert param_dict['param_group1.param14'] == ["'2'", "'5'", "'8'"] assert param_dict['param_group1.param15'] == ['2', '5', '8'] # Check remappings exist remappings = ld.describe_sub_entities()[3]._Node__remappings assert remappings is not None assert len(remappings) == 2 talker_node_action = ld.describe_sub_entities()[3] talker_node_cmd_string = ' '.join( talker_node_action.process_details['cmd']) assert '--ros-args --log-level info' in talker_node_cmd_string listener_node_action = ld.describe_sub_entities()[4] listener_node_cmd = listener_node_action.process_details['cmd'] assert [sys.executable, '-c', 'import sys; print(sys.argv[1:])'] == listener_node_cmd[:3]