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