xref: /aosp_15_r20/external/pigweed/pw_build/py/create_python_tree_test.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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