Skip to content

Commit ac1585b

Browse files
committed
Add tests for ament_python_install_package
Signed-off-by: R Kent James <[email protected]> Generated-by: Github Copilot v1.364.0
1 parent 17d4da4 commit ac1585b

File tree

13 files changed

+327
-0
lines changed

13 files changed

+327
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
build/
2+
install/
3+
log/
4+
__pycache__/
5+
*.py[codz]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
cmake_minimum_required(VERSION 3.12)
2+
3+
project(ament_cmake_python_test)
4+
5+
find_package(ament_cmake_core REQUIRED)
6+
7+
# Uncomment to debug CMake variables
8+
#
9+
#get_cmake_property(_variableNames VARIABLES)
10+
#list (SORT _variableNames)
11+
#foreach (_variableName ${_variableNames})
12+
# message(STATUS "${_variableName}=${${_variableName}}")
13+
#endforeach()
14+
15+
if(BUILD_TESTING)
16+
find_package(ament_cmake_pytest REQUIRED)
17+
find_package(ament_cmake_python REQUIRED)
18+
ament_get_python_install_dir(PYTHON_INSTALL_DIR)
19+
20+
set(_pytest_tests
21+
test/build_with_colcon.py
22+
# Add other test files here
23+
)
24+
foreach(_test_path ${_pytest_tests})
25+
get_filename_component(_test_name ${_test_path} NAME_WE)
26+
ament_add_pytest_test(${_test_name} ${_test_path}
27+
APPEND_ENV PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}
28+
ENV SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} PYTHON_INSTALL_DIR=${PYTHON_INSTALL_DIR} PYEGG_VERSION=py${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}
29+
TIMEOUT 60
30+
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
31+
)
32+
endforeach()
33+
endif()
34+
ament_package()

