blob: 5215c699fa2d07d7ffb126b98cab1788f6633978 [file]
# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Tests for the pw_build.bazel_query module."""
import json
import unittest
from unittest import mock
from unittest.mock import call
from pw_build.bazel_label import BazelLabel
from pw_build.bazel_query import (
ParseError,
BazelRule,
BazelRepo,
)
# Test fixtures.
class MockCalls:
def __init__(self, mock_run) -> None:
self._calls: list[tuple] = []
self._mock_run = mock_run
self._side_effects: list[mock.MagicMock] = []
self._mock_run.side_effect = self._side_effects
def add(self, call_obj: tuple, retval: str) -> None:
self._calls.extend([call_obj])
self._side_effects.extend(
[
mock.MagicMock(**r)
for r in [{'stdout.decode.return_value': retval}]
],
)
def check(self):
self._mock_run.assert_has_calls(self._calls)
# Unit tests.
class TestBazelRule(unittest.TestCase):
"""Tests for bazel_query.BazelRule."""
def test_rule_parse_invalid(self):
"""Test for parsing invalid rule attributes."""
rule = BazelRule('kind', '@repo//package:target')
with self.assertRaises(ParseError):
rule.parse_attrs(
json.loads(
'''[{
"name": "invalid_attr",
"type": "ESOTERIC",
"intValue": 0,
"stringValue": "false",
"explicitlySpecified": true,
"booleanValue": false
}]'''
)
)
def test_rule_parse_boolean_unspecified(self):
"""Test parsing an unset boolean rule attribute."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "bool_attr",
"type": "BOOLEAN",
"intValue": 0,
"stringValue": "false",
"explicitlySpecified": false,
"booleanValue": false
}]'''
)
)
self.assertFalse(rule.has_attr('bool_attr'))
def test_rule_parse_boolean_false(self):
"""Tests parsing boolean rule attribute set to false."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "bool_attr",
"type": "BOOLEAN",
"intValue": 0,
"stringValue": "false",
"explicitlySpecified": true,
"booleanValue": false
}]'''
)
)
self.assertTrue(rule.has_attr('bool_attr'))
self.assertFalse(rule.get_bool('bool_attr'))
def test_rule_parse_boolean_true(self):
"""Tests parsing a boolean rule attribute set to true."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "bool_attr",
"type": "BOOLEAN",
"intValue": 1,
"stringValue": "true",
"explicitlySpecified": true,
"booleanValue": true
}]'''
)
)
self.assertTrue(rule.has_attr('bool_attr'))
self.assertTrue(rule.get_bool('bool_attr'))
def test_rule_parse_integer_unspecified(self):
"""Tests parsing an unset integer rule attribute."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "int_attr",
"type": "INTEGER",
"intValue": 0,
"explicitlySpecified": false
}]'''
)
)
self.assertFalse(rule.has_attr('int_attr'))
def test_rule_parse_integer(self):
"""Tests parsing an integer rule attribute."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "int_attr",
"type": "INTEGER",
"intValue": 100,
"explicitlySpecified": true
}]'''
)
)
self.assertTrue(rule.has_attr('int_attr'))
self.assertEqual(rule.get_int('int_attr'), 100)
def test_rule_parse_string_unspecified(self):
"""Tests parsing an unset string rule attribute."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "string_attr",
"type": "STRING",
"stringValue": "",
"explicitlySpecified": false
}]'''
)
)
self.assertFalse(rule.has_attr('string_attr'))
def test_rule_parse_string(self):
"""Tests parsing a string rule attribute."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "string_attr",
"type": "STRING",
"stringValue": "hello, world!",
"explicitlySpecified": true
}]'''
)
)
self.assertTrue(rule.has_attr('string_attr'))
self.assertEqual(rule.get_str('string_attr'), 'hello, world!')
def test_rule_parse_string_list_unspecified(self):
"""Tests parsing an unset string list rule attribute."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "string_list_attr",
"type": "STRING_LIST",
"stringListValue": [],
"explicitlySpecified": false
}]'''
)
)
self.assertFalse(rule.has_attr('string_list_attr'))
def test_rule_parse_string_list(self):
"""Tests parsing a string list rule attribute."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "string_list_attr",
"type": "STRING_LIST",
"stringListValue": [ "hello", "world!" ],
"explicitlySpecified": true
}]'''
)
)
self.assertTrue(rule.has_attr('string_list_attr'))
self.assertEqual(rule.get_list('string_list_attr'), ['hello', 'world!'])
def test_rule_parse_label_list_unspecified(self):
"""Tests parsing an unset label list rule attribute."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "label_list_attr",
"type": "LABEL_LIST",
"stringListValue": [],
"explicitlySpecified": false
}]'''
)
)
self.assertFalse(rule.has_attr('label_list_attr'))
def test_rule_parse_label_list(self):
"""Tests parsing a label list rule attribute."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "label_list_attr",
"type": "LABEL_LIST",
"stringListValue": [ "hello", "world!" ],
"explicitlySpecified": true
}]'''
)
)
self.assertTrue(rule.has_attr('label_list_attr'))
self.assertEqual(rule.get_list('label_list_attr'), ['hello', 'world!'])
def test_rule_parse_string_dict_unspecified(self):
"""Tests parsing an unset string dict rule attribute."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "string_dict_attr",
"type": "LABEL_LIST",
"stringDictValue": [],
"explicitlySpecified": false
}]'''
)
)
self.assertFalse(rule.has_attr('string_dict_attr'))
def test_rule_parse_string_dict(self):
"""Tests parsing a string dict rule attribute."""
rule = BazelRule('kind', '@repo//package:target')
rule.parse_attrs(
json.loads(
'''[{
"name": "string_dict_attr",
"type": "STRING_DICT",
"stringDictValue": [
{
"key": "foo",
"value": "hello"
},
{
"key": "bar",
"value": "world"
}
],
"explicitlySpecified": true
}]'''
)
)
string_dict_attr = rule.get_dict('string_dict_attr')
self.assertTrue(rule.has_attr('string_dict_attr'))
self.assertEqual(string_dict_attr['foo'], 'hello')
self.assertEqual(string_dict_attr['bar'], 'world')
class TestRepo(unittest.TestCase):
"""Test for bazel_query.Repo."""
@mock.patch('subprocess.run')
def test_repo_get_rules(self, mock_run):
"""Tests querying a repo for a rule."""
mock_calls = MockCalls(mock_run)
mock_calls.add(
call(
[
'bazelisk',
'cquery',
'@repo//pkg:target',
'--@repo//pkg:use_optional=True',
'--output=jsonproto',
'--noshow_progress',
],
check=False,
capture_output=True,
),
json.dumps(
{
'results': [
{
'target': {
'rule': {
'name': '@repo//pkg:target',
'ruleClass': 'cc_library',
'attribute': [
{
'explicitlySpecified': True,
'name': 'hdrs',
'type': 'string_list',
'stringListValue': [
'foo/include/bar.h'
],
},
{
'explicitlySpecified': True,
'name': 'srcs',
'type': 'string_list',
'stringListValue': ['foo/bar.cc'],
},
{
'name': 'additional_linker_inputs',
'type': 'string_list',
'stringListValue': ['implicit'],
},
{
'explicitlySpecified': True,
'name': 'include_dirs',
'type': 'string_list',
'stringListValue': ['foo/include'],
},
{
'explicitlySpecified': True,
'name': 'copts',
'type': 'string_list',
'stringListValue': [
'-Wall',
'-Werror',
],
},
{
'explicitlySpecified': False,
'name': 'linkopts',
'type': 'string_list',
'stringListValue': ['implicit'],
},
{
'explicitlySpecified': True,
'name': 'defines',
'type': 'string_list',
'stringListValue': [
'-DFOO',
'-DBAR',
],
},
{
'explicitlySpecified': True,
'name': 'local_defines',
'type': 'string_list',
'stringListValue': ['-DBAZ'],
},
{
'explicitlySpecified': True,
'name': 'deps',
'type': 'string_list',
'stringListValue': [':baz'],
},
{
'explicitlySpecified': True,
'name': 'implementation_deps',
'type': 'string_list',
'stringListValue': [],
},
],
}
}
}
]
}
),
)
repo = BazelRepo('repo')
repo.generate = True
repo.options = {'@repo//pkg:use_optional': True}
labels = [BazelLabel.from_string('@repo//pkg:target')]
rules = list(repo.get_rules(labels))
rule = rules[0]
self.assertEqual(rule.get_list('hdrs'), ['foo/include/bar.h'])
self.assertEqual(rule.get_list('srcs'), ['foo/bar.cc'])
self.assertEqual(rule.get_list('additional_linker_inputs'), [])
self.assertEqual(rule.get_list('include_dirs'), ['foo/include'])
self.assertEqual(rule.get_list('copts'), ['-Wall', '-Werror'])
self.assertEqual(rule.get_list('linkopts'), [])
self.assertEqual(rule.get_list('defines'), ['-DFOO', '-DBAR'])
self.assertEqual(rule.get_list('local_defines'), ['-DBAZ'])
self.assertEqual(rule.get_list('deps'), [':baz'])
self.assertEqual(rule.get_list('implementation_deps'), [])
# Rules are cached, so a second call doesn't invoke Bazel again.
repo.get_rules(labels)
mock_calls.check()
@mock.patch('subprocess.run')
def test_repo_get_rule_no_generate(self, mock_run):
"""Tests querying a repo for a rule."""
repo = BazelRepo('repo')
repo.generate = False
labels = [BazelLabel.from_string('@repo//pkg:target')]
rules = list(repo.get_rules(labels))
self.assertEqual(rules, [])
self.assertFalse(mock_run.called)
@mock.patch('subprocess.run')
def test_repo_revision(self, mock_run):
"""Tests querying a repo for its git revision."""
mock_calls = MockCalls(mock_run)
mock_calls.add(
call(
['bazelisk', 'mod', 'show_repo', 'repo', '--noshow_progress'],
check=False,
capture_output=True,
),
'''
http_archive(
urls = ["https://github.com/fake/repo/releases/download/tag1/repo.tar.gz"],
)
''',
)
mock_calls.add(
call(
[
'git',
'ls-remote',
'https://github.com/fake/repo',
'-t',
'tag1',
],
check=True,
capture_output=True,
),
'deadbeef\trefs/tags/tag1',
)
repo = BazelRepo('repo')
self.assertEqual(repo.revision(), 'deadbeef')
mock_calls.check()
if __name__ == '__main__':
unittest.main()