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