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