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 24import shutil 25import sys 26import tempfile 27import time 28 29import atest_utils 30import constants 31 32from metrics import metrics 33 34# JSON file generated by build system that lists all buildable targets. 35_MODULE_INFO = 'module-info.json' 36# JSON file generated by build system that lists dependencies for java. 37_JAVA_DEP_INFO = 'module_bp_java_deps.json' 38# JSON file generated by build system that lists dependencies for cc. 39_CC_DEP_INFO = 'module_bp_cc_deps.json' 40# JSON file generated by atest merged the content from module-info, 41# module_bp_java_deps.json, and module_bp_cc_deps. 42_MERGED_INFO = 'atest_merged_dep.json' 43 44class ModuleInfo: 45 """Class that offers fast/easy lookup for Module related details.""" 46 47 def __init__(self, force_build=False, module_file=None): 48 """Initialize the ModuleInfo object. 49 50 Load up the module-info.json file and initialize the helper vars. 51 52 Args: 53 force_build: Boolean to indicate if we should rebuild the 54 module_info file regardless if it's created or not. 55 module_file: String of path to file to load up. Used for testing. 56 """ 57 module_info_target, name_to_module_info = self._load_module_info_file( 58 force_build, module_file) 59 self.name_to_module_info = name_to_module_info 60 self.module_info_target = module_info_target 61 self.path_to_module_info = self._get_path_to_module_info( 62 self.name_to_module_info) 63 self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP) 64 65 @staticmethod 66 def _discover_mod_file_and_target(force_build): 67 """Find the module file. 68 69 Args: 70 force_build: Boolean to indicate if we should rebuild the 71 module_info file regardless if it's created or not. 72 73 Returns: 74 Tuple of module_info_target and path to module file. 75 """ 76 logging.debug('Probing and validating module info...') 77 module_info_target = None 78 root_dir = os.environ.get(constants.ANDROID_BUILD_TOP, '/') 79 out_dir = os.environ.get(constants.ANDROID_PRODUCT_OUT, root_dir) 80 module_file_path = os.path.join(out_dir, _MODULE_INFO) 81 82 # Check if the user set a custom out directory by comparing the out_dir 83 # to the root_dir. 84 if out_dir.find(root_dir) == 0: 85 # Make target is simply file path no-absolute to root 86 module_info_target = os.path.relpath(module_file_path, root_dir) 87 else: 88 # If the user has set a custom out directory, generate an absolute 89 # path for module info targets. 90 logging.debug('User customized out dir!') 91 module_file_path = os.path.join( 92 os.environ.get(constants.ANDROID_PRODUCT_OUT), _MODULE_INFO) 93 module_info_target = module_file_path 94 # Make sure module-info exist and could be load properly. 95 if not atest_utils.is_valid_json_file(module_file_path) or force_build: 96 logging.debug('Generating %s - this is required for ' 97 'initial runs or forced rebuilds.', _MODULE_INFO) 98 build_env = dict(constants.ATEST_BUILD_ENV) 99 build_start = time.time() 100 if not atest_utils.build([module_info_target], 101 verbose=logging.getLogger().isEnabledFor( 102 logging.DEBUG), env_vars=build_env): 103 sys.exit(constants.EXIT_CODE_BUILD_FAILURE) 104 build_duration = time.time() - build_start 105 metrics.LocalDetectEvent( 106 detect_type=constants.DETECT_TYPE_ONLY_BUILD_MODULE_INFO, 107 result=int(build_duration)) 108 return module_info_target, module_file_path 109 110 def _load_module_info_file(self, force_build, module_file): 111 """Load the module file. 112 113 Args: 114 force_build: Boolean to indicate if we should rebuild the 115 module_info file regardless if it's created or not. 116 module_file: String of path to file to load up. Used for testing. 117 118 Returns: 119 Tuple of module_info_target and dict of json. 120 """ 121 # If module_file is specified, we're testing so we don't care if 122 # module_info_target stays None. 123 module_info_target = None 124 file_path = module_file 125 if not file_path: 126 module_info_target, file_path = self._discover_mod_file_and_target( 127 force_build) 128 merged_file_path = self.get_atest_merged_info_path() 129 if (not self.need_update_merged_file(force_build) 130 and os.path.exists(merged_file_path)): 131 file_path = merged_file_path 132 logging.debug('Loading %s as module-info.', file_path) 133 with open(file_path) as json_file: 134 mod_info = json.load(json_file) 135 if self.need_update_merged_file(force_build): 136 mod_info = self._merge_build_system_infos(mod_info) 137 return module_info_target, mod_info 138 139 @staticmethod 140 def _get_path_to_module_info(name_to_module_info): 141 """Return the path_to_module_info dict. 142 143 Args: 144 name_to_module_info: Dict of module name to module info dict. 145 146 Returns: 147 Dict of module path to module info dict. 148 """ 149 path_to_module_info = {} 150 for mod_name, mod_info in name_to_module_info.items(): 151 # Cross-compiled and multi-arch modules actually all belong to 152 # a single target so filter out these extra modules. 153 if mod_name != mod_info.get(constants.MODULE_NAME, ''): 154 continue 155 for path in mod_info.get(constants.MODULE_PATH, []): 156 mod_info[constants.MODULE_NAME] = mod_name 157 # There could be multiple modules in a path. 158 if path in path_to_module_info: 159 path_to_module_info[path].append(mod_info) 160 else: 161 path_to_module_info[path] = [mod_info] 162 return path_to_module_info 163 164 def is_module(self, name): 165 """Return True if name is a module, False otherwise.""" 166 if self.get_module_info(name): 167 return True 168 return False 169 170 def get_paths(self, name): 171 """Return paths of supplied module name, Empty list if non-existent.""" 172 info = self.get_module_info(name) 173 if info: 174 return info.get(constants.MODULE_PATH, []) 175 return [] 176 177 def get_module_names(self, rel_module_path): 178 """Get the modules that all have module_path. 179 180 Args: 181 rel_module_path: path of module in module-info.json 182 183 Returns: 184 List of module names. 185 """ 186 return [m.get(constants.MODULE_NAME) 187 for m in self.path_to_module_info.get(rel_module_path, [])] 188 189 def get_module_info(self, mod_name): 190 """Return dict of info for given module name, None if non-existence.""" 191 module_info = self.name_to_module_info.get(mod_name) 192 # Android's build system will automatically adding 2nd arch bitness 193 # string at the end of the module name which will make atest could not 194 # find the matched module. Rescan the module-info with the matched module 195 # name without bitness. 196 if not module_info: 197 for _, mod_info in self.name_to_module_info.items(): 198 if mod_name == mod_info.get(constants.MODULE_NAME, ''): 199 return mod_info 200 return module_info 201 202 def is_suite_in_compatibility_suites(self, suite, mod_info): 203 """Check if suite exists in the compatibility_suites of module-info. 204 205 Args: 206 suite: A string of suite name. 207 mod_info: Dict of module info to check. 208 209 Returns: 210 True if it exists in mod_info, False otherwise. 211 """ 212 return suite in mod_info.get(constants.MODULE_COMPATIBILITY_SUITES, []) 213 214 def get_testable_modules(self, suite=None): 215 """Return the testable modules of the given suite name. 216 217 Args: 218 suite: A string of suite name. Set to None to return all testable 219 modules. 220 221 Returns: 222 List of testable modules. Empty list if non-existent. 223 If suite is None, return all the testable modules in module-info. 224 """ 225 modules = set() 226 for _, info in self.name_to_module_info.items(): 227 if self.is_testable_module(info): 228 if suite: 229 if self.is_suite_in_compatibility_suites(suite, info): 230 modules.add(info.get(constants.MODULE_NAME)) 231 else: 232 modules.add(info.get(constants.MODULE_NAME)) 233 return modules 234 235 def is_testable_module(self, mod_info): 236 """Check if module is something we can test. 237 238 A module is testable if: 239 - it's installed, or 240 - it's a robolectric module (or shares path with one). 241 242 Args: 243 mod_info: Dict of module info to check. 244 245 Returns: 246 True if we can test this module, False otherwise. 247 """ 248 if not mod_info: 249 return False 250 if mod_info.get(constants.MODULE_INSTALLED) and self.has_test_config(mod_info): 251 return True 252 if self.is_robolectric_test(mod_info.get(constants.MODULE_NAME)): 253 return True 254 return False 255 256 def has_test_config(self, mod_info): 257 """Validate if this module has a test config. 258 259 A module can have a test config in the following manner: 260 - AndroidTest.xml at the module path. 261 - test_config be set in module-info.json. 262 - Auto-generated config via the auto_test_config key 263 in module-info.json. 264 265 Args: 266 mod_info: Dict of module info to check. 267 268 Returns: 269 True if this module has a test config, False otherwise. 270 """ 271 # Check if test_config in module-info is set. 272 for test_config in mod_info.get(constants.MODULE_TEST_CONFIG, []): 273 if os.path.isfile(os.path.join(self.root_dir, test_config)): 274 return True 275 # Check for AndroidTest.xml at the module path. 276 for path in mod_info.get(constants.MODULE_PATH, []): 277 if os.path.isfile(os.path.join(self.root_dir, path, 278 constants.MODULE_CONFIG)): 279 return True 280 # Check if the module has an auto-generated config. 281 return self.is_auto_gen_test_config(mod_info.get(constants.MODULE_NAME)) 282 283 def get_robolectric_test_name(self, module_name): 284 """Returns runnable robolectric module name. 285 286 There are at least 2 modules in every robolectric module path, return 287 the module that we can run as a build target. 288 289 Arg: 290 module_name: String of module. 291 292 Returns: 293 String of module that is the runnable robolectric module, None if 294 none could be found. 295 """ 296 module_name_info = self.get_module_info(module_name) 297 if not module_name_info: 298 return None 299 module_paths = module_name_info.get(constants.MODULE_PATH, []) 300 if module_paths: 301 for mod in self.get_module_names(module_paths[0]): 302 mod_info = self.get_module_info(mod) 303 if self.is_robolectric_module(mod_info): 304 return mod 305 return None 306 307 def is_robolectric_test(self, module_name): 308 """Check if module is a robolectric test. 309 310 A module can be a robolectric test if the specified module has their 311 class set as ROBOLECTRIC (or shares their path with a module that does). 312 313 Args: 314 module_name: String of module to check. 315 316 Returns: 317 True if the module is a robolectric module, else False. 318 """ 319 # Check 1, module class is ROBOLECTRIC 320 mod_info = self.get_module_info(module_name) 321 if self.is_robolectric_module(mod_info): 322 return True 323 # Check 2, shared modules in the path have class ROBOLECTRIC_CLASS. 324 if self.get_robolectric_test_name(module_name): 325 return True 326 return False 327 328 def is_auto_gen_test_config(self, module_name): 329 """Check if the test config file will be generated automatically. 330 331 Args: 332 module_name: A string of the module name. 333 334 Returns: 335 True if the test config file will be generated automatically. 336 """ 337 if self.is_module(module_name): 338 mod_info = self.get_module_info(module_name) 339 auto_test_config = mod_info.get('auto_test_config', []) 340 return auto_test_config and auto_test_config[0] 341 return False 342 343 def is_robolectric_module(self, mod_info): 344 """Check if a module is a robolectric module. 345 346 Args: 347 mod_info: ModuleInfo to check. 348 349 Returns: 350 True if module is a robolectric module, False otherwise. 351 """ 352 if mod_info: 353 return (mod_info.get(constants.MODULE_CLASS, [None])[0] == 354 constants.MODULE_CLASS_ROBOLECTRIC) 355 return False 356 357 def is_native_test(self, module_name): 358 """Check if the input module is a native test. 359 360 Args: 361 module_name: A string of the module name. 362 363 Returns: 364 True if the test is a native test, False otherwise. 365 """ 366 mod_info = self.get_module_info(module_name) 367 return constants.MODULE_CLASS_NATIVE_TESTS in mod_info.get( 368 constants.MODULE_CLASS, []) 369 370 def has_mainline_modules(self, module_name, mainline_modules): 371 """Check if the mainline modules are in module-info. 372 373 Args: 374 module_name: A string of the module name. 375 mainline_modules: A list of mainline modules. 376 377 Returns: 378 True if mainline_modules is in module-info, False otherwise. 379 """ 380 # TODO: (b/165425972)Check AndroidTest.xml or specific test config. 381 mod_info = self.get_module_info(module_name) 382 if mainline_modules in mod_info.get(constants.MODULE_MAINLINE_MODULES, 383 []): 384 return True 385 return False 386 387 def generate_atest_merged_dep_file(self): 388 """Method for generating atest_merged_dep.json.""" 389 self._merge_build_system_infos(self.name_to_module_info, 390 self.get_java_dep_info_path(), 391 self.get_cc_dep_info_path()) 392 393 def _merge_build_system_infos(self, name_to_module_info, 394 java_bp_info_path=None, cc_bp_info_path=None): 395 """Merge the full build system's info to name_to_module_info. 396 397 Args: 398 name_to_module_info: Dict of module name to module info dict. 399 java_bp_info_path: String of path to java dep file to load up. 400 Used for testing. 401 cc_bp_info_path: String of path to cc dep file to load up. 402 Used for testing. 403 404 Returns: 405 Dict of merged json of input def_file_path and name_to_module_info. 406 """ 407 # Merge _JAVA_DEP_INFO 408 if not java_bp_info_path: 409 java_bp_info_path = self.get_java_dep_info_path() 410 if atest_utils.is_valid_json_file(java_bp_info_path): 411 with open(java_bp_info_path) as json_file: 412 java_bp_infos = json.load(json_file) 413 logging.debug('Merging Java build info: %s', java_bp_info_path) 414 name_to_module_info = self._merge_soong_info( 415 name_to_module_info, java_bp_infos) 416 # Merge _CC_DEP_INFO 417 if not cc_bp_info_path: 418 cc_bp_info_path = self.get_cc_dep_info_path() 419 if atest_utils.is_valid_json_file(cc_bp_info_path): 420 with open(cc_bp_info_path) as json_file: 421 cc_bp_infos = json.load(json_file) 422 logging.debug('Merging CC build info: %s', cc_bp_info_path) 423 # CC's dep json format is different with java. 424 # Below is the example content: 425 # { 426 # "clang": "${ANDROID_ROOT}/bin/clang", 427 # "clang++": "${ANDROID_ROOT}/bin/clang++", 428 # "modules": { 429 # "ACameraNdkVendorTest": { 430 # "path": [ 431 # "frameworks/av/camera/ndk" 432 # ], 433 # "srcs": [ 434 # "frameworks/tests/AImageVendorTest.cpp", 435 # "frameworks/tests/ACameraManagerTest.cpp" 436 # ], 437 name_to_module_info = self._merge_soong_info( 438 name_to_module_info, cc_bp_infos.get('modules', {})) 439 return name_to_module_info 440 441 def _merge_soong_info(self, name_to_module_info, mod_bp_infos): 442 """Merge the dependency and srcs in mod_bp_infos to name_to_module_info. 443 444 Args: 445 name_to_module_info: Dict of module name to module info dict. 446 mod_bp_infos: Dict of module name to bp's module info dict. 447 448 Returns: 449 Dict of merged json of input def_file_path and name_to_module_info. 450 """ 451 merge_items = [constants.MODULE_DEPENDENCIES, constants.MODULE_SRCS] 452 for module_name, dep_info in mod_bp_infos.items(): 453 if name_to_module_info.get(module_name, None): 454 mod_info = name_to_module_info.get(module_name) 455 for merge_item in merge_items: 456 dep_info_values = dep_info.get(merge_item, []) 457 mod_info_values = mod_info.get(merge_item, []) 458 for dep_info_value in dep_info_values: 459 if dep_info_value not in mod_info_values: 460 mod_info_values.append(dep_info_value) 461 mod_info_values.sort() 462 name_to_module_info[ 463 module_name][merge_item] = mod_info_values 464 output_file = self.get_atest_merged_info_path() 465 if not os.path.isdir(os.path.dirname(output_file)): 466 os.makedirs(os.path.dirname(output_file)) 467 # b/178559543 saving merged module info in a temp file and copying it to 468 # atest_merged_dep.json can eliminate the possibility of accessing it 469 # concurrently and resulting in invalid JSON format. 470 temp_file = tempfile.NamedTemporaryFile() 471 with open(temp_file.name, 'w') as _temp: 472 json.dump(name_to_module_info, _temp, indent=0) 473 shutil.copy(temp_file.name, output_file) 474 temp_file.close() 475 return name_to_module_info 476 477 def get_module_dependency(self, module_name, depend_on=None): 478 """Get the dependency sets for input module. 479 480 Recursively find all the dependencies of the input module. 481 482 Args: 483 module_name: String of module to check. 484 depend_on: The list of parent dependencies. 485 486 Returns: 487 Set of dependency modules. 488 """ 489 if not depend_on: 490 depend_on = set() 491 deps = set() 492 mod_info = self.get_module_info(module_name) 493 if not mod_info: 494 return deps 495 mod_deps = set(mod_info.get(constants.MODULE_DEPENDENCIES, [])) 496 # Remove item in deps if it already in depend_on: 497 mod_deps = mod_deps - depend_on 498 deps = deps.union(mod_deps) 499 for mod_dep in mod_deps: 500 deps = deps.union(set(self.get_module_dependency( 501 mod_dep, depend_on=depend_on.union(deps)))) 502 return deps 503 504 def get_install_module_dependency(self, module_name, depend_on=None): 505 """Get the dependency set for the given modules with installed path. 506 507 Args: 508 module_name: String of module to check. 509 depend_on: The list of parent dependencies. 510 511 Returns: 512 Set of dependency modules which has installed path. 513 """ 514 install_deps = set() 515 deps = self.get_module_dependency(module_name, depend_on) 516 logging.debug('%s depends on: %s', module_name, deps) 517 for module in deps: 518 mod_info = self.get_module_info(module) 519 if mod_info and mod_info.get(constants.MODULE_INSTALLED, []): 520 install_deps.add(module) 521 logging.debug('modules %s required by %s were not installed', 522 install_deps, module_name) 523 return install_deps 524 525 @staticmethod 526 def get_atest_merged_info_path(): 527 """Returns the path for atest_merged_dep.json. 528 529 Returns: 530 String for atest_merged_dep.json. 531 """ 532 return os.path.join(atest_utils.get_build_out_dir(), 533 'soong', _MERGED_INFO) 534 535 @staticmethod 536 def get_java_dep_info_path(): 537 """Returns the path for atest_merged_dep.json. 538 539 Returns: 540 String for atest_merged_dep.json. 541 """ 542 return os.path.join(atest_utils.get_build_out_dir(), 543 'soong', _JAVA_DEP_INFO) 544 545 @staticmethod 546 def get_cc_dep_info_path(): 547 """Returns the path for atest_merged_dep.json. 548 549 Returns: 550 String for atest_merged_dep.json. 551 """ 552 return os.path.join(atest_utils.get_build_out_dir(), 553 'soong', _CC_DEP_INFO) 554 555 def has_soong_info(self): 556 """Ensure the existence of soong info files. 557 558 Returns: 559 True if soong info need to merge, false otherwise. 560 """ 561 return (os.path.isfile(self.get_java_dep_info_path()) and 562 os.path.isfile(self.get_cc_dep_info_path())) 563 564 def need_update_merged_file(self, force_build=False): 565 """Check if need to update/generated atest_merged_dep. 566 567 If force_build, always update merged info. 568 If not force build, if soong info exist but merged inforamtion not exist, 569 need to update merged file. 570 571 Args: 572 force_build: Boolean to indicate that if user want to rebuild 573 module_info file regardless if it's created or not. 574 575 Returns: 576 True if atest_merged_dep should be updated, false otherwise. 577 """ 578 return (force_build or 579 (self.has_soong_info() and 580 not os.path.exists(self.get_atest_merged_info_path()))) 581 582 def is_unit_test(self, mod_info): 583 """Return True if input module is unit test, False otherwise. 584 585 Args: 586 mod_info: ModuleInfo to check. 587 588 Returns: 589 True if if input module is unit test, False otherwise. 590 """ 591 return mod_info.get(constants.MODULE_IS_UNIT_TEST, '') == 'true' 592 593 def get_all_unit_tests(self): 594 """Get a list of all the module names which are unit tests.""" 595 unit_tests = [] 596 for mod_name, mod_info in self.name_to_module_info.items(): 597 if mod_info.get(constants.MODULE_NAME, '') == mod_name: 598 if self.is_unit_test(mod_info): 599 unit_tests.append(mod_name) 600 return unit_tests 601