• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 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.generate_3p_gn module."""
15
16import unittest
17
18from contextlib import AbstractContextManager
19from io import StringIO
20from pathlib import Path
21from tempfile import TemporaryDirectory
22from unittest import mock
23from types import TracebackType
24from typing import Iterator, Type
25
26from pw_build.generate_3p_gn import GnGenerator, write_owners
27from pw_build.gn_config import GnConfig
28from pw_build.gn_writer import GnWriter
29
30
31class GnGeneratorForTest(AbstractContextManager):
32    """Test fixture that creates a generator for a temporary directory."""
33
34    def __init__(self):
35        self._tmp = TemporaryDirectory()
36
37    def __enter__(self) -> GnGenerator:
38        """Creates a temporary directory and uses it to create a generator."""
39        tmp = self._tmp.__enter__()
40        generator = GnGenerator()
41        path = Path(tmp) / 'repo'
42        path.mkdir(parents=True)
43        generator.load_workspace(path)
44        return generator
45
46    def __exit__(
47        self,
48        exc_type: Type[BaseException] | None,
49        exc_val: BaseException | None,
50        exc_tb: TracebackType | None,
51    ) -> None:
52        """Removes the temporary directory."""
53        self._tmp.__exit__(exc_type, exc_val, exc_tb)
54
55
56def mock_return_values(mock_run, retvals: Iterator[str]) -> None:
57    """Mocks the return values of several calls to subprocess.run."""
58    side_effects = []
59    for retval in retvals:
60        attr = {'stdout.decode.return_value': retval}
61        side_effects.append(mock.MagicMock(**attr))
62    mock_run.side_effect = side_effects
63
64
65class TestGenerator(unittest.TestCase):
66    """Tests for generate_3p_gn.GnGenerator."""
67
68    def test_generate_configs(self):
69        """Tests finding the most common configs."""
70        generator = GnGenerator()
71        generator.set_repo('test')
72
73        generator.add_target(
74            json='''{
75                "target_name": "target0",
76                "cflags": ["common"]
77            }''',
78        )
79
80        generator.add_target(
81            json='''{
82                "target_name": "target1",
83                "package": "foo",
84                "include_dirs": ["foo"],
85                "cflags": ["common", "foo-flag1"]
86            }''',
87        )
88
89        generator.add_target(
90            json='''{
91                "target_name": "target2",
92                "package": "foo",
93                "include_dirs": ["foo"],
94                "cflags": ["common", "foo-flag1", "foo-flag2"]
95            }''',
96        )
97
98        generator.add_target(
99            json='''{
100                "target_name": "target3",
101                "package": "foo",
102                "include_dirs": ["foo"],
103                "cflags": ["common", "foo-flag1"]
104            }''',
105        )
106
107        generator.add_target(
108            json='''{
109                "target_name": "target4",
110                "package": "bar",
111                "include_dirs": ["bar"],
112                "cflags": ["common", "bar-flag"]
113            }''',
114        )
115
116        configs_to_add = ['//:added']
117        configs_to_remove = ['//remove:me']
118        generator.generate_configs(configs_to_add, configs_to_remove)
119
120        self.assertEqual(
121            generator.configs[''],
122            [
123                GnConfig(
124                    json='''{
125                      "label": "$dir_pw_third_party/test:test_config1",
126                      "cflags": ["common"],
127                      "usages": 5
128                    }'''
129                )
130            ],
131        )
132
133        self.assertEqual(
134            generator.configs['foo'],
135            [
136                GnConfig(
137                    json='''{
138                      "label": "$dir_pw_third_party/test/foo:foo_config1",
139                      "cflags": ["foo-flag1"],
140                      "public": false,
141                      "usages": 3
142                    }'''
143                ),
144                GnConfig(
145                    json='''{
146                      "label": "$dir_pw_third_party/test/foo:foo_public_config1",
147                      "include_dirs": ["foo"],
148                      "public": true,
149                      "usages": 3
150                    }'''
151                ),
152            ],
153        )
154
155        self.assertEqual(
156            generator.configs['bar'],
157            [
158                GnConfig(
159                    json='''{
160                      "label": "$dir_pw_third_party/test/bar:bar_public_config1",
161                      "include_dirs": ["bar"],
162                      "public": true,
163                      "usages": 1
164                    }'''
165                )
166            ],
167        )
168
169        targets = [
170            target
171            for targets in generator.targets.values()
172            for target in targets
173        ]
174        targets.sort(key=lambda target: target.name())
175
176        target0 = targets[0]
177        self.assertFalse(target0.config)
178        self.assertEqual(
179            target0.configs,
180            {'//:added', '$dir_pw_third_party/test:test_config1'},
181        )
182        self.assertFalse(target0.public_configs)
183        self.assertEqual(target0.remove_configs, {'//remove:me'})
184
185        target1 = targets[1]
186        self.assertFalse(target1.config)
187        self.assertEqual(
188            target1.configs,
189            {
190                '//:added',
191                '$dir_pw_third_party/test:test_config1',
192                '$dir_pw_third_party/test/foo:foo_config1',
193            },
194        )
195        self.assertEqual(
196            target1.public_configs,
197            {'$dir_pw_third_party/test/foo:foo_public_config1'},
198        )
199        self.assertEqual(target1.remove_configs, {'//remove:me'})
200
201        target2 = targets[2]
202        self.assertEqual(
203            target2.config,
204            GnConfig(
205                json='''{
206                  "cflags": ["foo-flag2"],
207                  "public": false,
208                  "usages": 0
209                }'''
210            ),
211        )
212        self.assertEqual(
213            target2.configs,
214            {
215                '//:added',
216                '$dir_pw_third_party/test:test_config1',
217                '$dir_pw_third_party/test/foo:foo_config1',
218            },
219        )
220        self.assertEqual(
221            target2.public_configs,
222            {'$dir_pw_third_party/test/foo:foo_public_config1'},
223        )
224        self.assertEqual(target2.remove_configs, {'//remove:me'})
225
226        target3 = targets[3]
227        self.assertFalse(target3.config)
228        self.assertEqual(
229            target3.configs,
230            {
231                '//:added',
232                '$dir_pw_third_party/test:test_config1',
233                '$dir_pw_third_party/test/foo:foo_config1',
234            },
235        )
236        self.assertEqual(
237            target3.public_configs,
238            {'$dir_pw_third_party/test/foo:foo_public_config1'},
239        )
240        self.assertEqual(target3.remove_configs, {'//remove:me'})
241
242        target4 = targets[4]
243        self.assertEqual(
244            target4.config,
245            GnConfig(
246                json='''{
247                  "cflags": ["bar-flag"],
248                  "public": false,
249                  "usages": 0
250                }'''
251            ),
252        )
253        self.assertEqual(
254            target4.configs,
255            {'//:added', '$dir_pw_third_party/test:test_config1'},
256        )
257        self.assertEqual(
258            target4.public_configs,
259            {'$dir_pw_third_party/test/bar:bar_public_config1'},
260        )
261        self.assertEqual(target4.remove_configs, {'//remove:me'})
262
263    def test_write_build_gn(self):
264        """Tests writing a complete BUILD.gn file."""
265        generator = GnGenerator()
266        generator.set_repo('test')
267        generator.exclude_from_gn_check(bazel='//bar:target3')
268
269        generator.add_configs(
270            '',
271            GnConfig(
272                json='''{
273                  "label": "$dir_pw_third_party/test:test_config1",
274                  "cflags": ["common"],
275                  "usages": 5
276                }'''
277            ),
278        )
279
280        generator.add_configs(
281            'foo',
282            GnConfig(
283                json='''{
284                  "label": "$dir_pw_third_party/test/foo:foo_config1",
285                  "cflags": ["foo-flag1"],
286                  "public": false,
287                  "usages": 3
288                }'''
289            ),
290            GnConfig(
291                json='''{
292                  "label": "$dir_pw_third_party/test/foo:foo_public_config1",
293                  "include_dirs": ["foo"],
294                  "public": true,
295                  "usages": 3
296                }'''
297            ),
298        )
299
300        generator.add_configs(
301            'bar',
302            GnConfig(
303                json='''{
304                  "label": "$dir_pw_third_party/test/bar:bar_public_config1",
305                  "include_dirs": ["bar"],
306                  "public": true,
307                  "usages": 1
308                }'''
309            ),
310        )
311
312        generator.add_target(
313            json='''{
314                "target_type": "pw_executable",
315                "target_name": "target0",
316                "configs": ["$dir_pw_third_party/test:test_config1"],
317                "sources": ["$dir_pw_third_party_test/target0.cc"],
318                "deps": [
319                    "$dir_pw_third_party/test/foo:target1",
320                    "$dir_pw_third_party/test/foo:target2"
321                ]
322            }''',
323        )
324
325        generator.add_target(
326            json='''{
327                "target_type": "pw_source_set",
328                "target_name": "target1",
329                "package": "foo",
330                "public": ["$dir_pw_third_party_test/foo/target1.h"],
331                "sources": ["$dir_pw_third_party_test/foo/target1.cc"],
332                "public_configs": ["$dir_pw_third_party/test/foo:foo_public_config1"],
333                "configs": [
334                    "$dir_pw_third_party/test:test_config1",
335                    "$dir_pw_third_party/test/foo:foo_config1"
336                ]
337            }''',
338        )
339
340        generator.add_target(
341            json='''{
342                "target_type": "pw_source_set",
343                "target_name": "target2",
344                "package": "foo",
345                "sources": ["$dir_pw_third_party_test/foo/target2.cc"],
346                "public_configs": ["$dir_pw_third_party/test/foo:foo_public_config1"],
347                "configs": [
348                    "$dir_pw_third_party/test:test_config1",
349                    "$dir_pw_third_party/test/foo:foo_config1"
350                ],
351                "cflags": ["foo-flag2"],
352                "public_deps": ["$dir_pw_third_party/test/bar:target3"]
353            }''',
354        )
355
356        generator.add_target(
357            json='''{
358                "target_type": "pw_source_set",
359                "target_name": "target3",
360                "package": "bar",
361                "include_dirs": ["bar"],
362                "public_configs": ["$dir_pw_third_party/test/bar:bar_public_config1"],
363                "configs": ["$dir_pw_third_party/test:test_config1"],
364                "cflags": ["bar-flag"]
365            }''',
366        )
367
368        output = StringIO()
369        build_gn = GnWriter(output)
370        generator.write_build_gn('', build_gn)
371        self.assertEqual(
372            output.getvalue(),
373            '''
374import("//build_overrides/pigweed.gni")
375
376import("$dir_pw_build/target_types.gni")
377import("$dir_pw_docgen/docs.gni")
378import("$dir_pw_third_party/test/test.gni")
379
380if (dir_pw_third_party_test != "") {
381  config("test_config1") {
382    cflags = [
383      "common",
384    ]
385  }
386
387  # Generated from //:target0
388  pw_executable("target0") {
389    sources = [
390      "$dir_pw_third_party_test/target0.cc",
391    ]
392    configs = [
393      ":test_config1",
394    ]
395    deps = [
396      "foo:target1",
397      "foo:target2",
398    ]
399  }
400}
401
402pw_doc_group("docs") {
403  sources = [
404    "docs.rst",
405  ]
406}
407'''.lstrip(),
408        )
409
410        output = StringIO()
411        build_gn = GnWriter(output)
412        generator.write_build_gn('foo', build_gn)
413        self.assertEqual(
414            output.getvalue(),
415            '''
416import("//build_overrides/pigweed.gni")
417
418import("$dir_pw_build/target_types.gni")
419import("$dir_pw_third_party/test/test.gni")
420
421config("foo_public_config1") {
422  include_dirs = [
423    "foo",
424  ]
425}
426
427config("foo_config1") {
428  cflags = [
429    "foo-flag1",
430  ]
431}
432
433# Generated from //foo:target1
434pw_source_set("target1") {
435  public = [
436    "$dir_pw_third_party_test/foo/target1.h",
437  ]
438  sources = [
439    "$dir_pw_third_party_test/foo/target1.cc",
440  ]
441  public_configs = [
442    ":foo_public_config1",
443  ]
444  configs = [
445    "..:test_config1",
446    ":foo_config1",
447  ]
448}
449
450# Generated from //foo:target2
451pw_source_set("target2") {
452  sources = [
453    "$dir_pw_third_party_test/foo/target2.cc",
454  ]
455  cflags = [
456    "foo-flag2",
457  ]
458  public_configs = [
459    ":foo_public_config1",
460  ]
461  configs = [
462    "..:test_config1",
463    ":foo_config1",
464  ]
465  public_deps = [
466    "../bar:target3",
467  ]
468}
469'''.lstrip(),
470        )
471
472        output = StringIO()
473        build_gn = GnWriter(output)
474        generator.write_build_gn('bar', build_gn)
475        self.assertEqual(
476            output.getvalue(),
477            '''
478import("//build_overrides/pigweed.gni")
479
480import("$dir_pw_build/target_types.gni")
481import("$dir_pw_third_party/test/test.gni")
482
483config("bar_public_config1") {
484  include_dirs = [
485    "bar",
486  ]
487}
488
489# Generated from //bar:target3
490pw_source_set("target3") {
491  check_includes = false
492  cflags = [
493    "bar-flag",
494  ]
495  include_dirs = [
496    "bar",
497  ]
498  public_configs = [
499    ":bar_public_config1",
500  ]
501  configs = [
502    "..:test_config1",
503  ]
504}
505'''.lstrip(),
506        )
507
508    def test_write_repo_gni(self):
509        """Tests writing the GN import file for a repo."""
510        output = StringIO()
511        with GnGeneratorForTest() as generator:
512            generator.write_repo_gni(GnWriter(output), 'Repo')
513
514        self.assertEqual(
515            output.getvalue(),
516            '''
517declare_args() {
518  # If compiling tests with Repo, this variable is set to the path to the Repo
519  # installation. When set, a pw_source_set for the Repo library is created at
520  # "$dir_pw_third_party/repo".
521  dir_pw_third_party_repo = ""
522}
523'''.lstrip(),
524        )
525
526    @mock.patch('subprocess.run')
527    def test_write_docs_rst(self, mock_run):
528        """Tests writing the reStructuredText docs for a repo."""
529        mock_return_values(
530            mock_run,
531            [
532                'https://host/repo.git',
533                'https://host/repo.git',
534                'deadbeeffeedface',
535            ],
536        )
537        output = StringIO()
538        with GnGeneratorForTest() as generator:
539            generator.write_docs_rst(output, 'Repo')
540
541        self.assertEqual(
542            output.getvalue(),
543            '''
544.. _module-pw_third_party_repo:
545
546====
547Repo
548====
549The ``$dir_pw_third_party/repo/`` module provides build files to allow
550optionally including upstream Repo.
551
552-------------------
553Using upstream Repo
554-------------------
555If you want to use Repo, you must do the following:
556
557Submodule
558=========
559Add Repo to your workspace with the following command.
560
561.. code-block:: sh
562
563  git submodule add https://host/repo.git \\
564    third_party/repo/src
565
566GN
567==
568* Set the GN var ``dir_pw_third_party_repo`` to the location of the
569  Repo source.
570
571  If you used the command above, this will be
572  ``//third_party/repo/src``
573
574  This can be set in your args.gn or .gn file like:
575  ``dir_pw_third_party_repo = "//third_party/repo/src"``
576
577Updating
578========
579The GN build files are generated from the third-party Bazel build files using
580$dir_pw_build/py/pw_build/generate_3p_gn.py.
581
582The script uses data taken from ``$dir_pw_third_party/repo/repo.json``.
583The schema of ``repo.json`` is described in :ref:`module-pw_build-third-party`.
584
585The script should be re-run whenever the submodule is updated or the JSON file
586is modified. Specify the location of the Bazel repository can be specified using
587the ``-w`` option, e.g.
588
589.. code-block:: sh
590
591  python pw_build/py/pw_build/generate_3p_gn.py \\
592    -w third_party/repo/src
593
594.. DO NOT EDIT BELOW THIS LINE. Generated section.
595
596Version
597=======
598The update script was last run for revision `deadbeef`_.
599
600.. _deadbeef: https://host/repo/tree/deadbeeffeedface
601'''.lstrip(),
602        )
603
604    @mock.patch('subprocess.run')
605    def test_update_docs_rst_same_rev(self, mock_run):
606        """Tests updating the docs with the same revision."""
607        mock_return_values(
608            mock_run,
609            [
610                'https://host/repo.git',
611                'https://host/repo.git',
612                'deadbeeffeedface',
613                'https://host/repo.git',
614                'deadbeeffeedface',
615            ],
616        )
617        output = StringIO()
618        with GnGeneratorForTest() as generator:
619            generator.write_docs_rst(output, 'Repo')
620            original = output.getvalue().split('\n')
621            updated = list(generator.update_version(original))
622
623        self.assertEqual(original, updated)
624
625    @mock.patch('subprocess.run')
626    def test_update_docs_rst_new_rev(self, mock_run):
627        """Tests updating the docs with the different revision."""
628        mock_return_values(
629            mock_run,
630            [
631                'https://host/repo.git',
632                'https://host/repo.git',
633                'deadbeeffeedface',
634                'https://host/repo.git',
635                '0123456789abcdef',
636            ],
637        )
638        output = StringIO()
639        with GnGeneratorForTest() as generator:
640            generator.write_docs_rst(output, 'Repo')
641            contents = output.getvalue()
642            original = contents.split('\n')
643
644            # Convert the contents to a list of lines similar to those returned
645            # by iterating over an open file. In particular, include a newline
646            # at the end of each line.
647            with_newlines = [s + '\n' for s in original]
648            updated = list(generator.update_version(with_newlines))
649
650        self.assertEqual(original[:-6], updated[:-6])
651        self.assertEqual(
652            '\n'.join(updated[-6:]),
653            '''
654Version
655=======
656The update script was last run for revision `01234567`_.
657
658.. _01234567: https://host/repo/tree/0123456789abcdef
659'''.lstrip(),
660        )
661
662    @mock.patch('subprocess.run')
663    def test_update_docs_rst_no_rev(self, mock_run):
664        """Tests updating docs that do not have a revision."""
665        mock_return_values(
666            mock_run,
667            [
668                'https://host/repo.git',
669                '0123456789abcdef',
670            ],
671        )
672        with GnGeneratorForTest() as generator:
673            updated = list(generator.update_version(['foo', 'bar', '']))
674
675        self.assertEqual(
676            '\n'.join(updated),
677            '''
678foo
679bar
680
681.. DO NOT EDIT BELOW THIS LINE. Generated section.
682
683Version
684=======
685The update script was last run for revision `01234567`_.
686
687.. _01234567: https://host/repo/tree/0123456789abcdef
688'''.lstrip(),
689        )
690
691    def test_update_third_party_docs(self):
692        """Tests adding docs to //docs::third_party_docs."""
693        with GnGeneratorForTest() as generator:
694            contents = generator.update_third_party_docs(
695                '''
696group("third_party_docs") {
697  deps = [
698    "$dir_pigweed/third_party/existing:docs",
699  ]
700}
701'''
702            )
703        # Formatting is performed separately.
704        self.assertEqual(
705            contents,
706            '''
707group("third_party_docs") {
708deps = ["$dir_pigweed/third_party/repo:docs",
709    "$dir_pigweed/third_party/existing:docs",
710  ]
711}
712''',
713        )
714
715    def test_update_third_party_docs_no_target(self):
716        """Tests adding docs to a file without a "third_party_docs" target."""
717        with GnGeneratorForTest() as generator:
718            with self.assertRaises(ValueError):
719                generator.update_third_party_docs('')
720
721    @mock.patch('subprocess.run')
722    def test_write_extra(self, mock_run):
723        """Tests extra files produced via `bazel run`."""
724        attr = {'stdout.decode.return_value': 'hello, world!'}
725        mock_run.return_value = mock.MagicMock(**attr)
726
727        output = StringIO()
728        with GnGeneratorForTest() as generator:
729            generator.write_extra(output, 'some_label')
730        self.assertEqual(output.getvalue(), 'hello, world!')
731
732    @mock.patch('subprocess.run')
733    def test_write_owners(self, mock_run):
734        """Tests writing an OWNERS file."""
735        attr = {'stdout.decode.return_value': 'someone@pigweed.dev'}
736        mock_run.return_value = mock.MagicMock(**attr)
737
738        output = StringIO()
739        write_owners(output)
740        self.assertEqual(output.getvalue(), 'someone@pigweed.dev')
741
742
743if __name__ == '__main__':
744    unittest.main()
745