xref: /aosp_15_r20/external/pigweed/pw_build/py/bazel_to_gn_test.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2024 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 the pw_build.bazel_to_gn module."""
15
16import json
17import unittest
18
19from io import StringIO
20from pathlib import PurePath
21
22from unittest import mock
23
24from pw_build.bazel_to_gn import BazelToGnConverter
25
26# Test fixtures.
27
28PW_ROOT = '/path/to/pigweed'
29FOO_SOURCE_DIR = '/path/to/foo'
30BAR_SOURCE_DIR = '../relative/path/to/bar'
31BAZ_SOURCE_DIR = '/path/to/baz'
32
33# Simulated out/args.gn file contents.
34ARGS_GN = f'''dir_pw_third_party_foo = "{FOO_SOURCE_DIR}"
35pw_log_BACKEND = "$dir_pw_log_basic"
36pw_unit_test_MAIN = "$dir_pw_unit_test:logging_main"
37dir_pw_third_party_bar = "{BAR_SOURCE_DIR}"
38pw_unit_test_GOOGLETEST_BACKEND == "$dir_pw_third_party/googletest"
39pw_unit_test_MAIN == "$dir_pw_third_party/googletest:gmock_main
40dir_pw_third_party_baz = "{BAZ_SOURCE_DIR}"'''
41
42# Simulated Bazel repo names.
43FOO_REPO = 'dev_pigweed_foo'
44BAR_REPO = 'dev_pigweed_bar'
45BAZ_REPO = 'dev_pigweed_baz'
46
47# Simulated //third_party.../bazel_to_gn.json file contents.
48FOO_B2G_JSON = f'''{{
49  "repo": "{FOO_REPO}",
50  "targets": [ "//package:target" ]
51}}'''
52BAR_B2G_JSON = f'''{{
53  "repo": "{BAR_REPO}",
54  "options": {{
55    "//package:my_flag": true
56  }},
57  "targets": [ "//bar/pkg:bar_target1" ]
58}}'''
59BAZ_B2G_JSON = f'''{{
60  "repo": "{BAZ_REPO}",
61  "generate": false
62}}'''
63
64# Simulated 'bazel cquery ...' results.
65FOO_RULE_JSON = f'''{{
66  "results": [
67    {{
68      "target": {{
69        "rule": {{
70          "name": "//package:target",
71          "ruleClass": "cc_library",
72          "attribute": [
73            {{
74              "explicitlySpecified": true,
75              "name": "hdrs",
76              "type": "label_list",
77              "stringListValue": [ "//include:foo.h" ]
78            }},
79            {{
80              "explicitlySpecified": true,
81              "name": "srcs",
82              "type": "label_list",
83              "stringListValue": [ "//src:foo.cc" ]
84            }},
85            {{
86              "explicitlySpecified": true,
87              "name": "additional_linker_inputs",
88              "type": "label_list",
89              "stringListValue": [ "//data:input" ]
90            }},
91            {{
92              "explicitlySpecified": true,
93              "name": "includes",
94              "type": "string_list",
95              "stringListValue": [ "include" ]
96            }},
97            {{
98              "explicitlySpecified": true,
99              "name": "copts",
100              "type": "string_list",
101              "stringListValue": [ "-cflag" ]
102            }},
103            {{
104              "explicitlySpecified": true,
105              "name": "linkopts",
106              "type": "string_list",
107              "stringListValue": [ "-ldflag" ]
108            }},
109            {{
110              "explicitlySpecified": true,
111              "name": "defines",
112              "type": "string_list",
113              "stringListValue": [ "DEFINE" ]
114            }},
115            {{
116              "explicitlySpecified": true,
117              "name": "local_defines",
118              "type": "string_list",
119              "stringListValue": [ "LOCAL_DEFINE" ]
120            }},
121            {{
122              "explicitlySpecified": true,
123              "name": "deps",
124              "type": "label_list",
125              "stringListValue": [
126                "@{BAR_REPO}//bar/pkg:bar_target1",
127                "@{BAR_REPO}//bar/pkg:bar_target2"
128              ]
129            }},
130            {{
131              "explicitlySpecified": true,
132              "name": "implementation_deps",
133              "type": "label_list",
134              "stringListValue": [ "@{BAZ_REPO}//baz/pkg:baz_target" ]
135            }}
136          ]
137        }}
138      }}
139    }}
140  ]
141}}
142'''
143BAR_RULE_JSON = '''
144{
145  "results": [
146    {
147      "target": {
148        "rule": {
149          "name": "//bar/pkg:bar_target1",
150          "ruleClass": "cc_library",
151          "attribute": [
152            {
153              "explicitlySpecified": true,
154              "name": "defines",
155              "type": "string_list",
156              "stringListValue": [ "FILTERED", "KEPT" ]
157            }
158          ]
159        }
160      }
161    }
162  ]
163}
164'''
165
166# Simulated Bazel WORKSPACE file for Pigweed.
167# Keep this in sync with PW_EXTERNAL_DEPS below.
168PW_WORKSPACE = f'''
169http_archive(
170    name = "{FOO_REPO}",
171    strip_prefix = "foo-feedface",
172    url = "http://localhost:9000/feedface.tgz",
173)
174
175http_archive(
176    name = "{BAR_REPO}",
177    strip_prefix = "bar-v1.0",
178    urls = ["http://localhost:9000/bar/v1.0.tgz"],
179)
180
181http_archive(
182    name = "{BAZ_REPO}",
183    strip_prefix = "baz-v1.5",
184    url = "http://localhost:9000/baz/v1.5.zip",
185)
186
187http_archive(
188    name = "other",
189    strip_prefix = "other-v2.0",
190    url = "http://localhost:9000/other/v2.0.zip",
191)
192
193another_rule(
194    # aribtrary contents
195)
196'''
197
198# Simulated 'bazel query //external:*' results for com_google_pigweed.
199# Keep this in sync with PW_WORKSPACE above.
200PW_EXTERNAL_DEPS = '\n'.join(
201    [
202        json.dumps(
203            {
204                'type': 'RULE',
205                'rule': {
206                    'name': f'//external:{FOO_REPO}',
207                    'ruleClass': 'http_archive',
208                    'attribute': [
209                        {
210                            'name': 'strip_prefix',
211                            'explicitlySpecified': True,
212                            "type": "string",
213                            'stringValue': 'foo-feedface',
214                        },
215                        {
216                            'name': 'url',
217                            'explicitlySpecified': True,
218                            "type": "string",
219                            'stringValue': 'http://localhost:9000/feedface.tgz',
220                        },
221                    ],
222                },
223            }
224        ),
225        json.dumps(
226            {
227                'type': 'RULE',
228                'rule': {
229                    'name': f'//external:{BAR_REPO}',
230                    'ruleClass': 'http_archive',
231                    'attribute': [
232                        {
233                            'name': 'strip_prefix',
234                            'explicitlySpecified': True,
235                            "type": "string",
236                            'stringValue': 'bar-v1.0',
237                        },
238                        {
239                            'name': 'urls',
240                            'explicitlySpecified': True,
241                            "type": "string_list",
242                            'stringListValue': [
243                                'http://localhost:9000/bar/v1.0.tgz'
244                            ],
245                        },
246                    ],
247                },
248            }
249        ),
250    ]
251)
252# Simulated 'bazel query //external:*' results for dev_pigweed_foo.
253FOO_EXTERNAL_DEPS = '\n'.join(
254    [
255        json.dumps(
256            {
257                'type': 'RULE',
258                'rule': {
259                    'name': f'//external:{BAR_REPO}',
260                    'ruleClass': 'http_archive',
261                    'attribute': [
262                        {
263                            'name': 'strip_prefix',
264                            'explicitlySpecified': True,
265                            "type": "string",
266                            'stringValue': 'bar-v2.0',
267                        },
268                        {
269                            'name': 'urls',
270                            'explicitlySpecified': True,
271                            "type": "string_list",
272                            'stringListValue': [
273                                'http://localhost:9000/bar/v2.0.tgz'
274                            ],
275                        },
276                    ],
277                },
278            }
279        ),
280        json.dumps(
281            {
282                'type': 'RULE',
283                'rule': {
284                    'name': f'//external:{BAZ_REPO}',
285                    'ruleClass': 'http_archive',
286                    'attribute': [
287                        {
288                            'name': 'url',
289                            'explicitlySpecified': True,
290                            "type": "string",
291                            'stringValue': 'http://localhost:9000/baz/v1.5.tgz',
292                        }
293                    ],
294                },
295            }
296        ),
297    ]
298)
299# Unit tests.
300
301
302class TestBazelToGnConverter(unittest.TestCase):
303    """Tests for bazel_to_gn.BazelToGnConverter."""
304
305    def test_parse_args_gn(self):
306        """Tests parsing args.gn."""
307        b2g = BazelToGnConverter(PW_ROOT)
308        b2g.parse_args_gn(StringIO(ARGS_GN))
309        self.assertEqual(b2g.get_source_dir('foo'), PurePath(FOO_SOURCE_DIR))
310        self.assertEqual(b2g.get_source_dir('bar'), PurePath(BAR_SOURCE_DIR))
311        self.assertEqual(b2g.get_source_dir('baz'), PurePath(BAZ_SOURCE_DIR))
312
313    @mock.patch('subprocess.run')
314    def test_load_workspace(self, _):
315        """Tests loading a workspace from a bazel_to_gn.json file."""
316        b2g = BazelToGnConverter(PW_ROOT)
317        b2g.parse_args_gn(StringIO(ARGS_GN))
318        b2g.load_workspace('foo', StringIO(FOO_B2G_JSON))
319        b2g.load_workspace('bar', StringIO(BAR_B2G_JSON))
320        b2g.load_workspace('baz', StringIO(BAZ_B2G_JSON))
321        self.assertEqual(b2g.get_name(repo=FOO_REPO), 'foo')
322        self.assertEqual(b2g.get_name(repo=BAR_REPO), 'bar')
323        self.assertEqual(b2g.get_name(repo=BAZ_REPO), 'baz')
324
325    @mock.patch('subprocess.run')
326    def test_get_initial_targets(self, _):
327        """Tests adding initial targets to the pending queue."""
328        b2g = BazelToGnConverter(PW_ROOT)
329        b2g.parse_args_gn(StringIO(ARGS_GN))
330        b2g.load_workspace('foo', StringIO(FOO_B2G_JSON))
331        targets = b2g.get_initial_targets('foo')
332        json_targets = json.loads(FOO_B2G_JSON)['targets']
333        self.assertEqual(len(targets), len(json_targets))
334        self.assertEqual(b2g.num_loaded(), 1)
335
336    @mock.patch('subprocess.run')
337    def test_load_rules(self, mock_run):
338        """Tests loading a rule from a Bazel workspace."""
339        mock_run.side_effect = [
340            mock.MagicMock(**retval)
341            for retval in [
342                {'stdout.decode.return_value': ''},  # foo: git fetch
343                {'stdout.decode.return_value': FOO_RULE_JSON},
344            ]
345        ]
346        b2g = BazelToGnConverter(PW_ROOT)
347        b2g.parse_args_gn(StringIO(ARGS_GN))
348        b2g.load_workspace('foo', StringIO(FOO_B2G_JSON))
349        labels = b2g.get_initial_targets('foo')
350        rule = list(b2g.load_rules(labels))[0]
351        self.assertEqual(
352            rule.get_list('deps'),
353            [
354                f'@{BAR_REPO}//bar/pkg:bar_target1',
355                f'@{BAR_REPO}//bar/pkg:bar_target2',
356            ],
357        )
358        self.assertEqual(
359            rule.get_list('implementation_deps'),
360            [
361                f'@{BAZ_REPO}//baz/pkg:baz_target',
362            ],
363        )
364        self.assertEqual(b2g.num_loaded(), 1)
365
366    @mock.patch('subprocess.run')
367    def test_convert_rule(self, mock_run):
368        """Tests converting a Bazel rule into a GN target."""
369        mock_run.side_effect = [
370            mock.MagicMock(**retval)
371            for retval in [
372                {'stdout.decode.return_value': ''},  # foo: git fetch
373                {'stdout.decode.return_value': ''},  # bar: git fetch
374                {'stdout.decode.return_value': ''},  # baz: git fetch
375                {'stdout.decode.return_value': FOO_RULE_JSON},
376            ]
377        ]
378        b2g = BazelToGnConverter(PW_ROOT)
379        b2g.parse_args_gn(StringIO(ARGS_GN))
380        b2g.load_workspace('foo', StringIO(FOO_B2G_JSON))
381        b2g.load_workspace('bar', StringIO(BAR_B2G_JSON))
382        b2g.load_workspace('baz', StringIO(BAZ_B2G_JSON))
383        labels = b2g.get_initial_targets('foo')
384        rule = list(b2g.load_rules(labels))[0]
385        gn_target = b2g.convert_rule(rule)
386        self.assertEqual(gn_target.attrs['cflags'], ['-cflag'])
387        self.assertEqual(gn_target.attrs['defines'], ['LOCAL_DEFINE'])
388        self.assertEqual(
389            gn_target.attrs['deps'],
390            ['$dir_pw_third_party/baz/baz/pkg:baz_target'],
391        )
392        self.assertEqual(
393            gn_target.attrs['include_dirs'], ['$dir_pw_third_party_foo/include']
394        )
395        self.assertEqual(
396            gn_target.attrs['inputs'], ['$dir_pw_third_party_foo/data/input']
397        )
398        self.assertEqual(gn_target.attrs['ldflags'], ['-ldflag'])
399        self.assertEqual(
400            gn_target.attrs['public'], ['$dir_pw_third_party_foo/include/foo.h']
401        )
402        self.assertEqual(gn_target.attrs['public_defines'], ['DEFINE'])
403        self.assertEqual(
404            gn_target.attrs['public_deps'],
405            [
406                '$dir_pw_third_party/bar/bar/pkg:bar_target1',
407                '$dir_pw_third_party/bar/bar/pkg:bar_target2',
408            ],
409        )
410        self.assertEqual(
411            gn_target.attrs['sources'], ['$dir_pw_third_party_foo/src/foo.cc']
412        )
413
414    @mock.patch('subprocess.run')
415    def test_update_pw_package(self, mock_run):
416        """Tests updating the pw_package file."""
417        mock_run.side_effect = [
418            mock.MagicMock(**retval)
419            for retval in [
420                {'stdout.decode.return_value': ''},  # foo: git fetch
421                {'stdout.decode.return_value': 'some-tag'},
422                {'stdout.decode.return_value': '2024-01-01 00:00:00'},
423                {'stdout.decode.return_value': '2024-01-01 00:00:01'},
424            ]
425        ]
426        b2g = BazelToGnConverter(PW_ROOT)
427        b2g.parse_args_gn(StringIO(ARGS_GN))
428        b2g.load_workspace('foo', StringIO(FOO_B2G_JSON))
429        contents = '''some_python_call(
430    name='foo',
431    commit='cafef00d',
432    **kwargs,
433)
434'''
435        inputs = contents.split('\n')
436        outputs = list(b2g.update_pw_package('foo', inputs))
437        self.assertEqual(outputs[0:2], inputs[0:2])
438        self.assertEqual(outputs[3], inputs[3].replace('cafef00d', 'some-tag'))
439        self.assertEqual(outputs[4:-1], inputs[4:])
440
441    @mock.patch('subprocess.run')
442    def test_get_imports(self, mock_run):
443        """Tests getting the GNI files needed for a GN target."""
444        mock_run.side_effect = [
445            mock.MagicMock(**retval)
446            for retval in [
447                {'stdout.decode.return_value': ''},  # foo: git fetch
448                {'stdout.decode.return_value': ''},  # bar: git fetch
449                {'stdout.decode.return_value': ''},  # baz: git fetch
450                {'stdout.decode.return_value': FOO_RULE_JSON},
451            ]
452        ]
453        b2g = BazelToGnConverter(PW_ROOT)
454        b2g.parse_args_gn(StringIO(ARGS_GN))
455        b2g.load_workspace('foo', StringIO(FOO_B2G_JSON))
456        b2g.load_workspace('bar', StringIO(BAR_B2G_JSON))
457        b2g.load_workspace('baz', StringIO(BAZ_B2G_JSON))
458        labels = b2g.get_initial_targets('foo')
459        rule = list(b2g.load_rules(labels))[0]
460        gn_target = b2g.convert_rule(rule)
461        imports = set(b2g.get_imports(gn_target))
462        self.assertEqual(imports, {'$dir_pw_third_party/foo/foo.gni'})
463
464    @mock.patch('subprocess.run')
465    def test_update_doc_rst(self, mock_run):
466        """Tests updating the git revision in the docs."""
467        mock_run.side_effect = [
468            mock.MagicMock(**retval)
469            for retval in [
470                {'stdout.decode.return_value': ''},  # foo: git fetch
471                {'stdout.decode.return_value': 'http://src/foo.git'},
472                {'stdout.decode.return_value': 'deadbeeffeedface'},
473            ]
474        ]
475        b2g = BazelToGnConverter(PW_ROOT)
476        b2g.parse_args_gn(StringIO(ARGS_GN))
477        b2g.load_workspace('foo', StringIO(FOO_B2G_JSON))
478        inputs = (
479            [f'preserved {i}' for i in range(10)]
480            + ['.. DO NOT EDIT BELOW THIS LINE. Generated section.']
481            + [f'overwritten {i}' for i in range(10)]
482        )
483        outputs = list(b2g.update_doc_rst('foo', inputs))
484        self.assertEqual(len(outputs), 18)
485        self.assertEqual(outputs[:11], inputs[:11])
486        self.assertEqual(outputs[11], '')
487        self.assertEqual(outputs[12], 'Version')
488        self.assertEqual(outputs[13], '=======')
489        self.assertEqual(
490            outputs[14],
491            'The update script was last run for revision `deadbeef`_.',
492        )
493        self.assertEqual(outputs[15], '')
494        self.assertEqual(
495            outputs[16],
496            '.. _deadbeef: http://src/foo/tree/deadbeeffeedface',
497        )
498        self.assertEqual(outputs[17], '')
499
500
501if __name__ == '__main__':
502    unittest.main()
503