# 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 tempfile import TemporaryDirectory from pw_build.bazel_query import ParseError, BazelRule, BazelWorkspace class TestBazelRule(unittest.TestCase): """Tests for bazel_query.Rule.""" def test_rule_top_level(self): """Tests a top-level rule with no package name.""" rule = BazelRule('//:no-package', 'custom-type') self.assertEqual(rule.package(), '') def test_rule_with_label(self): """Tests a rule with a package and target name.""" rule = BazelRule('//foo:target', 'custom-type') self.assertEqual(rule.package(), 'foo') self.assertEqual(rule.label(), '//foo:target') def test_rule_in_subdirectory(self): """Tests a rule in a subdirectory.""" rule = BazelRule('//foo:bar/target', 'custom-type') self.assertEqual(rule.package(), 'foo') self.assertEqual(rule.label(), '//foo:bar/target') def test_rule_in_subpackage(self): """Tests a rule in a subpackage.""" rule = BazelRule('//foo/bar:target', 'custom-type') self.assertEqual(rule.package(), 'foo/bar') self.assertEqual(rule.label(), '//foo/bar:target') def test_rule_no_target(self): """Tests a rule with only a package name.""" rule = BazelRule('//foo/bar', 'custom-type') self.assertEqual(rule.package(), 'foo/bar') self.assertEqual(rule.label(), '//foo/bar:bar') def test_rule_invalid_relative(self): """Tests a rule with an invalid (non-absolute) package name.""" with self.assertRaises(ParseError): BazelRule('../foo/bar:target', 'custom-type') def test_rule_invalid_double_colon(self): """Tests a rule with an invalid (non-absolute) package name.""" with self.assertRaises(ParseError): BazelRule('//foo:bar:target', 'custom-type') def test_rule_parse_invalid(self): """Test for parsing invalid rule attributes.""" rule = BazelRule('//package:target', 'kind') with self.assertRaises(ParseError): rule.parse( 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('//package:target', 'kind') rule.parse( 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('//package:target', 'kind') rule.parse( 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('//package:target', 'kind') rule.parse( 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('//package:target', 'kind') rule.parse( 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('//package:target', 'kind') rule.parse( 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('//package:target', 'kind') rule.parse( 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('//package:target', 'kind') rule.parse( 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('//package:target', 'kind') rule.parse( 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('//package:target', 'kind') rule.parse( 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('//package:target', 'kind') rule.parse( 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('//package:target', 'kind') rule.parse( 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('//package:target', 'kind') rule.parse( 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('//package:target', 'kind') rule.parse( 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 TestWorkspace(unittest.TestCase): """Test for bazel_query.Workspace.""" @mock.patch('subprocess.run') def test_workspace_get_rules(self, mock_run): """Tests querying a workspace for Bazel rules.""" attrs = [] # `bazel query //... --output=package attrs.append( { 'stdout.decode.return_value': ''' foo/pkg1 bar/pkg2''' } ) # bazel query buildfiles(//foo:*) --output=xml attrs.append( { 'stdout.decode.return_value': ''' ''' } ) # bazel query buildfiles(//bar:*) --output=xml attrs.append( { 'stdout.decode.return_value': ''' ''' } ) # bazel cquery kind(some_kind, //...) --output=jsonproto attrs.append( { 'stdout.decode.return_value': ''' { "results": [ { "target": { "type": "RULE", "rule": { "name": "//foo/pkg1:rule1", "ruleClass": "some_kind", "attribute": [] } } }, { "target": { "type": "RULE", "rule": { "name": "//bar/pkg2:rule2", "ruleClass": "some_kind", "attribute": [] } } } ] }''' } ) mock_run.side_effect = [mock.MagicMock(**attr) for attr in attrs] with TemporaryDirectory() as tmp: workspace = BazelWorkspace(tmp) rules = list(workspace.get_rules('some_kind')) actual = [r.label() for r in rules] self.assertEqual(actual, ['//foo/pkg1:rule1', '//bar/pkg2:rule2']) @mock.patch('subprocess.run') def test_revision(self, mock_run): """Tests writing an OWNERS file.""" attrs = {'stdout.decode.return_value': 'fake-hash'} mock_run.return_value = mock.MagicMock(**attrs) with TemporaryDirectory() as tmp: workspace = BazelWorkspace(tmp) self.assertEqual(workspace.revision(), 'fake-hash') args, kwargs = mock_run.call_args self.assertEqual(*args, ['git', 'rev-parse', 'HEAD']) self.assertEqual(kwargs['cwd'], tmp) @mock.patch('subprocess.run') def test_url(self, mock_run): """Tests writing an OWNERS file.""" attrs = {'stdout.decode.return_value': 'https://repohub.com/repo.git'} mock_run.return_value = mock.MagicMock(**attrs) with TemporaryDirectory() as tmp: workspace = BazelWorkspace(tmp) self.assertEqual(workspace.url(), 'https://repohub.com/repo.git') args, kwargs = mock_run.call_args self.assertEqual(*args, ['git', 'remote', 'get-url', 'origin']) self.assertEqual(kwargs['cwd'], tmp) if __name__ == '__main__': unittest.main()