| # 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 Info class used to hold cached module-info.json. |
| """ |
| |
| import json |
| import logging |
| import os |
| |
| import atest_utils |
| import constants |
| |
| # JSON file generated by build system that lists all buildable targets. |
| _MODULE_INFO = 'module-info.json' |
| |
| |
| class ModuleInfo(object): |
| """Class that offers fast/easy lookup for Module related details.""" |
| |
| def __init__(self, force_build=False, module_file=None): |
| """Initialize the ModuleInfo object. |
| |
| Load up the module-info.json file and initialize the helper vars. |
| |
| Args: |
| force_build: Boolean to indicate if we should rebuild the |
| module_info file regardless if it's created or not. |
| module_file: String of path to file to load up. Used for testing. |
| """ |
| module_info_target, name_to_module_info = self._load_module_info_file( |
| force_build, module_file) |
| self.name_to_module_info = name_to_module_info |
| self.module_info_target = module_info_target |
| self.path_to_module_info = self._get_path_to_module_info( |
| self.name_to_module_info) |
| self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP) |
| |
| @staticmethod |
| def _discover_mod_file_and_target(force_build): |
| """Find the module file. |
| |
| Args: |
| force_build: Boolean to indicate if we should rebuild the |
| module_info file regardless if it's created or not. |
| |
| Returns: |
| Tuple of module_info_target and path to module file. |
| """ |
| module_info_target = None |
| root_dir = os.environ.get(constants.ANDROID_BUILD_TOP, '/') |
| out_dir = os.environ.get(constants.ANDROID_PRODUCT_OUT, root_dir) |
| module_file_path = os.path.join(out_dir, _MODULE_INFO) |
| |
| # Check if the user set a custom out directory by comparing the out_dir |
| # to the root_dir. |
| if out_dir.find(root_dir) == 0: |
| # Make target is simply file path relative to root |
| module_info_target = os.path.relpath(module_file_path, root_dir) |
| else: |
| # If the user has set a custom out directory, generate an absolute |
| # path for module info targets. |
| logging.debug('User customized out dir!') |
| module_file_path = os.path.join( |
| os.environ.get(constants.ANDROID_PRODUCT_OUT), _MODULE_INFO) |
| module_info_target = module_file_path |
| if not os.path.isfile(module_file_path) or force_build: |
| logging.debug('Generating %s - this is required for ' |
| 'initial runs.', _MODULE_INFO) |
| build_env = dict(constants.ATEST_BUILD_ENV) |
| build_env.update(constants.DEPS_LICENSE_ENV) |
| # Also build the deps-license module to generate dependencies data. |
| atest_utils.build([module_info_target, constants.DEPS_LICENSE], |
| verbose=logging.getLogger().isEnabledFor(logging.DEBUG), |
| env_vars=build_env) |
| return module_info_target, module_file_path |
| |
| def _load_module_info_file(self, force_build, module_file): |
| """Load the module file. |
| |
| Args: |
| force_build: Boolean to indicate if we should rebuild the |
| module_info file regardless if it's created or not. |
| module_file: String of path to file to load up. Used for testing. |
| |
| Returns: |
| Tuple of module_info_target and dict of json. |
| """ |
| # If module_file is specified, we're testing so we don't care if |
| # module_info_target stays None. |
| module_info_target = None |
| file_path = module_file |
| if not file_path: |
| module_info_target, file_path = self._discover_mod_file_and_target( |
| force_build) |
| with open(file_path) as json_file: |
| mod_info = json.load(json_file) |
| return module_info_target, mod_info |
| |
| @staticmethod |
| def _get_path_to_module_info(name_to_module_info): |
| """Return the path_to_module_info dict. |
| |
| Args: |
| name_to_module_info: Dict of module name to module info dict. |
| |
| Returns: |
| Dict of module path to module info dict. |
| """ |
| path_to_module_info = {} |
| for mod_name, mod_info in name_to_module_info.items(): |
| # Cross-compiled and multi-arch modules actually all belong to |
| # a single target so filter out these extra modules. |
| if mod_name != mod_info.get(constants.MODULE_NAME, ''): |
| continue |
| for path in mod_info.get(constants.MODULE_PATH, []): |
| mod_info[constants.MODULE_NAME] = mod_name |
| # There could be multiple modules in a path. |
| if path in path_to_module_info: |
| path_to_module_info[path].append(mod_info) |
| else: |
| path_to_module_info[path] = [mod_info] |
| return path_to_module_info |
| |
| def is_module(self, name): |
| """Return True if name is a module, False otherwise.""" |
| return name in self.name_to_module_info |
| |
| def get_paths(self, name): |
| """Return paths of supplied module name, Empty list if non-existent.""" |
| info = self.name_to_module_info.get(name) |
| if info: |
| return info.get(constants.MODULE_PATH, []) |
| return [] |
| |
| def get_module_names(self, rel_module_path): |
| """Get the modules that all have module_path. |
| |
| Args: |
| rel_module_path: path of module in module-info.json |
| |
| Returns: |
| List of module names. |
| """ |
| return [m.get(constants.MODULE_NAME) |
| for m in self.path_to_module_info.get(rel_module_path, [])] |
| |
| def get_module_info(self, mod_name): |
| """Return dict of info for given module name, None if non-existent.""" |
| return self.name_to_module_info.get(mod_name) |
| |
| def is_suite_in_compatibility_suites(self, suite, mod_info): |
| """Check if suite exists in the compatibility_suites of module-info. |
| |
| Args: |
| suite: A string of suite name. |
| mod_info: Dict of module info to check. |
| |
| Returns: |
| True if it exists in mod_info, False otherwise. |
| """ |
| return suite in mod_info.get(constants.MODULE_COMPATIBILITY_SUITES, []) |
| |
| def get_testable_modules(self, suite=None): |
| """Return the testable modules of the given suite name. |
| |
| Args: |
| suite: A string of suite name. Set to None to return all testable |
| modules. |
| |
| Returns: |
| List of testable modules. Empty list if non-existent. |
| If suite is None, return all the testable modules in module-info. |
| """ |
| modules = set() |
| for _, info in self.name_to_module_info.items(): |
| if self.is_testable_module(info): |
| if suite: |
| if self.is_suite_in_compatibility_suites(suite, info): |
| modules.add(info.get(constants.MODULE_NAME)) |
| else: |
| modules.add(info.get(constants.MODULE_NAME)) |
| return modules |
| |
| def is_testable_module(self, mod_info): |
| """Check if module is something we can test. |
| |
| A module is testable if: |
| - it's installed, or |
| - it's a robolectric module (or shares path with one). |
| |
| Args: |
| mod_info: Dict of module info to check. |
| |
| Returns: |
| True if we can test this module, False otherwise. |
| """ |
| if not mod_info: |
| return False |
| if mod_info.get(constants.MODULE_INSTALLED) and self.has_test_config(mod_info): |
| return True |
| if self.is_robolectric_test(mod_info.get(constants.MODULE_NAME)): |
| return True |
| return False |
| |
| def has_test_config(self, mod_info): |
| """Validate if this module has a test config. |
| |
| A module can have a test config in the following manner: |
| - AndroidTest.xml at the module path. |
| - test_config be set in module-info.json. |
| - Auto-generated config via the auto_test_config key in module-info.json. |
| |
| Args: |
| mod_info: Dict of module info to check. |
| |
| Returns: |
| True if this module has a test config, False otherwise. |
| """ |
| # Check if test_config in module-info is set. |
| for test_config in mod_info.get(constants.MODULE_TEST_CONFIG, []): |
| if os.path.isfile(os.path.join(self.root_dir, test_config)): |
| return True |
| # Check for AndroidTest.xml at the module path. |
| for path in mod_info.get(constants.MODULE_PATH, []): |
| if os.path.isfile(os.path.join(self.root_dir, path, |
| constants.MODULE_CONFIG)): |
| return True |
| # Check if the module has an auto-generated config. |
| return self.is_auto_gen_test_config(mod_info.get(constants.MODULE_NAME)) |
| |
| def get_robolectric_test_name(self, module_name): |
| """Returns runnable robolectric module name. |
| |
| There are at least 2 modules in every robolectric module path, return |
| the module that we can run as a build target. |
| |
| Arg: |
| module_name: String of module. |
| |
| Returns: |
| String of module that is the runnable robolectric module, None if |
| none could be found. |
| """ |
| module_name_info = self.name_to_module_info.get(module_name) |
| if not module_name_info: |
| return None |
| module_paths = module_name_info.get(constants.MODULE_PATH, []) |
| if module_paths: |
| for mod in self.get_module_names(module_paths[0]): |
| mod_info = self.get_module_info(mod) |
| if self.is_robolectric_module(mod_info): |
| return mod |
| return None |
| |
| def is_robolectric_test(self, module_name): |
| """Check if module is a robolectric test. |
| |
| A module can be a robolectric test if the specified module has their |
| class set as ROBOLECTRIC (or shares their path with a module that does). |
| |
| Args: |
| module_name: String of module to check. |
| |
| Returns: |
| True if the module is a robolectric module, else False. |
| """ |
| # Check 1, module class is ROBOLECTRIC |
| mod_info = self.get_module_info(module_name) |
| if self.is_robolectric_module(mod_info): |
| return True |
| # Check 2, shared modules in the path have class ROBOLECTRIC_CLASS. |
| if self.get_robolectric_test_name(module_name): |
| return True |
| return False |
| |
| def is_auto_gen_test_config(self, module_name): |
| """Check if the test config file will be generated automatically. |
| |
| Args: |
| module_name: A string of the module name. |
| |
| Returns: |
| True if the test config file will be generated automatically. |
| """ |
| if self.is_module(module_name): |
| mod_info = self.name_to_module_info.get(module_name) |
| auto_test_config = mod_info.get('auto_test_config', []) |
| return auto_test_config and auto_test_config[0] |
| return False |
| |
| def is_robolectric_module(self, mod_info): |
| """Check if a module is a robolectric module. |
| |
| Args: |
| mod_info: ModuleInfo to check. |
| |
| Returns: |
| True if module is a robolectric module, False otherwise. |
| """ |
| if mod_info: |
| return (mod_info.get(constants.MODULE_CLASS, [None])[0] == |
| constants.MODULE_CLASS_ROBOLECTRIC) |
| return False |
| |
| def is_native_test(self, module_name): |
| """Check if the input module is a native test. |
| |
| Args: |
| module_name: A string of the module name. |
| |
| Returns: |
| True if the test is a native test, False otherwise. |
| """ |
| mod_info = self.get_module_info(module_name) |
| return constants.MODULE_CLASS_NATIVE_TESTS in mod_info.get( |
| constants.MODULE_CLASS, []) |