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