First check-in of configuration overhead measurement tool
i.e. first check-in of [Measuring Configuration Overhead](https://docs.google.com/document/d/10ZxO2wZdKJATnYBqAm22xT1k5r4Vp6QX96TkqSUIhs0/edit).
This just establishes supporting structure. The tool is not yet functional.
Specifically:
- `types.py`: defines data structures for "configuration" and "configured target"
- `bazel_api.py`: API to translate `bazel cquery` and `bazel config` calls into the above data structures
- `bazel_api_test.py`: tests
- `ctexplain.py`: stump of an entry point
The tests utilize an existing Python test framework for invoking Bazel (`//src/test/py/bazel:test_base`).
Work towards https://github.com/bazelbuild/bazel/issues/10613
Closes #11511.
PiperOrigin-RevId: 321409588
diff --git a/tools/ctexplain/bazel_api_test.py b/tools/ctexplain/bazel_api_test.py
new file mode 100644
index 0000000..3587251
--- /dev/null
+++ b/tools/ctexplain/bazel_api_test.py
@@ -0,0 +1,147 @@
+# Lint as: python3
+# Copyright 2020 The Bazel Authors. All rights reserved.
+#
+# 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
+#
+# http://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 bazel_api.py."""
+import os
+import unittest
+from src.test.py.bazel import test_base
+from tools.ctexplain.bazel_api import BazelApi
+from tools.ctexplain.types import HostConfiguration
+from tools.ctexplain.types import NullConfiguration
+
+
+class BazelApiTest(test_base.TestBase):
+
+ _bazel_api: BazelApi = None
+
+ def setUp(self):
+ test_base.TestBase.setUp(self)
+ self._bazel_api = BazelApi(self.RunBazel)
+ self.ScratchFile('WORKSPACE')
+ self.CreateWorkspaceWithDefaultRepos('repo/WORKSPACE')
+
+ def tearDown(self):
+ test_base.TestBase.tearDown(self)
+
+ def testBasicCquery(self):
+ self.ScratchFile('testapp/BUILD', [
+ 'filegroup(name = "fg", srcs = ["a.file"])',
+ ])
+ res = self._bazel_api.cquery(['//testapp:all'])
+ success = res[0]
+ cts = res[2]
+ self.assertTrue(success)
+ self.assertEqual(len(cts), 1)
+ self.assertEqual(cts[0].label, '//testapp:fg')
+ self.assertIsNone(cts[0].config)
+ self.assertGreater(len(cts[0].config_hash), 10)
+ self.assertIn('PlatformConfiguration', cts[0].transitive_fragments)
+
+ def testFailedCquery(self):
+ self.ScratchFile('testapp/BUILD', [
+ 'filegroup(name = "fg", srcs = ["a.file"])',
+ ])
+ (success, stderr, cts) = self._bazel_api.cquery(['//testapp:typo'])
+ self.assertFalse(success)
+ self.assertEqual(len(cts), 0)
+ self.assertIn("target 'typo' not declared in package 'testapp'",
+ os.linesep.join(stderr))
+
+ def testTransitiveFragmentsAccuracy(self):
+ self.ScratchFile('testapp/BUILD', [
+ 'filegroup(name = "fg", srcs = ["a.file"])',
+ 'filegroup(name = "ccfg", srcs = [":ccbin"])',
+ 'cc_binary(name = "ccbin", srcs = ["ccbin.cc"])'
+ ])
+ cts1 = self._bazel_api.cquery(['//testapp:fg'])[2]
+ self.assertNotIn('CppConfiguration', cts1[0].transitive_fragments)
+ cts2 = self._bazel_api.cquery(['//testapp:ccfg'])[2]
+ self.assertIn('CppConfiguration', cts2[0].transitive_fragments)
+
+ def testGetTargetConfig(self):
+ self.ScratchFile('testapp/BUILD', [
+ 'filegroup(name = "fg", srcs = ["a.file"])',
+ ])
+ cts = self._bazel_api.cquery(['//testapp:fg'])[2]
+ config = self._bazel_api.get_config(cts[0].config_hash)
+ expected_fragments = ['PlatformConfiguration', 'JavaConfiguration']
+ for exp in expected_fragments:
+ self.assertIn(exp, config.fragments)
+ core_options = config.options['CoreOptions']
+ self.assertIsNotNone(core_options)
+ self.assertIn(('stamp', 'false'), core_options.items())
+
+ def testGetHostConfig(self):
+ self.ScratchFile('testapp/BUILD', [
+ 'genrule(',
+ ' name = "g",',
+ ' srcs = [],',
+ ' cmd = "",',
+ ' outs = ["g.out"],',
+ ' tools = [":fg"])',
+ 'filegroup(name = "fg", srcs = ["a.file"])',
+ ])
+ query = ['//testapp:fg', '--universe_scope=//testapp:g']
+ cts = self._bazel_api.cquery(query)[2]
+ config = self._bazel_api.get_config(cts[0].config_hash)
+ self.assertIsInstance(config, HostConfiguration)
+ # We don't currently populate or read a host configuration's details.
+ self.assertEqual(len(config.fragments), 0)
+ self.assertEqual(len(config.options), 0)
+
+ def testGetNullConfig(self):
+ self.ScratchFile('testapp/BUILD', [
+ 'filegroup(name = "fg", srcs = ["a.file"])',
+ ])
+ cts = self._bazel_api.cquery(['//testapp:a.file'])[2]
+ config = self._bazel_api.get_config(cts[0].config_hash)
+ self.assertIsInstance(config, NullConfiguration)
+ # Null configurations have no information by definition.
+ self.assertEqual(len(config.fragments), 0)
+ self.assertEqual(len(config.options), 0)
+
+ def testConfigWithDefines(self):
+ self.ScratchFile('testapp/BUILD', [
+ 'filegroup(name = "fg", srcs = ["a.file"])',
+ ])
+ cquery_args = ['//testapp:fg', '--define', 'a=b']
+ cts = self._bazel_api.cquery(cquery_args)[2]
+ config = self._bazel_api.get_config(cts[0].config_hash)
+ user_defined_options = config.options['user-defined']
+ self.assertIsNotNone(user_defined_options)
+ self.assertDictEqual(user_defined_options._dict, {'--define:a': 'b'})
+
+ def testConfigWithStarlarkFlags(self):
+ self.ScratchFile('testapp/defs.bzl', [
+ 'def _flag_impl(settings, attr):', ' pass', 'string_flag = rule(',
+ ' implementation = _flag_impl,',
+ ' build_setting = config.string(flag = True)'
+ ')'
+ ])
+ self.ScratchFile('testapp/BUILD', [
+ 'load(":defs.bzl", "string_flag")',
+ 'string_flag(name = "my_flag", build_setting_default = "nada")',
+ 'filegroup(name = "fg", srcs = ["a.file"])',
+ ])
+ cquery_args = ['//testapp:fg', '--//testapp:my_flag', 'algo']
+ cts = self._bazel_api.cquery(cquery_args)[2]
+ config = self._bazel_api.get_config(cts[0].config_hash)
+ user_defined_options = config.options['user-defined']
+ self.assertIsNotNone(user_defined_options)
+ self.assertDictEqual(user_defined_options._dict,
+ {'//testapp:my_flag': 'algo'})
+
+
+if __name__ == '__main__':
+ unittest.main()