• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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