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