| # pylint: disable=g-backslash-continuation |
| # Copyright 2023 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 the bash completion for external repositories.""" |
| |
| import os |
| import subprocess |
| import tempfile |
| from absl.testing import absltest |
| import runfiles |
| from src.test.py.bazel import test_base |
| from src.test.py.bazel.bzlmod.test_utils import BazelRegistry |
| from src.test.py.bazel.bzlmod.test_utils import scratchFile |
| |
| |
| class ExternalRepoCompletionTest(test_base.TestBase): |
| """Test class for bash completion for external.""" |
| |
| def setUp(self): |
| test_base.TestBase.setUp(self) |
| r = runfiles.Create() |
| self.completion_script = r.Rlocation('io_bazel/scripts/bazel-complete.bash') |
| self.bazel_binary = r.Rlocation('io_bazel/src/bazel') |
| |
| self.registries_work_dir = tempfile.mkdtemp(dir=self._test_cwd) |
| self.main_registry = BazelRegistry( |
| os.path.join(self.registries_work_dir, 'main') |
| ) |
| self.main_registry.setModuleBasePath('projects') |
| self.projects_dir = self.main_registry.projects |
| self.maxDiff = None # there are some long diffs in this test |
| |
| self.ScratchFile( |
| '.bazelrc', |
| [ |
| # The command completion script invokes bazel with arguments we |
| # don't control, so we need to import the default test .bazelrc |
| # here. |
| 'import ' + self._test_bazelrc, |
| # In ipv6 only network, this has to be enabled. |
| # 'startup --host_jvm_args=-Djava.net.preferIPv6Addresses=true', |
| 'common --noenable_workspace', |
| 'common --registry=' + self.main_registry.getURL(), |
| # We need to have BCR here to make sure built-in modules like |
| # bazel_tools can work. |
| 'common --registry=https://bcr.bazel.build', |
| # Disable yanked version check so we are not affected BCR changes. |
| 'common --allow_yanked_versions=all', |
| # Make sure Bazel CI tests pass in all environments |
| 'common --charset=ascii', |
| ], |
| ) |
| |
| self.ScratchFile( |
| 'MODULE.bazel', |
| [ |
| 'module(name = "my_project", version = "1.0")', |
| '', |
| 'bazel_dep(name = "foo", version = "1.0", repo_name = "foo")', |
| 'bazel_dep(name = "foo", version = "2.0", repo_name = "foobar")', |
| 'bazel_dep(name = "ext", version = "1.0")', |
| 'bazel_dep(name = "ext2", version = "1.0")', |
| 'multiple_version_override(', |
| ' module_name= "foo",', |
| ' versions = ["1.0", "2.0"],', |
| ')', |
| 'ext = use_extension("@ext//:ext.bzl", "ext")', |
| 'use_repo(ext, myrepo="repo1")', |
| 'ext2 = use_extension("@ext2//:ext.bzl", "ext")', |
| 'ext2.dep(name="repo1")', |
| 'use_repo(ext2, myrepo2="repo1")', |
| ], |
| ) |
| self.ScratchFile('pkg/BUILD', ['cc_library(name = "my_lib")']) |
| self.main_registry.createCcModule( |
| 'foo', |
| '1.0', |
| {'bar': '1.0', 'ext': '1.0'}, |
| {'bar': 'bar_from_foo1'}, |
| extra_module_file_contents=[ |
| 'my_ext = use_extension("@ext//:ext.bzl", "ext")', |
| 'my_ext.dep(name="repo1")', |
| 'my_ext.dep(name="repo2")', |
| 'my_ext.dep(name="repo5")', |
| 'use_repo(my_ext, my_repo1="repo1")', |
| ], |
| ) |
| self.main_registry.createCcModule( |
| 'foo', |
| '2.0', |
| {'bar': '2.0', 'ext': '1.0'}, |
| {'bar': 'bar_from_foo2', 'ext': 'ext_mod'}, |
| extra_module_file_contents=[ |
| 'my_ext = use_extension("@ext_mod//:ext.bzl", "ext")', |
| 'my_ext.dep(name="repo4")', |
| 'use_repo(my_ext, my_repo3="repo3", my_repo4="repo4")', |
| ], |
| ) |
| self.main_registry.createCcModule('bar', '1.0', {'ext': '1.0'}) |
| self.main_registry.createCcModule( |
| 'bar', |
| '2.0', |
| {'ext': '1.0', 'ext2': '1.0'}, |
| extra_module_file_contents=[ |
| 'my_ext = use_extension("@ext//:ext.bzl", "ext")', |
| 'my_ext.dep(name="repo3")', |
| 'use_repo(my_ext, my_repo3="repo3")', |
| 'my_ext2 = use_extension("@ext2//:ext.bzl", "ext")', |
| 'my_ext2.dep(name="repo3")', |
| 'use_repo(my_ext2, my_repo2="repo3")', |
| ], |
| ) |
| |
| ext_src = [ |
| 'def _data_repo_impl(ctx): ctx.file("BUILD")', |
| 'data_repo = repository_rule(_data_repo_impl,', |
| ' attrs={"data":attr.string()},', |
| ')', |
| 'def _ext_impl(ctx):', |
| ' deps = {dep.name: 1 for mod in ctx.modules for dep in mod.tags.dep}', |
| ' for dep in deps:', |
| ' data_repo(name=dep, data="requested repo")', |
| 'ext=module_extension(_ext_impl,', |
| ' tag_classes={"dep":tag_class(attrs={"name":attr.string()})},', |
| ')', |
| ] |
| |
| self.main_registry.createLocalPathModule('ext', '1.0', 'ext') |
| scratchFile( |
| self.projects_dir.joinpath('ext', 'BUILD'), |
| ['cc_library(name="lib_ext")'], |
| ) |
| scratchFile( |
| self.projects_dir.joinpath('ext', 'tools', 'BUILD'), |
| ['cc_binary(name="tool")'], |
| ) |
| scratchFile( |
| self.projects_dir.joinpath('ext', 'tools', 'zip', 'BUILD'), |
| ['cc_binary(name="zipper")'], |
| ) |
| scratchFile(self.projects_dir.joinpath('ext', 'ext.bzl'), ext_src) |
| self.main_registry.createLocalPathModule('ext2', '1.0', 'ext2') |
| scratchFile( |
| self.projects_dir.joinpath('ext2', 'BUILD'), |
| ['cc_library(name="lib_ext2")'], |
| ) |
| scratchFile(self.projects_dir.joinpath('ext2', 'ext.bzl'), ext_src) |
| |
| def complete(self, bazel_args): |
| """Get the bash completions for the given "bazel" command line.""" |
| |
| # The full command line to complete as typed by the user |
| # (may end with a space). |
| comp_line = 'bazel ' + bazel_args |
| # The index of the cursor position relative to the beginning of COMP_LINE. |
| comp_point = len(comp_line) |
| # The index of the word to be completed in COMP_LINE. |
| comp_cword = len(comp_line.split(' ')) |
| script = """ |
| source {completion_script} |
| COMP_WORDS=({comp_line}) |
| _bazel__complete |
| echo ${{COMPREPLY[*]}} |
| """.format( |
| completion_script=self.completion_script, |
| comp_line=comp_line, |
| ) |
| env = os.environ.copy() |
| env.update({ |
| # Have the completion script use the Bazel binary provided by the test |
| # runner. |
| 'BAZEL': self.bazel_binary, |
| 'COMP_LINE': comp_line, |
| 'COMP_POINT': str(comp_point), |
| 'COMP_CWORD': str(comp_cword), |
| }) |
| p = subprocess.Popen( |
| ['bash', '-c', script], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| env=env, |
| ) |
| stdout, _ = p.communicate() |
| return stdout.decode('utf-8').split() |
| |
| def testCompletions(self): |
| # The completion requires an external repository to have been fetched to |
| # complete its contents. We use RunProgram instead of RunBazel as the latter |
| # would evaluate the test .bazelrc twice, which we explicitly import in the |
| # test workspace's .bazelrc. |
| self.RunProgram([self.bazel_binary, 'fetch', '@ext//...', '@foobar//...']) |
| |
| # Apparent repo names are completed. |
| self.assertCountEqual( |
| [ |
| '@', |
| '@//', |
| '@bazel_tools', |
| '@local_config_platform', |
| '@foo', |
| '@foobar', |
| '@my_project', |
| '@myrepo', |
| '@myrepo2', |
| '@ext', |
| '@ext2', |
| ], |
| self.complete('build @'), |
| ) |
| self.assertCountEqual(['@foo', '@foobar'], self.complete('build @fo')) |
| self.assertCountEqual( |
| ['@foo', '@foo//', '@foobar'], self.complete('build @foo') |
| ) |
| self.assertCountEqual( |
| ['@foobar', '@foobar//'], self.complete('build @foobar') |
| ) |
| self.assertCountEqual(['@my_project'], self.complete('build @my_')) |
| self.assertCountEqual([], self.complete('build @does_not_exist')) |
| |
| # Canonical repo names are not completed. |
| self.assertCountEqual([], self.complete('build @@')) |
| self.assertCountEqual([], self.complete('build @@foo~2.')) |
| |
| # Packages are completed in external repos with apparent repo names. |
| self.assertCountEqual( |
| ['@ext//tools/', '@ext//tools:'], self.complete('build @ext//tool') |
| ) |
| self.assertCountEqual( |
| ['@ext//tools/zip/', '@ext//tools/zip:'], |
| self.complete('build @ext//tools/zi'), |
| ) |
| self.assertCountEqual( |
| ['@my_project//pkg/', '@my_project//pkg:'], |
| self.complete('build @my_project//p'), |
| ) |
| self.assertCountEqual(['@//pkg/', '@//pkg:'], self.complete('build @//p')) |
| self.assertCountEqual([], self.complete('build @does_not_exist//')) |
| |
| # Packages are completed in external repos with canonical repo names. |
| self.assertCountEqual( |
| ['@@ext~//tools/', '@@ext~//tools:'], |
| self.complete('build @@ext~//tool'), |
| ) |
| self.assertCountEqual( |
| ['@@ext~//tools/zip/', '@@ext~//tools/zip:'], |
| self.complete('build @@ext~//tools/zi'), |
| ) |
| self.assertCountEqual( |
| ['@@//pkg/', '@@//pkg:'], self.complete('build @@//p') |
| ) |
| self.assertCountEqual([], self.complete('build @@does_not_exist//')) |
| |
| # Targets are completed in external repos with apparent repo names. |
| self.assertCountEqual(['@foobar//:'], self.complete('build @foobar/')) |
| self.assertCountEqual(['@foobar//:'], self.complete('build @foobar//')) |
| # Completions operate on the last word, which is broken on ':'. |
| self.assertCountEqual(['lib_foo'], self.complete('build @foobar//:')) |
| self.assertCountEqual( |
| ['zipper'], self.complete('build @ext//tools/zip:zipp') |
| ) |
| self.assertCountEqual( |
| ['my_lib'], self.complete('build @my_project//pkg:my_') |
| ) |
| |
| # Targets are completed in external repos with canonical repo names. |
| self.assertCountEqual(['lib_foo'], self.complete('build @@foo~2.0//:')) |
| self.assertCountEqual( |
| ['zipper'], self.complete('build @@ext~//tools/zip:zipp') |
| ) |
| self.assertCountEqual(['my_lib'], self.complete('build @@//pkg:my_')) |
| |
| |
| if __name__ == '__main__': |
| absltest.main() |