• 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.bazel_query module."""
15
16import json
17import unittest
18
19from unittest import mock
20from tempfile import TemporaryDirectory
21from pw_build.bazel_query import ParseError, BazelRule, BazelWorkspace
22
23
24class TestBazelRule(unittest.TestCase):
25    """Tests for bazel_query.Rule."""
26
27    def test_rule_top_level(self):
28        """Tests a top-level rule with no package name."""
29        rule = BazelRule('//:no-package', 'custom-type')
30        self.assertEqual(rule.package(), '')
31
32    def test_rule_with_label(self):
33        """Tests a rule with a package and target name."""
34        rule = BazelRule('//foo:target', 'custom-type')
35        self.assertEqual(rule.package(), 'foo')
36        self.assertEqual(rule.label(), '//foo:target')
37
38    def test_rule_in_subdirectory(self):
39        """Tests a rule in a subdirectory."""
40        rule = BazelRule('//foo:bar/target', 'custom-type')
41        self.assertEqual(rule.package(), 'foo')
42        self.assertEqual(rule.label(), '//foo:bar/target')
43
44    def test_rule_in_subpackage(self):
45        """Tests a rule in a subpackage."""
46        rule = BazelRule('//foo/bar:target', 'custom-type')
47        self.assertEqual(rule.package(), 'foo/bar')
48        self.assertEqual(rule.label(), '//foo/bar:target')
49
50    def test_rule_no_target(self):
51        """Tests a rule with only a package name."""
52        rule = BazelRule('//foo/bar', 'custom-type')
53        self.assertEqual(rule.package(), 'foo/bar')
54        self.assertEqual(rule.label(), '//foo/bar:bar')
55
56    def test_rule_invalid_relative(self):
57        """Tests a rule with an invalid (non-absolute) package name."""
58        with self.assertRaises(ParseError):
59            BazelRule('../foo/bar:target', 'custom-type')
60
61    def test_rule_invalid_double_colon(self):
62        """Tests a rule with an invalid (non-absolute) package name."""
63        with self.assertRaises(ParseError):
64            BazelRule('//foo:bar:target', 'custom-type')
65
66    def test_rule_parse_invalid(self):
67        """Test for parsing invalid rule attributes."""
68        rule = BazelRule('//package:target', 'kind')
69        with self.assertRaises(ParseError):
70            rule.parse(
71                json.loads(
72                    '''[{
73                        "name": "invalid_attr",
74                        "type": "ESOTERIC",
75                        "intValue": 0,
76                        "stringValue": "false",
77                        "explicitlySpecified": true,
78                        "booleanValue": false
79                    }]'''
80                )
81            )
82
83    def test_rule_parse_boolean_unspecified(self):
84        """Test parsing an unset boolean rule attribute."""
85        rule = BazelRule('//package:target', 'kind')
86        rule.parse(
87            json.loads(
88                '''[{
89                    "name": "bool_attr",
90                    "type": "BOOLEAN",
91                    "intValue": 0,
92                    "stringValue": "false",
93                    "explicitlySpecified": false,
94                    "booleanValue": false
95                }]'''
96            )
97        )
98        self.assertFalse(rule.has_attr('bool_attr'))
99
100    def test_rule_parse_boolean_false(self):
101        """Tests parsing boolean rule attribute set to false."""
102        rule = BazelRule('//package:target', 'kind')
103        rule.parse(
104            json.loads(
105                '''[{
106                    "name": "bool_attr",
107                    "type": "BOOLEAN",
108                    "intValue": 0,
109                    "stringValue": "false",
110                    "explicitlySpecified": true,
111                    "booleanValue": false
112                }]'''
113            )
114        )
115        self.assertTrue(rule.has_attr('bool_attr'))
116        self.assertFalse(rule.get_bool('bool_attr'))
117
118    def test_rule_parse_boolean_true(self):
119        """Tests parsing a boolean rule attribute set to true."""
120        rule = BazelRule('//package:target', 'kind')
121        rule.parse(
122            json.loads(
123                '''[{
124                    "name": "bool_attr",
125                    "type": "BOOLEAN",
126                    "intValue": 1,
127                    "stringValue": "true",
128                    "explicitlySpecified": true,
129                    "booleanValue": true
130                }]'''
131            )
132        )
133        self.assertTrue(rule.has_attr('bool_attr'))
134        self.assertTrue(rule.get_bool('bool_attr'))
135
136    def test_rule_parse_integer_unspecified(self):
137        """Tests parsing an unset integer rule attribute."""
138        rule = BazelRule('//package:target', 'kind')
139        rule.parse(
140            json.loads(
141                '''[{
142                    "name": "int_attr",
143                    "type": "INTEGER",
144                    "intValue": 0,
145                    "explicitlySpecified": false
146                }]'''
147            )
148        )
149        self.assertFalse(rule.has_attr('int_attr'))
150
151    def test_rule_parse_integer(self):
152        """Tests parsing an integer rule attribute."""
153        rule = BazelRule('//package:target', 'kind')
154        rule.parse(
155            json.loads(
156                '''[{
157                    "name": "int_attr",
158                    "type": "INTEGER",
159                    "intValue": 100,
160                    "explicitlySpecified": true
161                }]'''
162            )
163        )
164        self.assertTrue(rule.has_attr('int_attr'))
165        self.assertEqual(rule.get_int('int_attr'), 100)
166
167    def test_rule_parse_string_unspecified(self):
168        """Tests parsing an unset string rule attribute."""
169        rule = BazelRule('//package:target', 'kind')
170        rule.parse(
171            json.loads(
172                '''[{
173                    "name": "string_attr",
174                    "type": "STRING",
175                    "stringValue": "",
176                    "explicitlySpecified": false
177                }]'''
178            )
179        )
180        self.assertFalse(rule.has_attr('string_attr'))
181
182    def test_rule_parse_string(self):
183        """Tests parsing a string rule attribute."""
184        rule = BazelRule('//package:target', 'kind')
185        rule.parse(
186            json.loads(
187                '''[{
188                    "name": "string_attr",
189                    "type": "STRING",
190                    "stringValue": "hello, world!",
191                    "explicitlySpecified": true
192                }]'''
193            )
194        )
195        self.assertTrue(rule.has_attr('string_attr'))
196        self.assertEqual(rule.get_str('string_attr'), 'hello, world!')
197
198    def test_rule_parse_string_list_unspecified(self):
199        """Tests parsing an unset string list rule attribute."""
200        rule = BazelRule('//package:target', 'kind')
201        rule.parse(
202            json.loads(
203                '''[{
204                    "name": "string_list_attr",
205                    "type": "STRING_LIST",
206                    "stringListValue": [],
207                    "explicitlySpecified": false
208                }]'''
209            )
210        )
211        self.assertFalse(rule.has_attr('string_list_attr'))
212
213    def test_rule_parse_string_list(self):
214        """Tests parsing a string list rule attribute."""
215        rule = BazelRule('//package:target', 'kind')
216        rule.parse(
217            json.loads(
218                '''[{
219                    "name": "string_list_attr",
220                    "type": "STRING_LIST",
221                    "stringListValue": [ "hello", "world!" ],
222                    "explicitlySpecified": true
223                }]'''
224            )
225        )
226        self.assertTrue(rule.has_attr('string_list_attr'))
227        self.assertEqual(rule.get_list('string_list_attr'), ['hello', 'world!'])
228
229    def test_rule_parse_label_list_unspecified(self):
230        """Tests parsing an unset label list rule attribute."""
231        rule = BazelRule('//package:target', 'kind')
232        rule.parse(
233            json.loads(
234                '''[{
235                    "name": "label_list_attr",
236                    "type": "LABEL_LIST",
237                    "stringListValue": [],
238                    "explicitlySpecified": false
239                }]'''
240            )
241        )
242        self.assertFalse(rule.has_attr('label_list_attr'))
243
244    def test_rule_parse_label_list(self):
245        """Tests parsing a label list rule attribute."""
246        rule = BazelRule('//package:target', 'kind')
247        rule.parse(
248            json.loads(
249                '''[{
250                    "name": "label_list_attr",
251                    "type": "LABEL_LIST",
252                    "stringListValue": [ "hello", "world!" ],
253                    "explicitlySpecified": true
254                }]'''
255            )
256        )
257        self.assertTrue(rule.has_attr('label_list_attr'))
258        self.assertEqual(rule.get_list('label_list_attr'), ['hello', 'world!'])
259
260    def test_rule_parse_string_dict_unspecified(self):
261        """Tests parsing an unset string dict rule attribute."""
262        rule = BazelRule('//package:target', 'kind')
263        rule.parse(
264            json.loads(
265                '''[{
266                    "name": "string_dict_attr",
267                    "type": "LABEL_LIST",
268                    "stringDictValue": [],
269                    "explicitlySpecified": false
270                }]'''
271            )
272        )
273        self.assertFalse(rule.has_attr('string_dict_attr'))
274
275    def test_rule_parse_string_dict(self):
276        """Tests parsing a string dict rule attribute."""
277        rule = BazelRule('//package:target', 'kind')
278        rule.parse(
279            json.loads(
280                '''[{
281                    "name": "string_dict_attr",
282                    "type": "STRING_DICT",
283                    "stringDictValue": [
284                        {
285                            "key": "foo",
286                            "value": "hello"
287                        },
288                        {
289                            "key": "bar",
290                            "value": "world"
291                        }
292                    ],
293                    "explicitlySpecified": true
294                }]'''
295            )
296        )
297        string_dict_attr = rule.get_dict('string_dict_attr')
298        self.assertTrue(rule.has_attr('string_dict_attr'))
299        self.assertEqual(string_dict_attr['foo'], 'hello')
300        self.assertEqual(string_dict_attr['bar'], 'world')
301
302
303class TestWorkspace(unittest.TestCase):
304    """Test for bazel_query.Workspace."""
305
306    @mock.patch('subprocess.run')
307    def test_workspace_get_rules(self, mock_run):
308        """Tests querying a workspace for Bazel rules."""
309        attrs = []
310
311        # `bazel query //... --output=package
312        attrs.append(
313            {
314                'stdout.decode.return_value': '''
315foo/pkg1
316bar/pkg2'''
317            }
318        )
319
320        # bazel query buildfiles(//foo:*) --output=xml
321        attrs.append(
322            {
323                'stdout.decode.return_value': '''
324<query version="2">
325    <source-file name="//foo/pkg1:BUILD.bazel">
326        <visibility-label name="//visibility:public"/>
327    </source-file>
328</query>'''
329            }
330        )
331
332        # bazel query buildfiles(//bar:*) --output=xml
333        attrs.append(
334            {
335                'stdout.decode.return_value': '''
336<query version="2">
337    <source-file name="//bar/pkg2:BUILD.bazel">
338        <visibility-label name="//visibility:private"/>
339    </source-file>
340</query>'''
341            }
342        )
343
344        # bazel cquery kind(some_kind, //...) --output=jsonproto
345        attrs.append(
346            {
347                'stdout.decode.return_value': '''
348{
349  "results": [
350    {
351      "target": {
352        "type": "RULE",
353        "rule": {
354          "name": "//foo/pkg1:rule1",
355          "ruleClass": "some_kind",
356          "attribute": []
357        }
358      }
359    },
360    {
361      "target": {
362        "type": "RULE",
363        "rule": {
364          "name": "//bar/pkg2:rule2",
365          "ruleClass": "some_kind",
366          "attribute": []
367        }
368      }
369    }
370  ]
371}'''
372            }
373        )
374        mock_run.side_effect = [mock.MagicMock(**attr) for attr in attrs]
375        with TemporaryDirectory() as tmp:
376            workspace = BazelWorkspace(tmp)
377            rules = list(workspace.get_rules('some_kind'))
378            actual = [r.label() for r in rules]
379            self.assertEqual(actual, ['//foo/pkg1:rule1', '//bar/pkg2:rule2'])
380
381    @mock.patch('subprocess.run')
382    def test_revision(self, mock_run):
383        """Tests writing an OWNERS file."""
384        attrs = {'stdout.decode.return_value': 'fake-hash'}
385        mock_run.return_value = mock.MagicMock(**attrs)
386
387        with TemporaryDirectory() as tmp:
388            workspace = BazelWorkspace(tmp)
389            self.assertEqual(workspace.revision(), 'fake-hash')
390            args, kwargs = mock_run.call_args
391            self.assertEqual(*args, ['git', 'rev-parse', 'HEAD'])
392            self.assertEqual(kwargs['cwd'], tmp)
393
394    @mock.patch('subprocess.run')
395    def test_url(self, mock_run):
396        """Tests writing an OWNERS file."""
397        attrs = {'stdout.decode.return_value': 'https://repohub.com/repo.git'}
398        mock_run.return_value = mock.MagicMock(**attrs)
399
400        with TemporaryDirectory() as tmp:
401            workspace = BazelWorkspace(tmp)
402            self.assertEqual(workspace.url(), 'https://repohub.com/repo.git')
403            args, kwargs = mock_run.call_args
404            self.assertEqual(*args, ['git', 'remote', 'get-url', 'origin'])
405            self.assertEqual(kwargs['cwd'], tmp)
406
407
408if __name__ == '__main__':
409    unittest.main()
410