1# Copyright 2018, The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15""" 16Module Info class used to hold cached module-info.json. 17""" 18 19import json 20import logging 21import os 22 23import atest_utils 24import constants 25 26# JSON file generated by build system that lists all buildable targets. 27_MODULE_INFO = 'module-info.json' 28 29 30class ModuleInfo(object): 31 """Class that offers fast/easy lookup for Module related details.""" 32 33 def __init__(self, force_build=False, module_file=None): 34 """Initialize the ModuleInfo object. 35 36 Load up the module-info.json file and initialize the helper vars. 37 38 Args: 39 force_build: Boolean to indicate if we should rebuild the 40 module_info file regardless if it's created or not. 41 module_file: String of path to file to load up. Used for testing. 42 """ 43 module_info_target, name_to_module_info = self._load_module_info_file( 44 force_build, module_file) 45 self.name_to_module_info = name_to_module_info 46 self.module_info_target = module_info_target 47 self.path_to_module_info = self._get_path_to_module_info( 48 self.name_to_module_info) 49 self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP) 50 51 @staticmethod 52 def _discover_mod_file_and_target(force_build): 53 """Find the module file. 54 55 Args: 56 force_build: Boolean to indicate if we should rebuild the 57 module_info file regardless if it's created or not. 58 59 Returns: 60 Tuple of module_info_target and path to module file. 61 """ 62 module_info_target = None 63 root_dir = os.environ.get(constants.ANDROID_BUILD_TOP, '/') 64 out_dir = os.environ.get(constants.ANDROID_PRODUCT_OUT, root_dir) 65 module_file_path = os.path.join(out_dir, _MODULE_INFO) 66 67 # Check if the user set a custom out directory by comparing the out_dir 68 # to the root_dir. 69 if out_dir.find(root_dir) == 0: 70 # Make target is simply file path relative to root 71 module_info_target = os.path.relpath(module_file_path, root_dir) 72 else: 73 # If the user has set a custom out directory, generate an absolute 74 # path for module info targets. 75 logging.debug('User customized out dir!') 76 module_file_path = os.path.join( 77 os.environ.get(constants.ANDROID_PRODUCT_OUT), _MODULE_INFO) 78 module_info_target = module_file_path 79 if not os.path.isfile(module_file_path) or force_build: 80 logging.debug('Generating %s - this is required for ' 81 'initial runs.', _MODULE_INFO) 82 build_env = dict(constants.ATEST_BUILD_ENV) 83 atest_utils.build([module_info_target], 84 verbose=logging.getLogger().isEnabledFor(logging.DEBUG), 85 env_vars=build_env) 86 return module_info_target, module_file_path 87 88 def _load_module_info_file(self, force_build, module_file): 89 """Load the module file. 90 91 Args: 92 force_build: Boolean to indicate if we should rebuild the 93 module_info file regardless if it's created or not. 94 module_file: String of path to file to load up. Used for testing. 95 96 Returns: 97 Tuple of module_info_target and dict of json. 98 """ 99 # If module_file is specified, we're testing so we don't care if 100 # module_info_target stays None. 101 module_info_target = None 102 file_path = module_file 103 if not file_path: 104 module_info_target, file_path = self._discover_mod_file_and_target( 105 force_build) 106 with open(file_path) as json_file: 107 mod_info = json.load(json_file) 108 return module_info_target, mod_info 109 110 @staticmethod 111 def _get_path_to_module_info(name_to_module_info): 112 """Return the path_to_module_info dict. 113 114 Args: 115 name_to_module_info: Dict of module name to module info dict. 116 117 Returns: 118 Dict of module path to module info dict. 119 """ 120 path_to_module_info = {} 121 for mod_name, mod_info in name_to_module_info.items(): 122 # Cross-compiled and multi-arch modules actually all belong to 123 # a single target so filter out these extra modules. 124 if mod_name != mod_info.get(constants.MODULE_NAME, ''): 125 continue 126 for path in mod_info.get(constants.MODULE_PATH, []): 127 mod_info[constants.MODULE_NAME] = mod_name 128 # There could be multiple modules in a path. 129 if path in path_to_module_info: 130 path_to_module_info[path].append(mod_info) 131 else: 132 path_to_module_info[path] = [mod_info] 133 return path_to_module_info 134 135 def is_module(self, name): 136 """Return True if name is a module, False otherwise.""" 137 return name in self.name_to_module_info 138 139 def get_paths(self, name): 140 """Return paths of supplied module name, Empty list if non-existent.""" 141 info = self.name_to_module_info.get(name) 142 if info: 143 return info.get(constants.MODULE_PATH, []) 144 return [] 145 146 def get_module_names(self, rel_module_path): 147 """Get the modules that all have module_path. 148 149 Args: 150 rel_module_path: path of module in module-info.json 151 152 Returns: 153 List of module names. 154 """ 155 return [m.get(constants.MODULE_NAME) 156 for m in self.path_to_module_info.get(rel_module_path, [])] 157 158 def get_module_info(self, mod_name): 159 """Return dict of info for given module name, None if non-existent.""" 160 module_info = self.name_to_module_info.get(mod_name) 161 # Android's build system will automatically adding 2nd arch bitness 162 # string at the end of the module name which will make atest could not 163 # finding matched module. Rescan the module-info with matched module 164 # name without bitness. 165 if not module_info: 166 for _, module_info in self.name_to_module_info.items(): 167 if mod_name == module_info.get(constants.MODULE_NAME, ''): 168 break 169 return module_info 170 171 def is_suite_in_compatibility_suites(self, suite, mod_info): 172 """Check if suite exists in the compatibility_suites of module-info. 173 174 Args: 175 suite: A string of suite name. 176 mod_info: Dict of module info to check. 177 178 Returns: 179 True if it exists in mod_info, False otherwise. 180 """ 181 return suite in mod_info.get(constants.MODULE_COMPATIBILITY_SUITES, []) 182 183 def get_testable_modules(self, suite=None): 184 """Return the testable modules of the given suite name. 185 186 Args: 187 suite: A string of suite name. Set to None to return all testable 188 modules. 189 190 Returns: 191 List of testable modules. Empty list if non-existent. 192 If suite is None, return all the testable modules in module-info. 193 """ 194 modules = set() 195 for _, info in self.name_to_module_info.items(): 196 if self.is_testable_module(info): 197 if suite: 198 if self.is_suite_in_compatibility_suites(suite, info): 199 modules.add(info.get(constants.MODULE_NAME)) 200 else: 201 modules.add(info.get(constants.MODULE_NAME)) 202 return modules 203 204 def is_testable_module(self, mod_info): 205 """Check if module is something we can test. 206 207 A module is testable if: 208 - it's installed, or 209 - it's a robolectric module (or shares path with one). 210 211 Args: 212 mod_info: Dict of module info to check. 213 214 Returns: 215 True if we can test this module, False otherwise. 216 """ 217 if not mod_info: 218 return False 219 if mod_info.get(constants.MODULE_INSTALLED) and self.has_test_config(mod_info): 220 return True 221 if self.is_robolectric_test(mod_info.get(constants.MODULE_NAME)): 222 return True 223 return False 224 225 def has_test_config(self, mod_info): 226 """Validate if this module has a test config. 227 228 A module can have a test config in the following manner: 229 - AndroidTest.xml at the module path. 230 - test_config be set in module-info.json. 231 - Auto-generated config via the auto_test_config key in module-info.json. 232 233 Args: 234 mod_info: Dict of module info to check. 235 236 Returns: 237 True if this module has a test config, False otherwise. 238 """ 239 # Check if test_config in module-info is set. 240 for test_config in mod_info.get(constants.MODULE_TEST_CONFIG, []): 241 if os.path.isfile(os.path.join(self.root_dir, test_config)): 242 return True 243 # Check for AndroidTest.xml at the module path. 244 for path in mod_info.get(constants.MODULE_PATH, []): 245 if os.path.isfile(os.path.join(self.root_dir, path, 246 constants.MODULE_CONFIG)): 247 return True 248 # Check if the module has an auto-generated config. 249 return self.is_auto_gen_test_config(mod_info.get(constants.MODULE_NAME)) 250 251 def get_robolectric_test_name(self, module_name): 252 """Returns runnable robolectric module name. 253 254 There are at least 2 modules in every robolectric module path, return 255 the module that we can run as a build target. 256 257 Arg: 258 module_name: String of module. 259 260 Returns: 261 String of module that is the runnable robolectric module, None if 262 none could be found. 263 """ 264 module_name_info = self.name_to_module_info.get(module_name) 265 if not module_name_info: 266 return None 267 module_paths = module_name_info.get(constants.MODULE_PATH, []) 268 if module_paths: 269 for mod in self.get_module_names(module_paths[0]): 270 mod_info = self.get_module_info(mod) 271 if self.is_robolectric_module(mod_info): 272 return mod 273 return None 274 275 def is_robolectric_test(self, module_name): 276 """Check if module is a robolectric test. 277 278 A module can be a robolectric test if the specified module has their 279 class set as ROBOLECTRIC (or shares their path with a module that does). 280 281 Args: 282 module_name: String of module to check. 283 284 Returns: 285 True if the module is a robolectric module, else False. 286 """ 287 # Check 1, module class is ROBOLECTRIC 288 mod_info = self.get_module_info(module_name) 289 if self.is_robolectric_module(mod_info): 290 return True 291 # Check 2, shared modules in the path have class ROBOLECTRIC_CLASS. 292 if self.get_robolectric_test_name(module_name): 293 return True 294 return False 295 296 def is_auto_gen_test_config(self, module_name): 297 """Check if the test config file will be generated automatically. 298 299 Args: 300 module_name: A string of the module name. 301 302 Returns: 303 True if the test config file will be generated automatically. 304 """ 305 if self.is_module(module_name): 306 mod_info = self.name_to_module_info.get(module_name) 307 auto_test_config = mod_info.get('auto_test_config', []) 308 return auto_test_config and auto_test_config[0] 309 return False 310 311 def is_robolectric_module(self, mod_info): 312 """Check if a module is a robolectric module. 313 314 Args: 315 mod_info: ModuleInfo to check. 316 317 Returns: 318 True if module is a robolectric module, False otherwise. 319 """ 320 if mod_info: 321 return (mod_info.get(constants.MODULE_CLASS, [None])[0] == 322 constants.MODULE_CLASS_ROBOLECTRIC) 323 return False 324 325 def is_native_test(self, module_name): 326 """Check if the input module is a native test. 327 328 Args: 329 module_name: A string of the module name. 330 331 Returns: 332 True if the test is a native test, False otherwise. 333 """ 334 mod_info = self.get_module_info(module_name) 335 return constants.MODULE_CLASS_NATIVE_TESTS in mod_info.get( 336 constants.MODULE_CLASS, []) 337