# 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) atest_utils.build([module_info_target], logging.getLogger().isEnabledFor(logging.DEBUG)) 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, [])