diff --git a/rosidl_cli/completion/rosidl-argcomplete.bash b/rosidl_cli/completion/rosidl-argcomplete.bash new file mode 100644 index 000000000..0262ec2a0 --- /dev/null +++ b/rosidl_cli/completion/rosidl-argcomplete.bash @@ -0,0 +1,19 @@ +# Copyright 2021 Open Source Robotics Foundation, 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. + +if type register-python-argcomplete3 > /dev/null 2>&1; then + eval "$(register-python-argcomplete3 rosidl)" +elif type register-python-argcomplete > /dev/null 2>&1; then + eval "$(register-python-argcomplete rosidl)" +fi diff --git a/rosidl_cli/completion/rosidl-argcomplete.zsh b/rosidl_cli/completion/rosidl-argcomplete.zsh new file mode 100644 index 000000000..9a86dbd23 --- /dev/null +++ b/rosidl_cli/completion/rosidl-argcomplete.zsh @@ -0,0 +1,23 @@ +# Copyright 2021 Open Source Robotics Foundation, 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. + +autoload -U +X compinit && compinit +autoload -U +X bashcompinit && bashcompinit + +# Get this scripts directory +__rosidl_cli_completion_dir=${0:a:h} +# Just source the bash version, it works in zsh too +source "$__rosidl_cli_completion_dir/rosidl-argcomplete.bash" +# Cleanup +unset __rosidl_cli_completion_dir diff --git a/rosidl_cli/package.xml b/rosidl_cli/package.xml new file mode 100644 index 000000000..d9a85f7e1 --- /dev/null +++ b/rosidl_cli/package.xml @@ -0,0 +1,27 @@ + + + + rosidl_cli + 0.1.0 + + Command line tools for ROS interface generation. + + Chris Lalancette + Shane Loretz + Apache License 2.0 + + Michel Hidalgo + + python3-argcomplete + python3-importlib-metadata + + ament_copyright + ament_flake8 + ament_pep257 + ament_xmllint + python3-pytest + + + ament_python + + diff --git a/rosidl_cli/pytest.ini b/rosidl_cli/pytest.ini new file mode 100644 index 000000000..fe55d2ed6 --- /dev/null +++ b/rosidl_cli/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +junit_family=xunit2 diff --git a/rosidl_cli/resource/package.dsv b/rosidl_cli/resource/package.dsv new file mode 100644 index 000000000..9293e2a38 --- /dev/null +++ b/rosidl_cli/resource/package.dsv @@ -0,0 +1,2 @@ +source;share/rosidl_cli/environment/rosidl-argcomplete.bash +source;share/rosidl_cli/environment/rosidl-argcomplete.zsh diff --git a/rosidl_cli/resource/rosidl_cli b/rosidl_cli/resource/rosidl_cli new file mode 100644 index 000000000..e69de29bb diff --git a/rosidl_cli/rosidl_cli/__init__.py b/rosidl_cli/rosidl_cli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rosidl_cli/rosidl_cli/cli.py b/rosidl_cli/rosidl_cli/cli.py new file mode 100644 index 000000000..18eff8042 --- /dev/null +++ b/rosidl_cli/rosidl_cli/cli.py @@ -0,0 +1,103 @@ +# Copyright 2021 Open Source Robotics Foundation, 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 argparse +import signal + +from rosidl_cli.command.generate import GenerateCommand +from rosidl_cli.common import get_first_line_doc + + +def add_subparsers(parser, cli_name, commands): + """ + Create argparse subparser for each command. + + The ``cli_name`` is used for the title and description of the + ``add_subparsers`` function call. + + For each command a subparser is created. + + :param parser: the parent argument parser + :type parser: :py:class:`argparse.ArgumentParser` + :param str cli_name: name of the command line command to which the + subparsers are being added + :param commands: each of the commands contributing specific arguments + :type commands: :py:class:`List[Command]` + """ + # add subparser with description of available subparsers + description = '' + + commands = sorted(commands, key=lambda command: command.name) + max_length = max(len(command.name) for command in commands) + for command in commands: + description += '%s %s\n' % ( + command.name.ljust(max_length), + get_first_line_doc(command)) + subparser = parser.add_subparsers( + title='Commands', description=description, + metavar=f'Call `{cli_name} -h` for more detailed usage.') + subparser.dest = '_command' + subparser.required = True + + # add extension specific sub-sub-parser with its arguments + for command in commands: + command_parser = subparser.add_parser( + command.name, + description=get_first_line_doc(command), + formatter_class=argparse.RawDescriptionHelpFormatter) + command_parser.set_defaults(_command=command) + command.add_arguments(command_parser) + + return subparser + + +def main(): + script_name = 'rosidl' + description = f'{script_name} is an extensible command-line tool ' \ + 'for ROS interface generation.' + + # top level parser + parser = argparse.ArgumentParser( + description=description, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + commands = [GenerateCommand()] + + # add arguments for command extension(s) + add_subparsers( + parser, + script_name, + commands + ) + + # register argcomplete hook if available + try: + from argcomplete import autocomplete + except ImportError: + pass + else: + autocomplete(parser, exclude=['-h', '--help']) + + # parse the command line arguments + args = parser.parse_args() + + # call the main method of the command + try: + rc = args._command.main(args=args) + except KeyboardInterrupt: + rc = signal.SIGINT + except (ValueError, RuntimeError) as e: + rc = str(e) + return rc diff --git a/rosidl_cli/rosidl_cli/command/__init__.py b/rosidl_cli/rosidl_cli/command/__init__.py new file mode 100644 index 000000000..22d035bb5 --- /dev/null +++ b/rosidl_cli/rosidl_cli/command/__init__.py @@ -0,0 +1,29 @@ +# Copyright 2021 Open Source Robotics Foundation, 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. + + +class Command: + """ + The extension point for 'command' extensions. + + The following methods must be defined: + * `main` + * `add_arguments` + """ + + def add_arguments(self, parser): + pass + + def main(self, *, parser, args): + raise NotImplementedError() diff --git a/rosidl_cli/rosidl_cli/command/generate/__init__.py b/rosidl_cli/rosidl_cli/command/generate/__init__.py new file mode 100644 index 000000000..b65b95f57 --- /dev/null +++ b/rosidl_cli/rosidl_cli/command/generate/__init__.py @@ -0,0 +1,82 @@ +# Copyright 2021 Open Source Robotics Foundation, 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 pathlib + +from rosidl_cli.command import Command + +from .extensions import load_type_extensions +from .extensions import load_typesupport_extensions + + +class GenerateCommand(Command): + """Generate source code from interface definition files.""" + + name = 'generate' + + def add_arguments(self, parser): + parser.add_argument( + '-o', '--output-path', type=pathlib.Path, + metavar='PATH', default=pathlib.Path.cwd(), + help=('Path to directory to hold generated source code files. ' + "Defaults to '.'.")) + parser.add_argument( + '-t', '--type', metavar='TYPE_SPEC', + dest='type_specs', action='append', default=[], + help='Target type representations for generation.') + parser.add_argument( + '-ts', '--type-support', metavar='TYPESUPPORT_SPEC', + dest='typesupport_specs', action='append', default=[], + help='Target type supports for generation.') + parser.add_argument( + '-I', '--include-path', type=pathlib.Path, metavar='PATH', + dest='include_paths', action='append', default=[], + help='Paths to include dependency interface definition files from.') + parser.add_argument( + 'package_name', help='Name of the package to generate code for') + parser.add_argument( + 'interface_files', metavar='interface_file', nargs='+', + help=('Normalized relative path to interface definition file. ' + "If prefixed by another path followed by a colon ':', " + 'path resolution is performed against such path.')) + + def main(self, *, args): + extensions = [] + + unspecific_generation = \ + not args.type_specs and not args.typesupport_specs + + if args.type_specs or unspecific_generation: + extensions.extend(load_type_extensions( + specs=args.type_specs, + strict=not unspecific_generation)) + + if args.typesupport_specs or unspecific_generation: + extensions.extend(load_typesupport_extensions( + specs=args.typesupport_specs, + strict=not unspecific_generation)) + + if unspecific_generation and not extensions: + return 'No type nor typesupport extensions were found' + + if len(extensions) > 1: + for extension in extensions: + extension.generate( + args.package_name, args.interface_files, args.include_paths, + output_path=args.output_path / extension.name) + else: + extensions[0].generate( + args.package_name, args.interface_files, + args.include_paths, args.output_path + ) diff --git a/rosidl_cli/rosidl_cli/command/generate/extensions.py b/rosidl_cli/rosidl_cli/command/generate/extensions.py new file mode 100644 index 000000000..4c976d7e7 --- /dev/null +++ b/rosidl_cli/rosidl_cli/command/generate/extensions.py @@ -0,0 +1,57 @@ +# Copyright 2021 Open Source Robotics Foundation, 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. + +from rosidl_cli.extensions import Extension +from rosidl_cli.extensions import load_extensions + + +class GenerateCommandExtension(Extension): + """ + The extension point for source code generation. + + The following methods must be defined: + * `generate` + """ + + def generate( + self, + package_name, + interface_files, + include_paths, + output_path + ): + """ + Generate source code. + + Paths to interface definition files are relative paths optionally + prefixed by an absolute path followed by a colon ':', in which case + path resolution is to be performed against that absolute path. + + :param package_name: name of the package to generate source code for + :param interface_files: list of paths to interface definition files + :param include_paths: list of paths to include dependency interface + definition files from. + :param output_path: path to directory to hold generated source code files + """ + raise NotImplementedError() + + +def load_type_extensions(**kwargs): + """Load extensions for type representation source code generation.""" + return load_extensions('rosidl_cli.command.generate.type_extensions', **kwargs) + + +def load_typesupport_extensions(**kwargs): + """Load extensions for type support source code generation.""" + return load_extensions('rosidl_cli.command.generate.typesupport_extensions', **kwargs) diff --git a/rosidl_cli/rosidl_cli/command/generate/helpers.py b/rosidl_cli/rosidl_cli/command/generate/helpers.py new file mode 100644 index 000000000..643d86813 --- /dev/null +++ b/rosidl_cli/rosidl_cli/command/generate/helpers.py @@ -0,0 +1,152 @@ +# Copyright 2021 Open Source Robotics Foundation, 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 contextlib +import json +import os +import pathlib +import tempfile + + +def package_name_from_interface_file_path(path): + """ + Derive ROS package name from a ROS interface definition file path. + + This function assumes ROS interface definition files follow the typical + ``rosidl`` install space layout i.e. 'package_name/subfolder/interface.idl'. + """ + return pathlib.Path(os.path.abspath(path)).parents[1].name + + +def dependencies_from_include_paths(include_paths): + """ + Collect dependencies' ROS interface definition files from include paths. + + Interface definition file paths from dependencies are absolute paths + prefixed by the name of package they belong to followed by a colon ':'. + """ + return list({ + f'{package_name_from_interface_file_path(path)}:{path}' + for include_path in include_paths + for path in pathlib.Path( + os.path.abspath(include_path) + ).glob('**/*.idl') + }) + + +def idl_tuples_from_interface_files(interface_files): + """ + Express ROS interface definition file paths as IDL tuples. + + An IDL tuple is a relative path prefixed by by an absolute path against + which to resolve it followed by a colon ':'. This function then applies + the following logic: + - If a given path follows this pattern, it is passed through. + - If a given path is prefixed by a relative path, it is resolved + relative to the current working directory. + - If a given path has no prefixes, the current working directory is + used as prefix. + """ + idl_tuples = [] + for path in interface_files: + path_as_string = str(path) + if ':' not in path_as_string: + prefix = pathlib.Path.cwd() + stem = path + else: + prefix, _, stem = path_as_string.rpartition(':') + prefix = os.path.abspath(prefix) + stem = pathlib.Path(stem) + if stem.is_absolute(): + raise ValueError('Interface definition file path ' + f'{stem} cannot be absolute') + idl_tuples.append(f'{prefix}:{stem.as_posix()}') + return idl_tuples + + +@contextlib.contextmanager +def legacy_generator_arguments_file( + *, + package_name, + interface_files, + include_paths, + templates_path, + output_path +): + """ + Generate a temporary rosidl generator arguments file. + + :param package_name: Name of the ROS package for which to generate code + :param interface_files: Relative paths to ROS interface definition files, + optionally prefixed by another absolute or relative path followed by + a colon ':'. The former relative paths will be used as a prototype to + namespace generated code (if applicable). + :param include_paths: Paths where ROS package dependencies' interface + definition files may be found + :param templates_path: Path to the templates directory for the + generator script this arguments are for + :param output_path: Path to the output directory for generated code + """ + idl_tuples = idl_tuples_from_interface_files(interface_files) + interface_dependencies = dependencies_from_include_paths(include_paths) + output_path = os.path.abspath(output_path) + templates_path = os.path.abspath(templates_path) + # NOTE(hidmic): named temporary files cannot be opened twice on Windows, + # so close it and manually remove it when leaving the context + with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp: + tmp.write(json.dumps({ + 'package_name': package_name, + 'output_dir': output_path, + 'template_dir': templates_path, + 'idl_tuples': idl_tuples, + 'ros_interface_dependencies': interface_dependencies, + # TODO(hidmic): re-enable output file caching + 'target_dependencies': [] + })) + path_to_file = os.path.abspath(tmp.name) + try: + yield path_to_file + finally: + try: + os.unlink(path_to_file) + except FileNotFoundError: + pass + + +def generate_visibility_control_file( + *, + package_name, + template_path, + output_path +): + """ + Generate a visibility control file from a template. + + :param package_name: Name of the ROS package for which + to generate the file. + :param template_path: Path to template visibility control file. + May contain @PROJECT_NAME@ and @PROJECT_NAME_UPPER@ placeholders, + to be substituted by the package name, accordingly. + :param output_path: Path to visibility control file after interpolation. + """ + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + with open(template_path, 'r') as fd: + content = fd.read() + + content = content.replace('@PROJECT_NAME@', package_name) + content = content.replace('@PROJECT_NAME_UPPER@', package_name.upper()) + + with open(output_path, 'w') as fd: + fd.write(content) diff --git a/rosidl_cli/rosidl_cli/common.py b/rosidl_cli/rosidl_cli/common.py new file mode 100644 index 000000000..1f94c2c63 --- /dev/null +++ b/rosidl_cli/rosidl_cli/common.py @@ -0,0 +1,22 @@ +# Copyright 2021 Open Source Robotics Foundation, 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. + + +def get_first_line_doc(any_type): + if any_type.__doc__: + for line in any_type.__doc__.splitlines(): + line = line.strip() + if line: + return line.rstrip('.') + return '' diff --git a/rosidl_cli/rosidl_cli/entry_points.py b/rosidl_cli/rosidl_cli/entry_points.py new file mode 100644 index 000000000..cc5ee6107 --- /dev/null +++ b/rosidl_cli/rosidl_cli/entry_points.py @@ -0,0 +1,85 @@ +# Copyright 2021 Open Source Robotics Foundation, 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 logging + +try: + import importlib.metadata as importlib_metadata +except ModuleNotFoundError: + import importlib_metadata + + +logger = logging.getLogger(__name__) + + +def get_entry_points(group_name, *, specs=None, strict=False): + """ + Get entry points from a specific group. + + :param str group_name: the name of the entry point group + :param list specs: an optional collection of entry point names to retrieve + :param bool strict: whether to raise or warn on error + :returns: mapping from entry point names to ``EntryPoint`` instances + :rtype: dict + """ + if specs is not None: + specs = set(specs) + entry_points = {} + for entry_point in importlib_metadata.entry_points().get(group_name, []): + name = entry_point.name + if specs and name not in specs: + continue + if name in entry_points: + msg = (f"Found duplicate entry point '{name}': " + 'got {entry_point} and {entry_points[name]}') + if strict: + raise RuntimeError(msg) + logger.warning(msg) + continue + entry_points[name] = entry_point + if specs: + pending = specs - set(entry_points) + if pending: + msg = 'Some specs could not be met: ' + msg += ', '.join(map(str, pending)) + if strict: + raise RuntimeError(msg) + logger.warning(msg) + return entry_points + + +def load_entry_points(group_name, *, strict=False, **kwargs): + """ + Load entry points for a specific group. + + See :py:function:`get_entry_points` for further reference on + additional keyword arguments. + + :param str group_name: the name of the entry point group + :param bool strict: whether to raise or warn on error + :returns: mapping from entry point name to loaded entry point + :rtype: dict + """ + loaded_entry_points = {} + for name, entry_point in get_entry_points( + group_name, strict=strict, **kwargs + ).items(): + try: + loaded_entry_points[name] = entry_point.load() + except Exception as e: # noqa: F841 + msg = f"Failed to load entry point '{name}': {e}" + if strict: + raise RuntimeError(msg) + logger.warning(msg) + return loaded_entry_points diff --git a/rosidl_cli/rosidl_cli/extensions.py b/rosidl_cli/rosidl_cli/extensions.py new file mode 100644 index 000000000..14488df66 --- /dev/null +++ b/rosidl_cli/rosidl_cli/extensions.py @@ -0,0 +1,57 @@ +# Copyright 2021 Open Source Robotics Foundation, 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 logging + +from rosidl_cli.entry_points import load_entry_points + + +logger = logging.getLogger(__name__) + + +class Extension: + """A generic extension point.""" + + def __init__(self, name): + self.__name = name + + @property + def name(self): + return self.__name + + +def load_extensions(group_name, *, strict=False, **kwargs): + """ + Load extensions for a specific group. + + See :py:function:`load_entry_points` for further reference on + additional keyword arguments. + + :param str group_name: the name of the extension group + :param bool strict: whether to raise or warn on error + :returns: a list of :py:class:`Extension` instances + :rtype: list + """ + extensions = [] + for name, factory in load_entry_points( + group_name, strict=strict, **kwargs + ).items(): + try: + extensions.append(factory(name)) + except Exception as e: + msg = f"Failed to instantiate extension '{name}': {e}" + if strict: + raise RuntimeError(msg) + logger.warning(msg) + return extensions diff --git a/rosidl_cli/setup.py b/rosidl_cli/setup.py new file mode 100644 index 000000000..81b501f9e --- /dev/null +++ b/rosidl_cli/setup.py @@ -0,0 +1,48 @@ +from setuptools import find_packages +from setuptools import setup + +setup( + name='rosidl_cli', + version='0.1.0', + packages=find_packages(exclude=['test']), + extras_require={ + 'completion': ['argcomplete'], + }, + data_files=[ + ('share/ament_index/resource_index/packages', [ + 'resource/rosidl_cli', + ]), + ('share/rosidl_cli', [ + 'package.xml', + 'resource/package.dsv', + ]), + ('share/rosidl_cli/environment', [ + 'completion/rosidl-argcomplete.bash', + 'completion/rosidl-argcomplete.zsh' + ]), + ], + zip_safe=False, + author='Michel Hidalgo', + author_email='michel@ekumenlabs.com', + maintainer='Chris Lalancette, Shane Loretz', + maintainer_email='clalancette@openrobotics.org, sloretz@openrobotics.org', + url='https://github.com/ros2/rosidl/tree/master/rosidl_cli', + download_url='https://github.com/ros2/rosidl/releases', + keywords=[], + classifiers=[ + 'Environment :: Console', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + ], + description='Command line tools for ROS interface generation.', + long_description="""\ +The tooling provides a single command line script for ROS interface source code generation.""", + license='Apache License, Version 2.0', + tests_require=['pytest'], + entry_points={ + 'console_scripts': [ + 'rosidl = rosidl_cli.cli:main', + ], + } +) diff --git a/rosidl_cli/test/rosidl_cli/test_common.py b/rosidl_cli/test/rosidl_cli/test_common.py new file mode 100644 index 000000000..166f8bf0f --- /dev/null +++ b/rosidl_cli/test/rosidl_cli/test_common.py @@ -0,0 +1,39 @@ +# Copyright 2021 Open Source Robotics Foundation, 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. + +from rosidl_cli.common import get_first_line_doc + + +def test_getting_first_line_from_no_docstring(): + func = test_getting_first_line_from_no_docstring + line = get_first_line_doc(func) + assert line == '' + + +def test_getting_first_line_from_docstring(): + """Check it gets the first line.""" + func = test_getting_first_line_from_docstring + line = get_first_line_doc(func) + assert line == 'Check it gets the first line' + + +def test_getting_first_line_from_multiline_docstring(): + """ + Check it really gets the first non-empty line. + + Additional paragraph to please flake8. + """ + func = test_getting_first_line_from_multiline_docstring + line = get_first_line_doc(func) + assert line == 'Check it really gets the first non-empty line' diff --git a/rosidl_cli/test/rosidl_cli/test_files/bar/msg/Bar.idl b/rosidl_cli/test/rosidl_cli/test_files/bar/msg/Bar.idl new file mode 100644 index 000000000..e69de29bb diff --git a/rosidl_cli/test/rosidl_cli/test_generate_helpers.py b/rosidl_cli/test/rosidl_cli/test_generate_helpers.py new file mode 100644 index 000000000..52c390cc2 --- /dev/null +++ b/rosidl_cli/test/rosidl_cli/test_generate_helpers.py @@ -0,0 +1,55 @@ +# Copyright 2021 Open Source Robotics Foundation, 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 json +import os +import pathlib + +import pytest + +from rosidl_cli.command.generate.helpers import legacy_generator_arguments_file + + +@pytest.fixture +def current_path(request): + path = pathlib.Path(request.module.__file__) + path = path.resolve() + path = path.parent + cwd = pathlib.Path.cwd() + try: + os.chdir(str(path)) + yield path + finally: + os.chdir(str(cwd)) + + +def test_legacy_generator_arguments_file(current_path): + with legacy_generator_arguments_file( + package_name='foo', + interface_files=['msg/Foo.idl'], + include_paths=['test_files/bar'], + templates_path='templates', + output_path='tmp', + ) as path: + with open(path, 'r') as fd: + args = json.load(fd) + assert args['package_name'] == 'foo' + assert args['output_dir'] == str(current_path / 'tmp') + assert args['template_dir'] == str(current_path / 'templates') + assert args['idl_tuples'] == [f'{current_path}:msg/Foo.idl'] + path_to_dep = pathlib.Path('test_files/bar/msg/Bar.idl') + assert args['ros_interface_dependencies'] == [ + 'bar:' + str(current_path / path_to_dep) + ] + assert not pathlib.Path(path).exists() diff --git a/rosidl_cli/test/test_copyright.py b/rosidl_cli/test/test_copyright.py new file mode 100644 index 000000000..cf0fae31f --- /dev/null +++ b/rosidl_cli/test/test_copyright.py @@ -0,0 +1,23 @@ +# Copyright 2017 Open Source Robotics Foundation, 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. + +from ament_copyright.main import main +import pytest + + +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/rosidl_cli/test/test_flake8.py b/rosidl_cli/test/test_flake8.py new file mode 100644 index 000000000..27ee1078f --- /dev/null +++ b/rosidl_cli/test/test_flake8.py @@ -0,0 +1,25 @@ +# Copyright 2017 Open Source Robotics Foundation, 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. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors) diff --git a/rosidl_cli/test/test_pep257.py b/rosidl_cli/test/test_pep257.py new file mode 100644 index 000000000..0e38a6c60 --- /dev/null +++ b/rosidl_cli/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2017 Open Source Robotics Foundation, 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. + +from ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=[]) + assert rc == 0, 'Found code style errors / warnings' diff --git a/rosidl_cli/test/test_xmllint.py b/rosidl_cli/test/test_xmllint.py new file mode 100644 index 000000000..f46285e71 --- /dev/null +++ b/rosidl_cli/test/test_xmllint.py @@ -0,0 +1,23 @@ +# Copyright 2019 Open Source Robotics Foundation, 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. + +from ament_xmllint.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.xmllint +def test_xmllint(): + rc = main(argv=[]) + assert rc == 0, 'Found errors'