From 1ba52b99a37bf13c905d426fdbb3ad358e5120ec Mon Sep 17 00:00:00 2001 From: R Kent James Date: Thu, 25 Sep 2025 15:23:14 -0700 Subject: [PATCH] Add tests for ament_python_install_package building with cmake Signed-off-by: R Kent James --- .gitignore | 5 + ament_cmake_python_test/CMakeLists.txt | 67 ++++ ament_cmake_python_test/README.md | 10 + ament_cmake_python_test/package.xml | 34 ++ ament_cmake_python_test/test/options.py | 107 +++++++ .../test/packages/AMENT_IGNORE | 0 .../ament_python_test_package/CMakeLists.txt | 25 ++ .../ament_python_test_package/__init__.py | 0 .../ament_python_test_package/main.py | 18 ++ .../ament_python_test_package/package.xml | 20 ++ .../CMakeLists.txt | 32 ++ .../ament_python_test_package/__init__.py | 0 .../ament_python_test_package/main.py | 18 ++ .../__init__.py | 15 + .../subdir/__init__.py | 0 .../subdir/utils.py | 17 + .../package.xml | 20 ++ .../CMakeLists.txt | 30 ++ .../python_package_double_version/package.xml | 15 + .../python_package_double_version/__init__.py | 1 + .../python_package_double_version/doit.py | 15 + .../test/pkg_template/CMakeLists.txt.jinja | 28 ++ .../test/pkg_template/config/setup.cfg | 3 + .../test/pkg_template/msg/Dummy.msg | 1 + .../test/pkg_template/package.xml.jinja | 20 ++ .../package_directory/__init__.py.jinja | 1 + .../pkg_template/package_directory/doit.py | 15 + .../python_scripts/do_something.py | 20 ++ .../test/pkg_template/setup.cfg.jinja | 3 + ament_cmake_python_test/test/test_packages.py | 293 ++++++++++++++++++ 30 files changed, 833 insertions(+) create mode 100644 .gitignore create mode 100644 ament_cmake_python_test/CMakeLists.txt create mode 100644 ament_cmake_python_test/README.md create mode 100644 ament_cmake_python_test/package.xml create mode 100644 ament_cmake_python_test/test/options.py create mode 100644 ament_cmake_python_test/test/packages/AMENT_IGNORE create mode 100644 ament_cmake_python_test/test/packages/ament_python_test_package/CMakeLists.txt create mode 100644 ament_cmake_python_test/test/packages/ament_python_test_package/ament_python_test_package/__init__.py create mode 100644 ament_cmake_python_test/test/packages/ament_python_test_package/ament_python_test_package/main.py create mode 100644 ament_cmake_python_test/test/packages/ament_python_test_package/package.xml create mode 100644 ament_cmake_python_test/test/packages/ament_python_test_package_overlay/CMakeLists.txt create mode 100644 ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package/__init__.py create mode 100644 ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package/main.py create mode 100644 ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package_overlay/__init__.py create mode 100644 ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package_overlay/subdir/__init__.py create mode 100644 ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package_overlay/subdir/utils.py create mode 100644 ament_cmake_python_test/test/packages/ament_python_test_package_overlay/package.xml create mode 100644 ament_cmake_python_test/test/packages/python_package_double_version/CMakeLists.txt create mode 100644 ament_cmake_python_test/test/packages/python_package_double_version/package.xml create mode 100644 ament_cmake_python_test/test/packages/python_package_double_version/python_package_double_version/__init__.py create mode 100644 ament_cmake_python_test/test/packages/python_package_double_version/python_package_double_version/doit.py create mode 100644 ament_cmake_python_test/test/pkg_template/CMakeLists.txt.jinja create mode 100644 ament_cmake_python_test/test/pkg_template/config/setup.cfg create mode 100644 ament_cmake_python_test/test/pkg_template/msg/Dummy.msg create mode 100644 ament_cmake_python_test/test/pkg_template/package.xml.jinja create mode 100644 ament_cmake_python_test/test/pkg_template/package_directory/__init__.py.jinja create mode 100644 ament_cmake_python_test/test/pkg_template/package_directory/doit.py create mode 100644 ament_cmake_python_test/test/pkg_template/python_scripts/do_something.py create mode 100644 ament_cmake_python_test/test/pkg_template/setup.cfg.jinja create mode 100644 ament_cmake_python_test/test/test_packages.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..09b79363 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +install/ +log/ +__pycache__/ +*.py[codz] diff --git a/ament_cmake_python_test/CMakeLists.txt b/ament_cmake_python_test/CMakeLists.txt new file mode 100644 index 00000000..ac74b480 --- /dev/null +++ b/ament_cmake_python_test/CMakeLists.txt @@ -0,0 +1,67 @@ +# Copyright 2025 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. + +cmake_minimum_required(VERSION 3.12) + +project(ament_cmake_python_test) + +find_package(ament_cmake_core REQUIRED) + +# Uncomment to debug CMake variables +# +# get_cmake_property(_variableNames VARIABLES) +# list (SORT _variableNames) +# foreach (_variableName ${_variableNames}) +# message(STATUS "${_variableName}=${${_variableName}}") +# endforeach() + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + ament_lint_auto_find_test_dependencies() + + find_package(ament_cmake_pytest REQUIRED) + find_package(ament_cmake_python REQUIRED) + find_package(ament_cmake REQUIRED) + ament_get_python_install_dir(PYTHON_INSTALL_DIR) + + set(_pytest_tests + test/test_packages.py + # Add other test files here + ) + + set(_env SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}) + list(APPEND _env PYTHON_INSTALL_DIR=${PYTHON_INSTALL_DIR}) + list(APPEND _env PYEGG_VERSION=py${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}) + foreach(_test_path ${_pytest_tests}) + get_filename_component(_test_name ${_test_path} NAME_WE) + ament_add_pytest_test(${_test_name} ${_test_path} + APPEND_ENV PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR} + ENV ${_env} + TIMEOUT 360 + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + ) + endforeach() + + find_package(ament_lint_auto REQUIRED) + # the following line skips the linter which checks for copyrights + # comment the line when a copyright and license is added to all source files + set(ament_cmake_copyright_FOUND TRUE) + # the following line skips cpplint (only works in a git repo) + # comment the line when this package is in a git repo and when + # a copyright and license is added to all source files + set(ament_cmake_cpplint_FOUND TRUE) + ament_lint_auto_find_test_dependencies() + +endif() +ament_package() diff --git a/ament_cmake_python_test/README.md b/ament_cmake_python_test/README.md new file mode 100644 index 00000000..7edbfdb1 --- /dev/null +++ b/ament_cmake_python_test/README.md @@ -0,0 +1,10 @@ +# ament_cmake_python_test + +This package exists solely to test the ament_cmake_python package. + +Packages to test are prepared as typical ros-style packages (e.g. with packages.xml). Some of these packages +exist in subdirectory `test/packages`, others are generated dynamically. The dynamically generated packages, as well +as the `build` and `install` subdirectories, are generated in a user's pytest temporary directories (which is typically +at /tmp/pytest-of-{username}/pytest-NN) + +Testing can be initiated using normal ROS test commmands, that is `colcon test --packages-select ament_cmake_python_test` diff --git a/ament_cmake_python_test/package.xml b/ament_cmake_python_test/package.xml new file mode 100644 index 00000000..b30eed53 --- /dev/null +++ b/ament_cmake_python_test/package.xml @@ -0,0 +1,34 @@ + + + + ament_cmake_python_test + 0.0.1 + Test package for ament_cmake_python. + + Chris Lalancette + Apache License 2.0 + + R. Kent James + Nadav Elkabets + + ament_cmake_core + ament_cmake_python + ament_cmake + + python3-jinja2 + rosidl_default_generators + ament_cmake_pytest + ament_cmake_python + ament_cmake + + ament_lint_auto + ament_lint_common + + python3-pytest + + ament_cmake_core + + + ament_cmake + + diff --git a/ament_cmake_python_test/test/options.py b/ament_cmake_python_test/test/options.py new file mode 100644 index 00000000..3e9af084 --- /dev/null +++ b/ament_cmake_python_test/test/options.py @@ -0,0 +1,107 @@ +# Copyright 2025 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. + +"""Options used to generate test packages from a template.""" + +# Set True to generate both a python package and a msg package for each python package +# This is useful to test that python and msg packages can coexist in the same package +# See issue #514 and PR #587 +COMBINE_PYTHON_WITH_MSG = False + +# Set True to test ordering sensitivity of python and msg generation in CMakeLists.txt +TEST_ORDERING_SENSITIVITY = False + +DEFAULT_OPTIONS = { + 'name': 'SET_ME', + 'version': None, + 'description': 'SET_ME', + 'setup_cfg': None, + 'destination': None, + 'symlink_install': False, + 'python_subdir': None, + 'has_python': True, + 'has_python_before': False, # if both python and msg are in the same package + 'has_msg': False, + 'scripts_destination': None, +} + +TESTS_OPTIONS = [ + { + 'name': 'python_package', + 'description': 'Package with python code', + }, + { + 'name': 'msg_package', + 'description': 'Package with only msg files', + 'has_msg': True, + 'has_python': False, + }, + { + 'name': 'python_package_symlink', + 'description': 'Package with python code, installed with symlink in build', + 'symlink_install': True, + }, + { + 'name': 'python_package_rename', + 'description': 'Package with python code, installed from alternate directory name', + 'python_subdir': 'renamed_dir', + }, + { + 'name': 'python_package_version', + 'description': 'Package with python code, specifying version', + 'version': '6.7.89', + }, + { + 'name': 'python_package_setup', + 'description': 'Package with python code, using setup.cfg for metadata', + 'setup_cfg': 'config/setup.cfg', + }, + { + 'name': 'python_package_destination', + 'description': 'Package with python code, installed to alternate destination', + 'destination': 'new_destination', + }, + { + 'name': 'python_package_with_scripts', + 'description': 'Package with python code', + 'scripts_destination': 'lib/python_package_with_scripts', + }, +] + + +def get_options(): + """Return a list of options dictionaries for generating test packages.""" + tests_options = [] + for options in TESTS_OPTIONS: + options = DEFAULT_OPTIONS | options + tests_options.append(options) + + if COMBINE_PYTHON_WITH_MSG: + if options['name'].startswith('python_package'): + msg_options = options.copy() + msg_options['name'] += '_with_msg' + msg_options['description'] += 'and msg files' + msg_options['has_msg'] = True + tests_options.append(msg_options) + + # This only makes sense when both python and msg are in the same package + if TEST_ORDERING_SENSITIVITY: + if options['name'].startswith('msg_package'): + py_options = options.copy() + py_options['name'] += '_with_python_before' + py_options['description'] += ' and python code before msg' + py_options['has_python_before'] = True + tests_options.append(py_options) + + return tests_options diff --git a/ament_cmake_python_test/test/packages/AMENT_IGNORE b/ament_cmake_python_test/test/packages/AMENT_IGNORE new file mode 100644 index 00000000..e69de29b diff --git a/ament_cmake_python_test/test/packages/ament_python_test_package/CMakeLists.txt b/ament_cmake_python_test/test/packages/ament_python_test_package/CMakeLists.txt new file mode 100644 index 00000000..9e1770bb --- /dev/null +++ b/ament_cmake_python_test/test/packages/ament_python_test_package/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright 2025 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. + +cmake_minimum_required(VERSION 3.12) + +project(ament_python_test_package) + +find_package(ament_cmake_core REQUIRED) + +find_package(ament_cmake_python REQUIRED) + +ament_python_install_package(${PROJECT_NAME}) + +ament_package() diff --git a/ament_cmake_python_test/test/packages/ament_python_test_package/ament_python_test_package/__init__.py b/ament_cmake_python_test/test/packages/ament_python_test_package/ament_python_test_package/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ament_cmake_python_test/test/packages/ament_python_test_package/ament_python_test_package/main.py b/ament_cmake_python_test/test/packages/ament_python_test_package/ament_python_test_package/main.py new file mode 100644 index 00000000..9e69a6dc --- /dev/null +++ b/ament_cmake_python_test/test/packages/ament_python_test_package/ament_python_test_package/main.py @@ -0,0 +1,18 @@ +# Copyright 2025 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 Bar: + def __init__() -> None: + pass diff --git a/ament_cmake_python_test/test/packages/ament_python_test_package/package.xml b/ament_cmake_python_test/test/packages/ament_python_test_package/package.xml new file mode 100644 index 00000000..38718c65 --- /dev/null +++ b/ament_cmake_python_test/test/packages/ament_python_test_package/package.xml @@ -0,0 +1,20 @@ + + + + ament_python_test_package + 0.0.0 + Minimal python package to test python cmake install + + Nadav Elkabets + + Apache License 2.0 + + ament_cmake_core + ament_cmake_python + + ament_cmake_core + + + ament_cmake + + diff --git a/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/CMakeLists.txt b/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/CMakeLists.txt new file mode 100644 index 00000000..5722a6f9 --- /dev/null +++ b/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright 2025 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. + +cmake_minimum_required(VERSION 3.12) + +project(ament_python_test_package_overlay) + +find_package(ament_cmake_core REQUIRED) + +find_package(ament_cmake_python REQUIRED) + +ament_python_install_package( + ament_python_test_package_overlay + PACKAGE_DIR "${CMAKE_CURRENT_LIST_DIR}/ament_python_test_package" +) +ament_python_install_package( + ament_python_test_package_overlay + PACKAGE_DIR "${CMAKE_CURRENT_LIST_DIR}/ament_python_test_package_overlay" +) + +ament_package() diff --git a/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package/__init__.py b/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package/main.py b/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package/main.py new file mode 100644 index 00000000..9e69a6dc --- /dev/null +++ b/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package/main.py @@ -0,0 +1,18 @@ +# Copyright 2025 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 Bar: + def __init__() -> None: + pass diff --git a/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package_overlay/__init__.py b/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package_overlay/__init__.py new file mode 100644 index 00000000..32ee3cbd --- /dev/null +++ b/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package_overlay/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2025 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 subdir.utils import foo diff --git a/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package_overlay/subdir/__init__.py b/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package_overlay/subdir/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package_overlay/subdir/utils.py b/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package_overlay/subdir/utils.py new file mode 100644 index 00000000..64a6480e --- /dev/null +++ b/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/ament_python_test_package_overlay/subdir/utils.py @@ -0,0 +1,17 @@ +# Copyright 2025 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 foo() -> None: + pass diff --git a/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/package.xml b/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/package.xml new file mode 100644 index 00000000..d9a7228b --- /dev/null +++ b/ament_cmake_python_test/test/packages/ament_python_test_package_overlay/package.xml @@ -0,0 +1,20 @@ + + + + ament_python_test_package_overlay + 0.0.0 + Testing merging two python packages into one + + Nadav Elkabets + + Apache License 2.0 + + ament_cmake_core + ament_cmake_python + + ament_cmake_core + + + ament_cmake + + diff --git a/ament_cmake_python_test/test/packages/python_package_double_version/CMakeLists.txt b/ament_cmake_python_test/test/packages/python_package_double_version/CMakeLists.txt new file mode 100644 index 00000000..7c5175e8 --- /dev/null +++ b/ament_cmake_python_test/test/packages/python_package_double_version/CMakeLists.txt @@ -0,0 +1,30 @@ +# Copyright 2025 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. + +cmake_minimum_required(VERSION 3.12) +project(python_package_double_version) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# find dependencies +find_package(ament_cmake REQUIRED) + + +find_package(ament_cmake_python REQUIRED) +ament_python_install_package(${PROJECT_NAME} VERSION 1.2.34) +ament_python_install_package(some_other_name PACKAGE_DIR "${CMAKE_CURRENT_LIST_DIR}/${PROJECT_NAME}" VERSION 5.6.78) + +ament_package() diff --git a/ament_cmake_python_test/test/packages/python_package_double_version/package.xml b/ament_cmake_python_test/test/packages/python_package_double_version/package.xml new file mode 100644 index 00000000..6aa994cb --- /dev/null +++ b/ament_cmake_python_test/test/packages/python_package_double_version/package.xml @@ -0,0 +1,15 @@ + + + + python_package_double_version + 0.0.0 + Package with python code, specifying two versions in ament_python_install_package + No One + Apache-2.0 + + ament_cmake + ament_cmake_python + + ament_cmake + + diff --git a/ament_cmake_python_test/test/packages/python_package_double_version/python_package_double_version/__init__.py b/ament_cmake_python_test/test/packages/python_package_double_version/python_package_double_version/__init__.py new file mode 100644 index 00000000..1270e89d --- /dev/null +++ b/ament_cmake_python_test/test/packages/python_package_double_version/python_package_double_version/__init__.py @@ -0,0 +1 @@ +# This is python_package_double_version/__init__.py diff --git a/ament_cmake_python_test/test/packages/python_package_double_version/python_package_double_version/doit.py b/ament_cmake_python_test/test/packages/python_package_double_version/python_package_double_version/doit.py new file mode 100644 index 00000000..30aa7b55 --- /dev/null +++ b/ament_cmake_python_test/test/packages/python_package_double_version/python_package_double_version/doit.py @@ -0,0 +1,15 @@ +# Copyright 2025 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. + +print("Do it!") diff --git a/ament_cmake_python_test/test/pkg_template/CMakeLists.txt.jinja b/ament_cmake_python_test/test/pkg_template/CMakeLists.txt.jinja new file mode 100644 index 00000000..9f868239 --- /dev/null +++ b/ament_cmake_python_test/test/pkg_template/CMakeLists.txt.jinja @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.12) +project({{name}}) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# find dependencies +find_package(ament_cmake REQUIRED) + +{% if has_python_before %} +find_package(ament_cmake_python REQUIRED) +ament_python_install_package(${PROJECT_NAME}) +{% endif -%} + +{% if has_msg %} +find_package(rosidl_default_generators REQUIRED) +rosidl_generate_interfaces(${PROJECT_NAME} +"msg/Dummy.msg" +) +{% endif -%} + +{% if has_python %} +find_package(ament_cmake_python REQUIRED) +ament_python_install_package(${PROJECT_NAME} {{install_options}}) +{%- endif %} + +ament_package() diff --git a/ament_cmake_python_test/test/pkg_template/config/setup.cfg b/ament_cmake_python_test/test/pkg_template/config/setup.cfg new file mode 100644 index 00000000..3c8ff887 --- /dev/null +++ b/ament_cmake_python_test/test/pkg_template/config/setup.cfg @@ -0,0 +1,3 @@ +[metadata] +keywords = test_of_ament_cmake_python +license = Apache-2.0 diff --git a/ament_cmake_python_test/test/pkg_template/msg/Dummy.msg b/ament_cmake_python_test/test/pkg_template/msg/Dummy.msg new file mode 100644 index 00000000..dd2331bb --- /dev/null +++ b/ament_cmake_python_test/test/pkg_template/msg/Dummy.msg @@ -0,0 +1 @@ +uint8[] data diff --git a/ament_cmake_python_test/test/pkg_template/package.xml.jinja b/ament_cmake_python_test/test/pkg_template/package.xml.jinja new file mode 100644 index 00000000..7b1d425c --- /dev/null +++ b/ament_cmake_python_test/test/pkg_template/package.xml.jinja @@ -0,0 +1,20 @@ + + + + {{name}} + 0.0.0 + {{description}} + No One + Apache-2.0 + + ament_cmake +{% if has_python or has_python_before %} ament_cmake_python{% endif %} +{% if has_msg %} + rosidl_default_generators + rosidl_interface_packages +{% endif -%} + + + ament_cmake + + diff --git a/ament_cmake_python_test/test/pkg_template/package_directory/__init__.py.jinja b/ament_cmake_python_test/test/pkg_template/package_directory/__init__.py.jinja new file mode 100644 index 00000000..5da50315 --- /dev/null +++ b/ament_cmake_python_test/test/pkg_template/package_directory/__init__.py.jinja @@ -0,0 +1 @@ +# This is {{name}}/__init__.py diff --git a/ament_cmake_python_test/test/pkg_template/package_directory/doit.py b/ament_cmake_python_test/test/pkg_template/package_directory/doit.py new file mode 100644 index 00000000..c5f96ad5 --- /dev/null +++ b/ament_cmake_python_test/test/pkg_template/package_directory/doit.py @@ -0,0 +1,15 @@ +# Copyright 2025 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. + +print('Do it!') diff --git a/ament_cmake_python_test/test/pkg_template/python_scripts/do_something.py b/ament_cmake_python_test/test/pkg_template/python_scripts/do_something.py new file mode 100644 index 00000000..d99ad1f3 --- /dev/null +++ b/ament_cmake_python_test/test/pkg_template/python_scripts/do_something.py @@ -0,0 +1,20 @@ +# Copyright 2025 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 main(): + print('Doing something') + + +if __name__ == '__main__': + main() diff --git a/ament_cmake_python_test/test/pkg_template/setup.cfg.jinja b/ament_cmake_python_test/test/pkg_template/setup.cfg.jinja new file mode 100644 index 00000000..13b576c1 --- /dev/null +++ b/ament_cmake_python_test/test/pkg_template/setup.cfg.jinja @@ -0,0 +1,3 @@ +[options.entry_points] +console_scripts = + do_something = {{name}}.do_something:main diff --git a/ament_cmake_python_test/test/test_packages.py b/ament_cmake_python_test/test/test_packages.py new file mode 100644 index 00000000..e2c826b6 --- /dev/null +++ b/ament_cmake_python_test/test/test_packages.py @@ -0,0 +1,293 @@ +# Copyright 2025 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. +# +"""Test of ament_python_install_package.""" + +# Not used until PR #587 +# import filecmp +import multiprocessing as mp +import os +from pathlib import Path +import shutil +import subprocess + +from jinja2 import Template +from options import get_options +import pytest + + +SOURCE_DIR = Path(__file__).parent.parent +PYTHON_INSTALL_DIR = Path(os.environ.get('PYTHON_INSTALL_DIR')) +PYEGG_VERSION = os.environ.get('PYEGG_VERSION') +CMAKE_COMMAND = os.environ.get('CMAKE_COMMAND', 'cmake') + +AMENT_PYTHON_TEST_PACKAGE = 'ament_python_test_package' +AMENT_PYTHON_TEST_PACKAGE_OVERLAY = AMENT_PYTHON_TEST_PACKAGE + '_overlay' + + +@pytest.fixture(scope='module') +def module_dir(tmp_path_factory): + """Create a temporary directory for the module.""" + return tmp_path_factory.getbasetemp() + + +def generate_package(options, package_dir): + """Generate a package from the template using the given options.""" + template_dir = Path(__file__).parent / 'pkg_template' + python_subdir = options['python_subdir'] or options['name'] + + package_dir.mkdir(parents=True) + + install_options = '' + if options['has_msg']: + shutil.copytree(template_dir / 'msg', package_dir / 'msg') + + if options['has_python'] or options['has_python_before']: + ignore_patterns = shutil.ignore_patterns('*.jinja') + shutil.copytree(template_dir / 'package_directory', + package_dir / python_subdir, ignore=ignore_patterns) + template = Template( + Path.read_text(template_dir / 'package_directory' / '__init__.py.jinja')) + Path.write_text(package_dir / python_subdir / '__init__.py', template.render(options)) + + if options['version']: + install_options += f' VERSION {options["version"]}' + + if options['setup_cfg']: + (package_dir / options['setup_cfg']).parent.mkdir(parents=True, exist_ok=True) + shutil.copy(template_dir / options['setup_cfg'], + package_dir / (Path(options['setup_cfg']).parent)) + install_options += f' SETUP_CFG {options["setup_cfg"]}' + + if options['scripts_destination']: + scripts_dir = template_dir / 'python_scripts' + shutil.copytree(scripts_dir, package_dir / python_subdir, dirs_exist_ok=True) + template = Template(Path.read_text(template_dir / 'setup.cfg.jinja')) + Path.write_text(package_dir / 'setup.cfg', template.render(options)) + install_options += f' SCRIPTS_DESTINATION {options["scripts_destination"]}' + + if options['destination']: + install_options += f' DESTINATION {options["destination"]}' + + # The python package has a name that differs from the name in package.xml + if options['python_subdir']: + install_options += f' PACKAGE_DIR {options["python_subdir"]}' + + options['install_options'] = install_options + template = Template(Path.read_text(template_dir / 'package.xml.jinja')) + Path.write_text(package_dir / 'package.xml', template.render(options)) + template = Template(Path.read_text(template_dir / 'CMakeLists.txt.jinja')) + Path.write_text(package_dir / 'CMakeLists.txt', template.render(options)) + + +def do_build_package(package_name, packages_dir, module_dir, symlink_install=False): + """Build the package using cmake.""" + build_dir = module_dir / 'build' / package_name + build_dir.mkdir(parents=True, exist_ok=True) + package_dir = packages_dir / package_name + + configure_command = [CMAKE_COMMAND, + '-S', package_dir, + '-B', build_dir, '--install-prefix', + str(module_dir / 'install' / package_name)] + if symlink_install: + configure_command.append('-DAMENT_CMAKE_SYMLINK_INSTALL=1') + print(f'Configuring package {package_name} with command: {configure_command}') + result = subprocess.run(configure_command, text=True) + assert result.returncode == 0, f'cmake configure failed for package {package_name}' + + build_command = ['cmake', '--build', str(build_dir)] + print(f'Building package {package_name} with command: {build_command}') + result = subprocess.run(build_command, text=True) + assert result.returncode == 0, f'cmake build failed for package {package_name}' + + install_command = ['cmake', '--install', str(build_dir)] + print(f'Installing package {package_name} with command: {install_command}') + result = subprocess.run(install_command, text=True) + assert result.returncode == 0, f'cmake install failed for package {package_name}' + + +def do_test_package(options, module_dir): + """Test a package that was generated using the template.""" + install_base = module_dir / 'install' + package_name = options['name'] + if options['destination']: + install_path = install_base / package_name / options['destination'] / package_name + else: + install_path = install_base / package_name / PYTHON_INSTALL_DIR / package_name + print(f'install_path for package {package_name}: {install_path}') + assert install_path.exists(), f'install path does not exist for {package_name}: {install_path}' + assert (install_path / '__init__.py').exists(), f'missing __init__.py in {install_path}' + + if options['has_python'] or options['has_python_before']: + assert Path.read_text( + install_path / '__init__.py').startswith(f'# This is {package_name}'), \ + f'__init__.py should be from {package_name} python package' + + if options['has_msg']: + assert (install_path / 'msg').is_dir(), \ + f'There should be a msg directory in {install_path}' + + if options['symlink_install']: + print(f'Testing symlink install in package {package_name}') + assert (install_path / '__init__.py').is_symlink(), '__init__.py should be a symlink' + + if options['version']: + print(f'Testing version: {options["version"]} IN EGG-INFO in package {package_name}') + version = options['version'] + egg_info_dir = install_base / package_name / PYTHON_INSTALL_DIR / \ + f'{package_name}-{version}-{PYEGG_VERSION}.egg-info' + assert egg_info_dir.exists(), \ + f'egg-info dir does not exist for {package_name}: {egg_info_dir}' + egg_info_file = egg_info_dir / 'PKG-INFO' + assert Path.read_text(egg_info_file).find(f'Version: {version}') != -1, \ + f'egg-info file should contain "Version: {version}"' + + if options['setup_cfg']: + print(f'Testing setup.cfg metadata in package {package_name}') + print(f' options: {options}') + version = options['version'] or '0.0.0' + egg_info_dir = install_base / package_name / PYTHON_INSTALL_DIR / \ + f'{package_name}-{version}-{PYEGG_VERSION}.egg-info' + assert egg_info_dir.exists(), \ + f'egg-info dir does not exist for {package_name}: {egg_info_dir}' + egg_info_file = egg_info_dir / 'PKG-INFO' + assert Path.read_text(egg_info_file).find('Keywords: test_of_ament_cmake_python') != -1, \ + 'egg-info file should contain "Keywords: test_of_ament_cmake_python"' + + if options['scripts_destination']: + print(f'Testing script installed in package {package_name}') + script_path = install_base / package_name / \ + options['scripts_destination'] / 'do_something' + assert script_path.exists(), \ + f'script do_something does not exist for {package_name}: {script_path}' + + +def do_package(test_spec): + (options, module_dir) = test_spec + generated_packages_dir = module_dir / 'packages' + message = 'Unknown error' + return_value = -1 + + try: + # Create test package from template + package_dir = generated_packages_dir / options['name'] + print(f'Generating package {options["name"]}') + generate_package(options, package_dir) + + # Build each package using cmake + print(f'Building package {options["name"]}') + do_build_package( + options['name'], generated_packages_dir, module_dir, options['symlink_install']) + + # Test each generated package + print(f'Testing package {options["name"]}') + do_test_package(options, module_dir) + except AssertionError as e: + message = str(e) + return_value = 2 + except BaseException as e: # noqa: B902 + message = str(e) + return_value = 3 + else: + return_value = 0 + finally: + print(f'Package {options["name"]} finished with return code {return_value}: {message}') + return (options['name'], return_value, message) + + +def test_from_options(module_dir): + """Generate, build, and test packages from the options.""" + # Uncomment to debug environment issues + # print('Environment: ') + # for name, value in os.environ.items(): + # print(f'{name}={value}') + # assert False + + pool = mp.Pool() + pool_results = pool.imap_unordered( + do_package, [(options, module_dir) for options in get_options()]) + while True: + try: + (name, returns, message) = pool_results.next() + print(f'Package {name} returned {returns}: {message}') + assert returns == 0, f'Package {name} failed with code {returns}: {message}' + except StopIteration: + break + # I'd prefer close() then join() but that seems to sometimes hang. + pool.terminate() + + +'''Disable tests until PR #587 is merged +def test_ament_python_test_package(module_dir) -> None: + """Test installing a known package and comparing to the source files.""" + do_build_package(AMENT_PYTHON_TEST_PACKAGE, SOURCE_DIR / 'test' / 'packages', module_dir) + print(f'Checking installed package files for {AMENT_PYTHON_TEST_PACKAGE}') + test_dircmp = filecmp.dircmp( + SOURCE_DIR / 'test' / 'packages' / AMENT_PYTHON_TEST_PACKAGE / AMENT_PYTHON_TEST_PACKAGE, + module_dir / 'install' / AMENT_PYTHON_TEST_PACKAGE + / PYTHON_INSTALL_DIR / AMENT_PYTHON_TEST_PACKAGE + ) + assert not test_dircmp.left_only, \ + f'Files only in source package: {test_dircmp.left_only}' + assert not test_dircmp.right_only, \ + f'Files only in installed package: {test_dircmp.right_only}' + assert not test_dircmp.diff_files, \ + f'Two python packages should match after install: {test_dircmp.diff_files}' + + +def test_ament_python_test_package_with_overlay(module_dir) -> None: + """Test installing two python packages in the same install space.""" + compare_dir = module_dir / 'compare' + do_build_package(AMENT_PYTHON_TEST_PACKAGE_OVERLAY, + SOURCE_DIR / 'test' / 'packages', module_dir) + + INSTALL_DIR = module_dir / 'install' / AMENT_PYTHON_TEST_PACKAGE_OVERLAY / PYTHON_INSTALL_DIR + shutil.copytree(SOURCE_DIR / 'test' / 'packages' / AMENT_PYTHON_TEST_PACKAGE / + AMENT_PYTHON_TEST_PACKAGE, compare_dir, dirs_exist_ok=True) + shutil.copytree(SOURCE_DIR / 'test' / 'packages' / AMENT_PYTHON_TEST_PACKAGE_OVERLAY / + AMENT_PYTHON_TEST_PACKAGE_OVERLAY, compare_dir, dirs_exist_ok=True) + test_dircmp = filecmp.dircmp(compare_dir, INSTALL_DIR / AMENT_PYTHON_TEST_PACKAGE_OVERLAY) + assert not test_dircmp.left_only, \ + 'Files only in source package overlay' + assert not test_dircmp.right_only, \ + 'Files only in installed package overlay' + assert not test_dircmp.diff_files, \ + 'Two overlaid python packages should match after install' + + +def test_python_double_version(module_dir) -> None: + """Test installing two versions of the same package with different names.""" + package_name = 'python_package_double_version' + do_build_package(package_name, SOURCE_DIR / 'test' / 'packages', module_dir) + + # This package installs two versions of a package with different names and version numbers. + install_base = module_dir / 'install' / package_name / PYTHON_INSTALL_DIR + for additional_name in [package_name, 'some_other_name']: + install_path = install_base / additional_name + print(f'install_path for package {additional_name}: {install_path}') + assert install_path.exists(), \ + f'install path should exist for {package_name}: {install_path}' + assert (install_path / '__init__.py').exists(), \ + f'missing __init__.py in {install_path}' + print(f'Testing version IN EGG-INFO in package {package_name}') + version = '1.2.34' if additional_name == package_name else '5.6.78' + egg_info_dir = module_dir / 'install' / package_name / PYTHON_INSTALL_DIR / \ + f'{additional_name}-{version}-{PYEGG_VERSION}.egg-info' + assert egg_info_dir.exists(), \ + f'egg-info dir should exist for {package_name}: {egg_info_dir}' + egg_info_file = egg_info_dir / 'PKG-INFO' + assert Path.read_text(egg_info_file).find(f'Version: {version}') != -1, \ + f'egg-info file should contain "Version: {version}"' +'''