|
| 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}" |
0 commit comments