#!/usr/bin/env python3 # # Copyright (C) 2020 The Android Open Source Project # # 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. """Add or update tests to TEST_MAPPING.
This script uses Bazel to find reverse dependencies on a crate and generates a
TEST_MAPPING file. It accepts the absolute path to a crate as argument. If no
argument is provided, it assumes the crate is the current directory.
This script is automatically called by external_updater.
A test_mapping_config.json file can be defined in the project directory to
configure the generated TEST_MAPPING file, for example:
{
// Run tests in postsubmit instead of presubmit. "postsubmit_tests":["foo"]
}
"""
import argparse import glob import json import os import platform import re import subprocess import sys from datetime import datetime from pathlib import Path
# Some tests requires specific options. Consider fixing the upstream crate # before updating this dictionary.
TEST_OPTIONS = { "ring_test_tests_digest_tests": [{"test-timeout": "600000"}], "ring_test_src_lib": [{"test-timeout": "100000"}],
}
# Groups to add tests to. "presubmit" runs x86_64 device tests+host tests, and # "presubmit-rust" runs arm64 device tests on physical devices.
TEST_GROUPS = [ "presubmit", "presubmit-rust", "postsubmit",
]
# Excluded tests. These tests will be ignored by this script.
TEST_EXCLUDE = [ "ash_test_src_lib", "ash_test_tests_constant_size_arrays", "ash_test_tests_display", "shared_library_test_src_lib", "vulkano_test_src_lib",
# These are helper binaries for aidl_integration_test # and aren't actually meant to run as individual tests. "aidl_test_rust_client", "aidl_test_rust_service", "aidl_test_rust_service_async",
# This is a helper binary for AuthFsHostTest and shouldn't # be run directly. "open_then_run",
# TODO: Remove when b/198197213 is closed. "diced_client_test",
class UpdaterException(Exception): """Exception generated by this script."""
class Env(object): """Env captures the execution environment.
It ensures this script is executed within an AOSP repository.
Attributes:
ANDROID_BUILD_TOP: A string representing the absolute path to the top
of the repository. """ def __init__(self): try:
self.ANDROID_BUILD_TOP = os.environ['ANDROID_BUILD_TOP'] except KeyError: raise UpdaterException('$ANDROID_BUILD_TOP is not defined; you ' 'must first source build/envsetup.sh and ' 'select a target.')
class Bazel(object): """Bazel wrapper.
The wrapper is used to call bazel queryview and generate the list of
reverse dependencies.
Attributes:
path: The path to the bazel executable. """ def __init__(self, env): """Constructor.
Note that the current directory is changed to ANDROID_BUILD_TOP.
Args:
env: An instance of Env.
Raises:
UpdaterException: an error occurred while calling soong_ui. """ if platform.system() != 'Linux': raise UpdaterException('This script has only been tested on Linux.')
self.path = os.path.join(env.ANDROID_BUILD_TOP, "build", "bazel", "bin", "bazel")
soong_ui = os.path.join(env.ANDROID_BUILD_TOP, "build", "soong", "soong_ui.bash")
# soong_ui requires to be at the root of the repository.
os.chdir(env.ANDROID_BUILD_TOP)
print("Building Bazel Queryview. This can take a couple of minutes...")
cmd = [soong_ui, "--build-mode", "--all-modules", "--dir=.", "queryview"] try:
subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True) except subprocess.CalledProcessError as e: raise UpdaterException('Unable to update TEST_MAPPING: ' + e.output)
def query_modules(self, path): """Returns all modules for a given path."""
cmd = self.path + " query --config=queryview /" + path + ":all"
out = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, text=True).strip().split("\n")
modules = set() for line in out: # speed up by excluding unused modules. if"windows_x86"in line: continue
modules.add(line) return modules
def query_rdeps(self, module): """Returns all reverse dependencies for a single module."""
cmd = (self.path + " query --config=queryview \'rdeps(//..., " +
module + ")\' --output=label_kind")
out = (subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, text=True)
.strip().split("\n")) if''in out:
out.remove('') return out
def exclude_module(self, module): for path in EXCLUDE_PATHS: if module.startswith(path): returnTrue returnFalse
# Return all the TEST_MAPPING files within a given path. def find_all_test_mapping_files(self, path):
result = [] for root, dirs, files in os.walk(path): if"TEST_MAPPING"in files:
result.append(os.path.join(root, "TEST_MAPPING")) return result
# For a given test, return the TEST_MAPPING file where the test is mapped. # This limits the search to the directory specified in "path" along with its subdirs. def test_to_test_mapping(self, env, path, test):
test_mapping_files = self.find_all_test_mapping_files(env.ANDROID_BUILD_TOP + path) for file in test_mapping_files: with open(file) as fd: if"\""+ test + "\""in fd.read():
mapping_path = file.split("/TEST_MAPPING")[0].split("//")[1] return mapping_path
returnNone
# Returns: # rdep_test: for tests specified locally. # rdep_dirs: for paths to TEST_MAPPING files for reverse dependencies. # # We import directories for non-local tests because including tests directly has proven to be # fragile and burdensome. For example, whenever a project removes or renames a test, all the # TEST_MAPPING files for its reverse dependencies must be updated or we get test breakages. # That can be many tens of projects that must updated to prevent the reported breakage of tests # that no longer exist. Similarly when a test is added, it won't be run when the reverse # dependencies change unless/until update_crate_tests.py is run for its depenencies. # Importing TEST_MAPPING files instead of tests solves both of these problems. When tests are # removed, renamed, or added, only files local to the project need to be modified. # The downside is that we potentially miss some tests. But this seems like a reasonable # tradeoff. def query_rdep_tests_dirs(self, env, modules, path, exclude_dir): """Returns all reverse dependency tests for modules in this package."""
rdep_tests = set()
rdep_dirs = set()
path_pat = re.compile("^/%s:.*$" % path) for module in modules: for rdep in self.query_rdeps(module):
rule_type, _, mod = rdep.split(" ") if rule_type == "rust_test_"or rule_type == "rust_test": if self.exclude_module(mod): continue
path_match = path_pat.match(mod) if path_match ornot EXTERNAL_PAT.match(mod):
rdep_path = mod.split(":")[0]
rdep_test = mod.split(":")[1].split("--")[0]
mapping_path = self.test_to_test_mapping(env, rdep_path, rdep_test) # Only include tests directly if they're local to the project. if (mapping_path isnotNone) and exclude_dir.endswith(mapping_path):
rdep_tests.add(rdep_test) # All other tests are included by path. elif mapping_path isnotNone:
rdep_dirs.add(mapping_path) else:
label_match = LABEL_PAT.match(mod) if label_match:
rdep_dirs.add(label_match.group(1)) return (rdep_tests, rdep_dirs)
class Package(object): """A Bazel package.
Attributes:
dir: The absolute path to this package.
dir_rel: The relative path to this package.
rdep_tests: The list of computed reverse dependencies.
rdep_dirs: The list of computed reverse dependency directories. """ def __init__(self, path, env, bazel): """Constructor.
Note that the current directory is changed to the package location when
called.
Args:
path: Path to the package.
env: An instance of Env.
bazel: An instance of Bazel.
Raises:
UpdaterException: the package does not appear to belong to the
current repository. """
self.dir = path try:
self.dir_rel = self.dir.split(env.ANDROID_BUILD_TOP)[1] except IndexError: raise UpdaterException('The path ' + self.dir + ' is not under ' +
env.ANDROID_BUILD_TOP + '; You must be in the ' 'directory of a crate or pass its absolute path ' 'as the argument.')
# Move to the package_directory.
os.chdir(self.dir)
modules = bazel.query_modules(self.dir_rel)
(self.rdep_tests, self.rdep_dirs) = bazel.query_rdep_tests_dirs(env, modules,
self.dir_rel, self.dir)
def tests_dirs_to_mapping(self, tests, dirs): """Translate the test list into a dictionary."""
test_mapping = {"imports": []}
config = None if os.path.isfile(os.path.join(self.package.dir, "test_mapping_config.json")): with open(os.path.join(self.package.dir, "test_mapping_config.json"), 'r') as fd:
config = json.load(fd)
for test_group in TEST_GROUPS:
test_mapping[test_group] = [] for test in tests: if test in TEST_EXCLUDE: continue if config and'postsubmit_tests'in config: if test in config['postsubmit_tests'] and'postsubmit'notin test_group: continue if test notin config['postsubmit_tests'] and'postsubmit'in test_group: continue else: if'postsubmit'in test_group: # If postsubmit_tests is not configured, do not place # anything in postsubmit - presubmit groups are # automatically included in postsubmit in CI. continue if test in TEST_OPTIONS:
test_mapping[test_group].append({"name": test, "options": TEST_OPTIONS[test]}) else:
test_mapping[test_group].append({"name": test})
test_mapping[test_group] = sorted(test_mapping[test_group], key=lambda t: t["name"])
for dir in dirs:
test_mapping["imports"].append({"path": dir})
test_mapping["imports"] = sorted(test_mapping["imports"], key=lambda t: t["path"])
test_mapping = {section: entry for (section, entry) in test_mapping.items() if entry} return test_mapping
def write_test_mapping(self, test_mapping): """Writes the TEST_MAPPING file.""" with open("TEST_MAPPING", "w") as json_file:
json_file.write("// Generated by update_crate_tests.py for tests that depend on this crate.\n")
json.dump(test_mapping, json_file, indent=2, separators=(',', ': '), sort_keys=True)
json_file.write("\n")
print("TEST_MAPPING successfully updated for %s!" % self.package.dir_rel)
def parse_args():
parser = argparse.ArgumentParser('update_crate_tests')
parser.add_argument('paths',
nargs='*',
help='Absolute or relative paths of the projects as globs.')
parser.add_argument('--branch_and_commit',
action='store_true',
help='Starts a new branch and commit changes.')
parser.add_argument('--push_change',
action='store_true',
help='Pushes change to Gerrit.') return parser.parse_args()
def main():
args = parse_args()
paths = args.paths if len(args.paths) > 0else [os.getcwd()] # We want to use glob to get all the paths, so we first convert to absolute.
paths = [Path(path).resolve() for path in paths]
paths = sorted([path for abs_path in paths for path in glob.glob(str(abs_path))])
env = Env()
bazel = Bazel(env) for path in paths: try:
test_mapping = TestMapping(env, bazel, path)
test_mapping.create()
changed = (subprocess.call(['git', 'diff', '--quiet']) == 1)
untracked = (os.path.isfile('TEST_MAPPING') and
(subprocess.run(['git', 'ls-files', '--error-unmatch', 'TEST_MAPPING'],
stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL).returncode == 1)) if args.branch_and_commit and (changed or untracked):
subprocess.check_output(['repo', 'start', 'tmp_auto_test_mapping', '.'])
subprocess.check_output(['git', 'add', 'TEST_MAPPING']) # test_mapping_config.json is not always present
subprocess.call(['git', 'add', 'test_mapping_config.json'],
stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL)
subprocess.check_output(['git', 'commit', '-m', 'Update TEST_MAPPING\n\nTest: None']) if args.push_change and (changed or untracked):
date = datetime.today().strftime('%m-%d')
subprocess.check_output(['git', 'push', 'aosp', 'HEAD:refs/for/master', '-o', 'topic=test-mapping-%s' % date]) except (UpdaterException, subprocess.CalledProcessError) as err:
sys.exit("Error: " + str(err))
if __name__ == '__main__':
main()
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-06-27)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.