blob: e6fbbf2d545714b7017aaebafde5c021d6db58d5 [file] [log] [blame]
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
import string
import subprocess
from typing import Callable, Iterable, List, Set, Tuple
class BazelQuery:
"""A collection of functions useful for constructing Bazel queries."""
@staticmethod
def rule_exact(rule: str, input: str) -> str:
"""Match targets in `input` defined by `rule`."""
return f'kind("^{rule} rule$", {input})'
@staticmethod
def tag_exact(tag: str, input: str) -> str:
"""Match targets in `input` that are tagged with `tag`."""
regex = BazelQuery.regex_for_tag(tag)
return f'attr("tags", "{regex}", {input})'
@staticmethod
def regex_for_tag(tag: str) -> str:
"""Build a regex to find the given tag in a list.
The query `attr("tags", pattern, input)` would match the given pattern
against a serialized list of tags, e.g. `[foo, bar]`. Subtly, if the
pattern is "foo", it will also match targets that are tagged "foobar".
This function constructs a regex that matches only when the list of tags
actually contains `tag`, not just a superstring of `tag`, as recommended
by the Bazel query docs: https://bazel.build/query/language#attr
"""
return f"[\\[ ]{tag}[,\\]]"
class BazelQueryRunner:
def __init__(self, backend: Callable[[str], List[str]] = None):
self._backend = backend
def find_targets_with_banned_chars(self) -> Iterable[Tuple[str, Set[str]]]:
"""Find targets in //... with names containing disallowed characters.
Bazel allows a liberal set of characters in target names [0], which
enables type confusion bugs to go unnoticed. For example, a tuple's
string representation, complete with parentheses and commas, can
accidentally be inserted into a target name via `string.format()`.
[0]: https://bazel.build/concepts/labels#target-names
Yields:
A tuple for each target that has banned characters in its name. The
first element is the Bazel target's name. The second element is the
set of characters that must be removed from the target's name.
"""
allowed_chars = set(string.ascii_letters + string.digits + '/:_-.')
for target in self.query("//..."):
if bad_chars := set(target) - allowed_chars:
yield (target, bad_chars)
def find_empty_test_suites(self) -> Iterable[str]:
"""Find test_suite targets in //... that contain zero tests.
Finding empty test suites is not as simple as querying for test_suite
targets where `tests = []`. In fact, that is a special case of
test_suite that causes it to select all tests in the package.
There are a few ways to wind up with an empty test suite. One way is to
provide a nonempty `tests` argument containing only nonexistent labels.
Another way is to provide a non-empty list of tests that exist, but to
specify `tags` that exclude all the tests.
Yields:
Names of Bazel targets.
"""
def print_progress(i, n):
if n // 5 == 0 or i % (n // 5) == 0:
print(self.find_empty_test_suites.__name__, end="")
print(": Running query {}/{}...".format(i + 1, n))
query_test_suites = BazelQuery.rule_exact("test_suite", "//...")
all_test_suites = self.query(query_test_suites)
for i, test_suite in enumerate(all_test_suites):
print_progress(i, len(all_test_suites))
tests = self.query("tests({})".format(test_suite))
if len(tests) == 0:
yield test_suite
def find_non_manual_test_suites(self) -> Iterable[str]:
"""Find test_suite targets in //... that are not tagged with 'manual'."""
query_pieces = [
BazelQuery.rule_exact("test_suite", "//..."),
"except",
BazelQuery.tag_exact("manual", "//..."),
]
query_str = " ".join(query_pieces)
return self.query(query_str)
def query(self, query: str) -> List[str]:
"""Perform a Bazel query and return the resulting targets."""
if self._backend:
return self._backend(query)
bazel = subprocess.run(
["./bazelisk.sh", "query", "--output=label", query],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
encoding='utf-8',
check=True)
stdout_lines = bazel.stdout.split('\n')
return [s for s in stdout_lines if s != ""]