1# Copyright 2021 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Tests for pw_build.create_python_tree""" 15 16import importlib.resources 17import io 18import os 19from pathlib import Path 20import tempfile 21import unittest 22 23from parameterized import parameterized # type: ignore 24 25from pw_build.python_package import PythonPackage 26from pw_build.create_python_tree import ( 27 build_python_tree, 28 copy_extra_files, 29 load_common_config, 30 update_config_with_packages, 31) 32from pw_build.generate_python_package import PYPROJECT_FILE 33 34import test_dist1_data # type: ignore 35 36 37def _setup_cfg(package_name: str, install_requires: str = '') -> str: 38 return f''' 39[metadata] 40name = {package_name} 41version = 0.0.1 42author = Pigweed Authors 43author_email = [email protected] 44description = Pigweed swiss-army knife 45 46[options] 47packages = find: 48zip_safe = False 49{install_requires} 50 51[options.package_data] 52{package_name} = 53 py.typed 54 ''' 55 56 57def _create_fake_python_package( 58 location: Path, 59 package_name: str, 60 files: list[str], 61 install_requires: str = '', 62) -> None: 63 for file in files: 64 destination = location / file 65 destination.parent.mkdir(parents=True, exist_ok=True) 66 text = f'"""{package_name}"""' 67 if str(destination).endswith('setup.cfg'): 68 text = _setup_cfg(package_name, install_requires) 69 elif str(destination).endswith('pyproject.toml'): 70 # Make sure pyproject.toml file has valid syntax. 71 text = PYPROJECT_FILE 72 destination.write_text(text) 73 74 75class TestCreatePythonTree(unittest.TestCase): 76 """Integration tests for create_python_tree.""" 77 78 maxDiff = None 79 80 def setUp(self): 81 # Save the starting working directory for returning to later. 82 self.start_dir = Path.cwd() 83 # Create a temp out directory 84 self.temp_dir = tempfile.TemporaryDirectory() 85 86 def tearDown(self): 87 # cd to the starting dir before cleaning up the temp out directory 88 os.chdir(self.start_dir) 89 # Delete the TemporaryDirectory 90 self.temp_dir.cleanup() 91 92 def _check_result_paths_equal(self, install_dir, expected_results) -> None: 93 # Normalize path strings to posix before comparing. 94 expected_paths = set(Path(p).as_posix() for p in expected_results) 95 actual_paths = set( 96 p.relative_to(install_dir).as_posix() 97 for p in install_dir.glob('**/*') 98 if p.is_file() 99 ) 100 self.assertEqual(expected_paths, actual_paths) 101 102 def test_update_config_with_packages(self) -> None: 103 """Test merging package setup.cfg files.""" 104 temp_root = Path(self.temp_dir.name) 105 common_config = temp_root / 'common_setup.cfg' 106 common_config.write_text( 107 ''' 108[metadata] 109name = megapackage 110version = 0.0.1 111author = Pigweed Authors 112author_email = [email protected] 113description = Pigweed swiss-army knife 114 115[options] 116zip_safe = False 117 118[options.package_data] 119megapackage = 120 py.typed 121''' 122 ) 123 config = load_common_config( 124 common_config=common_config, append_git_sha=False, append_date=False 125 ) 126 config_metadata = dict(config['metadata'].items()) 127 self.assertIn('name', config_metadata) 128 129 pkg1_root = temp_root / 'pkg1' 130 pkg2_root = temp_root / 'pkg2' 131 _create_fake_python_package( 132 pkg1_root, 133 'mars', 134 [ 135 'planets/BUILD.mars_rocket', 136 'planets/mars/__init__.py', 137 'planets/mars/__main__.py', 138 'planets/mars/moons/__init__.py', 139 'planets/mars/moons/deimos.py', 140 'planets/mars/moons/phobos.py', 141 'planets/hohmann_transfer_test.py', 142 'planets/pyproject.toml', 143 'planets/setup.cfg', 144 ], 145 install_requires=''' 146install_requires = 147 coloredlogs 148 coverage 149 cryptography 150 graphlib-backport;python_version<'3.9' 151 httpwatcher 152''', 153 ) 154 155 os.chdir(pkg1_root) 156 pkg1 = PythonPackage.from_dict( 157 **{ 158 'generate_setup': { 159 'metadata': {'name': 'mars', 'version': '0.0.1'}, 160 }, 161 'inputs': [], 162 'setup_sources': [ 163 'planets/pyproject.toml', 164 'planets/setup.cfg', 165 ], 166 'sources': [ 167 'planets/mars/__init__.py', 168 'planets/mars/__main__.py', 169 'planets/mars/moons/__init__.py', 170 'planets/mars/moons/deimos.py', 171 'planets/mars/moons/phobos.py', 172 ], 173 'tests': [ 174 'planets/hohmann_transfer_test.py', 175 ], 176 } 177 ) 178 179 _create_fake_python_package( 180 pkg2_root, 181 'saturn', 182 [ 183 'planets/BUILD.saturn_rocket', 184 'planets/hohmann_transfer_test.py', 185 'planets/pyproject.toml', 186 'planets/saturn/__init__.py', 187 'planets/saturn/__main__.py', 188 'planets/saturn/misson.py', 189 'planets/saturn/moons/__init__.py', 190 'planets/saturn/moons/enceladus.py', 191 'planets/saturn/moons/iapetus.py', 192 'planets/saturn/moons/rhea.py', 193 'planets/saturn/moons/titan.py', 194 'planets/setup.cfg', 195 'planets/setup.py', 196 ], 197 install_requires=''' 198install_requires = 199 graphlib-backport;python_version<'3.9' 200 httpwatcher 201''', 202 ) 203 os.chdir(pkg2_root) 204 pkg2 = PythonPackage.from_dict( 205 **{ 206 'inputs': [], 207 'setup_sources': [ 208 'planets/pyproject.toml', 209 'planets/setup.cfg', 210 'planets/setup.py', 211 ], 212 'sources': [ 213 'planets/saturn/__init__.py', 214 'planets/saturn/__main__.py', 215 'planets/saturn/misson.py', 216 'planets/saturn/moons/__init__.py', 217 'planets/saturn/moons/enceladus.py', 218 'planets/saturn/moons/iapetus.py', 219 'planets/saturn/moons/rhea.py', 220 'planets/saturn/moons/titan.py', 221 ], 222 'tests': [ 223 'planets/hohmann_transfer_test.py', 224 ], 225 } 226 ) 227 228 update_config_with_packages(config=config, python_packages=[pkg1, pkg2]) 229 230 setup_cfg_text = io.StringIO() 231 config.write(setup_cfg_text) 232 expected_cfg = ''' 233[metadata] 234name = megapackage 235version = 0.0.1 236author = Pigweed Authors 237author_email = [email protected] 238description = Pigweed swiss-army knife 239 240[options] 241zip_safe = False 242packages = find: 243install_requires = 244 coloredlogs 245 coverage 246 cryptography 247 graphlib-backport;python_version<'3.9' 248 httpwatcher 249 250[options.package_data] 251megapackage = 252 py.typed 253mars = 254 py.typed 255saturn = 256 py.typed 257 258[options.entry_points] 259''' 260 result_cfg_lines = [ 261 line.rstrip().replace('\t', ' ') 262 for line in setup_cfg_text.getvalue().splitlines() 263 if line 264 ] 265 expected_cfg_lines = [ 266 line.rstrip() for line in expected_cfg.splitlines() if line 267 ] 268 self.assertEqual(expected_cfg_lines, result_cfg_lines) 269 270 @parameterized.expand( 271 [ 272 ( 273 # Test name 274 'working case', 275 # Package name 276 'mars', 277 # File list 278 [ 279 'planets/BUILD.mars_rocket', 280 'planets/mars/__init__.py', 281 'planets/mars/__main__.py', 282 'planets/mars/moons/__init__.py', 283 'planets/mars/moons/deimos.py', 284 'planets/mars/moons/phobos.py', 285 'planets/hohmann_transfer_test.py', 286 'planets/pyproject.toml', 287 'planets/setup.cfg', 288 ], 289 # Extra_files 290 [], 291 # Package definition 292 { 293 'generate_setup': { 294 'metadata': {'name': 'mars', 'version': '0.0.1'}, 295 }, 296 'inputs': [], 297 'setup_sources': [ 298 'planets/pyproject.toml', 299 'planets/setup.cfg', 300 ], 301 'sources': [ 302 'planets/mars/__init__.py', 303 'planets/mars/__main__.py', 304 'planets/mars/moons/__init__.py', 305 'planets/mars/moons/deimos.py', 306 'planets/mars/moons/phobos.py', 307 ], 308 'tests': [ 309 'planets/hohmann_transfer_test.py', 310 ], 311 }, 312 # Output file list 313 [ 314 'mars/__init__.py', 315 'mars/__main__.py', 316 'mars/moons/__init__.py', 317 'mars/moons/deimos.py', 318 'mars/moons/phobos.py', 319 'mars/tests/hohmann_transfer_test.py', 320 ], 321 ), 322 ( 323 # Test name 324 'with extra files', 325 # Package name 326 'saturn', 327 # File list 328 [ 329 'planets/BUILD.saturn_rocket', 330 'planets/hohmann_transfer_test.py', 331 'planets/pyproject.toml', 332 'planets/saturn/__init__.py', 333 'planets/saturn/__main__.py', 334 'planets/saturn/misson.py', 335 'planets/saturn/moons/__init__.py', 336 'planets/saturn/moons/enceladus.py', 337 'planets/saturn/moons/iapetus.py', 338 'planets/saturn/moons/rhea.py', 339 'planets/saturn/moons/titan.py', 340 'planets/setup.cfg', 341 'planets/setup.py', 342 ], 343 # Extra files 344 [ 345 'planets/BUILD.saturn_rocket > out/saturn/BUILD.rocket', 346 ], 347 # Package definition 348 { 349 'inputs': [], 350 'setup_sources': [ 351 'planets/pyproject.toml', 352 'planets/setup.cfg', 353 'planets/setup.py', 354 ], 355 'sources': [ 356 'planets/saturn/__init__.py', 357 'planets/saturn/__main__.py', 358 'planets/saturn/misson.py', 359 'planets/saturn/moons/__init__.py', 360 'planets/saturn/moons/enceladus.py', 361 'planets/saturn/moons/iapetus.py', 362 'planets/saturn/moons/rhea.py', 363 'planets/saturn/moons/titan.py', 364 ], 365 'tests': [ 366 'planets/hohmann_transfer_test.py', 367 ], 368 }, 369 # Output file list 370 [ 371 'saturn/BUILD.rocket', 372 'saturn/__init__.py', 373 'saturn/__main__.py', 374 'saturn/misson.py', 375 'saturn/moons/__init__.py', 376 'saturn/moons/enceladus.py', 377 'saturn/moons/iapetus.py', 378 'saturn/moons/rhea.py', 379 'saturn/moons/titan.py', 380 'saturn/tests/hohmann_transfer_test.py', 381 ], 382 ), 383 ] 384 ) 385 def test_build_python_tree( 386 self, 387 _test_name, 388 package_name, 389 file_list, 390 extra_files, 391 package_definition, 392 expected_file_list, 393 ) -> None: 394 """Check results of build_python_tree and copy_extra_files.""" 395 temp_root = Path(self.temp_dir.name) 396 _create_fake_python_package(temp_root, package_name, file_list) 397 398 os.chdir(temp_root) 399 install_dir = temp_root / 'out' 400 401 package = PythonPackage.from_dict(**package_definition) 402 build_python_tree( 403 python_packages=[package], 404 tree_destination_dir=install_dir, 405 include_tests=True, 406 ) 407 copy_extra_files(extra_files) 408 409 # Check expected files are in place. 410 self._check_result_paths_equal(install_dir, expected_file_list) 411 412 @parameterized.expand( 413 [ 414 ( 415 # Test name 416 'everything in correct locations', 417 # Package name 418 'planets', 419 # File list 420 [ 421 'BUILD.mars_rocket', 422 ], 423 # Extra_files 424 [ 425 'BUILD.mars_rocket > out/mars/BUILD.rocket', 426 ], 427 # Output file list 428 [ 429 'mars/BUILD.rocket', 430 ], 431 # Should raise exception 432 None, 433 ), 434 ( 435 # Test name 436 'missing source files', 437 # Package name 438 'planets', 439 # File list 440 [ 441 'BUILD.mars_rocket', 442 ], 443 # Extra_files 444 [ 445 'BUILD.venus_rocket > out/venus/BUILD.rocket', 446 ], 447 # Output file list 448 [], 449 # Should raise exception 450 FileNotFoundError, 451 ), 452 ( 453 # Test name 454 'existing destination files', 455 # Package name 456 'planets', 457 # File list 458 [ 459 'BUILD.jupiter_rocket', 460 'out/jupiter/BUILD.rocket', 461 ], 462 # Extra_files 463 [ 464 'BUILD.jupiter_rocket > out/jupiter/BUILD.rocket', 465 ], 466 # Output file list 467 [], 468 # Should raise exception 469 FileExistsError, 470 ), 471 ] 472 ) 473 def test_copy_extra_files( 474 self, 475 _test_name, 476 package_name, 477 file_list, 478 extra_files, 479 expected_file_list, 480 should_raise_exception, 481 ) -> None: 482 """Check results of build_python_tree and copy_extra_files.""" 483 temp_root = Path(self.temp_dir.name) 484 _create_fake_python_package(temp_root, package_name, file_list) 485 486 os.chdir(temp_root) 487 install_dir = temp_root / 'out' 488 489 # If exceptions should be raised 490 if should_raise_exception: 491 with self.assertRaises(should_raise_exception): 492 copy_extra_files(extra_files) 493 return 494 495 # Do the copy 496 copy_extra_files(extra_files) 497 # Check expected files are in place. 498 self._check_result_paths_equal(install_dir, expected_file_list) 499 500 def test_importing_package_data(self) -> None: 501 self.assertIn( 502 'EMPTY.CSV', 503 importlib.resources.read_text(test_dist1_data, 'empty.csv'), 504 ) 505 self.assertIn( 506 'EMPTY.CSV', 507 importlib.resources.read_text( 508 'test_dist1_data.subdir', 'empty.csv' 509 ), 510 ) 511 512 513if __name__ == '__main__': 514 unittest.main() 515