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 Finder class. 17""" 18 19import logging 20import os 21 22# pylint: disable=import-error 23import atest_error 24import atest_utils 25import constants 26from test_finders import test_info 27from test_finders import test_finder_base 28from test_finders import test_finder_utils 29from test_runners import atest_tf_test_runner 30from test_runners import robolectric_test_runner 31from test_runners import vts_tf_test_runner 32 33_MODULES_IN = 'MODULES-IN-%s' 34_ANDROID_MK = 'Android.mk' 35 36# These are suites in LOCAL_COMPATIBILITY_SUITE that aren't really suites so 37# we can ignore them. 38_SUITES_TO_IGNORE = frozenset({'general-tests', 'device-tests', 'tests'}) 39 40class ModuleFinder(test_finder_base.TestFinderBase): 41 """Module finder class.""" 42 NAME = 'MODULE' 43 _TEST_RUNNER = atest_tf_test_runner.AtestTradefedTestRunner.NAME 44 _ROBOLECTRIC_RUNNER = robolectric_test_runner.RobolectricTestRunner.NAME 45 _VTS_TEST_RUNNER = vts_tf_test_runner.VtsTradefedTestRunner.NAME 46 47 def __init__(self, module_info=None): 48 super(ModuleFinder, self).__init__() 49 self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP) 50 self.module_info = module_info 51 52 def _determine_testable_module(self, path): 53 """Determine which module the user is trying to test. 54 55 Returns the module to test. If there are multiple possibilities, will 56 ask the user. Otherwise will return the only module found. 57 58 Args: 59 path: String path of module to look for. 60 61 Returns: 62 A list of the module names. 63 """ 64 testable_modules = [] 65 for mod in self.module_info.get_module_names(path): 66 mod_info = self.module_info.get_module_info(mod) 67 # Robolectric tests always exist in pairs of 2, one module to build 68 # the test and another to run it. For now, we are assuming they are 69 # isolated in their own folders and will return if we find one. 70 if self.module_info.is_robolectric_test(mod): 71 # return a list with one module name if it is robolectric. 72 return [mod] 73 if self.module_info.is_testable_module(mod_info): 74 testable_modules.append(mod_info.get(constants.MODULE_NAME)) 75 return test_finder_utils.extract_test_from_tests(testable_modules) 76 77 def _is_vts_module(self, module_name): 78 """Returns True if the module is a vts10 module, else False.""" 79 mod_info = self.module_info.get_module_info(module_name) 80 suites = [] 81 if mod_info: 82 suites = mod_info.get('compatibility_suites', []) 83 # Pull out all *ts (cts, tvts, etc) suites. 84 suites = [suite for suite in suites if suite not in _SUITES_TO_IGNORE] 85 return len(suites) == 1 and 'vts10' in suites 86 87 def _update_to_vts_test_info(self, test): 88 """Fill in the fields with vts10 specific info. 89 90 We need to update the runner to use the vts10 runner and also find the 91 test specific dependencies. 92 93 Args: 94 test: TestInfo to update with vts10 specific details. 95 96 Return: 97 TestInfo that is ready for the vts10 test runner. 98 """ 99 test.test_runner = self._VTS_TEST_RUNNER 100 config_file = os.path.join(self.root_dir, 101 test.data[constants.TI_REL_CONFIG]) 102 # Need to get out dir (special logic is to account for custom out dirs). 103 # The out dir is used to construct the build targets for the test deps. 104 out_dir = os.environ.get(constants.ANDROID_HOST_OUT) 105 custom_out_dir = os.environ.get(constants.ANDROID_OUT_DIR) 106 # If we're not an absolute custom out dir, get relative out dir path. 107 if custom_out_dir is None or not os.path.isabs(custom_out_dir): 108 out_dir = os.path.relpath(out_dir, self.root_dir) 109 vts_out_dir = os.path.join(out_dir, 'vts10', 'android-vts10', 'testcases') 110 # Parse dependency of default staging plans. 111 xml_paths = test_finder_utils.search_integration_dirs( 112 constants.VTS_STAGING_PLAN, 113 self.module_info.get_paths(constants.VTS_TF_MODULE)) 114 vts_xmls = set() 115 vts_xmls.add(config_file) 116 for xml_path in xml_paths: 117 vts_xmls |= test_finder_utils.get_plans_from_vts_xml(xml_path) 118 for config_file in vts_xmls: 119 # Add in vts10 test build targets. 120 test.build_targets |= test_finder_utils.get_targets_from_vts_xml( 121 config_file, vts_out_dir, self.module_info) 122 test.build_targets.add('vts-test-core') 123 test.build_targets.add(test.test_name) 124 return test 125 126 def _update_to_robolectric_test_info(self, test): 127 """Update the fields for a robolectric test. 128 129 Args: 130 test: TestInfo to be updated with robolectric fields. 131 132 Returns: 133 TestInfo with robolectric fields. 134 """ 135 test.test_runner = self._ROBOLECTRIC_RUNNER 136 test.test_name = self.module_info.get_robolectric_test_name(test.test_name) 137 return test 138 139 def _process_test_info(self, test): 140 """Process the test info and return some fields updated/changed. 141 142 We need to check if the test found is a special module (like vts10) and 143 update the test_info fields (like test_runner) appropriately. 144 145 Args: 146 test: TestInfo that has been filled out by a find method. 147 148 Return: 149 TestInfo that has been modified as needed and return None if 150 this module can't be found in the module_info. 151 """ 152 module_name = test.test_name 153 mod_info = self.module_info.get_module_info(module_name) 154 if not mod_info: 155 return None 156 test.module_class = mod_info['class'] 157 test.install_locations = test_finder_utils.get_install_locations( 158 mod_info['installed']) 159 # Check if this is only a vts10 module. 160 if self._is_vts_module(test.test_name): 161 return self._update_to_vts_test_info(test) 162 elif self.module_info.is_robolectric_test(test.test_name): 163 return self._update_to_robolectric_test_info(test) 164 rel_config = test.data[constants.TI_REL_CONFIG] 165 test.build_targets = self._get_build_targets(module_name, rel_config) 166 return test 167 168 def _get_build_targets(self, module_name, rel_config): 169 """Get the test deps. 170 171 Args: 172 module_name: name of the test. 173 rel_config: XML for the given test. 174 175 Returns: 176 Set of build targets. 177 """ 178 targets = set() 179 if not self.module_info.is_auto_gen_test_config(module_name): 180 config_file = os.path.join(self.root_dir, rel_config) 181 targets = test_finder_utils.get_targets_from_xml(config_file, 182 self.module_info) 183 if constants.VTS_CORE_SUITE in self.module_info.get_module_info( 184 module_name).get(constants.MODULE_COMPATIBILITY_SUITES, []): 185 targets.add(constants.VTS_CORE_TF_MODULE) 186 for module_path in self.module_info.get_paths(module_name): 187 mod_dir = module_path.replace('/', '-') 188 targets.add(_MODULES_IN % mod_dir) 189 # (b/156457698) Force add vts_kernel_tests as build target if our test 190 # belong to REQUIRED_KERNEL_TEST_MODULES due to required_module option 191 # not working for sh_test in soong. 192 if module_name in constants.REQUIRED_KERNEL_TEST_MODULES: 193 targets.add('vts_kernel_tests') 194 return targets 195 196 def _get_module_test_config(self, module_name, rel_config=None): 197 """Get the value of test_config in module_info. 198 199 Get the value of 'test_config' in module_info if its 200 auto_test_config is not true. 201 In this case, the test_config is specified by user. 202 If not, return rel_config. 203 204 Args: 205 module_name: A string of the test's module name. 206 rel_config: XML for the given test. 207 208 Returns: 209 A string of test_config path if found, else return rel_config. 210 """ 211 mod_info = self.module_info.get_module_info(module_name) 212 if mod_info: 213 test_config = '' 214 test_config_list = mod_info.get(constants.MODULE_TEST_CONFIG, []) 215 if test_config_list: 216 test_config = test_config_list[0] 217 if not self.module_info.is_auto_gen_test_config(module_name) and test_config != '': 218 return test_config 219 return rel_config 220 221 def _get_test_info_filter(self, path, methods, **kwargs): 222 """Get test info filter. 223 224 Args: 225 path: A string of the test's path. 226 methods: A set of method name strings. 227 rel_module_dir: Optional. A string of the module dir relative to 228 root. 229 class_name: Optional. A string of the class name. 230 is_native_test: Optional. A boolean variable of whether to search 231 for a native test or not. 232 233 Returns: 234 A set of test info filter. 235 """ 236 _, file_name = test_finder_utils.get_dir_path_and_filename(path) 237 ti_filter = frozenset() 238 if kwargs.get('is_native_test', None): 239 ti_filter = frozenset([test_info.TestFilter( 240 test_finder_utils.get_cc_filter( 241 kwargs.get('class_name', '*'), methods), frozenset())]) 242 # Path to java file. 243 elif file_name and constants.JAVA_EXT_RE.match(file_name): 244 full_class_name = test_finder_utils.get_fully_qualified_class_name( 245 path) 246 ti_filter = frozenset( 247 [test_info.TestFilter(full_class_name, methods)]) 248 # Path to cc file. 249 elif file_name and constants.CC_EXT_RE.match(file_name): 250 if not test_finder_utils.has_cc_class(path): 251 raise atest_error.MissingCCTestCaseError( 252 "Can't find CC class in %s" % path) 253 if methods: 254 ti_filter = frozenset( 255 [test_info.TestFilter(test_finder_utils.get_cc_filter( 256 kwargs.get('class_name', '*'), methods), frozenset())]) 257 # Path to non-module dir, treat as package. 258 elif (not file_name 259 and kwargs.get('rel_module_dir', None) != 260 os.path.relpath(path, self.root_dir)): 261 dir_items = [os.path.join(path, f) for f in os.listdir(path)] 262 for dir_item in dir_items: 263 if constants.JAVA_EXT_RE.match(dir_item): 264 package_name = test_finder_utils.get_package_name(dir_item) 265 if package_name: 266 # methods should be empty frozenset for package. 267 if methods: 268 raise atest_error.MethodWithoutClassError( 269 '%s: Method filtering requires class' 270 % str(methods)) 271 ti_filter = frozenset( 272 [test_info.TestFilter(package_name, methods)]) 273 break 274 return ti_filter 275 276 def _get_rel_config(self, test_path): 277 """Get config file's relative path. 278 279 Args: 280 test_path: A string of the test absolute path. 281 282 Returns: 283 A string of config's relative path, else None. 284 """ 285 test_dir = os.path.dirname(test_path) 286 rel_module_dir = test_finder_utils.find_parent_module_dir( 287 self.root_dir, test_dir, self.module_info) 288 if rel_module_dir: 289 return os.path.join(rel_module_dir, constants.MODULE_CONFIG) 290 return None 291 292 def _get_test_infos(self, test_path, rel_config, module_name, test_filter): 293 """Get test_info for test_path. 294 295 Args: 296 test_path: A string of the test path. 297 rel_config: A string of rel path of config. 298 module_name: A string of the module name to use. 299 test_filter: A test info filter. 300 301 Returns: 302 A list of TestInfo namedtuple if found, else None. 303 """ 304 if not rel_config: 305 rel_config = self._get_rel_config(test_path) 306 if not rel_config: 307 return None 308 if module_name: 309 module_names = [module_name] 310 else: 311 module_names = self._determine_testable_module( 312 os.path.dirname(rel_config)) 313 test_infos = [] 314 if module_names: 315 for mname in module_names: 316 # The real test config might be record in module-info. 317 rel_config = self._get_module_test_config(mname, 318 rel_config=rel_config) 319 mod_info = self.module_info.get_module_info(mname) 320 tinfo = self._process_test_info(test_info.TestInfo( 321 test_name=mname, 322 test_runner=self._TEST_RUNNER, 323 build_targets=set(), 324 data={constants.TI_FILTER: test_filter, 325 constants.TI_REL_CONFIG: rel_config}, 326 compatibility_suites=mod_info.get( 327 constants.MODULE_COMPATIBILITY_SUITES, []))) 328 if tinfo: 329 test_infos.append(tinfo) 330 return test_infos 331 332 def find_test_by_module_name(self, module_name): 333 """Find test for the given module name. 334 335 Args: 336 module_name: A string of the test's module name. 337 338 Returns: 339 A list that includes only 1 populated TestInfo namedtuple 340 if found, otherwise None. 341 """ 342 mod_info = self.module_info.get_module_info(module_name) 343 if self.module_info.is_testable_module(mod_info): 344 # path is a list with only 1 element. 345 rel_config = os.path.join(mod_info['path'][0], 346 constants.MODULE_CONFIG) 347 rel_config = self._get_module_test_config(module_name, rel_config=rel_config) 348 tinfo = self._process_test_info(test_info.TestInfo( 349 test_name=module_name, 350 test_runner=self._TEST_RUNNER, 351 build_targets=set(), 352 data={constants.TI_REL_CONFIG: rel_config, 353 constants.TI_FILTER: frozenset()}, 354 compatibility_suites=mod_info.get( 355 constants.MODULE_COMPATIBILITY_SUITES, []))) 356 if tinfo: 357 return [tinfo] 358 return None 359 360 def find_test_by_kernel_class_name(self, module_name, class_name): 361 """Find kernel test for the given class name. 362 363 Args: 364 module_name: A string of the module name to use. 365 class_name: A string of the test's class name. 366 367 Returns: 368 A list of populated TestInfo namedtuple if test found, else None. 369 """ 370 class_name, methods = test_finder_utils.split_methods(class_name) 371 test_config = self._get_module_test_config(module_name) 372 test_config_path = os.path.join(self.root_dir, test_config) 373 mod_info = self.module_info.get_module_info(module_name) 374 ti_filter = frozenset( 375 [test_info.TestFilter(class_name, methods)]) 376 if test_finder_utils.is_test_from_kernel_xml(test_config_path, class_name): 377 tinfo = self._process_test_info(test_info.TestInfo( 378 test_name=module_name, 379 test_runner=self._TEST_RUNNER, 380 build_targets=set(), 381 data={constants.TI_REL_CONFIG: test_config, 382 constants.TI_FILTER: ti_filter}, 383 compatibility_suites=mod_info.get( 384 constants.MODULE_COMPATIBILITY_SUITES, []))) 385 if tinfo: 386 return [tinfo] 387 return None 388 389 def find_test_by_class_name(self, class_name, module_name=None, 390 rel_config=None, is_native_test=False): 391 """Find test files given a class name. 392 393 If module_name and rel_config not given it will calculate it determine 394 it by looking up the tree from the class file. 395 396 Args: 397 class_name: A string of the test's class name. 398 module_name: Optional. A string of the module name to use. 399 rel_config: Optional. A string of module dir relative to repo root. 400 is_native_test: A boolean variable of whether to search for a 401 native test or not. 402 403 Returns: 404 A list of populated TestInfo namedtuple if test found, else None. 405 """ 406 class_name, methods = test_finder_utils.split_methods(class_name) 407 if rel_config: 408 search_dir = os.path.join(self.root_dir, 409 os.path.dirname(rel_config)) 410 else: 411 search_dir = self.root_dir 412 test_paths = test_finder_utils.find_class_file(search_dir, class_name, 413 is_native_test, methods) 414 if not test_paths and rel_config: 415 logging.info('Did not find class (%s) under module path (%s), ' 416 'researching from repo root.', class_name, rel_config) 417 test_paths = test_finder_utils.find_class_file(self.root_dir, 418 class_name, 419 is_native_test, 420 methods) 421 if not test_paths: 422 return None 423 tinfos = [] 424 for test_path in test_paths: 425 test_filter = self._get_test_info_filter( 426 test_path, methods, class_name=class_name, 427 is_native_test=is_native_test) 428 tinfo = self._get_test_infos(test_path, rel_config, 429 module_name, test_filter) 430 if tinfo: 431 tinfos.extend(tinfo) 432 return tinfos 433 434 def find_test_by_module_and_class(self, module_class): 435 """Find the test info given a MODULE:CLASS string. 436 437 Args: 438 module_class: A string of form MODULE:CLASS or MODULE:CLASS#METHOD. 439 440 Returns: 441 A list of populated TestInfo namedtuple if found, else None. 442 """ 443 if ':' not in module_class: 444 return None 445 module_name, class_name = module_class.split(':') 446 # module_infos is a list with at most 1 element. 447 module_infos = self.find_test_by_module_name(module_name) 448 module_info = module_infos[0] if module_infos else None 449 if not module_info: 450 return None 451 find_result = None 452 # If the target module is NATIVE_TEST, search CC classes only. 453 if not self.module_info.is_native_test(module_name): 454 # Find by java class. 455 find_result = self.find_test_by_class_name( 456 class_name, module_info.test_name, 457 module_info.data.get(constants.TI_REL_CONFIG)) 458 # kernel target test is also define as NATIVE_TEST in build system. 459 # TODO (b/157210083) Update find_test_by_kernel_class_name method to 460 # support gen_rule use case. 461 if not find_result: 462 find_result = self.find_test_by_kernel_class_name( 463 module_name, class_name) 464 # Find by cc class. 465 if not find_result: 466 find_result = self.find_test_by_cc_class_name( 467 class_name, module_info.test_name, 468 module_info.data.get(constants.TI_REL_CONFIG)) 469 return find_result 470 471 def find_test_by_package_name(self, package, module_name=None, 472 rel_config=None): 473 """Find the test info given a PACKAGE string. 474 475 Args: 476 package: A string of the package name. 477 module_name: Optional. A string of the module name. 478 ref_config: Optional. A string of rel path of config. 479 480 Returns: 481 A list of populated TestInfo namedtuple if found, else None. 482 """ 483 _, methods = test_finder_utils.split_methods(package) 484 if methods: 485 raise atest_error.MethodWithoutClassError('%s: Method filtering ' 486 'requires class' % ( 487 methods)) 488 # Confirm that packages exists and get user input for multiples. 489 if rel_config: 490 search_dir = os.path.join(self.root_dir, 491 os.path.dirname(rel_config)) 492 else: 493 search_dir = self.root_dir 494 package_paths = test_finder_utils.run_find_cmd( 495 test_finder_utils.FIND_REFERENCE_TYPE.PACKAGE, search_dir, package) 496 # Package path will be the full path to the dir represented by package. 497 if not package_paths: 498 return None 499 test_filter = frozenset([test_info.TestFilter(package, frozenset())]) 500 test_infos = [] 501 for package_path in package_paths: 502 tinfo = self._get_test_infos(package_path, rel_config, 503 module_name, test_filter) 504 if tinfo: 505 test_infos.extend(tinfo) 506 return test_infos 507 508 def find_test_by_module_and_package(self, module_package): 509 """Find the test info given a MODULE:PACKAGE string. 510 511 Args: 512 module_package: A string of form MODULE:PACKAGE 513 514 Returns: 515 A list of populated TestInfo namedtuple if found, else None. 516 """ 517 module_name, package = module_package.split(':') 518 # module_infos is a list with at most 1 element. 519 module_infos = self.find_test_by_module_name(module_name) 520 module_info = module_infos[0] if module_infos else None 521 if not module_info: 522 return None 523 return self.find_test_by_package_name( 524 package, module_info.test_name, 525 module_info.data.get(constants.TI_REL_CONFIG)) 526 527 def find_test_by_path(self, path): 528 """Find the first test info matching the given path. 529 530 Strategy: 531 path_to_java_file --> Resolve to CLASS 532 path_to_cc_file --> Resolve to CC CLASS 533 path_to_module_file -> Resolve to MODULE 534 path_to_module_dir -> Resolve to MODULE 535 path_to_dir_with_class_files--> Resolve to PACKAGE 536 path_to_any_other_dir --> Resolve as MODULE 537 538 Args: 539 path: A string of the test's path. 540 541 Returns: 542 A list of populated TestInfo namedtuple if test found, else None 543 """ 544 logging.debug('Finding test by path: %s', path) 545 path, methods = test_finder_utils.split_methods(path) 546 # TODO: See if this can be generalized and shared with methods above 547 # create absolute path from cwd and remove symbolic links 548 path = os.path.realpath(path) 549 if not os.path.exists(path): 550 return None 551 if (methods and 552 not test_finder_utils.has_method_in_file(path, methods)): 553 return None 554 dir_path, _ = test_finder_utils.get_dir_path_and_filename(path) 555 # Module/Class 556 rel_module_dir = test_finder_utils.find_parent_module_dir( 557 self.root_dir, dir_path, self.module_info) 558 if not rel_module_dir: 559 return None 560 rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG) 561 test_filter = self._get_test_info_filter(path, methods, 562 rel_module_dir=rel_module_dir) 563 return self._get_test_infos(path, rel_config, None, test_filter) 564 565 def find_test_by_cc_class_name(self, class_name, module_name=None, 566 rel_config=None): 567 """Find test files given a cc class name. 568 569 If module_name and rel_config not given, test will be determined 570 by looking up the tree for files which has input class. 571 572 Args: 573 class_name: A string of the test's class name. 574 module_name: Optional. A string of the module name to use. 575 rel_config: Optional. A string of module dir relative to repo root. 576 577 Returns: 578 A list of populated TestInfo namedtuple if test found, else None. 579 """ 580 # Check if class_name is prepended with file name. If so, trim the 581 # prefix and keep only the class_name. 582 if '.' in class_name: 583 # Assume the class name has a format of file_name.class_name 584 class_name = class_name[class_name.rindex('.')+1:] 585 logging.info('Search with updated class name: %s', class_name) 586 return self.find_test_by_class_name( 587 class_name, module_name, rel_config, is_native_test=True) 588 589 def get_testable_modules_with_ld(self, user_input, ld_range=0): 590 """Calculate the edit distances of the input and testable modules. 591 592 The user input will be calculated across all testable modules and 593 results in integers generated by Levenshtein Distance algorithm. 594 To increase the speed of the calculation, a bound can be applied to 595 this method to prevent from calculating every testable modules. 596 597 Guessing from typos, e.g. atest atest_unitests, implies a tangible range 598 of length that Atest only needs to search within it, and the default of 599 the bound is 2. 600 601 Guessing from keywords however, e.g. atest --search Camera, means that 602 the uncertainty of the module name is way higher, and Atest should walk 603 through all testable modules and return the highest possibilities. 604 605 Args: 606 user_input: A string of the user input. 607 ld_range: An integer that range the searching scope. If the length of 608 user_input is 10, then Atest will calculate modules of which 609 length is between 8 and 12. 0 is equivalent to unlimited. 610 611 Returns: 612 A List of LDs and possible module names. If the user_input is "fax", 613 the output will be like: 614 [[2, "fog"], [2, "Fix"], [4, "duck"], [7, "Duckies"]] 615 616 Which means the most lilely names of "fax" are fog and Fix(LD=2), 617 while Dickies is the most unlikely one(LD=7). 618 """ 619 atest_utils.colorful_print('\nSearching for similar module names using ' 620 'fuzzy search...', constants.CYAN) 621 testable_modules = sorted(self.module_info.get_testable_modules(), key=len) 622 lower_bound = len(user_input) - ld_range 623 upper_bound = len(user_input) + ld_range 624 testable_modules_with_ld = [] 625 for module_name in testable_modules: 626 # Dispose those too short or too lengthy. 627 if ld_range != 0: 628 if len(module_name) < lower_bound: 629 continue 630 elif len(module_name) > upper_bound: 631 break 632 testable_modules_with_ld.append( 633 [test_finder_utils.get_levenshtein_distance( 634 user_input, module_name), module_name]) 635 return testable_modules_with_ld 636 637 def get_fuzzy_searching_results(self, user_input): 638 """Give results which have no more than allowance of edit distances. 639 640 Args: 641 user_input: the target module name for fuzzy searching. 642 643 Return: 644 A list of guessed modules. 645 """ 646 modules_with_ld = self.get_testable_modules_with_ld(user_input, 647 ld_range=constants.LD_RANGE) 648 guessed_modules = [] 649 for _distance, _module in modules_with_ld: 650 if _distance <= abs(constants.LD_RANGE): 651 guessed_modules.append(_module) 652 return guessed_modules 653