• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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