1# Copyright 2017, 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"""Command Line Translator for atest.""" 16 17# pylint: disable=too-many-lines 18 19from __future__ import print_function 20 21from dataclasses import dataclass 22import fnmatch 23import json 24import logging 25import os 26from pathlib import Path 27import re 28import sys 29import time 30from typing import List, Set 31 32from atest import atest_error 33from atest import atest_utils 34from atest import bazel_mode 35from atest import constants 36from atest import test_finder_handler 37from atest import test_mapping 38from atest.atest_enum import DetectType, ExitCode 39from atest.metrics import metrics 40from atest.metrics import metrics_utils 41from atest.test_finders import module_finder 42from atest.test_finders import test_finder_utils 43from atest.test_finders import test_info 44 45FUZZY_FINDER = 'FUZZY' 46CACHE_FINDER = 'CACHE' 47TESTNAME_CHARS = {'#', ':', '/'} 48 49MAINLINE_LOCAL_DOC = 'go/mainline-local-build' 50 51# Pattern used to identify comments start with '//' or '#' in TEST_MAPPING. 52_COMMENTS_RE = re.compile(r'(?m)[\s\t]*(#|//).*|(\".*?\")') 53_COMMENTS = frozenset(['//', '#']) 54 55 56@dataclass 57class TestIdentifier: 58 """Class that stores test and the corresponding mainline modules (if any).""" 59 60 test_name: str 61 module_names: List[str] 62 binary_names: List[str] 63 64 65class CLITranslator: 66 """CLITranslator class contains public method translate() and some private 67 68 helper methods. The atest tool can call the translate() method with a list 69 of strings, each string referencing a test to run. Translate() will 70 "translate" this list of test strings into a list of build targets and a 71 list of TradeFederation run commands. 72 73 Translation steps for a test string reference: 74 1. Narrow down the type of reference the test string could be, i.e. 75 whether it could be referencing a Module, Class, Package, etc. 76 2. Try to find the test files assuming the test string is one of these 77 types of reference. 78 3. If test files found, generate Build Targets and the Run Command. 79 """ 80 81 def __init__( 82 self, 83 mod_info=None, 84 print_cache_msg=True, 85 bazel_mode_enabled=False, 86 host=False, 87 bazel_mode_features: List[bazel_mode.Features] = None, 88 ): 89 """CLITranslator constructor 90 91 Args: 92 mod_info: ModuleInfo class that has cached module-info.json. 93 print_cache_msg: Boolean whether printing clear cache message or not. 94 True will print message while False won't print. 95 bazel_mode_enabled: Boolean of args.bazel_mode. 96 host: Boolean of args.host. 97 bazel_mode_features: List of args.bazel_mode_features. 98 """ 99 self.mod_info = mod_info 100 self.root_dir = os.getenv(constants.ANDROID_BUILD_TOP, os.sep) 101 self._bazel_mode = bazel_mode_enabled 102 self._bazel_mode_features = bazel_mode_features or [] 103 self._host = host 104 self.enable_file_patterns = False 105 self.msg = '' 106 if print_cache_msg: 107 self.msg = ( 108 '(Test info has been cached for speeding up the next ' 109 'run, if test info needs to be updated, please add -c ' 110 'to clean the old cache.)' 111 ) 112 self.fuzzy_search = True 113 114 # pylint: disable=too-many-locals 115 # pylint: disable=too-many-branches 116 # pylint: disable=too-many-statements 117 def _find_test_infos(self, test, tm_test_detail) -> List[test_info.TestInfo]: 118 """Return set of TestInfos based on a given test. 119 120 Args: 121 test: A string representing test references. 122 tm_test_detail: The TestDetail of test configured in TEST_MAPPING files. 123 124 Returns: 125 List of TestInfos based on the given test. 126 """ 127 test_infos = [] 128 test_find_starts = time.time() 129 test_found = False 130 test_finders = [] 131 test_info_str = '' 132 find_test_err_msg = None 133 test_identifier = parse_test_identifier(test) 134 test_name = test_identifier.test_name 135 if not self._verified_mainline_modules(test_identifier): 136 return test_infos 137 find_methods = test_finder_handler.get_find_methods_for_test( 138 self.mod_info, test 139 ) 140 if self._bazel_mode: 141 find_methods = [ 142 bazel_mode.create_new_finder( 143 self.mod_info, 144 f, 145 host=self._host, 146 enabled_features=self._bazel_mode_features, 147 ) 148 for f in find_methods 149 ] 150 for finder in find_methods: 151 # For tests in TEST_MAPPING, find method is only related to 152 # test name, so the details can be set after test_info object 153 # is created. 154 try: 155 found_test_infos = finder.find_method( 156 finder.test_finder_instance, test_name 157 ) 158 except atest_error.TestDiscoveryException as e: 159 find_test_err_msg = e 160 if found_test_infos: 161 finder_info = finder.finder_info 162 for t_info in found_test_infos: 163 test_deps = set() 164 if self.mod_info: 165 test_deps = self.mod_info.get_install_module_dependency( 166 t_info.test_name 167 ) 168 logging.debug( 169 '(%s) Test dependencies: %s', t_info.test_name, test_deps 170 ) 171 if tm_test_detail: 172 t_info.data[constants.TI_MODULE_ARG] = tm_test_detail.options 173 t_info.from_test_mapping = True 174 t_info.host = tm_test_detail.host 175 if finder_info != CACHE_FINDER: 176 t_info.test_finder = finder_info 177 mainline_modules = test_identifier.module_names 178 if mainline_modules: 179 t_info.test_name = test 180 # TODO(b/261607500): Replace usages of raw_test_name 181 # with test_name once we can ensure that it doesn't 182 # break any code that expects Mainline modules in the 183 # string. 184 t_info.raw_test_name = test_name 185 # TODO: remove below statement when soong can also 186 # parse TestConfig and inject mainline modules information 187 # to module-info. 188 for mod in mainline_modules: 189 t_info.add_mainline_module(mod) 190 191 # Only add dependencies to build_targets when they are in 192 # module info 193 test_deps_in_mod_info = [ 194 test_dep 195 for test_dep in test_deps 196 if self.mod_info.is_module(test_dep) 197 ] 198 for dep in test_deps_in_mod_info: 199 t_info.add_build_target(dep) 200 test_infos.append(t_info) 201 test_found = True 202 print("Found '%s' as %s" % (atest_utils.mark_green(test), finder_info)) 203 if finder_info == CACHE_FINDER and test_infos: 204 test_finders.append(list(test_infos)[0].test_finder) 205 test_finders.append(finder_info) 206 test_info_str = ','.join([str(x) for x in found_test_infos]) 207 break 208 if not test_found: 209 print('No test found for: {}'.format(atest_utils.mark_red(test))) 210 if self.fuzzy_search: 211 f_results = self._fuzzy_search_and_msg(test, find_test_err_msg) 212 if f_results: 213 test_infos.extend(f_results) 214 test_found = True 215 test_finders.append(FUZZY_FINDER) 216 metrics.FindTestFinishEvent( 217 duration=metrics_utils.convert_duration(time.time() - test_find_starts), 218 success=test_found, 219 test_reference=test, 220 test_finders=test_finders, 221 test_info=test_info_str, 222 ) 223 # Cache test_infos by default except running with TEST_MAPPING which may 224 # include customized flags and they are likely to mess up other 225 # non-test_mapping tests. 226 if test_infos and not tm_test_detail: 227 atest_utils.update_test_info_cache(test, test_infos) 228 if self.msg: 229 print(self.msg) 230 return test_infos 231 232 def _verified_mainline_modules(self, test_identifier: TestIdentifier) -> bool: 233 """Verify the test with mainline modules is acceptable. 234 235 The test must be a module and mainline modules are in module-info. 236 The syntax rule of mainline modules will check in build process. 237 The rule includes mainline modules are sorted alphabetically, no space, 238 and no duplication. 239 240 Args: 241 test_identifier: a TestIdentifier object. 242 243 Returns: 244 True if this test is acceptable. Otherwise, print the reason and 245 return False. 246 """ 247 mainline_binaries = test_identifier.binary_names 248 if not mainline_binaries: 249 return True 250 251 # Exit earlier when any test name or mainline modules are not valid. 252 if not self._valid_modules(test_identifier): 253 return False 254 255 # Exit earlier if Atest cannot find relationship between the test and 256 # the mainline binaries. 257 return self._declared_mainline_modules(test_identifier) 258 259 def _valid_modules(self, identifier: TestIdentifier) -> bool: 260 """Determine the test_name and mainline modules are modules.""" 261 if not self.mod_info.is_module(identifier.test_name): 262 print( 263 'Error: "{}" is not a testable module.'.format( 264 atest_utils.mark_red(identifier.test_name) 265 ) 266 ) 267 return False 268 269 # Exit earlier if the given mainline modules are unavailable in the 270 # branch. 271 unknown_modules = [ 272 module 273 for module in identifier.module_names 274 if not self.mod_info.is_module(module) 275 ] 276 if unknown_modules: 277 print( 278 'Error: Cannot find {} in module info!'.format( 279 atest_utils.mark_red(', '.join(unknown_modules)) 280 ) 281 ) 282 return False 283 284 # Exit earlier if found unsupported `capex` files. 285 unsupported_binaries = [] 286 for name in identifier.module_names: 287 info = self.mod_info.get_module_info(name) 288 if info.get('installed'): 289 for bin in info.get('installed'): 290 if not re.search(atest_utils.MAINLINE_MODULES_EXT_RE, bin): 291 unsupported_binaries.append(bin) 292 if unsupported_binaries: 293 print( 294 'The output format {} are not in a supported format; ' 295 'did you run mainline local setup script? ' 296 'Please refer to {}.'.format( 297 atest_utils.mark_red(', '.join(unsupported_binaries)), 298 atest_utils.mark_yellow(MAINLINE_LOCAL_DOC), 299 ) 300 ) 301 return False 302 303 return True 304 305 def _declared_mainline_modules(self, identifier: TestIdentifier) -> bool: 306 """Determine if all mainline modules were associated to the test.""" 307 test = identifier.test_name 308 mainline_binaries = identifier.binary_names 309 if not self.mod_info.has_mainline_modules(test, mainline_binaries): 310 print( 311 'Error: Mainline modules "{}" were not defined for {} in ' 312 'neither build file nor test config.'.format( 313 atest_utils.mark_red(', '.join(mainline_binaries)), 314 atest_utils.mark_red(test), 315 ) 316 ) 317 return False 318 319 return True 320 321 def _fuzzy_search_and_msg(self, test, find_test_err_msg): 322 """Fuzzy search and print message. 323 324 Args: 325 test: A string representing test references 326 find_test_err_msg: A string of find test error message. 327 328 Returns: 329 A list of TestInfos if found, otherwise None. 330 """ 331 # Currently we focus on guessing module names. Append names on 332 # results if more finders support fuzzy searching. 333 if atest_utils.has_chars(test, TESTNAME_CHARS): 334 return None 335 mod_finder = module_finder.ModuleFinder(self.mod_info) 336 results = mod_finder.get_fuzzy_searching_results(test) 337 if len(results) == 1 and self._confirm_running(results): 338 found_test_infos = mod_finder.find_test_by_module_name(results[0]) 339 # found_test_infos is a list with at most 1 element. 340 if found_test_infos: 341 return found_test_infos 342 elif len(results) > 1: 343 self._print_fuzzy_searching_results(results) 344 else: 345 print('No matching result for {0}.'.format(test)) 346 if find_test_err_msg: 347 print(f'{atest_utils.mark_magenta(find_test_err_msg)}\n') 348 return None 349 350 def _get_test_infos(self, tests, test_mapping_test_details=None): 351 """Return set of TestInfos based on passed in tests. 352 353 Args: 354 tests: List of strings representing test references. 355 test_mapping_test_details: List of TestDetail for tests configured in 356 TEST_MAPPING files. 357 358 Returns: 359 Set of TestInfos based on the passed in tests. 360 """ 361 test_infos = [] 362 if not test_mapping_test_details: 363 test_mapping_test_details = [None] * len(tests) 364 for test, tm_test_detail in zip(tests, test_mapping_test_details): 365 found_test_infos = self._find_test_infos(test, tm_test_detail) 366 test_infos.extend(found_test_infos) 367 return test_infos 368 369 def _confirm_running(self, results): 370 """Listen to an answer from raw input. 371 372 Args: 373 results: A list of results. 374 375 Returns: 376 True is the answer is affirmative. 377 """ 378 return atest_utils.prompt_with_yn_result( 379 'Did you mean {0}?'.format(atest_utils.mark_green(results[0])), True 380 ) 381 382 def _print_fuzzy_searching_results(self, results): 383 """Print modules when fuzzy searching gives multiple results. 384 385 If the result is lengthy, just print the first 10 items only since we 386 have already given enough-accurate result. 387 388 Args: 389 results: A list of guessed testable module names. 390 """ 391 atest_utils.colorful_print( 392 'Did you mean the following modules?', constants.WHITE 393 ) 394 for mod in results[:10]: 395 atest_utils.colorful_print(mod, constants.GREEN) 396 397 def filter_comments(self, test_mapping_file): 398 """Remove comments in TEST_MAPPING file to valid format. 399 400 Only '//' and '#' are regarded as comments. 401 402 Args: 403 test_mapping_file: Path to a TEST_MAPPING file. 404 405 Returns: 406 Valid json string without comments. 407 """ 408 409 def _replace(match): 410 """Replace comments if found matching the defined regular 411 412 expression. 413 414 Args: 415 match: The matched regex pattern 416 417 Returns: 418 "" if it matches _COMMENTS, otherwise original string. 419 """ 420 line = match.group(0).strip() 421 return '' if any(map(line.startswith, _COMMENTS)) else line 422 423 with open(test_mapping_file, encoding='utf-8') as json_file: 424 return re.sub(_COMMENTS_RE, _replace, json_file.read()) 425 426 def _read_tests_in_test_mapping(self, test_mapping_file): 427 """Read tests from a TEST_MAPPING file. 428 429 Args: 430 test_mapping_file: Path to a TEST_MAPPING file. 431 432 Returns: 433 A tuple of (all_tests, imports), where 434 all_tests is a dictionary of all tests in the TEST_MAPPING file, 435 grouped by test group. 436 imports is a list of test_mapping.Import to include other test 437 mapping files. 438 """ 439 all_tests = {} 440 imports = [] 441 test_mapping_dict = {} 442 try: 443 test_mapping_dict = json.loads(self.filter_comments(test_mapping_file)) 444 except json.JSONDecodeError as e: 445 msg = 'Test Mapping file has invalid format: %s.' % e 446 logging.debug(msg) 447 atest_utils.colorful_print(msg, constants.RED) 448 sys.exit(ExitCode.INVALID_TM_FORMAT) 449 for test_group_name, test_list in test_mapping_dict.items(): 450 if test_group_name == constants.TEST_MAPPING_IMPORTS: 451 for import_detail in test_list: 452 imports.append(test_mapping.Import(test_mapping_file, import_detail)) 453 else: 454 grouped_tests = all_tests.setdefault(test_group_name, set()) 455 tests = [] 456 for test in test_list: 457 if ( 458 self.enable_file_patterns 459 and not test_mapping.is_match_file_patterns( 460 test_mapping_file, test 461 ) 462 ): 463 continue 464 test_name = parse_test_identifier(test['name']).test_name 465 test_mod_info = self.mod_info.name_to_module_info.get(test_name) 466 if not test_mod_info: 467 print( 468 'WARNING: %s is not a valid build target and ' 469 'may not be discoverable by TreeHugger. If you ' 470 'want to specify a class or test-package, ' 471 "please set 'name' to the test module and use " 472 "'options' to specify the right tests via " 473 "'include-filter'.\nNote: this can also occur " 474 'if the test module is not built for your ' 475 'current lunch target.\n' 476 % atest_utils.mark_red(test['name']) 477 ) 478 elif not any( 479 x in test_mod_info.get('compatibility_suites', []) 480 for x in constants.TEST_MAPPING_SUITES 481 ): 482 print( 483 'WARNING: Please add %s to either suite: %s for ' 484 'this TEST_MAPPING file to work with TreeHugger.' 485 % ( 486 atest_utils.mark_red(test['name']), 487 atest_utils.mark_green(constants.TEST_MAPPING_SUITES), 488 ) 489 ) 490 tests.append(test_mapping.TestDetail(test)) 491 grouped_tests.update(tests) 492 return all_tests, imports 493 494 def _get_tests_from_test_mapping_files(self, test_groups, test_mapping_files): 495 """Get tests in the given test mapping files with the match group. 496 497 Args: 498 test_groups: Groups of tests to run. Default is set to `presubmit` and 499 `presubmit-large`. 500 test_mapping_files: A list of path of TEST_MAPPING files. 501 502 Returns: 503 A tuple of (tests, all_tests, imports), where, 504 tests is a set of tests (test_mapping.TestDetail) defined in 505 TEST_MAPPING file of the given path, and its parent directories, 506 with matching test_group. 507 all_tests is a dictionary of all tests in TEST_MAPPING files, 508 grouped by test group. 509 imports is a list of test_mapping.Import objects that contains the 510 details of where to import a TEST_MAPPING file. 511 """ 512 all_imports = [] 513 # Read and merge the tests in all TEST_MAPPING files. 514 merged_all_tests = {} 515 for test_mapping_file in test_mapping_files: 516 all_tests, imports = self._read_tests_in_test_mapping(test_mapping_file) 517 all_imports.extend(imports) 518 for test_group_name, test_list in all_tests.items(): 519 grouped_tests = merged_all_tests.setdefault(test_group_name, set()) 520 grouped_tests.update(test_list) 521 tests = set() 522 for test_group in test_groups: 523 temp_tests = set(merged_all_tests.get(test_group, [])) 524 tests.update(temp_tests) 525 if test_group == constants.TEST_GROUP_ALL: 526 for grouped_tests in merged_all_tests.values(): 527 tests.update(grouped_tests) 528 return tests, merged_all_tests, all_imports 529 530 # pylint: disable=too-many-arguments 531 # pylint: disable=too-many-locals 532 def _find_tests_by_test_mapping( 533 self, 534 path='', 535 test_groups=None, 536 file_name=constants.TEST_MAPPING, 537 include_subdirs=False, 538 checked_files=None, 539 ): 540 """Find tests defined in TEST_MAPPING in the given path. 541 542 Args: 543 path: A string of path in source. Default is set to '', i.e., CWD. 544 test_groups: A List of test groups to run. 545 file_name: Name of TEST_MAPPING file. Default is set to `TEST_MAPPING`. 546 The argument is added for testing purpose. 547 include_subdirs: True to include tests in TEST_MAPPING files in sub 548 directories. 549 checked_files: Paths of TEST_MAPPING files that have been checked. 550 551 Returns: 552 A tuple of (tests, all_tests), where, 553 tests is a set of tests (test_mapping.TestDetail) defined in 554 TEST_MAPPING file of the given path, and its parent directories, 555 with matching test_group. 556 all_tests is a dictionary of all tests in TEST_MAPPING files, 557 grouped by test group. 558 """ 559 path = os.path.realpath(path) 560 # Default test_groups is set to [`presubmit`, `presubmit-large`]. 561 if not test_groups: 562 test_groups = constants.DEFAULT_TEST_GROUPS 563 test_mapping_files = set() 564 all_tests = {} 565 test_mapping_file = os.path.join(path, file_name) 566 if os.path.exists(test_mapping_file): 567 test_mapping_files.add(test_mapping_file) 568 # Include all TEST_MAPPING files in sub-directories if `include_subdirs` 569 # is set to True. 570 if include_subdirs: 571 test_mapping_files.update(atest_utils.find_files(path, file_name)) 572 # Include all possible TEST_MAPPING files in parent directories. 573 while path not in (self.root_dir, os.sep): 574 path = os.path.dirname(path) 575 test_mapping_file = os.path.join(path, file_name) 576 if os.path.exists(test_mapping_file): 577 test_mapping_files.add(test_mapping_file) 578 579 if checked_files is None: 580 checked_files = set() 581 test_mapping_files.difference_update(checked_files) 582 checked_files.update(test_mapping_files) 583 if not test_mapping_files: 584 return test_mapping_files, all_tests 585 586 tests, all_tests, imports = self._get_tests_from_test_mapping_files( 587 test_groups, test_mapping_files 588 ) 589 590 # Load TEST_MAPPING files from imports recursively. 591 if imports: 592 for import_detail in imports: 593 path = import_detail.get_path() 594 # (b/110166535 #19) Import path might not exist if a project is 595 # located in different directory in different branches. 596 if path is None: 597 atest_utils.print_and_log_warning( 598 'Failed to import TEST_MAPPING at %s', import_detail 599 ) 600 continue 601 # Search for tests based on the imported search path. 602 import_tests, import_all_tests = self._find_tests_by_test_mapping( 603 path, test_groups, file_name, include_subdirs, checked_files 604 ) 605 # Merge the collections 606 tests.update(import_tests) 607 for group, grouped_tests in import_all_tests.items(): 608 all_tests.setdefault(group, set()).update(grouped_tests) 609 610 return tests, all_tests 611 612 def _get_test_mapping_tests(self, args, exit_if_no_test_found=True): 613 """Find the tests in TEST_MAPPING files. 614 615 Args: 616 args: arg parsed object. exit_if_no_test(s)_found: A flag to exit atest 617 if no test mapping tests found. 618 619 Returns: 620 A tuple of (test_names, test_details_list), where 621 test_names: a list of test name 622 test_details_list: a list of test_mapping.TestDetail objects for 623 the tests in TEST_MAPPING files with matching test group. 624 """ 625 # Pull out tests from test mapping 626 src_path = '' 627 test_groups = constants.DEFAULT_TEST_GROUPS 628 if args.tests: 629 if ':' in args.tests[0]: 630 src_path, test_group = args.tests[0].split(':') 631 test_groups = [test_group] 632 else: 633 src_path = args.tests[0] 634 635 test_details, all_test_details = self._find_tests_by_test_mapping( 636 path=src_path, 637 test_groups=test_groups, 638 include_subdirs=args.include_subdirs, 639 checked_files=set(), 640 ) 641 test_details_list = list(test_details) 642 if not test_details_list and exit_if_no_test_found: 643 atest_utils.print_and_log_warning( 644 'No tests of group `%s` found in %s or its ' 645 'parent directories. (Available groups: %s)\n' 646 'You might be missing atest arguments,' 647 ' try `atest --help` for more information.', 648 test_groups, 649 os.path.join(src_path, constants.TEST_MAPPING), 650 ', '.join(all_test_details.keys()), 651 ) 652 if all_test_details: 653 tests = '' 654 for test_group, test_list in all_test_details.items(): 655 tests += '%s:\n' % test_group 656 for test_detail in sorted(test_list, key=str): 657 tests += '\t%s\n' % test_detail 658 atest_utils.print_and_log_warning( 659 'All available tests in TEST_MAPPING files are:\n%s', tests 660 ) 661 metrics_utils.send_exit_event(ExitCode.TEST_NOT_FOUND) 662 sys.exit(ExitCode.TEST_NOT_FOUND) 663 664 logging.debug( 665 'Test details:\n%s', 666 '\n'.join([str(detail) for detail in test_details_list]), 667 ) 668 test_names = [detail.name for detail in test_details_list] 669 return test_names, test_details_list 670 671 def _extract_testable_modules_by_wildcard(self, user_input): 672 """Extract the given string with wildcard symbols to testable 673 674 module names. 675 676 Assume the available testable modules is: 677 ['Google', 'google', 'G00gle', 'g00gle'] 678 and the user_input is: 679 ['*oo*', 'g00gle'] 680 This method will return: 681 ['Google', 'google', 'g00gle'] 682 683 Args: 684 user_input: A list of input. 685 686 Returns: 687 A list of testable modules. 688 """ 689 testable_mods = self.mod_info.get_testable_modules() 690 extracted_tests = [] 691 for test in user_input: 692 if atest_utils.has_wildcard(test): 693 extracted_tests.extend(fnmatch.filter(testable_mods, test)) 694 else: 695 extracted_tests.append(test) 696 return extracted_tests 697 698 def translate(self, args): 699 """Translate atest command line into build targets and run commands. 700 701 Args: 702 args: arg parsed object. 703 704 Returns: 705 A tuple with set of build_target strings and list of TestInfos. 706 """ 707 tests = args.tests 708 detect_type = DetectType.TEST_WITH_ARGS 709 # Disable fuzzy searching when running with test mapping related args. 710 if not args.tests or atest_utils.is_test_mapping(args): 711 self.fuzzy_search = False 712 detect_type = DetectType.TEST_NULL_ARGS 713 start = time.time() 714 # Not including host unit tests if user specify --test-mapping. 715 host_unit_tests = [] 716 if not any((args.tests, args.test_mapping)): 717 logging.debug('Finding Host Unit Tests...') 718 host_unit_tests = test_finder_utils.find_host_unit_tests( 719 self.mod_info, str(Path(os.getcwd()).relative_to(self.root_dir)) 720 ) 721 logging.debug('Found host_unit_tests: %s', host_unit_tests) 722 # Test details from TEST_MAPPING files 723 test_details_list = None 724 if atest_utils.is_test_mapping(args): 725 if args.enable_file_patterns: 726 self.enable_file_patterns = True 727 tests, test_details_list = self._get_test_mapping_tests( 728 args, not bool(host_unit_tests) 729 ) 730 atest_utils.colorful_print('\nFinding Tests...', constants.CYAN) 731 logging.debug('Finding Tests: %s', tests) 732 # Clear cache if user pass -c option 733 if args.clear_cache: 734 atest_utils.clean_test_info_caches(tests + host_unit_tests) 735 # Process tests which might contain wildcard symbols in advance. 736 if atest_utils.has_wildcard(tests): 737 tests = self._extract_testable_modules_by_wildcard(tests) 738 test_infos = self._get_test_infos(tests, test_details_list) 739 if host_unit_tests: 740 host_unit_test_details = [ 741 test_mapping.TestDetail({'name': test, 'host': True}) 742 for test in host_unit_tests 743 ] 744 host_unit_test_infos = self._get_test_infos( 745 host_unit_tests, host_unit_test_details 746 ) 747 test_infos.extend(host_unit_test_infos) 748 if atest_utils.has_mixed_type_filters(test_infos): 749 atest_utils.colorful_print( 750 'Mixed type filters found. ' 751 'Please separate tests into different runs.', 752 constants.YELLOW, 753 ) 754 sys.exit(ExitCode.MIXED_TYPE_FILTER) 755 finished_time = time.time() - start 756 logging.debug('Finding tests finished in %ss', finished_time) 757 metrics.LocalDetectEvent(detect_type=detect_type, result=int(finished_time)) 758 for t_info in test_infos: 759 logging.debug('%s\n', t_info) 760 return test_infos 761 762 763# TODO: (b/265359291) Raise Exception when the brackets are not in pair. 764def parse_test_identifier(test: str) -> TestIdentifier: 765 """Get mainline module names and binaries information.""" 766 result = atest_utils.get_test_and_mainline_modules(test) 767 if not result: 768 return TestIdentifier(test, [], []) 769 test_name = result.group('test') 770 mainline_binaries = result.group('mainline_modules').split('+') 771 mainline_modules = [Path(m).stem for m in mainline_binaries] 772 logging.debug('mainline_modules: %s', mainline_modules) 773 return TestIdentifier(test_name, mainline_modules, mainline_binaries) 774