Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 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 | """ |
| 16 | Module Info class used to hold cached module-info.json. |
| 17 | """ |
| 18 | |
| 19 | import json |
| 20 | import logging |
| 21 | import os |
| 22 | |
| 23 | import atest_utils |
| 24 | import constants |
| 25 | |
| 26 | # JSON file generated by build system that lists all buildable targets. |
| 27 | _MODULE_INFO = 'module-info.json' |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 28 | |
| 29 | |
| 30 | class 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) |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 49 | self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 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, '/') |
yangbill | 1067e48 | 2019-01-29 15:57:54 +0800 | [diff] [blame] | 64 | out_dir = os.environ.get(constants.ANDROID_PRODUCT_OUT, root_dir) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 65 | module_file_path = os.path.join(out_dir, _MODULE_INFO) |
| 66 | |
yangbill | 1067e48 | 2019-01-29 15:57:54 +0800 | [diff] [blame] | 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: |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 70 | # Make target is simply file path relative to root |
| 71 | module_info_target = os.path.relpath(module_file_path, root_dir) |
| 72 | else: |
yangbill | 1067e48 | 2019-01-29 15:57:54 +0800 | [diff] [blame] | 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!') |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 76 | module_file_path = os.path.join( |
yangbill | 1067e48 | 2019-01-29 15:57:54 +0800 | [diff] [blame] | 77 | os.environ.get(constants.ANDROID_PRODUCT_OUT), _MODULE_INFO) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 78 | module_info_target = module_file_path |
| 79 | if not os.path.isfile(module_file_path) or force_build: |
nelsonli | 34997d5 | 2018-08-17 09:43:28 +0800 | [diff] [blame] | 80 | logging.debug('Generating %s - this is required for ' |
| 81 | 'initial runs.', _MODULE_INFO) |
patricktu | 5456df0 | 2019-11-01 20:15:02 +0800 | [diff] [blame] | 82 | build_env = dict(constants.ATEST_BUILD_ENV) |
| 83 | build_env.update(constants.DEPS_LICENSE_ENV) |
| 84 | # Also build the deps-license module to generate dependencies data. |
| 85 | atest_utils.build([module_info_target, constants.DEPS_LICENSE], |
Jim Tang | 6ed753e | 2019-07-23 10:39:58 +0800 | [diff] [blame] | 86 | verbose=logging.getLogger().isEnabledFor(logging.DEBUG), |
patricktu | 5456df0 | 2019-11-01 20:15:02 +0800 | [diff] [blame] | 87 | env_vars=build_env) |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 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 = {} |
yangbill | 78ef63e | 2018-08-31 00:11:25 +0800 | [diff] [blame] | 123 | for mod_name, mod_info in name_to_module_info.items(): |
yangbill | 3573e7f | 2018-08-15 21:56:21 +0800 | [diff] [blame] | 124 | # Cross-compiled and multi-arch modules actually all belong to |
| 125 | # a single target so filter out these extra modules. |
yangbill | 2262b81 | 2018-08-28 20:46:03 +0800 | [diff] [blame] | 126 | if mod_name != mod_info.get(constants.MODULE_NAME, ''): |
yangbill | 3573e7f | 2018-08-15 21:56:21 +0800 | [diff] [blame] | 127 | continue |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 128 | for path in mod_info.get(constants.MODULE_PATH, []): |
| 129 | mod_info[constants.MODULE_NAME] = mod_name |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 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): |
Kevin Cheng | 2f903be | 2018-03-05 10:30:26 -0800 | [diff] [blame] | 142 | """Return paths of supplied module name, Empty list if non-existent.""" |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 143 | info = self.name_to_module_info.get(name) |
| 144 | if info: |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 145 | return info.get(constants.MODULE_PATH, []) |
Kevin Cheng | 2f903be | 2018-03-05 10:30:26 -0800 | [diff] [blame] | 146 | return [] |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 147 | |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 148 | def get_module_names(self, rel_module_path): |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 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: |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 155 | List of module names. |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 156 | """ |
Kevin Cheng | 5be930e | 2018-02-20 09:39:22 -0800 | [diff] [blame] | 157 | return [m.get(constants.MODULE_NAME) |
| 158 | for m in self.path_to_module_info.get(rel_module_path, [])] |
Kevin Cheng | 8b2c94c | 2017-12-18 14:43:26 -0800 | [diff] [blame] | 159 | |
| 160 | def get_module_info(self, mod_name): |
| 161 | """Return dict of info for given module name, None if non-existent.""" |
| 162 | return self.name_to_module_info.get(mod_name) |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 163 | |
| 164 | def is_suite_in_compatibility_suites(self, suite, mod_info): |
| 165 | """Check if suite exists in the compatibility_suites of module-info. |
| 166 | |
| 167 | Args: |
| 168 | suite: A string of suite name. |
| 169 | mod_info: Dict of module info to check. |
| 170 | |
| 171 | Returns: |
| 172 | True if it exists in mod_info, False otherwise. |
| 173 | """ |
| 174 | return suite in mod_info.get(constants.MODULE_COMPATIBILITY_SUITES, []) |
| 175 | |
| 176 | def get_testable_modules(self, suite=None): |
| 177 | """Return the testable modules of the given suite name. |
| 178 | |
| 179 | Args: |
| 180 | suite: A string of suite name. Set to None to return all testable |
| 181 | modules. |
| 182 | |
| 183 | Returns: |
| 184 | List of testable modules. Empty list if non-existent. |
| 185 | If suite is None, return all the testable modules in module-info. |
| 186 | """ |
| 187 | modules = set() |
| 188 | for _, info in self.name_to_module_info.items(): |
| 189 | if self.is_testable_module(info): |
| 190 | if suite: |
| 191 | if self.is_suite_in_compatibility_suites(suite, info): |
| 192 | modules.add(info.get(constants.MODULE_NAME)) |
| 193 | else: |
| 194 | modules.add(info.get(constants.MODULE_NAME)) |
| 195 | return modules |
| 196 | |
| 197 | def is_testable_module(self, mod_info): |
| 198 | """Check if module is something we can test. |
| 199 | |
| 200 | A module is testable if: |
| 201 | - it's installed, or |
| 202 | - it's a robolectric module (or shares path with one). |
| 203 | |
| 204 | Args: |
| 205 | mod_info: Dict of module info to check. |
| 206 | |
| 207 | Returns: |
| 208 | True if we can test this module, False otherwise. |
| 209 | """ |
| 210 | if not mod_info: |
| 211 | return False |
| 212 | if mod_info.get(constants.MODULE_INSTALLED) and self.has_test_config(mod_info): |
| 213 | return True |
| 214 | if self.is_robolectric_test(mod_info.get(constants.MODULE_NAME)): |
| 215 | return True |
| 216 | return False |
| 217 | |
| 218 | def has_test_config(self, mod_info): |
| 219 | """Validate if this module has a test config. |
| 220 | |
| 221 | A module can have a test config in the following manner: |
| 222 | - AndroidTest.xml at the module path. |
| 223 | - test_config be set in module-info.json. |
| 224 | - Auto-generated config via the auto_test_config key in module-info.json. |
| 225 | |
| 226 | Args: |
| 227 | mod_info: Dict of module info to check. |
| 228 | |
| 229 | Returns: |
| 230 | True if this module has a test config, False otherwise. |
| 231 | """ |
| 232 | # Check if test_config in module-info is set. |
| 233 | for test_config in mod_info.get(constants.MODULE_TEST_CONFIG, []): |
| 234 | if os.path.isfile(os.path.join(self.root_dir, test_config)): |
| 235 | return True |
| 236 | # Check for AndroidTest.xml at the module path. |
| 237 | for path in mod_info.get(constants.MODULE_PATH, []): |
| 238 | if os.path.isfile(os.path.join(self.root_dir, path, |
| 239 | constants.MODULE_CONFIG)): |
| 240 | return True |
| 241 | # Check if the module has an auto-generated config. |
| 242 | return self.is_auto_gen_test_config(mod_info.get(constants.MODULE_NAME)) |
| 243 | |
| 244 | def get_robolectric_test_name(self, module_name): |
| 245 | """Returns runnable robolectric module name. |
| 246 | |
| 247 | There are at least 2 modules in every robolectric module path, return |
| 248 | the module that we can run as a build target. |
| 249 | |
| 250 | Arg: |
| 251 | module_name: String of module. |
| 252 | |
| 253 | Returns: |
| 254 | String of module that is the runnable robolectric module, None if |
| 255 | none could be found. |
| 256 | """ |
| 257 | module_name_info = self.name_to_module_info.get(module_name) |
| 258 | if not module_name_info: |
| 259 | return None |
| 260 | module_paths = module_name_info.get(constants.MODULE_PATH, []) |
| 261 | if module_paths: |
| 262 | for mod in self.get_module_names(module_paths[0]): |
| 263 | mod_info = self.get_module_info(mod) |
easoncylee | bf56bcf | 2019-03-05 18:46:02 +0800 | [diff] [blame] | 264 | if self.is_robolectric_module(mod_info): |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 265 | return mod |
| 266 | return None |
| 267 | |
| 268 | def is_robolectric_test(self, module_name): |
| 269 | """Check if module is a robolectric test. |
| 270 | |
| 271 | A module can be a robolectric test if the specified module has their |
| 272 | class set as ROBOLECTRIC (or shares their path with a module that does). |
| 273 | |
| 274 | Args: |
| 275 | module_name: String of module to check. |
| 276 | |
| 277 | Returns: |
| 278 | True if the module is a robolectric module, else False. |
| 279 | """ |
| 280 | # Check 1, module class is ROBOLECTRIC |
| 281 | mod_info = self.get_module_info(module_name) |
easoncylee | bf56bcf | 2019-03-05 18:46:02 +0800 | [diff] [blame] | 282 | if self.is_robolectric_module(mod_info): |
easoncylee | f0fb2b1 | 2019-01-22 15:49:09 +0800 | [diff] [blame] | 283 | return True |
| 284 | # Check 2, shared modules in the path have class ROBOLECTRIC_CLASS. |
| 285 | if self.get_robolectric_test_name(module_name): |
| 286 | return True |
| 287 | return False |
| 288 | |
| 289 | def is_auto_gen_test_config(self, module_name): |
| 290 | """Check if the test config file will be generated automatically. |
| 291 | |
| 292 | Args: |
| 293 | module_name: A string of the module name. |
| 294 | |
| 295 | Returns: |
| 296 | True if the test config file will be generated automatically. |
| 297 | """ |
| 298 | if self.is_module(module_name): |
| 299 | mod_info = self.name_to_module_info.get(module_name) |
| 300 | auto_test_config = mod_info.get('auto_test_config', []) |
| 301 | return auto_test_config and auto_test_config[0] |
| 302 | return False |
easoncylee | bf56bcf | 2019-03-05 18:46:02 +0800 | [diff] [blame] | 303 | |
| 304 | def is_robolectric_module(self, mod_info): |
| 305 | """Check if a module is a robolectric module. |
| 306 | |
| 307 | Args: |
| 308 | mod_info: ModuleInfo to check. |
| 309 | |
| 310 | Returns: |
| 311 | True if module is a robolectric module, False otherwise. |
| 312 | """ |
| 313 | if mod_info: |
| 314 | return (mod_info.get(constants.MODULE_CLASS, [None])[0] == |
| 315 | constants.MODULE_CLASS_ROBOLECTRIC) |
| 316 | return False |
yangbill | 57739f8 | 2019-03-28 18:25:43 +0800 | [diff] [blame] | 317 | |
| 318 | def is_native_test(self, module_name): |
| 319 | """Check if the input module is a native test. |
| 320 | |
| 321 | Args: |
| 322 | module_name: A string of the module name. |
| 323 | |
| 324 | Returns: |
| 325 | True if the test is a native test, False otherwise. |
| 326 | """ |
| 327 | mod_info = self.get_module_info(module_name) |
| 328 | return constants.MODULE_CLASS_NATIVE_TESTS in mod_info.get( |
| 329 | constants.MODULE_CLASS, []) |