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"""Module Finder class.""" 16 17import logging 18import os 19import time 20from typing import List 21 22from atest import atest_configs 23from atest import atest_error 24from atest import atest_utils 25from atest import constants 26from atest.atest_enum import DetectType 27from atest.metrics import metrics 28from atest.test_finders import test_filter_utils 29from atest.test_finders import test_finder_base 30from atest.test_finders import test_finder_utils 31from atest.test_finders import test_info 32from atest.test_runners import atest_tf_test_runner 33from atest.test_runners import mobly_test_runner 34from atest.test_runners import robolectric_test_runner 35from atest.test_runners import vts_tf_test_runner 36 37# These are suites in LOCAL_COMPATIBILITY_SUITE that aren't really suites so 38# we can ignore them. 39_SUITES_TO_IGNORE = frozenset({'general-tests', 'device-tests', 'tests'}) 40 41 42class ModuleFinder(test_finder_base.TestFinderBase): 43 """Module finder class.""" 44 45 NAME = 'MODULE' 46 _TEST_RUNNER = atest_tf_test_runner.AtestTradefedTestRunner.NAME 47 _MOBLY_RUNNER = mobly_test_runner.MoblyTestRunner.NAME 48 _ROBOLECTRIC_RUNNER = robolectric_test_runner.RobolectricTestRunner.NAME 49 _VTS_TEST_RUNNER = vts_tf_test_runner.VtsTradefedTestRunner.NAME 50 51 def __init__(self, module_info=None): 52 super().__init__() 53 self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP) 54 self.module_info = module_info 55 56 def _determine_modules_to_test( 57 self, module_path: str, test_file_path: str = None 58 ) -> set[str]: 59 """Determine which module the user is trying to test. 60 61 Returns the modules to test. If there are multiple possibilities, will 62 ask the user. Otherwise will return the only module found. 63 64 Args: 65 module_path: String path of module to look for. 66 test_file_path: String path of input file where the test is found. 67 68 Returns: 69 A set of the module names. 70 """ 71 modules_to_test = set() 72 73 if test_file_path: 74 modules_to_test = self.module_info.get_modules_by_path_in_srcs( 75 path=test_file_path, 76 testable_modules_only=True, 77 ) 78 79 # If a single module path matches contains the path of the given test file 80 # in its MODULE_SRCS, do not continue to extract modules. 81 if len(modules_to_test) == 1: 82 return modules_to_test 83 84 modules_to_test |= self.module_info.get_modules_by_path( 85 path=module_path, 86 testable_modules_only=True, 87 ) 88 89 return test_finder_utils.extract_selected_tests(modules_to_test) 90 91 def _is_vts_module(self, module_name): 92 """Returns True if the module is a vts10 module, else False.""" 93 mod_info = self.module_info.get_module_info(module_name) 94 suites = [] 95 if mod_info: 96 suites = mod_info.get(constants.MODULE_COMPATIBILITY_SUITES, []) 97 # Pull out all *ts (cts, tvts, etc) suites. 98 suites = [suite for suite in suites if suite not in _SUITES_TO_IGNORE] 99 return len(suites) == 1 and 'vts10' in suites 100 101 def _update_to_vts_test_info(self, test): 102 """Fill in the fields with vts10 specific info. 103 104 We need to update the runner to use the vts10 runner and also find the 105 test specific dependencies. 106 107 Args: 108 test: TestInfo to update with vts10 specific details. 109 110 Returns: 111 TestInfo that is ready for the vts10 test runner. 112 """ 113 test.test_runner = self._VTS_TEST_RUNNER 114 config_file = os.path.join( 115 self.root_dir, test.data[constants.TI_REL_CONFIG] 116 ) 117 # Need to get out dir (special logic is to account for custom out dirs). 118 # The out dir is used to construct the build targets for the test deps. 119 out_dir = os.environ.get(constants.ANDROID_HOST_OUT) 120 custom_out_dir = os.environ.get(constants.ANDROID_OUT_DIR) 121 # If we're not an absolute custom out dir, get no-absolute out dir path. 122 if custom_out_dir is None or not os.path.isabs(custom_out_dir): 123 out_dir = os.path.relpath(out_dir, self.root_dir) 124 vts_out_dir = os.path.join(out_dir, 'vts10', 'android-vts10', 'testcases') 125 # Parse dependency of default staging plans. 126 xml_paths = test_finder_utils.search_integration_dirs( 127 constants.VTS_STAGING_PLAN, 128 self.module_info.get_paths(constants.VTS_TF_MODULE), 129 ) 130 vts_xmls = set() 131 vts_xmls.add(config_file) 132 for xml_path in xml_paths: 133 vts_xmls |= test_finder_utils.get_plans_from_vts_xml(xml_path) 134 for config_file in vts_xmls: 135 # Add in vts10 test build targets. 136 for target in test_finder_utils.get_targets_from_vts_xml( 137 config_file, vts_out_dir, self.module_info 138 ): 139 test.add_build_target(target) 140 test.add_build_target('vts-test-core') 141 test.add_build_target(test.test_name) 142 return test 143 144 def _update_to_mobly_test_info(self, test): 145 """Update the fields for a Mobly test. 146 147 The runner will be updated to the Mobly runner. 148 149 The module's build output paths will be stored in the test_info data. 150 151 Args: 152 test: TestInfo to be updated with Mobly fields. 153 154 Returns: 155 TestInfo with updated Mobly fields. 156 """ 157 # Set test runner to MoblyTestRunner 158 test.test_runner = self._MOBLY_RUNNER 159 # Add test module as build target 160 module_name = test.test_name 161 test.add_build_target(module_name) 162 # Add module's installed paths to data, so the runner may access the 163 # module's build outputs. 164 installed_paths = self.module_info.get_installed_paths(module_name) 165 test.data[constants.MODULE_INSTALLED] = installed_paths 166 return test 167 168 def _update_legacy_robolectric_test_info(self, test): 169 """Update the fields for a legacy robolectric test. 170 171 This method is updating test_name when the given is a legacy robolectric 172 test, and assigning Robolectric Runner for it. 173 174 e.g. WallPaperPicker2RoboTests is a legacy robotest, and the test_name 175 will become RunWallPaperPicker2RoboTests and run it with Robolectric 176 Runner. 177 178 Args: 179 test: TestInfo to be updated with robolectric fields. 180 181 Returns: 182 TestInfo with updated robolectric fields. 183 """ 184 test.test_runner = self._ROBOLECTRIC_RUNNER 185 test.test_name = self.module_info.get_robolectric_test_name( 186 self.module_info.get_module_info(test.test_name) 187 ) 188 return test 189 190 # pylint: disable=too-many-branches 191 def _process_test_info(self, test): 192 """Process the test info and return some fields updated/changed. 193 194 We need to check if the test found is a special module (like vts10) and 195 update the test_info fields (like test_runner) appropriately. 196 197 Args: 198 test: TestInfo that has been filled out by a find method. 199 200 Returns: 201 TestInfo that has been modified as needed and return None if 202 this module can't be found in the module_info. 203 """ 204 module_name = test.test_name 205 mod_info = self.module_info.get_module_info(module_name) 206 if not mod_info: 207 return None 208 test.module_class = mod_info['class'] 209 test.install_locations = test_finder_utils.get_install_locations( 210 mod_info.get(constants.MODULE_INSTALLED, []) 211 ) 212 # Check if this is only a vts10 module. 213 if self._is_vts_module(test.test_name): 214 return self._update_to_vts_test_info(test) 215 # Check if this is a Mobly test module. 216 if self.module_info.is_mobly_module(mod_info): 217 return self._update_to_mobly_test_info(test) 218 test.robo_type = self.module_info.get_robolectric_type(test.test_name) 219 if test.robo_type: 220 test.install_locations = {constants.DEVICELESS_TEST} 221 if test.robo_type == constants.ROBOTYPE_MODERN: 222 test.add_build_target(test.test_name) 223 return test 224 if test.robo_type == constants.ROBOTYPE_LEGACY: 225 return self._update_legacy_robolectric_test_info(test) 226 rel_config = test.data[constants.TI_REL_CONFIG] 227 for target in self._get_build_targets(module_name, rel_config): 228 test.add_build_target(target) 229 # (b/177626045) Probe target APK for running instrumentation tests to 230 # prevent RUNNER ERROR by adding target application(module) to the 231 # build_targets, and install these target apks before testing. 232 artifact_map = self.module_info.get_instrumentation_target_apps(module_name) 233 if artifact_map: 234 logging.debug('Found %s an instrumentation test.', module_name) 235 for art in artifact_map.keys(): 236 test.add_build_target(art) 237 logging.debug( 238 'Add %s to build targets...', ', '.join(artifact_map.keys()) 239 ) 240 test.artifacts = [] 241 for p in artifact_map.values(): 242 test.artifacts += p 243 logging.debug('Will install target APK: %s\n', test.artifacts) 244 metrics.LocalDetectEvent( 245 detect_type=DetectType.FOUND_TARGET_ARTIFACTS, 246 result=len(test.artifacts), 247 ) 248 # For device side java test, it will use 249 # com.android.compatibility.testtype.DalvikTest as test runner in 250 # cts-dalvik-device-test-runner.jar 251 if self.module_info.is_auto_gen_test_config(module_name): 252 if constants.MODULE_CLASS_JAVA_LIBRARIES in test.module_class: 253 for dalvik_dep in test_finder_utils.DALVIK_TEST_DEPS: 254 if self.module_info.is_module(dalvik_dep): 255 test.add_build_target(dalvik_dep) 256 # Update test name if the test belong to extra config which means it's 257 # test config name is not the same as module name. For extra config, it 258 # index will be greater or equal to 1. 259 try: 260 if mod_info.get(constants.MODULE_TEST_CONFIG, []).index(rel_config) > 0: 261 config_test_name = os.path.splitext(os.path.basename(rel_config))[0] 262 logging.debug( 263 'Replace test_info.name(%s) to %s', test.test_name, config_test_name 264 ) 265 test.test_name = config_test_name 266 except ValueError: 267 pass 268 return test 269 270 def _get_build_targets(self, module_name, rel_config): 271 """Get the test deps. 272 273 Args: 274 module_name: name of the test. 275 rel_config: XML for the given test. 276 277 Returns: 278 Set of build targets. 279 """ 280 targets = set() 281 if not self.module_info.is_auto_gen_test_config(module_name): 282 config_file = os.path.join(self.root_dir, rel_config) 283 targets = test_finder_utils.get_targets_from_xml( 284 config_file, self.module_info 285 ) 286 if constants.VTS_CORE_SUITE in self.module_info.get_module_info( 287 module_name 288 ).get(constants.MODULE_COMPATIBILITY_SUITES, []): 289 targets.add(constants.VTS_CORE_TF_MODULE) 290 for suite in self.module_info.get_module_info(module_name).get( 291 constants.MODULE_COMPATIBILITY_SUITES, [] 292 ): 293 targets.update(constants.SUITE_DEPS.get(suite, [])) 294 for module_path in self.module_info.get_paths(module_name): 295 mod_dir = module_path.replace('/', '-') 296 targets.add(constants.MODULES_IN + mod_dir) 297 # (b/184567849) Force adding module_name as a build_target. This will 298 # allow excluding MODULES-IN-* and prevent from missing build targets. 299 if module_name and self.module_info.is_module(module_name): 300 targets.add(module_name) 301 # If it's a MTS test, add cts-tradefed as test dependency. 302 if constants.MTS_SUITE in self.module_info.get_module_info(module_name).get( 303 constants.MODULE_COMPATIBILITY_SUITES, [] 304 ): 305 if self.module_info.is_module(constants.CTS_JAR): 306 targets.add(constants.CTS_JAR) 307 return targets 308 309 def _get_module_test_config(self, module_name, rel_config=None) -> list[str]: 310 """Get the value of test_config in module_info. 311 312 Get the value of 'test_config' in module_info if its 313 auto_test_config is not true. 314 In this case, the test_config is specified by user. 315 If not, return rel_config. 316 317 Args: 318 module_name: A string of the test's module name. 319 rel_config: XML for the given test. 320 321 Returns: 322 A list of string of test_config path if found, else return rel_config. 323 """ 324 default_all_config = not ( 325 atest_configs.GLOBAL_ARGS 326 and atest_configs.GLOBAL_ARGS.test_config_select 327 ) 328 mod_info = self.module_info.get_module_info(module_name) 329 if mod_info: 330 test_configs = [] 331 test_config_list = mod_info.get(constants.MODULE_TEST_CONFIG, []) 332 if test_config_list: 333 # multiple test configs 334 if len(test_config_list) > 1: 335 test_configs = test_finder_utils.extract_selected_tests( 336 test_config_list, default_all=default_all_config 337 ) 338 else: 339 test_configs = test_config_list 340 if test_configs: 341 return test_configs 342 # Double check if below section is needed. 343 if ( 344 not self.module_info.is_auto_gen_test_config(module_name) 345 and test_configs 346 ): 347 return test_configs 348 return [rel_config] if rel_config else [] 349 350 # pylint: disable=too-many-branches 351 # pylint: disable=too-many-locals 352 def _get_test_info_filter( 353 self, path, methods, rel_module_dir=None, class_name=None, 354 is_native_test=False 355 ): 356 """Get test info filter. 357 358 Args: 359 path: A string of the test's path. 360 methods: A set of method name strings. 361 rel_module_dir: Optional. A string of the module dir no-absolute to 362 root. 363 class_name: Optional. A string of the class name. 364 is_native_test: Optional. A boolean variable of whether to search for 365 a native test or not. 366 367 Returns: 368 A set of test info filter. 369 """ 370 _, file_name = test_finder_utils.get_dir_path_and_filename(path) 371 ti_filter = frozenset() 372 if os.path.isfile(path) and is_native_test: 373 class_info = test_finder_utils.get_cc_class_info(path) 374 ti_filter = frozenset([ 375 test_info.TestFilter( 376 test_filter_utils.get_cc_filter( 377 class_info, 378 class_name if class_name is not None else '*', methods 379 ), 380 frozenset(), 381 ) 382 ]) 383 # Path to java file. 384 elif file_name and constants.JAVA_EXT_RE.match(file_name): 385 full_class_name = test_filter_utils.get_fully_qualified_class_name(path) 386 methods = frozenset( 387 test_filter_utils.get_java_method_filters(path, methods) 388 ) 389 ti_filter = frozenset([test_info.TestFilter(full_class_name, methods)]) 390 # Path to cc file. 391 elif file_name and constants.CC_EXT_RE.match(file_name): 392 # TODO: b/173019813 - Should setup correct filter for an input file. 393 if not test_finder_utils.has_cc_class(path): 394 raise atest_error.MissingCCTestCaseError( 395 "Can't find CC class in %s" % path 396 ) 397 class_info = test_finder_utils.get_cc_class_info(path) 398 cc_filters = [] 399 for classname, _ in class_info.items(): 400 cc_filters.append( 401 test_info.TestFilter( 402 test_filter_utils.get_cc_filter(class_info, classname, methods), 403 frozenset(), 404 ) 405 ) 406 ti_filter = frozenset(cc_filters) 407 # If input path is a folder and have class_name information. 408 elif not file_name and class_name: 409 ti_filter = frozenset( 410 [test_info.TestFilter(class_name, methods)] 411 ) 412 # Path to non-module dir, treat as package. 413 elif not file_name and rel_module_dir != os.path.relpath( 414 path, self.root_dir): 415 dir_items = [os.path.join(path, f) for f in os.listdir(path)] 416 for dir_item in dir_items: 417 if constants.JAVA_EXT_RE.match(dir_item): 418 package_name = test_filter_utils.get_package_name(dir_item) 419 if package_name: 420 # methods should be empty frozenset for package. 421 if methods: 422 raise atest_error.MethodWithoutClassError( 423 '%s: Method filtering requires class' % str(methods) 424 ) 425 ti_filter = frozenset([test_info.TestFilter(package_name, methods)]) 426 break 427 logging.debug('_get_test_info_filter() ti_filter: %s', ti_filter) 428 return ti_filter 429 430 def _get_rel_config(self, test_path): 431 """Get config file's no-absolute path. 432 433 Args: 434 test_path: A string of the test absolute path. 435 436 Returns: 437 A string of config's no-absolute path, else None. 438 """ 439 test_dir = os.path.dirname(test_path) 440 rel_module_dir = test_finder_utils.find_parent_module_dir( 441 self.root_dir, test_dir, self.module_info 442 ) 443 if rel_module_dir: 444 return os.path.join(rel_module_dir, constants.MODULE_CONFIG) 445 return None 446 447 def _get_test_infos(self, test_path, rel_config, module_name, test_filter): 448 """Get test_info for test_path. 449 450 Args: 451 test_path: A string of the test path. 452 rel_config: A string of rel path of config. 453 module_name: A string of the module name to use. 454 test_filter: A test info filter. 455 456 Returns: 457 A list of TestInfo namedtuple if found, else None. 458 """ 459 if not rel_config: 460 rel_config = self._get_rel_config(test_path) 461 if not rel_config: 462 return None 463 if module_name: 464 module_names = [module_name] 465 else: 466 module_names = self._determine_modules_to_test( 467 os.path.dirname(rel_config), 468 test_path if self._is_comparted_src(test_path) else None, 469 ) 470 test_infos = [] 471 if module_names: 472 for mname in module_names: 473 # The real test config might be record in module-info. 474 mod_info = self.module_info.get_module_info(mname) 475 if not mod_info: 476 continue 477 rel_configs = self._get_module_test_config(mname, rel_config=rel_config) 478 for rel_cfg in rel_configs: 479 tinfo = self._process_test_info( 480 test_info.TestInfo( 481 test_name=mname, 482 test_runner=self._TEST_RUNNER, 483 build_targets=set(), 484 data={ 485 constants.TI_FILTER: test_filter, 486 constants.TI_REL_CONFIG: rel_cfg, 487 }, 488 compatibility_suites=mod_info.get( 489 constants.MODULE_COMPATIBILITY_SUITES, [] 490 ), 491 ) 492 ) 493 if tinfo: 494 test_infos.append(tinfo) 495 return test_infos 496 497 def find_test_by_module_name(self, module_name): 498 """Find test for the given module name. 499 500 Args: 501 module_name: A string of the test's module name. 502 503 Returns: 504 A list that includes only 1 populated TestInfo namedtuple 505 if found, otherwise None. 506 """ 507 tinfos = [] 508 mod_info = self.module_info.get_module_info(module_name) 509 if self.module_info.is_testable_module(mod_info): 510 # path is a list with only 1 element. 511 rel_config = os.path.join( 512 mod_info[constants.MODULE_PATH][0], constants.MODULE_CONFIG 513 ) 514 rel_configs = self._get_module_test_config( 515 module_name, rel_config=rel_config 516 ) 517 for rel_config in rel_configs: 518 tinfo = self._process_test_info( 519 test_info.TestInfo( 520 test_name=module_name, 521 test_runner=self._TEST_RUNNER, 522 build_targets=set(), 523 data={ 524 constants.TI_REL_CONFIG: rel_config, 525 constants.TI_FILTER: frozenset(), 526 }, 527 compatibility_suites=mod_info.get( 528 constants.MODULE_COMPATIBILITY_SUITES, [] 529 ), 530 ) 531 ) 532 if tinfo: 533 tinfos.append(tinfo) 534 if tinfos: 535 return tinfos 536 return None 537 538 def find_test_by_kernel_class_name(self, module_name, class_name): 539 """Find kernel test for the given class name. 540 541 Args: 542 module_name: A string of the module name to use. 543 class_name: A string of the test's class name. 544 545 Returns: 546 A list of populated TestInfo namedtuple if test found, else None. 547 """ 548 549 class_name, methods = test_filter_utils.split_methods(class_name) 550 test_configs = self._get_module_test_config(module_name) 551 if not test_configs: 552 return None 553 tinfos = [] 554 for test_config in test_configs: 555 test_config_path = os.path.join(self.root_dir, test_config) 556 mod_info = self.module_info.get_module_info(module_name) 557 ti_filter = frozenset([test_info.TestFilter(class_name, methods)]) 558 if test_finder_utils.is_test_from_kernel_xml( 559 test_config_path, class_name 560 ): 561 tinfo = self._process_test_info( 562 test_info.TestInfo( 563 test_name=module_name, 564 test_runner=self._TEST_RUNNER, 565 build_targets=set(), 566 data={ 567 constants.TI_REL_CONFIG: test_config, 568 constants.TI_FILTER: ti_filter, 569 }, 570 compatibility_suites=mod_info.get( 571 constants.MODULE_COMPATIBILITY_SUITES, [] 572 ), 573 ) 574 ) 575 if tinfo: 576 tinfos.append(tinfo) 577 if tinfos: 578 return tinfos 579 return None 580 581 def find_test_by_class_name( 582 self, 583 class_name: str, 584 module_name: str = None, 585 rel_config_path: str = None, 586 is_native_test: bool = False, 587 ) -> list[test_info.TestInfo] | None: 588 """Find test files given a class name. 589 590 If module_name and rel_config not given it will calculate it determine 591 it by looking up the tree from the class file. 592 593 Args: 594 class_name: A string of the test's class name. 595 module_name: Optional. A string of the module name to use. 596 rel_config_path: Optional. A string of module dir relative to repo root. 597 is_native_test: A boolean variable of whether to search for a native 598 test or not. 599 600 Returns: 601 A list of populated TestInfo namedtuple if test found, else None. 602 """ 603 class_name, methods = test_filter_utils.split_methods(class_name) 604 search_class_name = class_name 605 # For parameterized gtest, test class will be automerged to 606 # $(class_prefix)/$(base_class) name. Using $(base_class) for searching 607 # matched TEST_P to make sure test class is matched. 608 if '/' in search_class_name: 609 search_class_name = str(search_class_name).split('/')[-1] 610 611 test_paths = [] 612 # Search using the path where the config file is located. 613 if rel_config_path: 614 test_paths = test_finder_utils.find_class_file( 615 os.path.join(self.root_dir, os.path.dirname(rel_config_path)), 616 search_class_name, 617 is_native_test, 618 methods, 619 ) 620 if not test_paths: 621 atest_utils.print_and_log_info( 622 'Did not find class (%s) under module path (%s), ' 623 'researching from repo root.', 624 class_name, 625 rel_config_path, 626 ) 627 # Search from the root dir. 628 if not test_paths: 629 test_paths = test_finder_utils.find_class_file( 630 self.root_dir, search_class_name, is_native_test, methods 631 ) 632 # If we already have module name, use path in module-info as test_path. 633 if not test_paths: 634 if not module_name: 635 return None 636 # Use the module path as test_path. 637 module_paths = self.module_info.get_paths(module_name) 638 test_paths = [] 639 for rel_module_path in module_paths: 640 test_paths.append(os.path.join(self.root_dir, rel_module_path)) 641 642 tinfos = [] 643 for test_path in test_paths: 644 test_filter = self._get_test_info_filter( 645 test_path, 646 methods, 647 class_name=class_name, 648 is_native_test=is_native_test, 649 ) 650 test_infos = self._get_test_infos( 651 test_path, rel_config_path, module_name, test_filter 652 ) 653 # If input include methods, check if tinfo match. 654 if test_infos and len(test_infos) > 1 and methods: 655 test_infos = self._get_matched_test_infos(test_infos, methods) 656 if test_infos: 657 tinfos.extend(test_infos) 658 659 return tinfos if tinfos else None 660 661 def _get_matched_test_infos(self, test_infos, methods): 662 """Get the test_infos matched the given methods. 663 664 Args: 665 test_infos: A list of TestInfo obj. 666 methods: A set of method name strings. 667 668 Returns: 669 A list of matched TestInfo namedtuple, else None. 670 """ 671 matched_test_infos = set() 672 for tinfo in test_infos: 673 test_config, test_srcs = test_finder_utils.get_test_config_and_srcs( 674 tinfo, self.module_info 675 ) 676 if test_config: 677 filter_dict = atest_utils.get_android_junit_config_filters(test_config) 678 # Always treat the test_info is matched if no filters found. 679 if not filter_dict.keys(): 680 matched_test_infos.add(tinfo) 681 continue 682 for method in methods: 683 if self._is_srcs_match_method_annotation( 684 method, test_srcs, filter_dict 685 ): 686 logging.debug( 687 'For method:%s Test:%s matched filter_dict: %s', 688 method, 689 tinfo.test_name, 690 filter_dict, 691 ) 692 matched_test_infos.add(tinfo) 693 return list(matched_test_infos) 694 695 def _is_srcs_match_method_annotation(self, method, srcs, annotation_dict): 696 """Check if input srcs matched annotation. 697 698 Args: 699 method: A string of test method name. 700 srcs: A list of source file of test. 701 annotation_dict: A dictionary record the include and exclude 702 annotations. 703 704 Returns: 705 True if input method matched the annotation of input srcs, else 706 None. 707 """ 708 include_annotations = annotation_dict.get(constants.INCLUDE_ANNOTATION, []) 709 exclude_annotations = annotation_dict.get(constants.EXCLUDE_ANNOTATION, []) 710 for src in srcs: 711 include_methods = set() 712 src_path = os.path.join(self.root_dir, src) 713 # Add methods matched include_annotations. 714 for annotation in include_annotations: 715 include_methods.update( 716 test_finder_utils.get_annotated_methods(annotation, src_path) 717 ) 718 if exclude_annotations: 719 # For exclude annotation, get all the method in the input srcs, 720 # and filter out the matched annotation. 721 exclude_methods = set() 722 all_methods = test_finder_utils.get_java_methods(src_path) 723 for annotation in exclude_annotations: 724 exclude_methods.update( 725 test_finder_utils.get_annotated_methods(annotation, src_path) 726 ) 727 include_methods = all_methods - exclude_methods 728 if method in include_methods: 729 return True 730 return False 731 732 def find_test_by_module_and_class( 733 self, module_class: str 734 ) -> list[test_info.TestInfo]: 735 """Find the test info given a MODULE:CLASS string. 736 737 Args: 738 module_class: A string of form MODULE:CLASS or MODULE:CLASS#METHOD. 739 740 Returns: 741 A list of populated TestInfo if found, else None. 742 """ 743 parse_result = test_finder_utils.parse_test_reference(module_class) 744 if not parse_result: 745 return None 746 module_name = parse_result['module_name'] 747 class_name = parse_result['pkg_class_name'] 748 method_name = parse_result.get('method_name', '') 749 if method_name: 750 class_name = class_name + '#' + method_name 751 752 # module_infos is a list of TestInfo with at most 1 element. 753 module_infos = self.find_test_by_module_name(module_name) 754 module_info = module_infos[0] if module_infos else None 755 if not module_info: 756 return None 757 find_result = None 758 # If the target module is JAVA or Python test, search class name. 759 find_result = self.find_test_by_class_name( 760 class_name, 761 module_name, 762 module_info.data.get(constants.TI_REL_CONFIG), 763 self.module_info.is_native_test(module_name), 764 ) 765 # kernel target test is also define as NATIVE_TEST in build system. 766 # TODO: b/157210083 - Update find_test_by_kernel_class_name method to 767 # support gen_rule use case. 768 if not find_result: 769 find_result = self.find_test_by_kernel_class_name(module_name, class_name) 770 # Find by cc class. 771 if not find_result: 772 find_result = self.find_test_by_cc_class_name( 773 class_name, 774 module_info.test_name, 775 module_info.data.get(constants.TI_REL_CONFIG), 776 ) 777 return find_result 778 779 def find_test_by_package_name( 780 self, package, module_name=None, rel_config=None 781 ): 782 """Find the test info given a PACKAGE string. 783 784 Args: 785 package: A string of the package name. 786 module_name: Optional. A string of the module name. 787 rel_config: Optional. A string of rel path of config. 788 789 Returns: 790 A list of populated TestInfo namedtuple if found, else None. 791 """ 792 _, methods = test_filter_utils.split_methods(package) 793 if methods: 794 raise atest_error.MethodWithoutClassError( 795 '%s: Method filtering requires class' % (methods) 796 ) 797 # Confirm that packages exists and get user input for multiples. 798 if rel_config: 799 search_dir = os.path.join(self.root_dir, os.path.dirname(rel_config)) 800 else: 801 search_dir = self.root_dir 802 package_paths = test_finder_utils.run_find_cmd( 803 test_finder_utils.TestReferenceType.PACKAGE, search_dir, package 804 ) 805 package_paths = package_paths if package_paths is not None else [] 806 # Package path will be the full path to the dir represented by package. 807 if not package_paths: 808 if not module_name: 809 return None 810 module_paths = self.module_info.get_paths(module_name) 811 for rel_module_path in module_paths: 812 package_paths.append(os.path.join(self.root_dir, rel_module_path)) 813 test_filter = frozenset([test_info.TestFilter(package, frozenset())]) 814 test_infos = [] 815 for package_path in package_paths: 816 tinfo = self._get_test_infos( 817 package_path, rel_config, module_name, test_filter 818 ) 819 if tinfo: 820 test_infos.extend(tinfo) 821 return test_infos if test_infos else None 822 823 def find_test_by_module_and_package(self, module_package): 824 """Find the test info given a MODULE:PACKAGE string. 825 826 Args: 827 module_package: A string of form MODULE:PACKAGE 828 829 Returns: 830 A list of populated TestInfo namedtuple if found, else None. 831 """ 832 parse_result = test_finder_utils.parse_test_reference(module_package) 833 if not parse_result: 834 return None 835 module_name = parse_result['module_name'] 836 package = parse_result['pkg_class_name'] 837 method = parse_result.get('method_name', '') 838 if method: 839 package = package + '#' + method 840 841 # module_infos is a list with at most 1 element. 842 module_infos = self.find_test_by_module_name(module_name) 843 module_info = module_infos[0] if module_infos else None 844 if not module_info: 845 return None 846 return self.find_test_by_package_name( 847 package, 848 module_info.test_name, 849 module_info.data.get(constants.TI_REL_CONFIG), 850 ) 851 852 def find_test_by_path(self, rel_path: str) -> List[test_info.TestInfo]: 853 """Find the first test info matching the given path. 854 855 Strategy: 856 path_to_java_file --> Resolve to CLASS 857 path_to_cc_file --> Resolve to CC CLASS 858 path_to_module_file -> Resolve to MODULE 859 path_to_module_dir -> Resolve to MODULE 860 path_to_dir_with_class_files--> Resolve to PACKAGE 861 path_to_any_other_dir --> Resolve as MODULE 862 863 Args: 864 rel_path: A string of the relative path to $BUILD_TOP. 865 866 Returns: 867 A list of populated TestInfo namedtuple if test found, else None 868 """ 869 logging.debug('Finding test by path: %s', rel_path) 870 path, methods = test_filter_utils.split_methods(rel_path) 871 # create absolute path from cwd and remove symbolic links 872 path = os.path.realpath(path) 873 if not os.path.exists(path): 874 return None 875 if methods and not test_finder_utils.has_method_in_file(path, methods): 876 return None 877 dir_path, _ = test_finder_utils.get_dir_path_and_filename(path) 878 # Module/Class 879 rel_module_dir = test_finder_utils.find_parent_module_dir( 880 self.root_dir, dir_path, self.module_info 881 ) 882 883 # If the input file path does not belong to a module(by searching 884 # upwards to the build_top), check whether it belongs to the dependency 885 # of modules. 886 if not rel_module_dir: 887 testable_modules = self.module_info.get_modules_by_include_deps( 888 self.module_info.get_modules_by_path_in_srcs(rel_path), 889 testable_module_only=True, 890 ) 891 if testable_modules: 892 test_filter = self._get_test_info_filter( 893 path, methods, rel_module_dir=rel_module_dir 894 ) 895 tinfos = [] 896 for testable_module in testable_modules: 897 rel_config = os.path.join( 898 self.module_info.get_paths(testable_module)[0], 899 constants.MODULE_CONFIG, 900 ) 901 tinfos.extend( 902 self._get_test_infos( 903 path, rel_config, testable_module, test_filter 904 ) 905 ) 906 metrics.LocalDetectEvent( 907 detect_type=DetectType.FIND_TEST_IN_DEPS, result=1 908 ) 909 return tinfos 910 911 if not rel_module_dir: 912 # Try to find unit-test for input path. 913 path = os.path.relpath( 914 os.path.realpath(rel_path), 915 os.environ.get(constants.ANDROID_BUILD_TOP, ''), 916 ) 917 unit_tests = test_finder_utils.find_host_unit_tests( 918 self.module_info, path 919 ) 920 if unit_tests: 921 tinfos = [] 922 for unit_test in unit_tests: 923 tinfo = self._get_test_infos( 924 path, constants.MODULE_CONFIG, unit_test, frozenset() 925 ) 926 if tinfo: 927 tinfos.extend(tinfo) 928 return tinfos 929 return None 930 rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG) 931 test_filter = self._get_test_info_filter( 932 path, methods, rel_module_dir=rel_module_dir 933 ) 934 return self._get_test_infos(path, rel_config, None, test_filter) 935 936 def find_test_by_cc_class_name( 937 self, class_name, module_name=None, rel_config=None 938 ): 939 """Find test files given a cc class name. 940 941 If module_name and rel_config not given, test will be determined 942 by looking up the tree for files which has input class. 943 944 Args: 945 class_name: A string of the test's class name. 946 module_name: Optional. A string of the module name to use. 947 rel_config: Optional. A string of module dir no-absolute to repo root. 948 949 Returns: 950 A list of populated TestInfo namedtuple if test found, else None. 951 """ 952 # Check if class_name is prepended with file name. If so, trim the 953 # prefix and keep only the class_name. 954 if '.' in class_name: 955 # (b/202764540) Strip prefixes of a cc class. 956 # Assume the class name has a format of file_name.class_name 957 class_name = class_name[class_name.rindex('.') + 1 :] 958 atest_utils.print_and_log_info( 959 'Search with updated class name: %s', class_name 960 ) 961 return self.find_test_by_class_name( 962 class_name, module_name, rel_config, is_native_test=True 963 ) 964 965 def get_testable_modules_with_ld(self, user_input, ld_range=0): 966 """Calculate the edit distances of the input and testable modules. 967 968 The user input will be calculated across all testable modules and 969 results in integers generated by Levenshtein Distance algorithm. 970 To increase the speed of the calculation, a bound can be applied to 971 this method to prevent from calculating every testable modules. 972 973 Guessing from typos, e.g. atest atest_unitests, implies a tangible range 974 of length that Atest only needs to search within it, and the default of 975 the bound is 2. 976 977 Guessing from keywords however, e.g. atest --search Camera, means that 978 the uncertainty of the module name is way higher, and Atest should walk 979 through all testable modules and return the highest possibilities. 980 981 Args: 982 user_input: A string of the user input. 983 ld_range: An integer that range the searching scope. If the length of 984 user_input is 10, then Atest will calculate modules of which length is 985 between 8 and 12. 0 is equivalent to unlimited. 986 987 Returns: 988 A List of LDs and possible module names. If the user_input is "fax", 989 the output will be like: 990 [[2, "fog"], [2, "Fix"], [4, "duck"], [7, "Duckies"]] 991 992 Which means the most lilely names of "fax" are fog and Fix(LD=2), 993 while Dickies is the most unlikely one(LD=7). 994 """ 995 atest_utils.colorful_print( 996 '\nSearching for similar module names using fuzzy search...', 997 constants.CYAN, 998 ) 999 search_start = time.time() 1000 testable_modules = sorted(self.module_info.get_testable_modules(), key=len) 1001 lower_bound = len(user_input) - ld_range 1002 upper_bound = len(user_input) + ld_range 1003 testable_modules_with_ld = [] 1004 for module_name in testable_modules: 1005 # Dispose those too short or too lengthy. 1006 if ld_range != 0: 1007 if len(module_name) < lower_bound: 1008 continue 1009 if len(module_name) > upper_bound: 1010 break 1011 testable_modules_with_ld.append([ 1012 test_finder_utils.get_levenshtein_distance(user_input, module_name), 1013 module_name, 1014 ]) 1015 search_duration = time.time() - search_start 1016 logging.debug('Fuzzy search took %ss', search_duration) 1017 metrics.LocalDetectEvent( 1018 detect_type=DetectType.FUZZY_SEARCH_TIME, result=round(search_duration) 1019 ) 1020 return testable_modules_with_ld 1021 1022 def get_fuzzy_searching_results(self, user_input): 1023 """Give results which have no more than allowance of edit distances. 1024 1025 Args: 1026 user_input: the target module name for fuzzy searching. 1027 1028 Returns: 1029 A list of guessed modules. 1030 """ 1031 modules_with_ld = self.get_testable_modules_with_ld( 1032 user_input, ld_range=constants.LD_RANGE 1033 ) 1034 guessed_modules = [] 1035 for distance_, module_ in modules_with_ld: 1036 if distance_ <= abs(constants.LD_RANGE): 1037 guessed_modules.append(module_) 1038 return guessed_modules 1039 1040 def find_test_by_config_name(self, config_name): 1041 """Find test for the given config name. 1042 1043 Args: 1044 config_name: A string of the test's config name. 1045 1046 Returns: 1047 A list that includes only 1 populated TestInfo namedtuple 1048 if found, otherwise None. 1049 """ 1050 for module_name, mod_info in self.module_info.name_to_module_info.items(): 1051 test_configs = mod_info.get(constants.MODULE_TEST_CONFIG, []) 1052 for test_config in test_configs: 1053 test_config_name = os.path.splitext(os.path.basename(test_config))[0] 1054 if test_config_name == config_name: 1055 tinfo = test_info.TestInfo( 1056 test_name=test_config_name, 1057 test_runner=self._TEST_RUNNER, 1058 build_targets=self._get_build_targets(module_name, test_config), 1059 data={ 1060 constants.TI_REL_CONFIG: test_config, 1061 constants.TI_FILTER: frozenset(), 1062 }, 1063 compatibility_suites=mod_info.get( 1064 constants.MODULE_COMPATIBILITY_SUITES, [] 1065 ), 1066 ) 1067 test_config_path = os.path.join(self.root_dir, test_config) 1068 if test_finder_utils.need_aggregate_metrics_result(test_config_path): 1069 tinfo.aggregate_metrics_result = True 1070 if tinfo: 1071 # There should have only one test_config with the same 1072 # name in source tree. 1073 return [tinfo] 1074 return None 1075 1076 @staticmethod 1077 def _is_comparted_src(path): 1078 """Check if the input path need to match srcs information in module. 1079 1080 If path is a folder or android build file, we don't need to compart 1081 with module's srcs. 1082 1083 Args: 1084 path: A string of the test's path. 1085 1086 Returns: 1087 True if input path need to match with module's src info, else False. 1088 """ 1089 if os.path.isdir(path): 1090 return False 1091 if atest_utils.is_build_file(path): 1092 return False 1093 return True 1094 1095 1096class MainlineModuleFinder(ModuleFinder): 1097 """Mainline Module finder class.""" 1098 1099 NAME = 'MAINLINE_MODULE' 1100 1101 def __init__(self, module_info=None): 1102 super().__init__() 1103