Skip to content

Commit a3c6de0

Browse files
committed
Add tests for ament_python_install_package
Signed-off-by: R Kent James <[email protected]>
1 parent 17d4da4 commit a3c6de0

File tree

13 files changed

+338
-0
lines changed

13 files changed

+338
-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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
find_package(ament_cmake REQUIRED)
19+
ament_get_python_install_dir(PYTHON_INSTALL_DIR)
20+
21+
set(_pytest_tests
22+
test/build_with_colcon.py
23+
# Add other test files here
24+
)
25+
foreach(_test_path ${_pytest_tests})
26+
get_filename_component(_test_name ${_test_path} NAME_WE)
27+
ament_add_pytest_test(${_test_name} ${_test_path}
28+
APPEND_ENV PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}
29+
ENV SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} PYTHON_INSTALL_DIR=${PYTHON_INSTALL_DIR} PYEGG_VERSION=py${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}
30+
TIMEOUT 360
31+
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
32+
)
33+
endforeach()
34+
endif()
35+
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` (when run locally).
6+
That means that the normal `build` and `install` directories are subdirectories of `build/ament_cmake_package_test` named `build_test` and `install_test` respectively.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
<build_depend>ament_cmake</build_depend>
16+
17+
<test_depend>python3-jinja2</test_depend>
18+
<test_depend>rosidl_default_generators</test_depend>
19+
<test_depend>ament_cmake_pytest</test_depend>
20+
<test_depend>ament_cmake_python</test_depend>
21+
<test_depend>ament_cmake</test_depend>
22+
23+
<buildtool_export_depend>ament_cmake_core</buildtool_export_depend>
24+
25+
<export>
26+
<build_type>ament_cmake</build_type>
27+
</export>
28+
</package>
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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+
INSTALL_BASE = 'install_test'
19+
BUILD_BASE = 'build_test'
20+
21+
DEFAULT_OPTIONS = {
22+
'name': 'SET_ME',
23+
'version': None,
24+
'description': 'SET_ME',
25+
'setup_cfg': None,
26+
'destination': None,
27+
'symlink_install': False,
28+
'package_subdir': None,
29+
'has_python': True,
30+
'has_python_before': False, # This will only make sense when both python and msg are in the same package
31+
'has_msg': False,
32+
'scripts_destination': None
33+
}
34+
35+
TESTS_OPTIONS = [
36+
{
37+
'name': 'python_package',
38+
'description': 'Package with python code',
39+
},
40+
{
41+
'name': 'python_package_symlink',
42+
'description': 'Package with python code, installed with symlink in build',
43+
'symlink_install': True,
44+
},
45+
{
46+
'name': 'python_package_rename',
47+
'description': 'Package with python code, installed from alternate directory name',
48+
'package_subdir': 'renamed_dir',
49+
},
50+
{
51+
'name': 'python_package_version',
52+
'description': 'Package with python code, specifying version in ament_python_install_package',
53+
'version': '6.7.89',
54+
},
55+
{
56+
'name': 'python_package_setup',
57+
'description': 'Package with python code, using setup.cfg for metadata',
58+
'setup_cfg': Path('config') / Path('setup.cfg'),
59+
},
60+
{
61+
'name': 'python_package_destination',
62+
'description': 'Package with python code, installed to alternate destination',
63+
'destination': 'new_destination',
64+
},
65+
{
66+
'name': 'python_package_with_scripts',
67+
'description': 'Package with python code',
68+
'scripts_destination': 'lib/python_package_with_scripts',
69+
},
70+
{
71+
'name': 'msg_package',
72+
'description': 'Package with only msg files',
73+
'has_msg': True,
74+
'has_python': False,
75+
},
76+
]
77+
78+
79+
def test_from_template():
80+
print(f"PWD: {PWD}")
81+
82+
# delete any existing package directory
83+
packages_dir = PWD / 'packages'
84+
shutil.rmtree(packages_dir, ignore_errors=True)
85+
86+
# Create test packages from template
87+
template_dir = SOURCE_DIR / 'test' / 'pkg_template'
88+
for options in TESTS_OPTIONS:
89+
options = DEFAULT_OPTIONS | options
90+
print(f"Generating package {options['name']}")
91+
print(f" options: {options}")
92+
package_dir = packages_dir / options['name']
93+
shutil.rmtree(package_dir, ignore_errors=True)
94+
package_subdir = options['package_subdir'] or options['name']
95+
96+
package_dir.mkdir(parents=True)
97+
98+
install_options = ''
99+
if options['has_msg']:
100+
shutil.copytree(template_dir / 'msg', package_dir / 'msg')
101+
102+
if options['has_python']:
103+
ignore_patterns = shutil.ignore_patterns('*.jinja')
104+
shutil.copytree(template_dir / 'package_directory', package_dir / package_subdir, ignore=ignore_patterns)
105+
template = Template(Path.read_text(template_dir / 'package_directory' / '__init__.py.jinja'))
106+
Path.write_text(package_dir / package_subdir / '__init__.py', template.render(options))
107+
108+
if options['version']:
109+
install_options += f' VERSION {options["version"]}'
110+
111+
if options['setup_cfg']:
112+
(package_dir / options['setup_cfg']).parent.mkdir(parents=True, exist_ok=True)
113+
shutil.copy(template_dir / options['setup_cfg'], package_dir / options['setup_cfg'].parent)
114+
install_options += f' SETUP_CFG {options["setup_cfg"]}'
115+
116+
if options['scripts_destination']:
117+
scripts_dir = template_dir / 'python_scripts'
118+
shutil.copytree(scripts_dir, package_dir / package_subdir, dirs_exist_ok=True)
119+
template = Template(Path.read_text(template_dir / 'setup.cfg.jinja'))
120+
Path.write_text(package_dir / 'setup.cfg', template.render(options))
121+
install_options += f' SCRIPTS_DESTINATION {options["scripts_destination"]}'
122+
123+
if options['destination']:
124+
install_options += f' DESTINATION {options["destination"]}'
125+
126+
if options['package_subdir']:
127+
install_options += f' PACKAGE_DIR {options["package_subdir"]}'
128+
129+
options['install_options'] = install_options
130+
template = Template(Path.read_text(template_dir / 'package.xml.jinja'))
131+
Path.write_text(package_dir / 'package.xml', template.render(options))
132+
template = Template(Path.read_text(template_dir / 'CMakeLists.txt.jinja'))
133+
Path.write_text(package_dir / 'CMakeLists.txt', template.render(options))
134+
135+
do_build_package(options['name'], options, base_prefix=PWD)
136+
do_test_package(options['name'], options)
137+
138+
139+
def do_build_package(package_name, options=None, base_prefix=SOURCE_DIR / 'test'):
140+
if options and 'build' in options:
141+
build_options = options['build']
142+
elif options and 'symlink_install' in options and options['symlink_install']:
143+
build_options = '--symlink-install'
144+
else:
145+
build_options = None
146+
147+
print(f"Building package {package_name} with colcon options: {build_options}")
148+
build_command = ['colcon', 'build',
149+
'--base-paths', base_prefix / 'packages' / package_name,
150+
'--build-base', BUILD_BASE,
151+
'--install-base', INSTALL_BASE]
152+
if build_options:
153+
build_command.append(build_options)
154+
result = subprocess.run(build_command, capture_output=True, text=True)
155+
156+
print("\nCOLCON stdout:\n\n" + result.stdout + '\n---(end stdout)', file=sys.stdout)
157+
print("\nCOLCON stderr:\n\n" + result.stderr + '\n---(end stderr)', file=sys.stderr)
158+
159+
160+
def do_test_package(package_name, options):
161+
if options['destination']:
162+
install_path = PWD / INSTALL_BASE / package_name / options['destination'] / package_name
163+
else:
164+
install_path = PWD / INSTALL_BASE / package_name / PYTHON_INSTALL_DIR / package_name
165+
print(f"install_path for package {package_name}: {install_path}")
166+
assert install_path.exists(), f"install path does not exist for {package_name}: {install_path}"
167+
assert (install_path / '__init__.py').exists(), f"missing __init__.py in {install_path}"
168+
169+
if options['has_python']:
170+
assert Path.read_text (install_path / '__init__.py').startswith(f"# This is {package_name}"), \
171+
f"__init__.py should be from {package_name} python package"
172+
173+
if options['has_msg']:
174+
assert(install_path/ 'msg').is_dir(), f"There should be a msg directory in {install_path}"
175+
176+
if options['symlink_install']:
177+
print(f"Testing symlink install in package {package_name}")
178+
assert (install_path / '__init__.py').is_symlink(), "__init__.py should be a symlink"
179+
180+
if options['version']:
181+
print(f"Testing version: {options['version']} IN EGG-INFO in package {package_name}")
182+
version = options['version']
183+
egg_info_dir = PWD / INSTALL_BASE / package_name / PYTHON_INSTALL_DIR / f'{package_name}-{version}-{PYEGG_VERSION}.egg-info'
184+
assert egg_info_dir.exists(), f"egg-info dir does not exist for {package_name}: {egg_info_dir}"
185+
egg_info_file = egg_info_dir / 'PKG-INFO'
186+
assert Path.read_text(egg_info_file).find(f"Version: {version}") != -1, \
187+
f"egg-info file should contain 'Version: {version}'"
188+
189+
if options['setup_cfg']:
190+
print(f"Testing setup.cfg metadata in package {package_name}")
191+
print(f" options: {options}")
192+
version = options['version'] or "0.0.0"
193+
egg_info_dir = PWD / INSTALL_BASE / package_name / PYTHON_INSTALL_DIR / f'{package_name}-{version}-{PYEGG_VERSION}.egg-info'
194+
assert egg_info_dir.exists(), f"egg-info dir does not exist for {package_name}: {egg_info_dir}"
195+
egg_info_file = egg_info_dir / 'PKG-INFO'
196+
assert Path.read_text(egg_info_file).find(f"Keywords: test_of_ament_cmake_python") != -1, \
197+
f"egg-info file should contain 'Keywords: test_of_ament_cmake_python' specified in setup.cfg"
198+
199+
if options['scripts_destination']:
200+
print(f"Testing script installed in package {package_name}")
201+
script_path = PWD / INSTALL_BASE / package_name / options['scripts_destination'] / 'do_something'
202+
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)