| # Copyright 2018, 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. |
| |
| """ |
| Module Finder class. |
| """ |
| |
| # pylint: disable=line-too-long |
| |
| import logging |
| import os |
| import time |
| |
| import atest_configs |
| import atest_error |
| import atest_utils |
| import constants |
| |
| from metrics import metrics |
| from test_finders import test_info |
| from test_finders import test_finder_base |
| from test_finders import test_finder_utils |
| from test_runners import atest_tf_test_runner |
| from test_runners import robolectric_test_runner |
| from test_runners import vts_tf_test_runner |
| |
| _ANDROID_MK = 'Android.mk' |
| |
| # These are suites in LOCAL_COMPATIBILITY_SUITE that aren't really suites so |
| # we can ignore them. |
| _SUITES_TO_IGNORE = frozenset({'general-tests', 'device-tests', 'tests'}) |
| |
| class ModuleFinder(test_finder_base.TestFinderBase): |
| """Module finder class.""" |
| NAME = 'MODULE' |
| _TEST_RUNNER = atest_tf_test_runner.AtestTradefedTestRunner.NAME |
| _ROBOLECTRIC_RUNNER = robolectric_test_runner.RobolectricTestRunner.NAME |
| _VTS_TEST_RUNNER = vts_tf_test_runner.VtsTradefedTestRunner.NAME |
| |
| def __init__(self, module_info=None): |
| super().__init__() |
| self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP) |
| self.module_info = module_info |
| |
| def _determine_testable_module(self, path, file_path=None): |
| """Determine which module the user is trying to test. |
| |
| Returns the module to test. If there are multiple possibilities, will |
| ask the user. Otherwise will return the only module found. |
| |
| Args: |
| path: String path of module to look for. |
| file_path: String path of input file. |
| |
| Returns: |
| A list of the module names. |
| """ |
| testable_modules = [] |
| # A list to save those testable modules but srcs information is empty. |
| testable_modules_no_srcs = [] |
| for mod in self.module_info.get_module_names(path): |
| mod_info = self.module_info.get_module_info(mod) |
| # Robolectric tests always exist in pairs of 2, one module to build |
| # the test and another to run it. For now, we are assuming they are |
| # isolated in their own folders and will return if we find one. |
| if self.module_info.is_robolectric_test(mod): |
| # return a list with one module name if it is robolectric. |
| return [mod] |
| if self.module_info.is_testable_module(mod_info): |
| # If test module defined srcs, input file_path should be defined |
| # in the src list of module. |
| module_srcs = mod_info.get(constants.MODULE_SRCS, []) |
| if file_path and os.path.relpath( |
| file_path, self.root_dir) not in module_srcs: |
| logging.debug('Skip module: %s for %s', mod, file_path) |
| # Collect those modules if they don't have srcs information |
| # in module-info, use this list if there's no other matched |
| # module with src information. |
| if not module_srcs: |
| testable_modules_no_srcs.append( |
| mod_info.get(constants.MODULE_NAME)) |
| continue |
| testable_modules.append(mod_info.get(constants.MODULE_NAME)) |
| if not testable_modules: |
| testable_modules.extend(testable_modules_no_srcs) |
| return test_finder_utils.extract_test_from_tests(testable_modules) |
| |
| def _is_vts_module(self, module_name): |
| """Returns True if the module is a vts10 module, else False.""" |
| mod_info = self.module_info.get_module_info(module_name) |
| suites = [] |
| if mod_info: |
| suites = mod_info.get('compatibility_suites', []) |
| # Pull out all *ts (cts, tvts, etc) suites. |
| suites = [suite for suite in suites if suite not in _SUITES_TO_IGNORE] |
| return len(suites) == 1 and 'vts10' in suites |
| |
| def _update_to_vts_test_info(self, test): |
| """Fill in the fields with vts10 specific info. |
| |
| We need to update the runner to use the vts10 runner and also find the |
| test specific dependencies. |
| |
| Args: |
| test: TestInfo to update with vts10 specific details. |
| |
| Return: |
| TestInfo that is ready for the vts10 test runner. |
| """ |
| test.test_runner = self._VTS_TEST_RUNNER |
| config_file = os.path.join(self.root_dir, |
| test.data[constants.TI_REL_CONFIG]) |
| # Need to get out dir (special logic is to account for custom out dirs). |
| # The out dir is used to construct the build targets for the test deps. |
| out_dir = os.environ.get(constants.ANDROID_HOST_OUT) |
| custom_out_dir = os.environ.get(constants.ANDROID_OUT_DIR) |
| # If we're not an absolute custom out dir, get no-absolute out dir path. |
| if custom_out_dir is None or not os.path.isabs(custom_out_dir): |
| out_dir = os.path.relpath(out_dir, self.root_dir) |
| vts_out_dir = os.path.join(out_dir, 'vts10', 'android-vts10', 'testcases') |
| # Parse dependency of default staging plans. |
| xml_paths = test_finder_utils.search_integration_dirs( |
| constants.VTS_STAGING_PLAN, |
| self.module_info.get_paths(constants.VTS_TF_MODULE)) |
| vts_xmls = set() |
| vts_xmls.add(config_file) |
| for xml_path in xml_paths: |
| vts_xmls |= test_finder_utils.get_plans_from_vts_xml(xml_path) |
| for config_file in vts_xmls: |
| # Add in vts10 test build targets. |
| test.build_targets |= test_finder_utils.get_targets_from_vts_xml( |
| config_file, vts_out_dir, self.module_info) |
| test.build_targets.add('vts-test-core') |
| test.build_targets.add(test.test_name) |
| return test |
| |
| def _update_legacy_robolectric_test_info(self, test): |
| """Update the fields for a legacy robolectric test. |
| |
| This method is updating test_name when the given is a legacy robolectric |
| test, and assigning Robolectric Runner for it. |
| |
| e.g. WallPaperPicker2RoboTests is a legacy robotest, and the test_name |
| will become RunWallPaperPicker2RoboTests and run it with Robolectric |
| Runner. |
| |
| Args: |
| test: TestInfo to be updated with robolectric fields. |
| |
| Returns: |
| TestInfo with updated robolectric fields. |
| """ |
| test.test_runner = self._ROBOLECTRIC_RUNNER |
| test.test_name = self.module_info.get_robolectric_test_name(test.test_name) |
| return test |
| |
| def _process_test_info(self, test): |
| """Process the test info and return some fields updated/changed. |
| |
| We need to check if the test found is a special module (like vts10) and |
| update the test_info fields (like test_runner) appropriately. |
| |
| Args: |
| test: TestInfo that has been filled out by a find method. |
| |
| Return: |
| TestInfo that has been modified as needed and return None if |
| this module can't be found in the module_info. |
| """ |
| module_name = test.test_name |
| mod_info = self.module_info.get_module_info(module_name) |
| if not mod_info: |
| return None |
| test.module_class = mod_info['class'] |
| test.install_locations = test_finder_utils.get_install_locations( |
| mod_info['installed']) |
| # Check if this is only a vts10 module. |
| if self._is_vts_module(test.test_name): |
| return self._update_to_vts_test_info(test) |
| test.robo_type = self.module_info.get_robolectric_type(test.test_name) |
| if test.robo_type: |
| test.install_locations = {constants.DEVICELESS_TEST} |
| if test.robo_type == constants.ROBOTYPE_MODERN: |
| test.build_targets.add(test.test_name) |
| return test |
| if test.robo_type == constants.ROBOTYPE_LEGACY: |
| return self._update_legacy_robolectric_test_info(test) |
| rel_config = test.data[constants.TI_REL_CONFIG] |
| test.build_targets = self._get_build_targets(module_name, rel_config) |
| # For device side java test, it will use |
| # com.android.compatibility.testtype.DalvikTest as test runner in |
| # cts-dalvik-device-test-runner.jar |
| if self.module_info.is_auto_gen_test_config(module_name): |
| if constants.MODULE_CLASS_JAVA_LIBRARIES in test.module_class: |
| for dalvik_dep in test_finder_utils.DALVIK_TEST_DEPS: |
| if self.module_info.is_module(dalvik_dep): |
| test.build_targets.add(dalvik_dep) |
| # Update test name if the test belong to extra config which means it's |
| # test config name is not the same as module name. For extra config, it |
| # index will be greater or equal to 1. |
| try: |
| if (mod_info.get(constants.MODULE_TEST_CONFIG, []).index(rel_config) |
| > 0): |
| config_test_name = os.path.splitext(os.path.basename( |
| rel_config))[0] |
| logging.debug('Replace test_info.name(%s) to %s', |
| test.test_name, config_test_name) |
| test.test_name = config_test_name |
| except ValueError: |
| pass |
| return test |
| |
| def _get_build_targets(self, module_name, rel_config): |
| """Get the test deps. |
| |
| Args: |
| module_name: name of the test. |
| rel_config: XML for the given test. |
| |
| Returns: |
| Set of build targets. |
| """ |
| targets = set() |
| if not self.module_info.is_auto_gen_test_config(module_name): |
| config_file = os.path.join(self.root_dir, rel_config) |
| targets = test_finder_utils.get_targets_from_xml(config_file, |
| self.module_info) |
| if constants.VTS_CORE_SUITE in self.module_info.get_module_info( |
| module_name).get(constants.MODULE_COMPATIBILITY_SUITES, []): |
| targets.add(constants.VTS_CORE_TF_MODULE) |
| for suite in self.module_info.get_module_info( |
| module_name).get(constants.MODULE_COMPATIBILITY_SUITES, []): |
| targets.update(constants.SUITE_DEPS.get(suite, [])) |
| for module_path in self.module_info.get_paths(module_name): |
| mod_dir = module_path.replace('/', '-') |
| targets.add(constants.MODULES_IN + mod_dir) |
| # (b/156457698) Force add vts_kernel_tests as build target if our test |
| # belong to REQUIRED_KERNEL_TEST_MODULES due to required_module option |
| # not working for sh_test in soong. |
| if module_name in constants.REQUIRED_KERNEL_TEST_MODULES: |
| targets.add('vts_kernel_tests') |
| # (b/184567849) Force adding module_name as a build_target. This will |
| # allow excluding MODULES-IN-* and prevent from missing build targets. |
| if module_name and self.module_info.is_module(module_name): |
| targets.add(module_name) |
| # If it's a MTS test, add cts-tradefed as test dependency. |
| if constants.MTS_SUITE in self.module_info.get_module_info( |
| module_name).get(constants.MODULE_COMPATIBILITY_SUITES, []): |
| if self.module_info.is_module(constants.CTS_JAR): |
| targets.add(constants.CTS_JAR) |
| return targets |
| |
| def _get_module_test_config(self, module_name, rel_config=None): |
| """Get the value of test_config in module_info. |
| |
| Get the value of 'test_config' in module_info if its |
| auto_test_config is not true. |
| In this case, the test_config is specified by user. |
| If not, return rel_config. |
| |
| Args: |
| module_name: A string of the test's module name. |
| rel_config: XML for the given test. |
| |
| Returns: |
| A list of string of test_config path if found, else return rel_config. |
| """ |
| default_all_config = not (atest_configs.GLOBAL_ARGS and |
| atest_configs.GLOBAL_ARGS.test_config_select) |
| mod_info = self.module_info.get_module_info(module_name) |
| if mod_info: |
| test_configs = [] |
| test_config_list = mod_info.get(constants.MODULE_TEST_CONFIG, []) |
| if test_config_list: |
| # multiple test configs |
| if len(test_config_list) > 1: |
| test_configs = test_finder_utils.extract_test_from_tests( |
| test_config_list, default_all=default_all_config) |
| else: |
| test_configs = test_config_list |
| if test_configs: |
| return test_configs |
| # Double check if below section is needed. |
| if (not self.module_info.is_auto_gen_test_config(module_name) |
| and len(test_configs) > 0): |
| return test_configs |
| return [rel_config] if rel_config else [] |
| |
| # pylint: disable=too-many-branches |
| # pylint: disable=too-many-locals |
| def _get_test_info_filter(self, path, methods, **kwargs): |
| """Get test info filter. |
| |
| Args: |
| path: A string of the test's path. |
| methods: A set of method name strings. |
| rel_module_dir: Optional. A string of the module dir no-absolute to |
| root. |
| class_name: Optional. A string of the class name. |
| is_native_test: Optional. A boolean variable of whether to search |
| for a native test or not. |
| |
| Returns: |
| A set of test info filter. |
| """ |
| _, file_name = test_finder_utils.get_dir_path_and_filename(path) |
| ti_filter = frozenset() |
| if os.path.isfile(path) and kwargs.get('is_native_test', None): |
| class_info = test_finder_utils.get_cc_class_info(path) |
| ti_filter = frozenset([test_info.TestFilter( |
| test_finder_utils.get_cc_filter( |
| class_info, kwargs.get('class_name', '*'), methods), |
| frozenset())]) |
| # Path to java file. |
| elif file_name and constants.JAVA_EXT_RE.match(file_name): |
| full_class_name = test_finder_utils.get_fully_qualified_class_name( |
| path) |
| # If input class is parameterized java class, adding * to the end of |
| # method filter string to make sure the generated method name could |
| # be run. |
| if test_finder_utils.is_parameterized_java_class(path): |
| update_methods = [] |
| for method in methods: |
| update_methods.append(method + '*') |
| methods = frozenset(update_methods) |
| ti_filter = frozenset( |
| [test_info.TestFilter(full_class_name, methods)]) |
| # Path to cc file. |
| elif file_name and constants.CC_EXT_RE.match(file_name): |
| # TODO (b/173019813) Should setup correct filter for an input file. |
| if not test_finder_utils.has_cc_class(path): |
| raise atest_error.MissingCCTestCaseError( |
| "Can't find CC class in %s" % path) |
| class_info = test_finder_utils.get_cc_class_info(path) |
| cc_filters = [] |
| for classname, _ in class_info.items(): |
| cc_filters.append( |
| test_info.TestFilter( |
| test_finder_utils.get_cc_filter(class_info, classname, methods), |
| frozenset())) |
| ti_filter = frozenset(cc_filters) |
| # If input path is a folder and have class_name information. |
| elif (not file_name and kwargs.get('class_name', None)): |
| ti_filter = frozenset( |
| [test_info.TestFilter(kwargs.get('class_name', None), methods)]) |
| # Path to non-module dir, treat as package. |
| elif (not file_name |
| and kwargs.get('rel_module_dir', None) != |
| os.path.relpath(path, self.root_dir)): |
| dir_items = [os.path.join(path, f) for f in os.listdir(path)] |
| for dir_item in dir_items: |
| if constants.JAVA_EXT_RE.match(dir_item): |
| package_name = test_finder_utils.get_package_name(dir_item) |
| if package_name: |
| # methods should be empty frozenset for package. |
| if methods: |
| raise atest_error.MethodWithoutClassError( |
| '%s: Method filtering requires class' |
| % str(methods)) |
| ti_filter = frozenset( |
| [test_info.TestFilter(package_name, methods)]) |
| break |
| logging.debug('_get_test_info_filter() ti_filter: %s', ti_filter) |
| return ti_filter |
| |
| def _get_rel_config(self, test_path): |
| """Get config file's no-absolute path. |
| |
| Args: |
| test_path: A string of the test absolute path. |
| |
| Returns: |
| A string of config's no-absolute path, else None. |
| """ |
| test_dir = os.path.dirname(test_path) |
| rel_module_dir = test_finder_utils.find_parent_module_dir( |
| self.root_dir, test_dir, self.module_info) |
| if rel_module_dir: |
| return os.path.join(rel_module_dir, constants.MODULE_CONFIG) |
| return None |
| |
| def _get_test_infos(self, test_path, rel_config, module_name, test_filter): |
| """Get test_info for test_path. |
| |
| Args: |
| test_path: A string of the test path. |
| rel_config: A string of rel path of config. |
| module_name: A string of the module name to use. |
| test_filter: A test info filter. |
| |
| Returns: |
| A list of TestInfo namedtuple if found, else None. |
| """ |
| if not rel_config: |
| rel_config = self._get_rel_config(test_path) |
| if not rel_config: |
| return None |
| if module_name: |
| module_names = [module_name] |
| else: |
| module_names = self._determine_testable_module( |
| os.path.dirname(rel_config), |
| test_path if self._is_comparted_src(test_path) else None) |
| test_infos = [] |
| if module_names: |
| for mname in module_names: |
| # The real test config might be record in module-info. |
| rel_configs = self._get_module_test_config( |
| mname, rel_config=rel_config) |
| for rel_cfg in rel_configs: |
| mod_info = self.module_info.get_module_info(mname) |
| tinfo = self._process_test_info(test_info.TestInfo( |
| test_name=mname, |
| test_runner=self._TEST_RUNNER, |
| build_targets=set(), |
| data={constants.TI_FILTER: test_filter, |
| constants.TI_REL_CONFIG: rel_cfg}, |
| compatibility_suites=mod_info.get( |
| constants.MODULE_COMPATIBILITY_SUITES, []))) |
| if tinfo: |
| test_infos.append(tinfo) |
| return test_infos |
| |
| def find_test_by_module_name(self, module_name): |
| """Find test for the given module name. |
| |
| Args: |
| module_name: A string of the test's module name. |
| |
| Returns: |
| A list that includes only 1 populated TestInfo namedtuple |
| if found, otherwise None. |
| """ |
| tinfos = [] |
| mod_info = self.module_info.get_module_info(module_name) |
| if self.module_info.is_testable_module(mod_info): |
| # path is a list with only 1 element. |
| rel_config = os.path.join(mod_info['path'][0], |
| constants.MODULE_CONFIG) |
| rel_configs = self._get_module_test_config(module_name, |
| rel_config=rel_config) |
| for rel_config in rel_configs: |
| tinfo = self._process_test_info(test_info.TestInfo( |
| test_name=module_name, |
| test_runner=self._TEST_RUNNER, |
| build_targets=set(), |
| data={constants.TI_REL_CONFIG: rel_config, |
| constants.TI_FILTER: frozenset()}, |
| compatibility_suites=mod_info.get( |
| constants.MODULE_COMPATIBILITY_SUITES, []))) |
| if tinfo: |
| tinfos.append(tinfo) |
| if tinfos: |
| return tinfos |
| return None |
| |
| def find_test_by_kernel_class_name(self, module_name, class_name): |
| """Find kernel test for the given class name. |
| |
| Args: |
| module_name: A string of the module name to use. |
| class_name: A string of the test's class name. |
| |
| Returns: |
| A list of populated TestInfo namedtuple if test found, else None. |
| """ |
| |
| class_name, methods = test_finder_utils.split_methods(class_name) |
| test_configs = self._get_module_test_config(module_name) |
| if not test_configs: |
| return None |
| tinfos = [] |
| for test_config in test_configs: |
| test_config_path = os.path.join(self.root_dir, test_config) |
| mod_info = self.module_info.get_module_info(module_name) |
| ti_filter = frozenset( |
| [test_info.TestFilter(class_name, methods)]) |
| if test_finder_utils.is_test_from_kernel_xml(test_config_path, class_name): |
| tinfo = self._process_test_info(test_info.TestInfo( |
| test_name=module_name, |
| test_runner=self._TEST_RUNNER, |
| build_targets=set(), |
| data={constants.TI_REL_CONFIG: test_config, |
| constants.TI_FILTER: ti_filter}, |
| compatibility_suites=mod_info.get( |
| constants.MODULE_COMPATIBILITY_SUITES, []))) |
| if tinfo: |
| tinfos.append(tinfo) |
| if tinfos: |
| return tinfos |
| return None |
| |
| def find_test_by_class_name(self, class_name, module_name=None, |
| rel_config=None, is_native_test=False): |
| """Find test files given a class name. |
| |
| If module_name and rel_config not given it will calculate it determine |
| it by looking up the tree from the class file. |
| |
| Args: |
| class_name: A string of the test's class name. |
| module_name: Optional. A string of the module name to use. |
| rel_config: Optional. A string of module dir no-absolute to repo root. |
| is_native_test: A boolean variable of whether to search for a |
| native test or not. |
| |
| Returns: |
| A list of populated TestInfo namedtuple if test found, else None. |
| """ |
| class_name, methods = test_finder_utils.split_methods(class_name) |
| search_class_name = class_name |
| # For parameterized gtest, test class will be automerged to |
| # $(class_prefix)/$(base_class) name. Using $(base_class) for searching |
| # matched TEST_P to make sure test class is matched. |
| if '/' in search_class_name: |
| search_class_name = str(search_class_name).split('/')[-1] |
| if rel_config: |
| search_dir = os.path.join(self.root_dir, |
| os.path.dirname(rel_config)) |
| else: |
| search_dir = self.root_dir |
| test_paths = test_finder_utils.find_class_file(search_dir, search_class_name, |
| is_native_test, methods) |
| if not test_paths and rel_config: |
| logging.info('Did not find class (%s) under module path (%s), ' |
| 'researching from repo root.', class_name, rel_config) |
| test_paths = test_finder_utils.find_class_file(self.root_dir, |
| search_class_name, |
| is_native_test, |
| methods) |
| test_paths = test_paths if test_paths is not None else [] |
| # If we already have module name, use path in module-info as test_path. |
| if not test_paths: |
| if not module_name: |
| return None |
| # Use the module path as test_path. |
| module_paths = self.module_info.get_paths(module_name) |
| test_paths = [] |
| for rel_module_path in module_paths: |
| test_paths.append(os.path.join(self.root_dir, rel_module_path)) |
| tinfos = [] |
| for test_path in test_paths: |
| test_filter = self._get_test_info_filter( |
| test_path, methods, class_name=class_name, |
| is_native_test=is_native_test) |
| test_infos = self._get_test_infos( |
| test_path, rel_config, module_name, test_filter) |
| # If input include methods, check if tinfo match. |
| if test_infos and len(test_infos) > 1 and methods: |
| test_infos = self._get_matched_test_infos(test_infos, methods) |
| if test_infos: |
| tinfos.extend(test_infos) |
| return tinfos if tinfos else None |
| |
| def _get_matched_test_infos(self, test_infos, methods): |
| """Get the test_infos matched the given methods. |
| |
| Args: |
| test_infos: A list of TestInfo obj. |
| methods: A set of method name strings. |
| |
| Returns: |
| A list of matched TestInfo namedtuple, else None. |
| """ |
| matched_test_infos = set() |
| for tinfo in test_infos: |
| test_config, test_srcs = test_finder_utils.get_test_config_and_srcs( |
| tinfo, self.module_info) |
| if test_config: |
| filter_dict = atest_utils.get_android_junit_config_filters( |
| test_config) |
| # Always treat the test_info is matched if no filters found. |
| if not filter_dict.keys(): |
| matched_test_infos.add(tinfo) |
| continue |
| for method in methods: |
| if self._is_srcs_match_method_annotation(method, test_srcs, |
| filter_dict): |
| logging.debug('For method:%s Test:%s matched ' |
| 'filter_dict: %s', method, |
| tinfo.test_name, filter_dict) |
| matched_test_infos.add(tinfo) |
| return list(matched_test_infos) |
| |
| def _is_srcs_match_method_annotation(self, method, srcs, annotation_dict): |
| """Check if input srcs matched annotation. |
| |
| Args: |
| method: A string of test method name. |
| srcs: A list of source file of test. |
| annotation_dict: A dictionary record the include and exclude |
| annotations. |
| |
| Returns: |
| True if input method matched the annotation of input srcs, else |
| None. |
| """ |
| include_annotations = annotation_dict.get( |
| constants.INCLUDE_ANNOTATION, []) |
| exclude_annotations = annotation_dict.get( |
| constants.EXCLUDE_ANNOTATION, []) |
| for src in srcs: |
| include_methods = set() |
| src_path = os.path.join(self.root_dir, src) |
| # Add methods matched include_annotations. |
| for annotation in include_annotations: |
| include_methods.update( |
| test_finder_utils.get_annotated_methods( |
| annotation, src_path)) |
| if exclude_annotations: |
| # For exclude annotation, get all the method in the input srcs, |
| # and filter out the matched annotation. |
| exclude_methods = set() |
| all_methods = test_finder_utils.get_java_methods(src_path) |
| for annotation in exclude_annotations: |
| exclude_methods.update( |
| test_finder_utils.get_annotated_methods( |
| annotation, src_path)) |
| include_methods = all_methods - exclude_methods |
| if method in include_methods: |
| return True |
| return False |
| |
| def find_test_by_module_and_class(self, module_class): |
| """Find the test info given a MODULE:CLASS string. |
| |
| Args: |
| module_class: A string of form MODULE:CLASS or MODULE:CLASS#METHOD. |
| |
| Returns: |
| A list of populated TestInfo namedtuple if found, else None. |
| """ |
| if ':' not in module_class: |
| return None |
| module_name, class_name = module_class.split(':') |
| # module_infos is a list with at most 1 element. |
| module_infos = self.find_test_by_module_name(module_name) |
| module_info = module_infos[0] if module_infos else None |
| if not module_info: |
| return None |
| find_result = None |
| # If the target module is NATIVE_TEST, search CC classes only. |
| if not self.module_info.is_native_test(module_name): |
| # Find by java class. |
| find_result = self.find_test_by_class_name( |
| class_name, module_info.test_name, |
| module_info.data.get(constants.TI_REL_CONFIG)) |
| # kernel target test is also define as NATIVE_TEST in build system. |
| # TODO (b/157210083) Update find_test_by_kernel_class_name method to |
| # support gen_rule use case. |
| if not find_result: |
| find_result = self.find_test_by_kernel_class_name( |
| module_name, class_name) |
| # Find by cc class. |
| if not find_result: |
| find_result = self.find_test_by_cc_class_name( |
| class_name, module_info.test_name, |
| module_info.data.get(constants.TI_REL_CONFIG)) |
| return find_result |
| |
| def find_test_by_package_name(self, package, module_name=None, |
| rel_config=None): |
| """Find the test info given a PACKAGE string. |
| |
| Args: |
| package: A string of the package name. |
| module_name: Optional. A string of the module name. |
| ref_config: Optional. A string of rel path of config. |
| |
| Returns: |
| A list of populated TestInfo namedtuple if found, else None. |
| """ |
| _, methods = test_finder_utils.split_methods(package) |
| if methods: |
| raise atest_error.MethodWithoutClassError('%s: Method filtering ' |
| 'requires class' % ( |
| methods)) |
| # Confirm that packages exists and get user input for multiples. |
| if rel_config: |
| search_dir = os.path.join(self.root_dir, |
| os.path.dirname(rel_config)) |
| else: |
| search_dir = self.root_dir |
| package_paths = test_finder_utils.run_find_cmd( |
| test_finder_utils.FIND_REFERENCE_TYPE.PACKAGE, search_dir, package) |
| package_paths = package_paths if package_paths is not None else [] |
| # Package path will be the full path to the dir represented by package. |
| if not package_paths: |
| if not module_name: |
| return None |
| module_paths = self.module_info.get_paths(module_name) |
| for rel_module_path in module_paths: |
| package_paths.append(os.path.join(self.root_dir, rel_module_path)) |
| test_filter = frozenset([test_info.TestFilter(package, frozenset())]) |
| test_infos = [] |
| for package_path in package_paths: |
| tinfo = self._get_test_infos(package_path, rel_config, |
| module_name, test_filter) |
| if tinfo: |
| test_infos.extend(tinfo) |
| return test_infos if test_infos else None |
| |
| def find_test_by_module_and_package(self, module_package): |
| """Find the test info given a MODULE:PACKAGE string. |
| |
| Args: |
| module_package: A string of form MODULE:PACKAGE |
| |
| Returns: |
| A list of populated TestInfo namedtuple if found, else None. |
| """ |
| module_name, package = module_package.split(':') |
| # module_infos is a list with at most 1 element. |
| module_infos = self.find_test_by_module_name(module_name) |
| module_info = module_infos[0] if module_infos else None |
| if not module_info: |
| return None |
| return self.find_test_by_package_name( |
| package, module_info.test_name, |
| module_info.data.get(constants.TI_REL_CONFIG)) |
| |
| def find_test_by_path(self, rel_path): |
| """Find the first test info matching the given path. |
| |
| Strategy: |
| path_to_java_file --> Resolve to CLASS |
| path_to_cc_file --> Resolve to CC CLASS |
| path_to_module_file -> Resolve to MODULE |
| path_to_module_dir -> Resolve to MODULE |
| path_to_dir_with_class_files--> Resolve to PACKAGE |
| path_to_any_other_dir --> Resolve as MODULE |
| |
| Args: |
| rel_path: A string of the relative path to $BUILD_TOP. |
| |
| Returns: |
| A list of populated TestInfo namedtuple if test found, else None |
| """ |
| logging.debug('Finding test by path: %s', rel_path) |
| path, methods = test_finder_utils.split_methods(rel_path) |
| # TODO: See if this can be generalized and shared with methods above |
| # create absolute path from cwd and remove symbolic links |
| path = os.path.realpath(path) |
| if not os.path.exists(path): |
| return None |
| if (methods and |
| not test_finder_utils.has_method_in_file(path, methods)): |
| return None |
| dir_path, _ = test_finder_utils.get_dir_path_and_filename(path) |
| # Module/Class |
| rel_module_dir = test_finder_utils.find_parent_module_dir( |
| self.root_dir, dir_path, self.module_info) |
| if not rel_module_dir: |
| # Try to find unit-test for input path. |
| path = os.path.relpath( |
| os.path.realpath(rel_path), |
| os.environ.get(constants.ANDROID_BUILD_TOP, '')) |
| unit_tests = test_finder_utils.find_host_unit_tests( |
| self.module_info, path) |
| if unit_tests: |
| tinfos = [] |
| for unit_test in unit_tests: |
| tinfo = self._get_test_infos(path, constants.MODULE_CONFIG, |
| unit_test, frozenset()) |
| if tinfo: |
| tinfos.extend(tinfo) |
| return tinfos |
| return None |
| rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG) |
| test_filter = self._get_test_info_filter(path, methods, |
| rel_module_dir=rel_module_dir) |
| return self._get_test_infos(path, rel_config, None, test_filter) |
| |
| def find_test_by_cc_class_name(self, class_name, module_name=None, |
| rel_config=None): |
| """Find test files given a cc class name. |
| |
| If module_name and rel_config not given, test will be determined |
| by looking up the tree for files which has input class. |
| |
| Args: |
| class_name: A string of the test's class name. |
| module_name: Optional. A string of the module name to use. |
| rel_config: Optional. A string of module dir no-absolute to repo root. |
| |
| Returns: |
| A list of populated TestInfo namedtuple if test found, else None. |
| """ |
| # Check if class_name is prepended with file name. If so, trim the |
| # prefix and keep only the class_name. |
| if '.' in class_name: |
| # (b/202764540) Strip prefixes of a cc class. |
| # Assume the class name has a format of file_name.class_name |
| class_name = class_name[class_name.rindex('.')+1:] |
| logging.info('Search with updated class name: %s', class_name) |
| return self.find_test_by_class_name( |
| class_name, module_name, rel_config, is_native_test=True) |
| |
| def get_testable_modules_with_ld(self, user_input, ld_range=0): |
| """Calculate the edit distances of the input and testable modules. |
| |
| The user input will be calculated across all testable modules and |
| results in integers generated by Levenshtein Distance algorithm. |
| To increase the speed of the calculation, a bound can be applied to |
| this method to prevent from calculating every testable modules. |
| |
| Guessing from typos, e.g. atest atest_unitests, implies a tangible range |
| of length that Atest only needs to search within it, and the default of |
| the bound is 2. |
| |
| Guessing from keywords however, e.g. atest --search Camera, means that |
| the uncertainty of the module name is way higher, and Atest should walk |
| through all testable modules and return the highest possibilities. |
| |
| Args: |
| user_input: A string of the user input. |
| ld_range: An integer that range the searching scope. If the length |
| of user_input is 10, then Atest will calculate modules of |
| which length is between 8 and 12. 0 is equivalent to |
| unlimited. |
| |
| Returns: |
| A List of LDs and possible module names. If the user_input is "fax", |
| the output will be like: |
| [[2, "fog"], [2, "Fix"], [4, "duck"], [7, "Duckies"]] |
| |
| Which means the most lilely names of "fax" are fog and Fix(LD=2), |
| while Dickies is the most unlikely one(LD=7). |
| """ |
| atest_utils.colorful_print('\nSearching for similar module names using ' |
| 'fuzzy search...', constants.CYAN) |
| search_start = time.time() |
| testable_modules = sorted(self.module_info.get_testable_modules(), |
| key=len) |
| lower_bound = len(user_input) - ld_range |
| upper_bound = len(user_input) + ld_range |
| testable_modules_with_ld = [] |
| for module_name in testable_modules: |
| # Dispose those too short or too lengthy. |
| if ld_range != 0: |
| if len(module_name) < lower_bound: |
| continue |
| if len(module_name) > upper_bound: |
| break |
| testable_modules_with_ld.append( |
| [test_finder_utils.get_levenshtein_distance( |
| user_input, module_name), module_name]) |
| search_duration = time.time() - search_start |
| logging.debug('Fuzzy search took %ss', search_duration) |
| metrics.LocalDetectEvent( |
| detect_type=constants.DETECT_TYPE_FUZZY_SEARCH_TIME, |
| result=round(search_duration)) |
| return testable_modules_with_ld |
| |
| def get_fuzzy_searching_results(self, user_input): |
| """Give results which have no more than allowance of edit distances. |
| |
| Args: |
| user_input: the target module name for fuzzy searching. |
| |
| Return: |
| A list of guessed modules. |
| """ |
| modules_with_ld = self.get_testable_modules_with_ld( |
| user_input, ld_range=constants.LD_RANGE) |
| guessed_modules = [] |
| for _distance, _module in modules_with_ld: |
| if _distance <= abs(constants.LD_RANGE): |
| guessed_modules.append(_module) |
| return guessed_modules |
| |
| def find_test_by_config_name(self, config_name): |
| """Find test for the given config name. |
| |
| Args: |
| config_name: A string of the test's config name. |
| |
| Returns: |
| A list that includes only 1 populated TestInfo namedtuple |
| if found, otherwise None. |
| """ |
| for module_name, mod_info in self.module_info.name_to_module_info.items(): |
| test_configs = mod_info.get(constants.MODULE_TEST_CONFIG, []) |
| for test_config in test_configs: |
| test_config_name = os.path.splitext( |
| os.path.basename(test_config))[0] |
| if test_config_name == config_name: |
| tinfo = test_info.TestInfo( |
| test_name=test_config_name, |
| test_runner=self._TEST_RUNNER, |
| build_targets=self._get_build_targets(module_name, |
| test_config), |
| data={constants.TI_REL_CONFIG: test_config, |
| constants.TI_FILTER: frozenset()}, |
| compatibility_suites=mod_info.get( |
| constants.MODULE_COMPATIBILITY_SUITES, [])) |
| test_config_path = os.path.join(self.root_dir, test_config) |
| if test_finder_utils.need_aggregate_metrics_result(test_config_path): |
| tinfo.aggregate_metrics_result = True |
| if tinfo: |
| # There should have only one test_config with the same |
| # name in source tree. |
| return [tinfo] |
| return None |
| |
| def _is_comparted_src(self, path): |
| """Check if the input path need to match srcs information in module. |
| |
| If path is a folder or android build file, we don't need to compart |
| with module's srcs. |
| |
| Args: |
| path: A string of the test's path. |
| |
| Returns: |
| True if input path need to match with module's src info, else False. |
| """ |
| if os.path.isdir(path): |
| return False |
| if atest_utils.is_build_file(path): |
| return False |
| return True |
| |
| class MainlineModuleFinder(ModuleFinder): |
| """Mainline Module finder class.""" |
| NAME = 'MAINLINE_MODULE' |
| |
| def __init__(self, module_info=None): |
| super().__init__() |