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