ament_cmake_python_test/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# ament_cmake_python_test
2+
3+
This package exists solely to test the ament_cmake_python package.
4+
5+
It runs `colcon build` on some test packages, with the working directory `build/ament_cmake_package_test`.
6+
That means that the normal `build`, `install`, and `log` directories are subdirectories of `build/ament_cmake_package_test`
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0"?>
2+
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
3+
<package format="3">
4+
<name>ament_cmake_python_test</name>
5+
<version>0.0.1</version>
6+
<description>Test package for ament_cmake_python.</description>
7+
8+
<maintainer email="[email protected]">Kent James</maintainer>
9+
10+
<license>Apache License 2.0</license>
11+
12+
<buildtool_depend>ament_cmake_core</buildtool_depend>
13+
<build_depend>ament_cmake_pytest</build_depend>
14+
<build_depend>ament_cmake_python</build_depend>
15+
16+
<test_depend>python-jinja2</test_depend>
17+
18+
<buildtool_export_depend>ament_cmake_core</buildtool_export_depend>
19+
20+
<export>
21+
<build_type>ament_cmake</build_type>
22+
</export>
23+
</package>
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""
2+
Test building python packages with colcon
3+
"""
4+
5+
import os
6+
from pathlib import Path
7+
import shutil
8+
import sys
9+
import subprocess
10+
11+
from jinja2 import Template
12+
13+
PWD = Path(os.environ.get('PWD'))
14+
SOURCE_DIR = Path(os.environ.get('SOURCE_DIR'))
15+
PYTHON_INSTALL_DIR = Path(os.environ.get('PYTHON_INSTALL_DIR'))
16+
PYEGG_VERSION = os.environ.get('PYEGG_VERSION')
17+
18+
DEFAULT_OPTIONS = {
19+
'name': 'SET_ME',
20+
'version': None,
21+
'description': 'SET_ME',
22+
'setup_cfg': None,
23+
'destination': None,
24+
'symlink_install': False,
25+
'package_subdir': None,
26+
'has_python': True,
27+
'has_python_before': False, # This will only make sense when both python and msg are in the same package
28+
'has_msg': False,
29+
'scripts_destination': None
30+
}
31+
32+
TESTS_OPTIONS = [
33+
{
34+
'name': 'python_package',
35+
'description': 'Package with python code',
36+
},
37+
{
38+
'name': 'python_package_symlink',
39+
'description': 'Package with python code, installed with symlink in build',
40+
'symlink_install': True,
41+
},
42+
{
43+
'name': 'python_package_rename',
44+
'description': 'Package with python code, installed from alternate directory name',
45+
'package_subdir': 'renamed_dir',
46+
},
47+
{
48+
'name': 'python_package_version',
49+
'description': 'Package with python code, specifying version in ament_python_install_package',
50+
'version': '6.7.89',
51+
},
52+
{
53+
'name': 'python_package_setup',
54+
'description': 'Package with python code, using setup.cfg for metadata',
55+
'setup_cfg': Path('config') / Path('setup.cfg'),
56+
},
57+
{
58+
'name': 'python_package_destination',
59+
'description': 'Package with python code, installed to alternate destination',
60+
'destination': 'new_destination',
61+
},
62+
{
63+
'name': 'python_package_with_scripts',
64+
'description': 'Package with python code',
65+
'scripts_destination': 'lib/python_package_with_scripts',
66+
},
67+
{
68+
'name': 'msg_package',
69+
'description': 'Package with only msg files',
70+
'has_msg': True,
71+
'has_python': False,
72+
},
73+
]
74+
75+
76+
def test_from_template():
77+
print(f"PWD: {PWD}")
78+
79+
# delete any existing package directory
80+
packages_dir = PWD / 'packages'
81+
shutil.rmtree(packages_dir, ignore_errors=True)
82+
83+
# Create test packages from template
84+
template_dir = SOURCE_DIR / 'test' / 'pkg_template'
85+
for options in TESTS_OPTIONS:
86+
options = DEFAULT_OPTIONS | options
87+
print(f"Generating package {options['name']}")
88+
print(f" options: {options}")
89+
package_dir = packages_dir / options['name']
90+
shutil.rmtree(package_dir, ignore_errors=True)
91+
package_subdir = options['package_subdir'] or options['name']
92+
93+
package_dir.mkdir(parents=True)
94+
95+
install_options = ''
96+
if options['has_msg']:
97+
shutil.copytree(template_dir / 'msg', package_dir / 'msg')
98+
99+
if options['has_python']:
100+
ignore_patterns = shutil.ignore_patterns('*.jinja')
101+
shutil.copytree(template_dir / 'package_directory', package_dir / package_subdir, ignore=ignore_patterns)
102+
template = Template(Path.read_text(template_dir / 'package_directory' / '__init__.py.jinja'))
103+
Path.write_text(package_dir / package_subdir / '__init__.py', template.render(options))
104+
105+
if options['version']:
106+
install_options += f' VERSION {options["version"]}'
107+
108+
if options['setup_cfg']:
109+
(package_dir / options['setup_cfg']).parent.mkdir(parents=True, exist_ok=True)
110+
shutil.copy(template_dir / options['setup_cfg'], package_dir / options['setup_cfg'].parent)
111+
install_options += f' SETUP_CFG {options["setup_cfg"]}'
112+
113+
if options['scripts_destination']:
114+
scripts_dir = template_dir / 'python_scripts'
115+
shutil.copytree(scripts_dir, package_dir / package_subdir, dirs_exist_ok=True)
116+
template = Template(Path.read_text(template_dir / 'setup.cfg.jinja'))
117+
Path.write_text(package_dir / 'setup.cfg', template.render(options))
118+
install_options += f' SCRIPTS_DESTINATION {options["scripts_destination"]}'
119+
120+
if options['destination']:
121+
install_options += f' DESTINATION {options["destination"]}'
122+
123+
if options['package_subdir']:
124+
install_options += f' PACKAGE_DIR {options["package_subdir"]}'
125+
126+
options['install_options'] = install_options
127+
template = Template(Path.read_text(template_dir / 'package.xml.jinja'))
128+
Path.write_text(package_dir / 'package.xml', template.render(options))
129+
template = Template(Path.read_text(template_dir / 'CMakeLists.txt.jinja'))
130+
Path.write_text(package_dir / 'CMakeLists.txt', template.render(options))
131+
132+
do_build_package(options['name'], options, base_prefix=PWD)
133+
do_test_package(options['name'], options)
134+
135+
136+
def do_build_package(package_name, options=None, base_prefix=SOURCE_DIR / 'test'):
137+
if options and 'build' in options:
138+
build_options = options['build']
139+
elif options and 'symlink_install' in options and options['symlink_install']:
140+
build_options = '--symlink-install'
141+
else:
142+
build_options = None
143+
144+
print(f"Building package {package_name} with colcon options: {build_options}")
145+
build_command = ['colcon', 'build',
146+
'--base-paths', base_prefix / 'packages' / package_name]
147+
if build_options:
148+
build_command.append(build_options)
149+
result = subprocess.run(build_command, capture_output=True, text=True)
150+
151+
print("\nCOLCON stdout:\n\n" + result.stdout + '\n---(end stdout)', file=sys.stdout)
152+
print("\nCOLCON stderr:\n\n" + result.stderr + '\n---(end stderr)', file=sys.stderr)
153+
154+
155+
def do_test_package(package_name, options):
156+
if options['destination']:
157+
install_path = PWD / 'install' / package_name / options['destination'] / package_name
158+
else:
159+
install_path = PWD / 'install' / package_name / PYTHON_INSTALL_DIR / package_name
160+
print(f"install_path for package {package_name}: {install_path}")
161+
assert install_path.exists(), f"install path does not exist for {package_name}: {install_path}"
162+
assert (install_path / '__init__.py').exists(), f"missing __init__.py in {install_path}"
163+
164+
if options['has_python']:
165+
assert Path.read_text (install_path / '__init__.py').startswith(f"# This is {package_name}"), \
166+
f"__init__.py should be from {package_name} python package"
167+
168+
if options['has_msg']:
169+
assert(install_path/ 'msg').is_dir(), f"There should be a msg directory in {install_path}"
170+
171+
if options['symlink_install']:
172+
print(f"Testing symlink install in package {package_name}")
173+
assert (install_path / '__init__.py').is_symlink(), "__init__.py should be a symlink"
174+
175+
if options['version']:
176+
print(f"Testing version: {options['version']} IN EGG-INFO in package {package_name}")
177+
version = options['version']
178+
egg_info_dir = PWD / 'install' / package_name / PYTHON_INSTALL_DIR / f'{package_name}-{version}-{PYEGG_VERSION}.egg-info'
179+
assert egg_info_dir.exists(), f"egg-info dir does not exist for {package_name}: {egg_info_dir}"
180+
egg_info_file = egg_info_dir / 'PKG-INFO'
181+
assert Path.read_text(egg_info_file).find(f"Version: {version}") != -1, \
182+
f"egg-info file should contain 'Version: {version}'"
183+
184+
if options['setup_cfg']:
185+
print(f"Testing setup.cfg metadata in package {package_name}")
186+
print(f" options: {options}")
187+
version = options['version'] or "0.0.0"
188+
egg_info_dir = PWD / 'install' / package_name / PYTHON_INSTALL_DIR / f'{package_name}-{version}-{PYEGG_VERSION}.egg-info'
189+
assert egg_info_dir.exists(), f"egg-info dir does not exist for {package_name}: {egg_info_dir}"
190+
egg_info_file = egg_info_dir / 'PKG-INFO'
191+
assert Path.read_text(egg_info_file).find(f"Keywords: test_of_ament_cmake_python") != -1, \
192+
f"egg-info file should contain 'Keywords: test_of_ament_cmake_python' specified in setup.cfg"
193+
194+
if options['scripts_destination']:
195+
print(f"Testing script installed in package {package_name}")
196+
script_path = PWD / 'install' / package_name / options['scripts_destination'] / 'do_something'
197+
assert script_path.exists(), f"script do_something does not exist for {package_name}: {script_path}"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
cmake_minimum_required(VERSION 3.20)
2+
project({{name}})
3+
4+
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
5+
add_compile_options(-Wall -Wextra -Wpedantic)
6+
endif()
7+
8+
# find dependencies
9+
find_package(ament_cmake REQUIRED)
10+
11+
{% if has_python_before %}
12+
find_package(ament_cmake_python REQUIRED)
13+
ament_python_install_package(${PROJECT_NAME})
14+
{% endif -%}
15+
16+
{% if has_msg %}
17+
find_package(rosidl_default_generators REQUIRED)
18+
rosidl_generate_interfaces(${PROJECT_NAME}
19+
"msg/Dummy.msg"
20+
)
21+
{% endif -%}
22+
23+
{% if has_python %}
24+
find_package(ament_cmake_python REQUIRED)
25+
ament_python_install_package(${PROJECT_NAME} {{install_options}})
26+
{%- endif %}
27+
28+
ament_package()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[metadata]
2+
keywords = test_of_ament_cmake_python
3+
license = Apache-2.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uint8[] data
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0"?>
2+
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
3+
<package format="3">
4+
<name>{{name}}</name>
5+
<version>{% if version %}{{version}}{% else %}0.0.0{% endif %}</version>
6+
<description>{{description}}</description>
7+
<maintainer email="[email protected]">No One</maintainer>
8+
<license>Apache-2.0</license>
9+
10+
<buildtool_depend>ament_cmake</buildtool_depend>
11+
{% if has_python or has_python_before %} <build_depend>ament_cmake_python</build_depend>{% endif %}
12+
{% if has_msg %}
13+
<build_depend>rosidl_default_generators</build_depend>
14+
<member_of_group>rosidl_interface_packages</member_of_group>
15+
{% endif -%}
16+
17+
<export>
18+
<build_type>ament_cmake</build_type>
19+
</export>
20+
</package>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This is {{name}}/__init__.py

0 commit comments

Comments
 (0)