diff --git a/test_ros2trace_analysis/.coveragerc b/test_ros2trace_analysis/.coveragerc new file mode 100644 index 0000000..2536a4b --- /dev/null +++ b/test_ros2trace_analysis/.coveragerc @@ -0,0 +1,4 @@ +[run] +omit = + setup.py + test/* diff --git a/test_ros2trace_analysis/.gitignore b/test_ros2trace_analysis/.gitignore new file mode 100644 index 0000000..eef29c1 --- /dev/null +++ b/test_ros2trace_analysis/.gitignore @@ -0,0 +1,3 @@ +*~ +*.pyc + diff --git a/test_ros2trace_analysis/CHANGELOG.rst b/test_ros2trace_analysis/CHANGELOG.rst new file mode 100644 index 0000000..e69de29 diff --git a/test_ros2trace_analysis/package.xml b/test_ros2trace_analysis/package.xml new file mode 100644 index 0000000..f18feea --- /dev/null +++ b/test_ros2trace_analysis/package.xml @@ -0,0 +1,32 @@ + + + + test_ros2trace_analysis + 8.3.0 + Tests for the ros2trace_analysis package. + Christophe Bedard + Apache 2.0 + https://index.ros.org/p/test_ros2trace_analysis/ + https://github.com/ros-tracing/tracetools_analysis + https://github.com/ros-tracing/tracetools_analysis/issues + Christophe Bedard + + ament_copyright + ament_flake8 + ament_mypy + ament_pep257 + ament_xmllint + launch + launch_ros + python3-pytest + ros2run + ros2trace + ros2trace_analysis + test_tracetools + tracetools + tracetools_trace + + + ament_python + + diff --git a/test_ros2trace_analysis/resource/test_ros2trace_analysis b/test_ros2trace_analysis/resource/test_ros2trace_analysis new file mode 100644 index 0000000..e69de29 diff --git a/test_ros2trace_analysis/setup.py b/test_ros2trace_analysis/setup.py new file mode 100644 index 0000000..582f6df --- /dev/null +++ b/test_ros2trace_analysis/setup.py @@ -0,0 +1,26 @@ +from setuptools import find_packages +from setuptools import setup + +package_name = 'test_ros2trace_analysis' + +setup( + name=package_name, + version='3.0.0', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/' + package_name, ['package.xml']), + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='Christophe Bedard', + maintainer_email='bedard.christophe@gmail.com', + author='Christophe Bedard', + author_email='bedard.christophe@gmail.com', + url='https://github.com/ros-tracing/tracetools_analysis', + keywords=[], + description='Tests for the ros2trace_analysis package.', + license='Apache 2.0', + tests_require=['pytest'], +) diff --git a/test_ros2trace_analysis/test/test_copyright.py b/test_ros2trace_analysis/test/test_copyright.py new file mode 100644 index 0000000..cf0fae3 --- /dev/null +++ b/test_ros2trace_analysis/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/test_ros2trace_analysis/test/test_flake8.py b/test_ros2trace_analysis/test/test_flake8.py new file mode 100644 index 0000000..27ee107 --- /dev/null +++ b/test_ros2trace_analysis/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/test_ros2trace_analysis/test/test_mypy.py b/test_ros2trace_analysis/test/test_mypy.py new file mode 100644 index 0000000..331a3b8 --- /dev/null +++ b/test_ros2trace_analysis/test/test_mypy.py @@ -0,0 +1,22 @@ +# Copyright 2019 Canonical Ltd +# +# 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_mypy.main import main +import pytest + + +@pytest.mark.mypy +@pytest.mark.linter +def test_mypy(): + assert main(argv=[]) == 0, 'Found errors' diff --git a/test_ros2trace_analysis/test/test_pep257.py b/test_ros2trace_analysis/test/test_pep257.py new file mode 100644 index 0000000..0e38a6c --- /dev/null +++ b/test_ros2trace_analysis/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/test_ros2trace_analysis/test/test_ros2trace_analysis/test_process.py b/test_ros2trace_analysis/test/test_ros2trace_analysis/test_process.py new file mode 100644 index 0000000..d2f7eb1 --- /dev/null +++ b/test_ros2trace_analysis/test/test_ros2trace_analysis/test_process.py @@ -0,0 +1,175 @@ +# Copyright 2024 Apex.AI, 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 os +from pathlib import Path +import shutil +import subprocess +import tempfile +from typing import Dict +from typing import List +from typing import Optional +import unittest + +from launch import LaunchDescription +from launch import LaunchService +from launch_ros.actions import Node +from tracetools_trace.tools.lttng import is_lttng_installed + + +def are_tracepoints_included() -> bool: + """ + Check if tracing instrumentation is enabled and if tracepoints are included. + + :return: True if tracepoints are included, False otherwise + """ + if not is_lttng_installed(): + return False + process = subprocess.run( + ['ros2', 'run', 'tracetools', 'status'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding='utf-8', + ) + return 0 == process.returncode + + +@unittest.skipIf(not is_lttng_installed(minimum_version='2.9.0'), 'LTTng is required') +class TestROS2TraceAnalysisCLI(unittest.TestCase): + + def __init__(self, *args) -> None: + super().__init__( + *args, + ) + + def create_test_tmpdir(self, test_name: str) -> str: + prefix = self.__class__.__name__ + '__' + test_name + return tempfile.mkdtemp(prefix=prefix) + + def run_command( + self, + args: List[str], + *, + env: Optional[Dict[str, str]] = None, + ) -> subprocess.Popen: + print('=>running:', args) + process_env = os.environ.copy() + process_env['PYTHONUNBUFFERED'] = '1' + if env: + process_env.update(env) + return subprocess.Popen( + args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding='utf-8', + env=process_env, + ) + + def wait_and_print_command_output( + self, + process: subprocess.Popen, + ) -> int: + stdout, stderr = process.communicate() + stdout = stdout.strip(' \r\n\t') + stderr = stderr.strip(' \r\n\t') + print('=>stdout:\n' + stdout) + print('=>stderr:\n' + stderr) + return process.wait() + + def run_command_and_wait( + self, + args: List[str], + *, + env: Optional[Dict[str, str]] = None, + ) -> int: + process = self.run_command(args, env=env) + return self.wait_and_print_command_output(process) + + def run_nodes(self) -> None: + nodes = [ + Node( + package='test_tracetools', + executable='test_ping', + output='screen', + ), + Node( + package='test_tracetools', + executable='test_pong', + output='screen', + ), + ] + ld = LaunchDescription(nodes) + ls = LaunchService() + ls.include_launch_description(ld) + exit_code = ls.run() + self.assertEqual(0, exit_code) + + def test_process_bad_input_path(self) -> None: + tmpdir = self.create_test_tmpdir('test_process_bad_input_path') + + # No input path + ret = self.run_command_and_wait(['ros2', 'trace-analysis', 'process']) + self.assertEqual(2, ret) + + # Does not exist + ret = self.run_command_and_wait(['ros2', 'trace-analysis', 'process', '']) + self.assertEqual(1, ret) + fake_input = os.path.join(tmpdir, 'doesnt_exist') + ret = self.run_command_and_wait(['ros2', 'trace-analysis', 'process', fake_input]) + self.assertEqual(1, ret) + + # Exists but empty + empty_input = os.path.join(tmpdir, 'empty') + os.mkdir(empty_input) + ret = self.run_command_and_wait(['ros2', 'trace-analysis', 'process', empty_input]) + self.assertEqual(1, ret) + + # Exists but converted file empty + empty_converted_file = os.path.join(empty_input, 'converted') + Path(empty_converted_file).touch() + ret = self.run_command_and_wait(['ros2', 'trace-analysis', 'process', empty_input]) + self.assertEqual(1, ret) + + shutil.rmtree(tmpdir) + + @unittest.skipIf(not are_tracepoints_included(), 'tracepoints are required') + def test_process(self) -> None: + tmpdir = self.create_test_tmpdir('test_process') + session_name = 'test_process' + + # Run and trace nodes + ret = self.run_command_and_wait( + [ + 'ros2', 'trace', + 'start', session_name, + '--path', tmpdir, + ], + ) + self.assertEqual(0, ret) + trace_dir = os.path.join(tmpdir, session_name) + self.run_nodes() + ret = self.run_command_and_wait(['ros2', 'trace', 'stop', session_name]) + self.assertEqual(0, ret) + + # Process trace + ret = self.run_command_and_wait(['ros2', 'trace-analysis', 'process', trace_dir]) + self.assertEqual(0, ret) + + # Check that converted file exists and isn't empty + converted_file = os.path.join(trace_dir, 'converted') + self.assertTrue(os.path.isfile(converted_file)) + self.assertGreater(os.path.getsize(converted_file), 0) + + shutil.rmtree(tmpdir) diff --git a/test_ros2trace_analysis/test/test_xmllint.py b/test_ros2trace_analysis/test/test_xmllint.py new file mode 100644 index 0000000..f46285e --- /dev/null +++ b/test_ros2trace_analysis/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' diff --git a/test_ros2trace_analysis/test_ros2trace_analysis/__init__.py b/test_ros2trace_analysis/test_ros2trace_analysis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tracetools_analysis/package.xml b/tracetools_analysis/package.xml index 00fc67b..5b45a93 100644 --- a/tracetools_analysis/package.xml +++ b/tracetools_analysis/package.xml @@ -14,6 +14,7 @@ Christophe Bedard tracetools_read + tracetools_trace python3-pandas jupyter-notebook diff --git a/tracetools_analysis/tracetools_analysis/processor/ros2.py b/tracetools_analysis/tracetools_analysis/processor/ros2.py index 8e69965..9c45e4e 100644 --- a/tracetools_analysis/tracetools_analysis/processor/ros2.py +++ b/tracetools_analysis/tracetools_analysis/processor/ros2.py @@ -20,6 +20,7 @@ from typing import Set from typing import Tuple from tracetools_read import get_field +from tracetools_trace.tools import tracepoints as tp from . import EventHandler from . import EventMetadata @@ -41,55 +42,55 @@ class Ros2Handler(EventHandler): """Create a Ros2Handler.""" # Link a ROS trace event to its corresponding handling method handler_map: HandlerMap = { - 'ros2:rcl_init': + tp.rcl_init: self._handle_rcl_init, - 'ros2:rcl_node_init': + tp.rcl_node_init: self._handle_rcl_node_init, - 'ros2:rmw_publisher_init': + tp.rmw_publisher_init: self._handle_rmw_publisher_init, - 'ros2:rcl_publisher_init': + tp.rcl_publisher_init: self._handle_rcl_publisher_init, - 'ros2:rclcpp_publish': + tp.rclcpp_publish: self._handle_rclcpp_publish, - 'ros2:rcl_publish': + tp.rcl_publish: self._handle_rcl_publish, - 'ros2:rmw_publish': + tp.rmw_publish: self._handle_rmw_publish, - 'ros2:rmw_subscription_init': + tp.rmw_subscription_init: self._handle_rmw_subscription_init, - 'ros2:rcl_subscription_init': + tp.rcl_subscription_init: self._handle_rcl_subscription_init, - 'ros2:rclcpp_subscription_init': + tp.rclcpp_subscription_init: self._handle_rclcpp_subscription_init, - 'ros2:rclcpp_subscription_callback_added': + tp.rclcpp_subscription_callback_added: self._handle_rclcpp_subscription_callback_added, - 'ros2:rmw_take': + tp.rmw_take: self._handle_rmw_take, - 'ros2:rcl_take': + tp.rcl_take: self._handle_rcl_take, - 'ros2:rclcpp_take': + tp.rclcpp_take: self._handle_rclcpp_take, - 'ros2:rcl_service_init': + tp.rcl_service_init: self._handle_rcl_service_init, - 'ros2:rclcpp_service_callback_added': + tp.rclcpp_service_callback_added: self._handle_rclcpp_service_callback_added, - 'ros2:rcl_client_init': + tp.rcl_client_init: self._handle_rcl_client_init, - 'ros2:rcl_timer_init': + tp.rcl_timer_init: self._handle_rcl_timer_init, - 'ros2:rclcpp_timer_callback_added': + tp.rclcpp_timer_callback_added: self._handle_rclcpp_timer_callback_added, - 'ros2:rclcpp_timer_link_node': + tp.rclcpp_timer_link_node: self._handle_rclcpp_timer_link_node, - 'ros2:rclcpp_callback_register': + tp.rclcpp_callback_register: self._handle_rclcpp_callback_register, - 'ros2:callback_start': + tp.callback_start: self._handle_callback_start, - 'ros2:callback_end': + tp.callback_end: self._handle_callback_end, - 'ros2:rcl_lifecycle_state_machine_init': + tp.rcl_lifecycle_state_machine_init: self._handle_rcl_lifecycle_state_machine_init, - 'ros2:rcl_lifecycle_transition': + tp.rcl_lifecycle_transition: self._handle_rcl_lifecycle_transition, } super().__init__( @@ -104,7 +105,7 @@ class Ros2Handler(EventHandler): @staticmethod def required_events() -> Set[str]: return { - 'ros2:rcl_init', + tp.rcl_init, } @property