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 atest_utils.build([module_info_target], 83 logging.getLogger().isEnabledFor(logging.DEBUG)) 84 return module_info_target, module_file_path 85 86 def _load_module_info_file(self, force_build, module_file): 87 """Load the module file. 88 89 Args: 90 force_build: Boolean to indicate if we should rebuild the 91 module_info file regardless if it's created or not. 92 module_file: String of path to file to load up. Used for testing. 93 94 Returns: 95 Tuple of module_info_target and dict of json. 96 """ 97 # If module_file is specified, we're testing so we don't care if 98 # module_info_target stays None. 99 module_info_target = None 100 file_path = module_file 101 if not file_path: 102 module_info_target, file_path = self._discover_mod_file_and_target( 103 force_build) 104 with open(file_path) as json_file: 105 mod_info = json.load(json_file) 106 return module_info_target, mod_info 107 108 @staticmethod 109 def _get_path_to_module_info(name_to_module_info): 110 """Return the path_to_module_info dict. 111 112 Args: 113 name_to_module_info: Dict of module name to module info dict. 114 115 Returns: 116 Dict of module path to module info dict. 117 """ 118 path_to_module_info = {} 119 for mod_name, mod_info in name_to_module_info.items(): 120 # Cross-compiled and multi-arch modules actually all belong to 121 # a single target so filter out these extra modules. 122 if mod_name != mod_info.get(constants.MODULE_NAME, ''): 123 continue 124 for path in mod_info.get(constants.MODULE_PATH, []): 125 mod_info[constants.MODULE_NAME] = mod_name 126 # There could be multiple modules in a path. 127 if path in path_to_module_info: 128 path_to_module_info[path].append(mod_info) 129 else: 130 path_to_module_info[path] = [mod_info] 131 return path_to_module_info 132 133 def is_module(self, name): 134 """Return True if name is a module, False otherwise.""" 135 return name in self.name_to_module_info 136 137 def get_paths(self, name): 138 """Return paths of supplied module name, Empty list if non-existent.""" 139 info = self.name_to_module_info.get(name) 140 if info: 141 return info.get(constants.MODULE_PATH, []) 142 return [] 143 144 def get_module_names(self, rel_module_path): 145 """Get the modules that all have module_path. 146 147 Args: 148 rel_module_path: path of module in module-info.json 149 150 Returns: 151 List of module names. 152 """ 153 return [m.get(constants.MODULE_NAME) 154 for m in self.path_to_module_info.get(rel_module_path, [])] 155 156 def get_module_info(self, mod_name): 157 """Return dict of info for given module name, None if non-existent.""" 158 return self.name_to_module_info.get(mod_name) 159 160 def is_suite_in_compatibility_suites(self, suite, mod_info): 161 """Check if suite exists in the compatibility_suites of module-info. 162 163 Args: 164 suite: A string of suite name. 165 mod_info: Dict of module info to check. 166 167 Returns: 168 True if it exists in mod_info, False otherwise. 169 """ 170 return suite in mod_info.get(constants.MODULE_COMPATIBILITY_SUITES, []) 171 172 def get_testable_modules(self, suite=None): 173 """Return the testable modules of the given suite name. 174 175 Args: 176 suite: A string of suite name. Set to None to return all testable 177 modules. 178 179 Returns: 180 List of testable modules. Empty list if non-existent. 181 If suite is None, return all the testable modules in module-info. 182 """ 183 modules = set() 184 for _, info in self.name_to_module_info.items(): 185 if self.is_testable_module(info): 186 if suite: 187 if self.is_suite_in_compatibility_suites(suite, info): 188 modules.add(info.get(constants.MODULE_NAME)) 189 else: 190 modules.add(info.get(constants.MODULE_NAME)) 191 return modules 192 193 def is_testable_module(self, mod_info): 194 """Check if module is something we can test. 195 196 A module is testable if: 197 - it's installed, or 198 - it's a robolectric module (or shares path with one). 199 200 Args: 201 mod_info: Dict of module info to check. 202 203 Returns: 204 True if we can test this module, False otherwise. 205 """ 206 if not mod_info: 207 return False 208 if mod_info.get(constants.MODULE_INSTALLED) and self.has_test_config(mod_info): 209 return True 210 if self.is_robolectric_test(mod_info.get(constants.MODULE_NAME)): 211 return True 212 return False 213 214 def has_test_config(self, mod_info): 215 """Validate if this module has a test config. 216 217 A module can have a test config in the following manner: 218 - AndroidTest.xml at the module path. 219 - test_config be set in module-info.json. 220 - Auto-generated config via the auto_test_config key in module-info.json. 221 222 Args: 223 mod_info: Dict of module info to check. 224 225 Returns: 226 True if this module has a test config, False otherwise. 227 """ 228 # Check if test_config in module-info is set. 229 for test_config in mod_info.get(constants.MODULE_TEST_CONFIG, []): 230 if os.path.isfile(os.path.join(self.root_dir, test_config)): 231 return True 232 # Check for AndroidTest.xml at the module path. 233 for path in mod_info.get(constants.MODULE_PATH, []): 234 if os.path.isfile(os.path.join(self.root_dir, path, 235 constants.MODULE_CONFIG)): 236 return True 237 # Check if the module has an auto-generated config. 238 return self.is_auto_gen_test_config(mod_info.get(constants.MODULE_NAME)) 239 240 def get_robolectric_test_name(self, module_name): 241 """Returns runnable robolectric module name. 242 243 There are at least 2 modules in every robolectric module path, return 244 the module that we can run as a build target. 245 246 Arg: 247 module_name: String of module. 248 249 Returns: 250 String of module that is the runnable robolectric module, None if 251 none could be found. 252 """ 253 module_name_info = self.name_to_module_info.get(module_name) 254 if not module_name_info: 255 return None 256 module_paths = module_name_info.get(constants.MODULE_PATH, []) 257 if module_paths: 258 for mod in self.get_module_names(module_paths[0]): 259 mod_info = self.get_module_info(mod) 260 if self.is_robolectric_module(mod_info): 261 return mod 262 return None 263 264 def is_robolectric_test(self, module_name): 265 """Check if module is a robolectric test. 266 267 A module can be a robolectric test if the specified module has their 268 class set as ROBOLECTRIC (or shares their path with a module that does). 269 270 Args: 271 module_name: String of module to check. 272 273 Returns: 274 True if the module is a robolectric module, else False. 275 """ 276 # Check 1, module class is ROBOLECTRIC 277 mod_info = self.get_module_info(module_name) 278 if self.is_robolectric_module(mod_info): 279 return True 280 # Check 2, shared modules in the path have class ROBOLECTRIC_CLASS. 281 if self.get_robolectric_test_name(module_name): 282 return True 283 return False 284 285 def is_auto_gen_test_config(self, module_name): 286 """Check if the test config file will be generated automatically. 287 288 Args: 289 module_name: A string of the module name. 290 291 Returns: 292 True if the test config file will be generated automatically. 293 """ 294 if self.is_module(module_name): 295 mod_info = self.name_to_module_info.get(module_name) 296 auto_test_config = mod_info.get('auto_test_config', []) 297 return auto_test_config and auto_test_config[0] 298 return False 299 300 def is_robolectric_module(self, mod_info): 301 """Check if a module is a robolectric module. 302 303 Args: 304 mod_info: ModuleInfo to check. 305 306 Returns: 307 True if module is a robolectric module, False otherwise. 308 """ 309 if mod_info: 310 return (mod_info.get(constants.MODULE_CLASS, [None])[0] == 311 constants.MODULE_CLASS_ROBOLECTRIC) 312 return False 313 314 def is_native_test(self, module_name): 315 """Check if the input module is a native test. 316 317 Args: 318 module_name: A string of the module name. 319 320 Returns: 321 True if the test is a native test, False otherwise. 322 """ 323 mod_info = self.get_module_info(module_name) 324 return constants.MODULE_CLASS_NATIVE_TESTS in mod_info.get( 325 constants.MODULE_CLASS, []) 326