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