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 ( 33 DEFAULT_INIT_PY, 34 PYPROJECT_FILE, 35) 36 37import test_dist1_data # type: ignore 38 39 40def _setup_cfg(package_name: str, install_requires: str = '') -> str: 41 return f''' 42[metadata] 43name = {package_name} 44version = 0.0.1 45author = Pigweed Authors 46author_email = pigweed-developers@googlegroups.com 47description = Pigweed swiss-army knife 48 49[options] 50packages = find: 51zip_safe = False 52{install_requires} 53 54[options.package_data] 55{package_name} = 56 py.typed 57 ''' 58 59 60def _create_fake_python_package( 61 location: Path, 62 package_name: str, 63 files: list[str], 64 install_requires: str = '', 65) -> None: 66 for file in files: 67 destination = location / file 68 destination.parent.mkdir(parents=True, exist_ok=True) 69 text = f'"""{package_name}"""' 70 if str(destination).endswith('setup.cfg'): 71 text = _setup_cfg(package_name, install_requires) 72 elif str(destination).endswith('pyproject.toml'): 73 # Make sure pyproject.toml file has valid syntax. 74 text = PYPROJECT_FILE 75 elif str(destination).endswith('__init__.py'): 76 text = DEFAULT_INIT_PY 77 destination.write_text(text) 78 79 80class TestCreatePythonTree(unittest.TestCase): 81 """Integration tests for create_python_tree.""" 82 83 maxDiff = None 84 85 def setUp(self): 86 # Save the starting working directory for returning to later. 87 self.start_dir = Path.cwd() 88 # Create a temp out directory 89 self.temp_dir = tempfile.TemporaryDirectory() 90 91 def tearDown(self): 92 # cd to the starting dir before cleaning up the temp out directory 93 os.chdir(self.start_dir) 94 # Delete the TemporaryDirectory 95 self.temp_dir.cleanup() 96 97 def _check_result_paths_equal(self, install_dir, expected_results) -> None: 98 # Normalize path strings to posix before comparing. 99 expected_paths = set(Path(p).as_posix() for p in expected_results) 100 actual_paths = set( 101 p.relative_to(install_dir).as_posix() 102 for p in install_dir.glob('**/*') 103 if p.is_file() 104 ) 105 self.assertEqual(expected_paths, actual_paths) 106 107 def test_update_config_with_packages(self) -> None: 108 """Test merging package setup.cfg files.""" 109 temp_root = Path(self.temp_dir.name) 110 common_config = temp_root / 'common_setup.cfg' 111 common_config.write_text( 112 ''' 113[metadata] 114name = megapackage 115version = 0.0.1 116author = Pigweed Authors 117author_email = pigweed-developers@googlegroups.com 118description = Pigweed swiss-army knife 119 120[options] 121zip_safe = False 122 123[options.package_data] 124megapackage = 125 py.typed 126''' 127 ) 128 config = load_common_config( 129 common_config=common_config, append_git_sha=False, append_date=False 130 ) 131 config_metadata = dict(config['metadata'].items()) 132 self.assertIn('name', config_metadata) 133 134 pkg1_root = temp_root / 'pkg1' 135 pkg2_root = temp_root / 'pkg2' 136 _create_fake_python_package( 137 pkg1_root, 138 'mars', 139 [ 140 'planets/BUILD.mars_rocket', 141 'planets/mars/__init__.py', 142 'planets/mars/__main__.py', 143 'planets/mars/moons/__init__.py', 144 'planets/mars/moons/deimos.py', 145 'planets/mars/moons/phobos.py', 146 'planets/hohmann_transfer_test.py', 147 'planets/pyproject.toml', 148 'planets/setup.cfg', 149 ], 150 install_requires=''' 151install_requires = 152 coloredlogs 153 coverage 154 cryptography 155 graphlib-backport;python_version<'3.9' 156 httpwatcher 157''', 158 ) 159 160 os.chdir(pkg1_root) 161 pkg1 = PythonPackage.from_dict( 162 **{ 163 'generate_setup': { 164 'metadata': {'name': 'mars', 'version': '0.0.1'}, 165 }, 166 'inputs': [], 167 'setup_sources': [ 168 'planets/pyproject.toml', 169 'planets/setup.cfg', 170 ], 171 'sources': [ 172 'planets/mars/__init__.py', 173 'planets/mars/__main__.py', 174 'planets/mars/moons/__init__.py', 175 'planets/mars/moons/deimos.py', 176 'planets/mars/moons/phobos.py', 177 ], 178 'tests': [ 179 'planets/hohmann_transfer_test.py', 180 ], 181 } 182 ) 183 184 _create_fake_python_package( 185 pkg2_root, 186 'saturn', 187 [ 188 'planets/BUILD.saturn_rocket', 189 'planets/hohmann_transfer_test.py', 190 'planets/pyproject.toml', 191 'planets/saturn/__init__.py', 192 'planets/saturn/__main__.py', 193 'planets/saturn/misson.py', 194 'planets/saturn/moons/__init__.py', 195 'planets/saturn/moons/enceladus.py', 196 'planets/saturn/moons/iapetus.py', 197 'planets/saturn/moons/rhea.py', 198 'planets/saturn/moons/titan.py', 199 'planets/setup.cfg', 200 'planets/setup.py', 201 ], 202 install_requires=''' 203install_requires = 204 graphlib-backport;python_version<'3.9' 205 httpwatcher 206''', 207 ) 208 os.chdir(pkg2_root) 209 pkg2 = PythonPackage.from_dict( 210 **{ 211 'inputs': [], 212 'setup_sources': [ 213 'planets/pyproject.toml', 214 'planets/setup.cfg', 215 'planets/setup.py', 216 ], 217 'sources': [ 218 'planets/saturn/__init__.py', 219 'planets/saturn/__main__.py', 220 'planets/saturn/misson.py', 221 'planets/saturn/moons/__init__.py', 222 'planets/saturn/moons/enceladus.py', 223 'planets/saturn/moons/iapetus.py', 224 'planets/saturn/moons/rhea.py', 225 'planets/saturn/moons/titan.py', 226 ], 227 'tests': [ 228 'planets/hohmann_transfer_test.py', 229 ], 230 } 231 ) 232 233 update_config_with_packages(config=config, python_packages=[pkg1, pkg2]) 234 235 setup_cfg_text = io.StringIO() 236 config.write(setup_cfg_text) 237 expected_cfg = ''' 238[metadata] 239name = megapackage 240version = 0.0.1 241author = Pigweed Authors 242author_email = pigweed-developers@googlegroups.com 243description = Pigweed swiss-army knife 244 245[options] 246zip_safe = False 247packages = find: 248install_requires = 249 coloredlogs 250 coverage 251 cryptography 252 graphlib-backport;python_version<'3.9' 253 httpwatcher 254 255[options.package_data] 256megapackage = 257 py.typed 258mars = 259 py.typed 260saturn = 261 py.typed 262 263[options.entry_points] 264''' 265 result_cfg_lines = [ 266 line.rstrip().replace('\t', ' ') 267 for line in setup_cfg_text.getvalue().splitlines() 268 if line 269 ] 270 expected_cfg_lines = [ 271 line.rstrip() for line in expected_cfg.splitlines() if line 272 ] 273 self.assertEqual(expected_cfg_lines, result_cfg_lines) 274 275 @parameterized.expand( 276 [ 277 ( 278 # Test name 279 'working case', 280 # Package name 281 'mars', 282 # File list 283 [ 284 'planets/BUILD.mars_rocket', 285 'planets/mars/__init__.py', 286 'planets/mars/__main__.py', 287 'planets/mars/moons/__init__.py', 288 'planets/mars/moons/deimos.py', 289 'planets/mars/moons/phobos.py', 290 'planets/hohmann_transfer_test.py', 291 'planets/pyproject.toml', 292 'planets/setup.cfg', 293 ], 294 # Extra_files 295 [], 296 # Package definition 297 { 298 'generate_setup': { 299 'metadata': {'name': 'mars', 'version': '0.0.1'}, 300 }, 301 'inputs': [], 302 'setup_sources': [ 303 'planets/pyproject.toml', 304 'planets/setup.cfg', 305 ], 306 'sources': [ 307 'planets/mars/__init__.py', 308 'planets/mars/__main__.py', 309 'planets/mars/moons/__init__.py', 310 'planets/mars/moons/deimos.py', 311 'planets/mars/moons/phobos.py', 312 ], 313 'tests': [ 314 'planets/hohmann_transfer_test.py', 315 ], 316 }, 317 # Output file list 318 [ 319 'mars/__init__.py', 320 'mars/__main__.py', 321 'mars/moons/__init__.py', 322 'mars/moons/deimos.py', 323 'mars/moons/phobos.py', 324 'mars/tests/hohmann_transfer_test.py', 325 ], 326 ), 327 ( 328 # Test name 329 'with extra files', 330 # Package name 331 'saturn', 332 # File list 333 [ 334 'planets/BUILD.saturn_rocket', 335 'planets/hohmann_transfer_test.py', 336 'planets/pyproject.toml', 337 'planets/saturn/__init__.py', 338 'planets/saturn/__main__.py', 339 'planets/saturn/misson.py', 340 'planets/saturn/moons/__init__.py', 341 'planets/saturn/moons/enceladus.py', 342 'planets/saturn/moons/iapetus.py', 343 'planets/saturn/moons/rhea.py', 344 'planets/saturn/moons/titan.py', 345 'planets/setup.cfg', 346 'planets/setup.py', 347 ], 348 # Extra files 349 [ 350 'planets/BUILD.saturn_rocket > out/saturn/BUILD.rocket', 351 ], 352 # Package definition 353 { 354 'inputs': [], 355 'setup_sources': [ 356 'planets/pyproject.toml', 357 'planets/setup.cfg', 358 'planets/setup.py', 359 ], 360 'sources': [ 361 'planets/saturn/__init__.py', 362 'planets/saturn/__main__.py', 363 'planets/saturn/misson.py', 364 'planets/saturn/moons/__init__.py', 365 'planets/saturn/moons/enceladus.py', 366 'planets/saturn/moons/iapetus.py', 367 'planets/saturn/moons/rhea.py', 368 'planets/saturn/moons/titan.py', 369 ], 370 'tests': [ 371 'planets/hohmann_transfer_test.py', 372 ], 373 }, 374 # Output file list 375 [ 376 'saturn/BUILD.rocket', 377 'saturn/__init__.py', 378 'saturn/__main__.py', 379 'saturn/misson.py', 380 'saturn/moons/__init__.py', 381 'saturn/moons/enceladus.py', 382 'saturn/moons/iapetus.py', 383 'saturn/moons/rhea.py', 384 'saturn/moons/titan.py', 385 'saturn/tests/hohmann_transfer_test.py', 386 ], 387 ), 388 ] 389 ) 390 def test_build_python_tree( 391 self, 392 _test_name, 393 package_name, 394 file_list, 395 extra_files, 396 package_definition, 397 expected_file_list, 398 ) -> None: 399 """Check results of build_python_tree and copy_extra_files.""" 400 temp_root = Path(self.temp_dir.name) 401 _create_fake_python_package(temp_root, package_name, file_list) 402 403 os.chdir(temp_root) 404 install_dir = temp_root / 'out' 405 406 package = PythonPackage.from_dict(**package_definition) 407 build_python_tree( 408 python_packages=[package], 409 tree_destination_dir=install_dir, 410 include_tests=True, 411 ) 412 copy_extra_files(extra_files) 413 414 # Check expected files are in place. 415 self._check_result_paths_equal(install_dir, expected_file_list) 416 417 def test_build_python_tree_file_overwriting(self) -> None: 418 """Check file overwrite logic.""" 419 temp_root = Path(self.temp_dir.name) 420 421 pkg1: dict[str, list] = { 422 'inputs': [], 423 'setup_sources': [ 424 'pkg1/pyproject.toml', 425 'pkg1/setup.cfg', 426 'pkg1/setup.py', 427 ], 428 'sources': [ 429 'pkg1/planets/__init__.py', 430 'pkg1/planets/saturn/__init__.py', 431 'pkg1/planets/saturn/__main__.py', 432 'pkg1/planets/saturn/misson.py', 433 'pkg1/planets/saturn/moons/__init__.py', 434 ], 435 'tests': [], 436 } 437 438 pkg2: dict[str, list] = { 439 'inputs': [], 440 'setup_sources': [ 441 'pkg2/pyproject.toml', 442 'pkg2/setup.cfg', 443 'pkg2/setup.py', 444 ], 445 'sources': [ 446 'pkg2/planets/__init__.py', 447 'pkg2/planets/saturn/moons/__init__.py', 448 'pkg2/planets/saturn/moons/enceladus.py', 449 'pkg2/planets/saturn/moons/iapetus.py', 450 'pkg2/planets/saturn/moons/rhea.py', 451 'pkg2/planets/saturn/moons/titan.py', 452 ], 453 'tests': [], 454 } 455 _create_fake_python_package( 456 temp_root, 457 'saturn', 458 pkg1['sources'] + pkg1['setup_sources'], 459 ) 460 _create_fake_python_package( 461 temp_root, 462 'saturn_moons', 463 pkg2['sources'] + pkg2['setup_sources'], 464 ) 465 466 os.chdir(temp_root) 467 package1 = PythonPackage.from_dict(**pkg1) 468 package2 = PythonPackage.from_dict(**pkg2) 469 470 expected_init_py = '''"""All of Saturn's moons."""\n''' 471 Path('pkg1/planets/saturn/moons/__init__.py').write_text( 472 expected_init_py 473 ) 474 Path('pkg2/planets/saturn/moons/__init__.py').write_text( 475 DEFAULT_INIT_PY 476 ) 477 478 install_dir = temp_root / 'out' 479 480 build_python_tree( 481 python_packages=[package1, package2], 482 tree_destination_dir=install_dir, 483 include_tests=True, 484 ) 485 486 # Check the first moon __init__.py is not overwritten by the second 487 # autogenerated version. 488 self.assertEqual( 489 Path('out/planets/saturn/moons/__init__.py').read_text(), 490 expected_init_py, 491 ) 492 493 @parameterized.expand( 494 [ 495 ( 496 # Test name 497 'everything in correct locations', 498 # Package name 499 'planets', 500 # File list 501 [ 502 'BUILD.mars_rocket', 503 ], 504 # Extra_files 505 [ 506 'BUILD.mars_rocket > out/mars/BUILD.rocket', 507 ], 508 # Output file list 509 [ 510 'mars/BUILD.rocket', 511 ], 512 # Should raise exception 513 None, 514 ), 515 ( 516 # Test name 517 'missing source files', 518 # Package name 519 'planets', 520 # File list 521 [ 522 'BUILD.mars_rocket', 523 ], 524 # Extra_files 525 [ 526 'BUILD.venus_rocket > out/venus/BUILD.rocket', 527 ], 528 # Output file list 529 [], 530 # Should raise exception 531 FileNotFoundError, 532 ), 533 ( 534 # Test name 535 'existing destination files', 536 # Package name 537 'planets', 538 # File list 539 [ 540 'BUILD.jupiter_rocket', 541 'out/jupiter/BUILD.rocket', 542 ], 543 # Extra_files 544 [ 545 'BUILD.jupiter_rocket > out/jupiter/BUILD.rocket', 546 ], 547 # Output file list 548 [], 549 # Should raise exception 550 FileExistsError, 551 ), 552 ] 553 ) 554 def test_copy_extra_files( 555 self, 556 _test_name, 557 package_name, 558 file_list, 559 extra_files, 560 expected_file_list, 561 should_raise_exception, 562 ) -> None: 563 """Check results of build_python_tree and copy_extra_files.""" 564 temp_root = Path(self.temp_dir.name) 565 _create_fake_python_package(temp_root, package_name, file_list) 566 567 os.chdir(temp_root) 568 install_dir = temp_root / 'out' 569 570 # If exceptions should be raised 571 if should_raise_exception: 572 with self.assertRaises(should_raise_exception): 573 copy_extra_files(extra_files) 574 return 575 576 # Do the copy 577 copy_extra_files(extra_files) 578 # Check expected files are in place. 579 self._check_result_paths_equal(install_dir, expected_file_list) 580 581 def test_importing_package_data(self) -> None: 582 self.assertIn( 583 'EMPTY.CSV', 584 importlib.resources.read_text(test_dist1_data, 'empty.csv'), 585 ) 586 self.assertIn( 587 'EMPTY.CSV', 588 importlib.resources.read_text( 589 'test_dist1_data.subdir', 'empty.csv' 590 ), 591 ) 592 593 594if __name__ == '__main__': 595 unittest.main() 596