xref: /aosp_15_r20/external/pigweed/pw_build/py/python_runner_test.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1#!/usr/bin/env python3
2# Copyright 2020 The Pigweed Authors
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may not
5# use this file except in compliance with the License. You may obtain a copy of
6# the License at
7#
8#     https://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations under
14# the License.
15"""Tests for the Python runner."""
16
17import os
18from pathlib import Path
19import platform
20import tempfile
21import unittest
22
23from pw_build.gn_resolver import ExpressionError, GnPaths, Label, TargetInfo
24from pw_build.gn_resolver import expand_expressions
25
26ROOT = Path(r'C:\gn_root' if platform.system() == 'Windows' else '/gn_root')
27
28TEST_PATHS = GnPaths(
29    ROOT,
30    ROOT / 'out',
31    ROOT / 'some' / 'cwd',
32    '//toolchains/cool:ToolChain',
33)
34
35
36class LabelTest(unittest.TestCase):
37    """Tests GN label parsing."""
38
39    def setUp(self):
40        self._paths_and_toolchain_name = [
41            (TEST_PATHS, 'ToolChain'),
42            (GnPaths(*TEST_PATHS[:3], ''), ''),
43        ]
44
45    def test_root(self):
46        for paths, toolchain in self._paths_and_toolchain_name:
47            label = Label(paths, '//')
48            self.assertEqual(label.name, '')
49            self.assertEqual(label.dir, ROOT)
50            self.assertEqual(
51                label.out_dir, ROOT.joinpath('out', toolchain, 'obj')
52            )
53            self.assertEqual(
54                label.gen_dir, ROOT.joinpath('out', toolchain, 'gen')
55            )
56
57    def test_absolute(self):
58        for paths, toolchain in self._paths_and_toolchain_name:
59            label = Label(paths, '//foo/bar:baz')
60            self.assertEqual(label.name, 'baz')
61            self.assertEqual(label.dir, ROOT.joinpath('foo/bar'))
62            self.assertEqual(
63                label.out_dir, ROOT.joinpath('out', toolchain, 'obj/foo/bar')
64            )
65            self.assertEqual(
66                label.gen_dir, ROOT.joinpath('out', toolchain, 'gen/foo/bar')
67            )
68
69    def test_absolute_implicit_target(self):
70        for paths, toolchain in self._paths_and_toolchain_name:
71            label = Label(paths, '//foo/bar')
72            self.assertEqual(label.name, 'bar')
73            self.assertEqual(label.dir, ROOT.joinpath('foo/bar'))
74            self.assertEqual(
75                label.out_dir, ROOT.joinpath('out', toolchain, 'obj/foo/bar')
76            )
77            self.assertEqual(
78                label.gen_dir, ROOT.joinpath('out', toolchain, 'gen/foo/bar')
79            )
80
81    def test_relative(self):
82        for paths, toolchain in self._paths_and_toolchain_name:
83            label = Label(paths, ':tgt')
84            self.assertEqual(label.name, 'tgt')
85            self.assertEqual(label.dir, ROOT.joinpath('some/cwd'))
86            self.assertEqual(
87                label.out_dir, ROOT.joinpath('out', toolchain, 'obj/some/cwd')
88            )
89            self.assertEqual(
90                label.gen_dir, ROOT.joinpath('out', toolchain, 'gen/some/cwd')
91            )
92
93    def test_relative_subdir(self):
94        for paths, toolchain in self._paths_and_toolchain_name:
95            label = Label(paths, 'tgt')
96            self.assertEqual(label.name, 'tgt')
97            self.assertEqual(label.dir, ROOT.joinpath('some/cwd/tgt'))
98            self.assertEqual(
99                label.out_dir,
100                ROOT.joinpath('out', toolchain, 'obj/some/cwd/tgt'),
101            )
102            self.assertEqual(
103                label.gen_dir,
104                ROOT.joinpath('out', toolchain, 'gen/some/cwd/tgt'),
105            )
106
107    def test_relative_parent_dir(self):
108        for paths, toolchain in self._paths_and_toolchain_name:
109            label = Label(paths, '..:tgt')
110            self.assertEqual(label.name, 'tgt')
111            self.assertEqual(label.dir, ROOT.joinpath('some'))
112            self.assertEqual(
113                label.out_dir, ROOT.joinpath('out', toolchain, 'obj/some')
114            )
115            self.assertEqual(
116                label.gen_dir, ROOT.joinpath('out', toolchain, 'gen/some')
117            )
118
119
120class ResolvePathTest(unittest.TestCase):
121    """Tests GN path resolution."""
122
123    def test_resolve_absolute(self):
124        self.assertEqual(TEST_PATHS.resolve('//'), TEST_PATHS.root)
125        self.assertEqual(
126            TEST_PATHS.resolve('//foo/bar'), TEST_PATHS.root / 'foo' / 'bar'
127        )
128        self.assertEqual(
129            TEST_PATHS.resolve('//foo/../baz'), TEST_PATHS.root / 'baz'
130        )
131
132    def test_resolve_relative(self):
133        self.assertEqual(TEST_PATHS.resolve(''), TEST_PATHS.cwd)
134        self.assertEqual(TEST_PATHS.resolve('foo'), TEST_PATHS.cwd / 'foo')
135        self.assertEqual(TEST_PATHS.resolve('..'), TEST_PATHS.root / 'some')
136
137
138NINJA_EXECUTABLE = '''\
139defines =
140framework_dirs =
141include_dirs = -I../fake_module/public
142cflags = -g3 -Og -fdiagnostics-color -g -fno-common -Wall -Wextra -Werror
143cflags_c =
144cflags_cc = -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register
145target_output_name = this_is_a_test
146
147build fake_toolchain/obj/fake_module/fake_test.fake_test.cc.o: fake_toolchain_cxx ../fake_module/fake_test.cc
148  source_file_dir = ../fake_module
149  source_file_name = fake_test.cc
150
151build fake_toolchain/obj/fake_module/fake_test.fake_test_c.c.o: fake_toolchain_cc ../fake_module/fake_test_c.c
152
153build fake_toolchain/obj/fake_module/test/fake_test.elf fake_toolchain/obj/fake_module/test/fake_test.map: fake_toolchain_link fake_toolchain/obj/fake_module/fake_test.fake_test.cc.o fake_toolchain/obj/fake_module/fake_test.fake_test_c.c.o
154  ldflags = -Og -fdiagnostics-color
155  libs =
156  frameworks =
157  output_extension =
158  output_dir = host_clang_debug/obj/fake_module/test
159'''
160
161_SOURCE_SET_TEMPLATE = '''\
162defines =
163framework_dirs =
164include_dirs = -I../fake_module/public
165cflags = -g3 -Og -fdiagnostics-color -g -fno-common -Wall -Wextra -Werror
166cflags_c =
167cflags_cc = -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register
168target_output_name = this_is_a_test
169
170build fake_toolchain/obj/fake_module/fake_source_set.file_a.cc.o: fake_toolchain_cxx ../fake_module/file_a.cc
171  source_file_dir = ../fake_module
172  source_file_name = file_a.cc
173
174build fake_toolchain/obj/fake_module/fake_source_set.file_b.c.o: fake_toolchain_cc ../fake_module/file_b.c
175
176build {path} fake_toolchain/obj/fake_module/fake_source_set.file_a.cc.o fake_toolchain/obj/fake_module/fake_source_set.file_b.c.o
177  ldflags = -Og -fdiagnostics-color -Wno-error=deprecated
178  libs =
179  frameworks =
180  output_extension =
181  output_dir = host_clang_debug/obj/fake_module
182'''
183
184# GN originally used empty .stamp files to mark the completion of a group of
185# dependencies. GN switched to using 'phony' Ninja targets instead, which don't
186# require creating a new file.
187_PHONY_BUILD_PATH = 'fake_toolchain/phony/fake_module/fake_source_set: phony'
188_STAMP_BUILD_PATH = 'fake_toolchain/obj/fake_module/fake_source_set.stamp:'
189
190NINJA_SOURCE_SET = _SOURCE_SET_TEMPLATE.format(path=_PHONY_BUILD_PATH)
191NINJA_SOURCE_SET_STAMP = _SOURCE_SET_TEMPLATE.format(path=_STAMP_BUILD_PATH)
192
193
194def _create_ninja_files(source_set: str) -> tuple:
195    tempdir = tempfile.TemporaryDirectory(prefix='pw_build_test_')
196
197    module = Path(tempdir.name, 'out', 'fake_toolchain', 'obj', 'fake_module')
198    os.makedirs(module)
199    module.joinpath('fake_test.ninja').write_text(NINJA_EXECUTABLE)
200    module.joinpath('fake_source_set.ninja').write_text(source_set)
201    module.joinpath('fake_no_objects.ninja').write_text('\n')
202
203    outdir = Path(tempdir.name, 'out', 'fake_toolchain', 'obj', 'fake_module')
204
205    paths = GnPaths(
206        root=Path(tempdir.name),
207        build=Path(tempdir.name, 'out'),
208        cwd=Path(tempdir.name, 'some', 'module'),
209        toolchain='//tools:fake_toolchain',
210    )
211
212    return tempdir, outdir, paths
213
214
215class TargetTest(unittest.TestCase):
216    """Tests querying GN target information."""
217
218    def setUp(self):
219        self._tempdir, self._outdir, self._paths = _create_ninja_files(
220            NINJA_SOURCE_SET
221        )
222
223        self._rel_outdir = self._outdir.relative_to(self._paths.build)
224
225    def tearDown(self):
226        self._tempdir.cleanup()
227
228    def test_source_set_artifact(self):
229        target = TargetInfo(self._paths, '//fake_module:fake_source_set')
230        self.assertTrue(target.generated)
231        self.assertIsNone(target.artifact)
232
233    def test_source_set_object_files(self):
234        target = TargetInfo(self._paths, '//fake_module:fake_source_set')
235        self.assertTrue(target.generated)
236        self.assertEqual(
237            set(target.object_files),
238            {
239                self._rel_outdir / 'fake_source_set.file_a.cc.o',
240                self._rel_outdir / 'fake_source_set.file_b.c.o',
241            },
242        )
243
244    def test_executable_object_files(self):
245        target = TargetInfo(self._paths, '//fake_module:fake_test')
246        self.assertEqual(
247            set(target.object_files),
248            {
249                self._rel_outdir / 'fake_test.fake_test.cc.o',
250                self._rel_outdir / 'fake_test.fake_test_c.c.o',
251            },
252        )
253
254    def test_executable_artifact(self):
255        target = TargetInfo(self._paths, '//fake_module:fake_test')
256        self.assertEqual(
257            target.artifact, self._rel_outdir / 'test' / 'fake_test.elf'
258        )
259
260    def test_non_existent_target(self):
261        target = TargetInfo(
262            self._paths, '//fake_module:definitely_not_a_real_target'
263        )
264        self.assertFalse(target.generated)
265        self.assertIsNone(target.artifact)
266
267    def test_non_existent_toolchain(self):
268        target = TargetInfo(
269            self._paths, '//fake_module:fake_source_set(//not_a:toolchain)'
270        )
271        self.assertFalse(target.generated)
272        self.assertIsNone(target.artifact)
273
274
275class StampTargetTest(TargetTest):
276    """Test with old-style .stamp files instead of phony Ninja targets."""
277
278    def setUp(self):
279        self._tempdir, self._outdir, self._paths = _create_ninja_files(
280            NINJA_SOURCE_SET_STAMP
281        )
282
283        self._rel_outdir = self._outdir.relative_to(self._paths.build)
284
285
286class ExpandExpressionsTest(unittest.TestCase):
287    """Tests expansion of expressions like <TARGET_FILE(//foo)>."""
288
289    def setUp(self):
290        self._tempdir, self._outdir, self._paths = _create_ninja_files(
291            NINJA_SOURCE_SET
292        )
293
294    def tearDown(self):
295        self._tempdir.cleanup()
296
297    def _path(self, *segments: str, create: bool = False) -> str:
298        path = Path(self._outdir, *segments)
299        if create:
300            os.makedirs(path.parent)
301            path.touch()
302        else:
303            assert not path.exists()
304        return str(path.relative_to(self._paths.build))
305
306    def test_empty(self):
307        self.assertEqual(list(expand_expressions(self._paths, '')), [''])
308
309    def test_no_expressions(self):
310        self.assertEqual(
311            list(expand_expressions(self._paths, 'foobar')), ['foobar']
312        )
313        self.assertEqual(
314            list(expand_expressions(self._paths, '<NOT_AN_EXPRESSION()>')),
315            ['<NOT_AN_EXPRESSION()>'],
316        )
317
318    def test_incomplete_expression(self):
319        for incomplete_expression in [
320            '<TARGET_FILE(',
321            '<TARGET_FILE(//foo)',
322            '<TARGET_FILE(//foo>',
323            '<TARGET_FILE(//foo) >',
324            '--arg=<TARGET_FILE_IF_EXISTS(//foo) Hello>',
325        ]:
326            with self.assertRaises(ExpressionError):
327                expand_expressions(self._paths, incomplete_expression)
328
329    def test_target_file(self):
330        path = self._path('test', 'fake_test.elf')
331
332        for expr, expected in [
333            ('<TARGET_FILE(//fake_module:fake_test)>', path),
334            ('--arg=<TARGET_FILE(//fake_module:fake_test)>', f'--arg={path}'),
335            (
336                '--argument=<TARGET_FILE(//fake_module:fake_test)>;'
337                '<TARGET_FILE(//fake_module:fake_test)>',
338                f'--argument={path};{path}',
339            ),
340        ]:
341            self.assertEqual(
342                list(expand_expressions(self._paths, expr)), [expected]
343            )
344
345    def test_target_objects_no_target_file(self):
346        with self.assertRaisesRegex(ExpressionError, 'no output file'):
347            expand_expressions(
348                self._paths, '<TARGET_FILE(//fake_module:fake_source_set)>'
349            )
350
351    def test_target_file_non_existent_target(self):
352        with self.assertRaisesRegex(ExpressionError, 'generated'):
353            expand_expressions(self._paths, '<TARGET_FILE(//not_real:abc123)>')
354
355    def test_target_file_if_exists(self):
356        path = self._path('test', 'fake_test.elf', create=True)
357
358        for expr, expected in [
359            ('<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', path),
360            (
361                '--arg=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
362                f'--arg={path}',
363            ),
364            (
365                '--argument=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>;'
366                '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
367                f'--argument={path};{path}',
368            ),
369        ]:
370            self.assertEqual(
371                list(expand_expressions(self._paths, expr)), [expected]
372            )
373
374    def test_target_file_if_exists_arg_omitted(self):
375        for expr in [
376            '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
377            '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test(fake)>',
378            '<TARGET_FILE_IF_EXISTS(//not_a_module:nothing)>',
379            '--arg=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
380            '--argument=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>;'
381            '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
382        ]:
383            self.assertEqual(list(expand_expressions(self._paths, expr)), [])
384
385    def test_target_file_if_exists_error_if_never_has_artifact(self):
386        for expr in [
387            '<TARGET_FILE_IF_EXISTS(//fake_module:fake_source_set)>'
388            'bar=<TARGET_FILE_IF_EXISTS(//fake_module:fake_source_set)>'
389            '<TARGET_FILE_IF_EXISTS(//fake_module:fake_no_objects)>',
390            '--foo=<TARGET_FILE_IF_EXISTS(//fake_module:fake_no_objects)>',
391        ]:
392            with self.assertRaises(ExpressionError):
393                expand_expressions(self._paths, expr)
394
395    def test_target_objects(self):
396        self.assertEqual(
397            set(
398                expand_expressions(
399                    self._paths,
400                    '<TARGET_OBJECTS(//fake_module:fake_source_set)>',
401                )
402            ),
403            {
404                self._path('fake_source_set.file_a.cc.o'),
405                self._path('fake_source_set.file_b.c.o'),
406            },
407        )
408        self.assertEqual(
409            set(
410                expand_expressions(
411                    self._paths, '<TARGET_OBJECTS(//fake_module:fake_test)>'
412                )
413            ),
414            {
415                self._path('fake_test.fake_test.cc.o'),
416                self._path('fake_test.fake_test_c.c.o'),
417            },
418        )
419
420    def test_target_objects_no_objects(self):
421        self.assertEqual(
422            list(
423                expand_expressions(
424                    self._paths,
425                    '<TARGET_OBJECTS(//fake_module:fake_no_objects)>',
426                )
427            ),
428            [],
429        )
430
431    def test_target_objects_other_content_in_arg(self):
432        for arg in [
433            '--foo=<TARGET_OBJECTS(//fake_module:fake_no_objects)>',
434            '<TARGET_OBJECTS(//fake_module:fake_no_objects)>bar',
435            '--foo<TARGET_OBJECTS(//fake_module:fake_no_objects)>bar',
436            '<TARGET_OBJECTS(//fake_module:fake_no_objects)>'
437            '<TARGET_OBJECTS(//fake_module:fake_no_objects)>',
438            '<TARGET_OBJECTS(//fake_module:fake_source_set)>'
439            '<TARGET_OBJECTS(//fake_module:fake_source_set)>',
440        ]:
441            with self.assertRaises(ExpressionError):
442                expand_expressions(self._paths, arg)
443
444    def test_target_objects_non_existent_target(self):
445        with self.assertRaisesRegex(ExpressionError, 'generated'):
446            expand_expressions(self._paths, '<TARGET_OBJECTS(//not_real)>')
447
448
449class StampExpandExpressionsTest(TargetTest):
450    """Test with old-style .stamp files instead of phony Ninja targets."""
451
452    def setUp(self):
453        self._tempdir, self._outdir, self._paths = _create_ninja_files(
454            NINJA_SOURCE_SET_STAMP
455        )
456
457        self._rel_outdir = self._outdir.relative_to(self._paths.build)
458
459
460if __name__ == '__main__':
461    unittest.main()
462