xref: /aosp_15_r20/tools/asuite/atest/test_finders/module_finder.py (revision c2e18aaa1096c836b086f94603d04f4eb9cf37f5)
1# Copyright 2018, 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"""Module Finder class."""
16
17import logging
18import os
19import time
20from typing import List
21
22from atest import atest_configs
23from atest import atest_error
24from atest import atest_utils
25from atest import constants
26from atest.atest_enum import DetectType
27from atest.metrics import metrics
28from atest.test_finders import test_filter_utils
29from atest.test_finders import test_finder_base
30from atest.test_finders import test_finder_utils
31from atest.test_finders import test_info
32from atest.test_runners import atest_tf_test_runner
33from atest.test_runners import mobly_test_runner
34from atest.test_runners import robolectric_test_runner
35from atest.test_runners import vts_tf_test_runner
36
37# These are suites in LOCAL_COMPATIBILITY_SUITE that aren't really suites so
38# we can ignore them.
39_SUITES_TO_IGNORE = frozenset({'general-tests', 'device-tests', 'tests'})
40
41
42class ModuleFinder(test_finder_base.TestFinderBase):
43  """Module finder class."""
44
45  NAME = 'MODULE'
46  _TEST_RUNNER = atest_tf_test_runner.AtestTradefedTestRunner.NAME
47  _MOBLY_RUNNER = mobly_test_runner.MoblyTestRunner.NAME
48  _ROBOLECTRIC_RUNNER = robolectric_test_runner.RobolectricTestRunner.NAME
49  _VTS_TEST_RUNNER = vts_tf_test_runner.VtsTradefedTestRunner.NAME
50
51  def __init__(self, module_info=None):
52    super().__init__()
53    self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
54    self.module_info = module_info
55
56  def _determine_modules_to_test(
57      self, module_path: str, test_file_path: str = None
58  ) -> set[str]:
59    """Determine which module the user is trying to test.
60
61    Returns the modules to test. If there are multiple possibilities, will
62    ask the user. Otherwise will return the only module found.
63
64    Args:
65        module_path: String path of module to look for.
66        test_file_path: String path of input file where the test is found.
67
68    Returns:
69        A set of the module names.
70    """
71    modules_to_test = set()
72
73    if test_file_path:
74      modules_to_test = self.module_info.get_modules_by_path_in_srcs(
75          path=test_file_path,
76          testable_modules_only=True,
77      )
78
79    # If a single module path matches contains the path of the given test file
80    # in its MODULE_SRCS, do not continue to extract modules.
81    if len(modules_to_test) == 1:
82      return modules_to_test
83
84    modules_to_test |= self.module_info.get_modules_by_path(
85        path=module_path,
86        testable_modules_only=True,
87    )
88
89    return test_finder_utils.extract_selected_tests(modules_to_test)
90
91  def _is_vts_module(self, module_name):
92    """Returns True if the module is a vts10 module, else False."""
93    mod_info = self.module_info.get_module_info(module_name)
94    suites = []
95    if mod_info:
96      suites = mod_info.get(constants.MODULE_COMPATIBILITY_SUITES, [])
97    # Pull out all *ts (cts, tvts, etc) suites.
98    suites = [suite for suite in suites if suite not in _SUITES_TO_IGNORE]
99    return len(suites) == 1 and 'vts10' in suites
100
101  def _update_to_vts_test_info(self, test):
102    """Fill in the fields with vts10 specific info.
103
104    We need to update the runner to use the vts10 runner and also find the
105    test specific dependencies.
106
107    Args:
108        test: TestInfo to update with vts10 specific details.
109
110    Returns:
111        TestInfo that is ready for the vts10 test runner.
112    """
113    test.test_runner = self._VTS_TEST_RUNNER
114    config_file = os.path.join(
115        self.root_dir, test.data[constants.TI_REL_CONFIG]
116    )
117    # Need to get out dir (special logic is to account for custom out dirs).
118    # The out dir is used to construct the build targets for the test deps.
119    out_dir = os.environ.get(constants.ANDROID_HOST_OUT)
120    custom_out_dir = os.environ.get(constants.ANDROID_OUT_DIR)
121    # If we're not an absolute custom out dir, get no-absolute out dir path.
122    if custom_out_dir is None or not os.path.isabs(custom_out_dir):
123      out_dir = os.path.relpath(out_dir, self.root_dir)
124    vts_out_dir = os.path.join(out_dir, 'vts10', 'android-vts10', 'testcases')
125    # Parse dependency of default staging plans.
126    xml_paths = test_finder_utils.search_integration_dirs(
127        constants.VTS_STAGING_PLAN,
128        self.module_info.get_paths(constants.VTS_TF_MODULE),
129    )
130    vts_xmls = set()
131    vts_xmls.add(config_file)
132    for xml_path in xml_paths:
133      vts_xmls |= test_finder_utils.get_plans_from_vts_xml(xml_path)
134    for config_file in vts_xmls:
135      # Add in vts10 test build targets.
136      for target in test_finder_utils.get_targets_from_vts_xml(
137          config_file, vts_out_dir, self.module_info
138      ):
139        test.add_build_target(target)
140    test.add_build_target('vts-test-core')
141    test.add_build_target(test.test_name)
142    return test
143
144  def _update_to_mobly_test_info(self, test):
145    """Update the fields for a Mobly test.
146
147    The runner will be updated to the Mobly runner.
148
149    The module's build output paths will be stored in the test_info data.
150
151    Args:
152        test: TestInfo to be updated with Mobly fields.
153
154    Returns:
155        TestInfo with updated Mobly fields.
156    """
157    # Set test runner to MoblyTestRunner
158    test.test_runner = self._MOBLY_RUNNER
159    # Add test module as build target
160    module_name = test.test_name
161    test.add_build_target(module_name)
162    # Add module's installed paths to data, so the runner may access the
163    # module's build outputs.
164    installed_paths = self.module_info.get_installed_paths(module_name)
165    test.data[constants.MODULE_INSTALLED] = installed_paths
166    return test
167
168  def _update_legacy_robolectric_test_info(self, test):
169    """Update the fields for a legacy robolectric test.
170
171    This method is updating test_name when the given is a legacy robolectric
172    test, and assigning Robolectric Runner for it.
173
174    e.g. WallPaperPicker2RoboTests is a legacy robotest, and the test_name
175    will become RunWallPaperPicker2RoboTests and run it with Robolectric
176    Runner.
177
178    Args:
179        test: TestInfo to be updated with robolectric fields.
180
181    Returns:
182        TestInfo with updated robolectric fields.
183    """
184    test.test_runner = self._ROBOLECTRIC_RUNNER
185    test.test_name = self.module_info.get_robolectric_test_name(
186        self.module_info.get_module_info(test.test_name)
187    )
188    return test
189
190  # pylint: disable=too-many-branches
191  def _process_test_info(self, test):
192    """Process the test info and return some fields updated/changed.
193
194    We need to check if the test found is a special module (like vts10) and
195    update the test_info fields (like test_runner) appropriately.
196
197    Args:
198        test: TestInfo that has been filled out by a find method.
199
200    Returns:
201        TestInfo that has been modified as needed and return None if
202        this module can't be found in the module_info.
203    """
204    module_name = test.test_name
205    mod_info = self.module_info.get_module_info(module_name)
206    if not mod_info:
207      return None
208    test.module_class = mod_info['class']
209    test.install_locations = test_finder_utils.get_install_locations(
210        mod_info.get(constants.MODULE_INSTALLED, [])
211    )
212    # Check if this is only a vts10 module.
213    if self._is_vts_module(test.test_name):
214      return self._update_to_vts_test_info(test)
215    # Check if this is a Mobly test module.
216    if self.module_info.is_mobly_module(mod_info):
217      return self._update_to_mobly_test_info(test)
218    test.robo_type = self.module_info.get_robolectric_type(test.test_name)
219    if test.robo_type:
220      test.install_locations = {constants.DEVICELESS_TEST}
221      if test.robo_type == constants.ROBOTYPE_MODERN:
222        test.add_build_target(test.test_name)
223        return test
224      if test.robo_type == constants.ROBOTYPE_LEGACY:
225        return self._update_legacy_robolectric_test_info(test)
226    rel_config = test.data[constants.TI_REL_CONFIG]
227    for target in self._get_build_targets(module_name, rel_config):
228      test.add_build_target(target)
229    # (b/177626045) Probe target APK for running instrumentation tests to
230    # prevent RUNNER ERROR by adding target application(module) to the
231    # build_targets, and install these target apks before testing.
232    artifact_map = self.module_info.get_instrumentation_target_apps(module_name)
233    if artifact_map:
234      logging.debug('Found %s an instrumentation test.', module_name)
235      for art in artifact_map.keys():
236        test.add_build_target(art)
237      logging.debug(
238          'Add %s to build targets...', ', '.join(artifact_map.keys())
239      )
240      test.artifacts = []
241      for p in artifact_map.values():
242        test.artifacts += p
243      logging.debug('Will install target APK: %s\n', test.artifacts)
244      metrics.LocalDetectEvent(
245          detect_type=DetectType.FOUND_TARGET_ARTIFACTS,
246          result=len(test.artifacts),
247      )
248    # For device side java test, it will use
249    # com.android.compatibility.testtype.DalvikTest as test runner in
250    # cts-dalvik-device-test-runner.jar
251    if self.module_info.is_auto_gen_test_config(module_name):
252      if constants.MODULE_CLASS_JAVA_LIBRARIES in test.module_class:
253        for dalvik_dep in test_finder_utils.DALVIK_TEST_DEPS:
254          if self.module_info.is_module(dalvik_dep):
255            test.add_build_target(dalvik_dep)
256    # Update test name if the test belong to extra config which means it's
257    # test config name is not the same as module name. For extra config, it
258    # index will be greater or equal to 1.
259    try:
260      if mod_info.get(constants.MODULE_TEST_CONFIG, []).index(rel_config) > 0:
261        config_test_name = os.path.splitext(os.path.basename(rel_config))[0]
262        logging.debug(
263            'Replace test_info.name(%s) to %s', test.test_name, config_test_name
264        )
265        test.test_name = config_test_name
266    except ValueError:
267      pass
268    return test
269
270  def _get_build_targets(self, module_name, rel_config):
271    """Get the test deps.
272
273    Args:
274        module_name: name of the test.
275        rel_config: XML for the given test.
276
277    Returns:
278        Set of build targets.
279    """
280    targets = set()
281    if not self.module_info.is_auto_gen_test_config(module_name):
282      config_file = os.path.join(self.root_dir, rel_config)
283      targets = test_finder_utils.get_targets_from_xml(
284          config_file, self.module_info
285      )
286    if constants.VTS_CORE_SUITE in self.module_info.get_module_info(
287        module_name
288    ).get(constants.MODULE_COMPATIBILITY_SUITES, []):
289      targets.add(constants.VTS_CORE_TF_MODULE)
290    for suite in self.module_info.get_module_info(module_name).get(
291        constants.MODULE_COMPATIBILITY_SUITES, []
292    ):
293      targets.update(constants.SUITE_DEPS.get(suite, []))
294    for module_path in self.module_info.get_paths(module_name):
295      mod_dir = module_path.replace('/', '-')
296      targets.add(constants.MODULES_IN + mod_dir)
297    # (b/184567849) Force adding module_name as a build_target. This will
298    # allow excluding MODULES-IN-* and prevent from missing build targets.
299    if module_name and self.module_info.is_module(module_name):
300      targets.add(module_name)
301    # If it's a MTS test, add cts-tradefed as test dependency.
302    if constants.MTS_SUITE in self.module_info.get_module_info(module_name).get(
303        constants.MODULE_COMPATIBILITY_SUITES, []
304    ):
305      if self.module_info.is_module(constants.CTS_JAR):
306        targets.add(constants.CTS_JAR)
307    return targets
308
309  def _get_module_test_config(self, module_name, rel_config=None) -> list[str]:
310    """Get the value of test_config in module_info.
311
312    Get the value of 'test_config' in module_info if its
313    auto_test_config is not true.
314    In this case, the test_config is specified by user.
315    If not, return rel_config.
316
317    Args:
318        module_name: A string of the test's module name.
319        rel_config: XML for the given test.
320
321    Returns:
322        A list of string of test_config path if found, else return rel_config.
323    """
324    default_all_config = not (
325        atest_configs.GLOBAL_ARGS
326        and atest_configs.GLOBAL_ARGS.test_config_select
327    )
328    mod_info = self.module_info.get_module_info(module_name)
329    if mod_info:
330      test_configs = []
331      test_config_list = mod_info.get(constants.MODULE_TEST_CONFIG, [])
332      if test_config_list:
333        # multiple test configs
334        if len(test_config_list) > 1:
335          test_configs = test_finder_utils.extract_selected_tests(
336              test_config_list, default_all=default_all_config
337          )
338        else:
339          test_configs = test_config_list
340      if test_configs:
341        return test_configs
342      # Double check if below section is needed.
343      if (
344          not self.module_info.is_auto_gen_test_config(module_name)
345          and test_configs
346      ):
347        return test_configs
348    return [rel_config] if rel_config else []
349
350  # pylint: disable=too-many-branches
351  # pylint: disable=too-many-locals
352  def _get_test_info_filter(
353      self, path, methods, rel_module_dir=None, class_name=None,
354      is_native_test=False
355  ):
356    """Get test info filter.
357
358    Args:
359        path: A string of the test's path.
360        methods: A set of method name strings.
361        rel_module_dir: Optional. A string of the module dir no-absolute to
362          root.
363        class_name: Optional. A string of the class name.
364        is_native_test: Optional. A boolean variable of whether to search for
365          a native test or not.
366
367    Returns:
368        A set of test info filter.
369    """
370    _, file_name = test_finder_utils.get_dir_path_and_filename(path)
371    ti_filter = frozenset()
372    if os.path.isfile(path) and is_native_test:
373      class_info = test_finder_utils.get_cc_class_info(path)
374      ti_filter = frozenset([
375          test_info.TestFilter(
376              test_filter_utils.get_cc_filter(
377                  class_info,
378                  class_name if class_name is not None else '*', methods
379              ),
380              frozenset(),
381          )
382      ])
383    # Path to java file.
384    elif file_name and constants.JAVA_EXT_RE.match(file_name):
385      full_class_name = test_filter_utils.get_fully_qualified_class_name(path)
386      methods = frozenset(
387          test_filter_utils.get_java_method_filters(path, methods)
388      )
389      ti_filter = frozenset([test_info.TestFilter(full_class_name, methods)])
390    # Path to cc file.
391    elif file_name and constants.CC_EXT_RE.match(file_name):
392      # TODO: b/173019813 - Should setup correct filter for an input file.
393      if not test_finder_utils.has_cc_class(path):
394        raise atest_error.MissingCCTestCaseError(
395            "Can't find CC class in %s" % path
396        )
397      class_info = test_finder_utils.get_cc_class_info(path)
398      cc_filters = []
399      for classname, _ in class_info.items():
400        cc_filters.append(
401            test_info.TestFilter(
402                test_filter_utils.get_cc_filter(class_info, classname, methods),
403                frozenset(),
404            )
405        )
406      ti_filter = frozenset(cc_filters)
407    # If input path is a folder and have class_name information.
408    elif not file_name and class_name:
409      ti_filter = frozenset(
410          [test_info.TestFilter(class_name, methods)]
411      )
412    # Path to non-module dir, treat as package.
413    elif not file_name and rel_module_dir != os.path.relpath(
414        path, self.root_dir):
415      dir_items = [os.path.join(path, f) for f in os.listdir(path)]
416      for dir_item in dir_items:
417        if constants.JAVA_EXT_RE.match(dir_item):
418          package_name = test_filter_utils.get_package_name(dir_item)
419          if package_name:
420            # methods should be empty frozenset for package.
421            if methods:
422              raise atest_error.MethodWithoutClassError(
423                  '%s: Method filtering requires class' % str(methods)
424              )
425            ti_filter = frozenset([test_info.TestFilter(package_name, methods)])
426            break
427    logging.debug('_get_test_info_filter() ti_filter: %s', ti_filter)
428    return ti_filter
429
430  def _get_rel_config(self, test_path):
431    """Get config file's no-absolute path.
432
433    Args:
434        test_path: A string of the test absolute path.
435
436    Returns:
437        A string of config's no-absolute path, else None.
438    """
439    test_dir = os.path.dirname(test_path)
440    rel_module_dir = test_finder_utils.find_parent_module_dir(
441        self.root_dir, test_dir, self.module_info
442    )
443    if rel_module_dir:
444      return os.path.join(rel_module_dir, constants.MODULE_CONFIG)
445    return None
446
447  def _get_test_infos(self, test_path, rel_config, module_name, test_filter):
448    """Get test_info for test_path.
449
450    Args:
451        test_path: A string of the test path.
452        rel_config: A string of rel path of config.
453        module_name: A string of the module name to use.
454        test_filter: A test info filter.
455
456    Returns:
457        A list of TestInfo namedtuple if found, else None.
458    """
459    if not rel_config:
460      rel_config = self._get_rel_config(test_path)
461      if not rel_config:
462        return None
463    if module_name:
464      module_names = [module_name]
465    else:
466      module_names = self._determine_modules_to_test(
467          os.path.dirname(rel_config),
468          test_path if self._is_comparted_src(test_path) else None,
469      )
470    test_infos = []
471    if module_names:
472      for mname in module_names:
473        # The real test config might be record in module-info.
474        mod_info = self.module_info.get_module_info(mname)
475        if not mod_info:
476          continue
477        rel_configs = self._get_module_test_config(mname, rel_config=rel_config)
478        for rel_cfg in rel_configs:
479          tinfo = self._process_test_info(
480              test_info.TestInfo(
481                  test_name=mname,
482                  test_runner=self._TEST_RUNNER,
483                  build_targets=set(),
484                  data={
485                      constants.TI_FILTER: test_filter,
486                      constants.TI_REL_CONFIG: rel_cfg,
487                  },
488                  compatibility_suites=mod_info.get(
489                      constants.MODULE_COMPATIBILITY_SUITES, []
490                  ),
491              )
492          )
493          if tinfo:
494            test_infos.append(tinfo)
495    return test_infos
496
497  def find_test_by_module_name(self, module_name):
498    """Find test for the given module name.
499
500    Args:
501        module_name: A string of the test's module name.
502
503    Returns:
504        A list that includes only 1 populated TestInfo namedtuple
505        if found, otherwise None.
506    """
507    tinfos = []
508    mod_info = self.module_info.get_module_info(module_name)
509    if self.module_info.is_testable_module(mod_info):
510      # path is a list with only 1 element.
511      rel_config = os.path.join(
512          mod_info[constants.MODULE_PATH][0], constants.MODULE_CONFIG
513      )
514      rel_configs = self._get_module_test_config(
515          module_name, rel_config=rel_config
516      )
517      for rel_config in rel_configs:
518        tinfo = self._process_test_info(
519            test_info.TestInfo(
520                test_name=module_name,
521                test_runner=self._TEST_RUNNER,
522                build_targets=set(),
523                data={
524                    constants.TI_REL_CONFIG: rel_config,
525                    constants.TI_FILTER: frozenset(),
526                },
527                compatibility_suites=mod_info.get(
528                    constants.MODULE_COMPATIBILITY_SUITES, []
529                ),
530            )
531        )
532        if tinfo:
533          tinfos.append(tinfo)
534      if tinfos:
535        return tinfos
536    return None
537
538  def find_test_by_kernel_class_name(self, module_name, class_name):
539    """Find kernel test for the given class name.
540
541    Args:
542        module_name: A string of the module name to use.
543        class_name: A string of the test's class name.
544
545    Returns:
546        A list of populated TestInfo namedtuple if test found, else None.
547    """
548
549    class_name, methods = test_filter_utils.split_methods(class_name)
550    test_configs = self._get_module_test_config(module_name)
551    if not test_configs:
552      return None
553    tinfos = []
554    for test_config in test_configs:
555      test_config_path = os.path.join(self.root_dir, test_config)
556      mod_info = self.module_info.get_module_info(module_name)
557      ti_filter = frozenset([test_info.TestFilter(class_name, methods)])
558      if test_finder_utils.is_test_from_kernel_xml(
559          test_config_path, class_name
560      ):
561        tinfo = self._process_test_info(
562            test_info.TestInfo(
563                test_name=module_name,
564                test_runner=self._TEST_RUNNER,
565                build_targets=set(),
566                data={
567                    constants.TI_REL_CONFIG: test_config,
568                    constants.TI_FILTER: ti_filter,
569                },
570                compatibility_suites=mod_info.get(
571                    constants.MODULE_COMPATIBILITY_SUITES, []
572                ),
573            )
574        )
575        if tinfo:
576          tinfos.append(tinfo)
577    if tinfos:
578      return tinfos
579    return None
580
581  def find_test_by_class_name(
582      self,
583      class_name: str,
584      module_name: str = None,
585      rel_config_path: str = None,
586      is_native_test: bool = False,
587  ) -> list[test_info.TestInfo] | None:
588    """Find test files given a class name.
589
590    If module_name and rel_config not given it will calculate it determine
591    it by looking up the tree from the class file.
592
593    Args:
594        class_name: A string of the test's class name.
595        module_name: Optional. A string of the module name to use.
596        rel_config_path: Optional. A string of module dir relative to repo root.
597        is_native_test: A boolean variable of whether to search for a native
598          test or not.
599
600    Returns:
601        A list of populated TestInfo namedtuple if test found, else None.
602    """
603    class_name, methods = test_filter_utils.split_methods(class_name)
604    search_class_name = class_name
605    # For parameterized gtest, test class will be automerged to
606    # $(class_prefix)/$(base_class) name. Using $(base_class) for searching
607    # matched TEST_P to make sure test class is matched.
608    if '/' in search_class_name:
609      search_class_name = str(search_class_name).split('/')[-1]
610
611    test_paths = []
612    # Search using the path where the config file is located.
613    if rel_config_path:
614      test_paths = test_finder_utils.find_class_file(
615          os.path.join(self.root_dir, os.path.dirname(rel_config_path)),
616          search_class_name,
617          is_native_test,
618          methods,
619      )
620      if not test_paths:
621        atest_utils.print_and_log_info(
622            'Did not find class (%s) under module path (%s), '
623            'researching from repo root.',
624            class_name,
625            rel_config_path,
626        )
627    # Search from the root dir.
628    if not test_paths:
629      test_paths = test_finder_utils.find_class_file(
630          self.root_dir, search_class_name, is_native_test, methods
631      )
632    # If we already have module name, use path in module-info as test_path.
633    if not test_paths:
634      if not module_name:
635        return None
636      # Use the module path as test_path.
637      module_paths = self.module_info.get_paths(module_name)
638      test_paths = []
639      for rel_module_path in module_paths:
640        test_paths.append(os.path.join(self.root_dir, rel_module_path))
641
642    tinfos = []
643    for test_path in test_paths:
644      test_filter = self._get_test_info_filter(
645          test_path,
646          methods,
647          class_name=class_name,
648          is_native_test=is_native_test,
649      )
650      test_infos = self._get_test_infos(
651          test_path, rel_config_path, module_name, test_filter
652      )
653      # If input include methods, check if tinfo match.
654      if test_infos and len(test_infos) > 1 and methods:
655        test_infos = self._get_matched_test_infos(test_infos, methods)
656      if test_infos:
657        tinfos.extend(test_infos)
658
659    return tinfos if tinfos else None
660
661  def _get_matched_test_infos(self, test_infos, methods):
662    """Get the test_infos matched the given methods.
663
664    Args:
665        test_infos: A list of TestInfo obj.
666        methods: A set of method name strings.
667
668    Returns:
669        A list of matched TestInfo namedtuple, else None.
670    """
671    matched_test_infos = set()
672    for tinfo in test_infos:
673      test_config, test_srcs = test_finder_utils.get_test_config_and_srcs(
674          tinfo, self.module_info
675      )
676      if test_config:
677        filter_dict = atest_utils.get_android_junit_config_filters(test_config)
678        # Always treat the test_info is matched if no filters found.
679        if not filter_dict.keys():
680          matched_test_infos.add(tinfo)
681          continue
682        for method in methods:
683          if self._is_srcs_match_method_annotation(
684              method, test_srcs, filter_dict
685          ):
686            logging.debug(
687                'For method:%s Test:%s matched filter_dict: %s',
688                method,
689                tinfo.test_name,
690                filter_dict,
691            )
692            matched_test_infos.add(tinfo)
693    return list(matched_test_infos)
694
695  def _is_srcs_match_method_annotation(self, method, srcs, annotation_dict):
696    """Check if input srcs matched annotation.
697
698    Args:
699        method: A string of test method name.
700        srcs: A list of source file of test.
701        annotation_dict: A dictionary record the include and exclude
702          annotations.
703
704    Returns:
705        True if input method matched the annotation of input srcs, else
706        None.
707    """
708    include_annotations = annotation_dict.get(constants.INCLUDE_ANNOTATION, [])
709    exclude_annotations = annotation_dict.get(constants.EXCLUDE_ANNOTATION, [])
710    for src in srcs:
711      include_methods = set()
712      src_path = os.path.join(self.root_dir, src)
713      # Add methods matched include_annotations.
714      for annotation in include_annotations:
715        include_methods.update(
716            test_finder_utils.get_annotated_methods(annotation, src_path)
717        )
718      if exclude_annotations:
719        # For exclude annotation, get all the method in the input srcs,
720        # and filter out the matched annotation.
721        exclude_methods = set()
722        all_methods = test_finder_utils.get_java_methods(src_path)
723        for annotation in exclude_annotations:
724          exclude_methods.update(
725              test_finder_utils.get_annotated_methods(annotation, src_path)
726          )
727        include_methods = all_methods - exclude_methods
728      if method in include_methods:
729        return True
730    return False
731
732  def find_test_by_module_and_class(
733      self, module_class: str
734  ) -> list[test_info.TestInfo]:
735    """Find the test info given a MODULE:CLASS string.
736
737    Args:
738        module_class: A string of form MODULE:CLASS or MODULE:CLASS#METHOD.
739
740    Returns:
741        A list of populated TestInfo if found, else None.
742    """
743    parse_result = test_finder_utils.parse_test_reference(module_class)
744    if not parse_result:
745      return None
746    module_name = parse_result['module_name']
747    class_name = parse_result['pkg_class_name']
748    method_name = parse_result.get('method_name', '')
749    if method_name:
750      class_name = class_name + '#' + method_name
751
752    # module_infos is a list of TestInfo with at most 1 element.
753    module_infos = self.find_test_by_module_name(module_name)
754    module_info = module_infos[0] if module_infos else None
755    if not module_info:
756      return None
757    find_result = None
758    # If the target module is JAVA or Python test, search class name.
759    find_result = self.find_test_by_class_name(
760        class_name,
761        module_name,
762        module_info.data.get(constants.TI_REL_CONFIG),
763        self.module_info.is_native_test(module_name),
764    )
765    # kernel target test is also define as NATIVE_TEST in build system.
766    # TODO: b/157210083 - Update find_test_by_kernel_class_name method to
767    # support gen_rule use case.
768    if not find_result:
769      find_result = self.find_test_by_kernel_class_name(module_name, class_name)
770    # Find by cc class.
771    if not find_result:
772      find_result = self.find_test_by_cc_class_name(
773          class_name,
774          module_info.test_name,
775          module_info.data.get(constants.TI_REL_CONFIG),
776      )
777    return find_result
778
779  def find_test_by_package_name(
780      self, package, module_name=None, rel_config=None
781  ):
782    """Find the test info given a PACKAGE string.
783
784    Args:
785        package: A string of the package name.
786        module_name: Optional. A string of the module name.
787        rel_config: Optional. A string of rel path of config.
788
789    Returns:
790        A list of populated TestInfo namedtuple if found, else None.
791    """
792    _, methods = test_filter_utils.split_methods(package)
793    if methods:
794      raise atest_error.MethodWithoutClassError(
795          '%s: Method filtering requires class' % (methods)
796      )
797    # Confirm that packages exists and get user input for multiples.
798    if rel_config:
799      search_dir = os.path.join(self.root_dir, os.path.dirname(rel_config))
800    else:
801      search_dir = self.root_dir
802    package_paths = test_finder_utils.run_find_cmd(
803        test_finder_utils.TestReferenceType.PACKAGE, search_dir, package
804    )
805    package_paths = package_paths if package_paths is not None else []
806    # Package path will be the full path to the dir represented by package.
807    if not package_paths:
808      if not module_name:
809        return None
810      module_paths = self.module_info.get_paths(module_name)
811      for rel_module_path in module_paths:
812        package_paths.append(os.path.join(self.root_dir, rel_module_path))
813    test_filter = frozenset([test_info.TestFilter(package, frozenset())])
814    test_infos = []
815    for package_path in package_paths:
816      tinfo = self._get_test_infos(
817          package_path, rel_config, module_name, test_filter
818      )
819      if tinfo:
820        test_infos.extend(tinfo)
821    return test_infos if test_infos else None
822
823  def find_test_by_module_and_package(self, module_package):
824    """Find the test info given a MODULE:PACKAGE string.
825
826    Args:
827        module_package: A string of form MODULE:PACKAGE
828
829    Returns:
830        A list of populated TestInfo namedtuple if found, else None.
831    """
832    parse_result = test_finder_utils.parse_test_reference(module_package)
833    if not parse_result:
834      return None
835    module_name = parse_result['module_name']
836    package = parse_result['pkg_class_name']
837    method = parse_result.get('method_name', '')
838    if method:
839      package = package + '#' + method
840
841    # module_infos is a list with at most 1 element.
842    module_infos = self.find_test_by_module_name(module_name)
843    module_info = module_infos[0] if module_infos else None
844    if not module_info:
845      return None
846    return self.find_test_by_package_name(
847        package,
848        module_info.test_name,
849        module_info.data.get(constants.TI_REL_CONFIG),
850    )
851
852  def find_test_by_path(self, rel_path: str) -> List[test_info.TestInfo]:
853    """Find the first test info matching the given path.
854
855    Strategy:
856        path_to_java_file --> Resolve to CLASS
857        path_to_cc_file --> Resolve to CC CLASS
858        path_to_module_file -> Resolve to MODULE
859        path_to_module_dir -> Resolve to MODULE
860        path_to_dir_with_class_files--> Resolve to PACKAGE
861        path_to_any_other_dir --> Resolve as MODULE
862
863    Args:
864        rel_path: A string of the relative path to $BUILD_TOP.
865
866    Returns:
867        A list of populated TestInfo namedtuple if test found, else None
868    """
869    logging.debug('Finding test by path: %s', rel_path)
870    path, methods = test_filter_utils.split_methods(rel_path)
871    # create absolute path from cwd and remove symbolic links
872    path = os.path.realpath(path)
873    if not os.path.exists(path):
874      return None
875    if methods and not test_finder_utils.has_method_in_file(path, methods):
876      return None
877    dir_path, _ = test_finder_utils.get_dir_path_and_filename(path)
878    # Module/Class
879    rel_module_dir = test_finder_utils.find_parent_module_dir(
880        self.root_dir, dir_path, self.module_info
881    )
882
883    # If the input file path does not belong to a module(by searching
884    # upwards to the build_top), check whether it belongs to the dependency
885    # of modules.
886    if not rel_module_dir:
887      testable_modules = self.module_info.get_modules_by_include_deps(
888          self.module_info.get_modules_by_path_in_srcs(rel_path),
889          testable_module_only=True,
890      )
891      if testable_modules:
892        test_filter = self._get_test_info_filter(
893            path, methods, rel_module_dir=rel_module_dir
894        )
895        tinfos = []
896        for testable_module in testable_modules:
897          rel_config = os.path.join(
898              self.module_info.get_paths(testable_module)[0],
899              constants.MODULE_CONFIG,
900          )
901          tinfos.extend(
902              self._get_test_infos(
903                  path, rel_config, testable_module, test_filter
904              )
905          )
906        metrics.LocalDetectEvent(
907            detect_type=DetectType.FIND_TEST_IN_DEPS, result=1
908        )
909        return tinfos
910
911    if not rel_module_dir:
912      # Try to find unit-test for input path.
913      path = os.path.relpath(
914          os.path.realpath(rel_path),
915          os.environ.get(constants.ANDROID_BUILD_TOP, ''),
916      )
917      unit_tests = test_finder_utils.find_host_unit_tests(
918          self.module_info, path
919      )
920      if unit_tests:
921        tinfos = []
922        for unit_test in unit_tests:
923          tinfo = self._get_test_infos(
924              path, constants.MODULE_CONFIG, unit_test, frozenset()
925          )
926          if tinfo:
927            tinfos.extend(tinfo)
928        return tinfos
929      return None
930    rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG)
931    test_filter = self._get_test_info_filter(
932        path, methods, rel_module_dir=rel_module_dir
933    )
934    return self._get_test_infos(path, rel_config, None, test_filter)
935
936  def find_test_by_cc_class_name(
937      self, class_name, module_name=None, rel_config=None
938  ):
939    """Find test files given a cc class name.
940
941    If module_name and rel_config not given, test will be determined
942    by looking up the tree for files which has input class.
943
944    Args:
945        class_name: A string of the test's class name.
946        module_name: Optional. A string of the module name to use.
947        rel_config: Optional. A string of module dir no-absolute to repo root.
948
949    Returns:
950        A list of populated TestInfo namedtuple if test found, else None.
951    """
952    # Check if class_name is prepended with file name. If so, trim the
953    # prefix and keep only the class_name.
954    if '.' in class_name:
955      # (b/202764540) Strip prefixes of a cc class.
956      # Assume the class name has a format of file_name.class_name
957      class_name = class_name[class_name.rindex('.') + 1 :]
958      atest_utils.print_and_log_info(
959          'Search with updated class name: %s', class_name
960      )
961    return self.find_test_by_class_name(
962        class_name, module_name, rel_config, is_native_test=True
963    )
964
965  def get_testable_modules_with_ld(self, user_input, ld_range=0):
966    """Calculate the edit distances of the input and testable modules.
967
968    The user input will be calculated across all testable modules and
969    results in integers generated by Levenshtein Distance algorithm.
970    To increase the speed of the calculation, a bound can be applied to
971    this method to prevent from calculating every testable modules.
972
973    Guessing from typos, e.g. atest atest_unitests, implies a tangible range
974    of length that Atest only needs to search within it, and the default of
975    the bound is 2.
976
977    Guessing from keywords however, e.g. atest --search Camera, means that
978    the uncertainty of the module name is way higher, and Atest should walk
979    through all testable modules and return the highest possibilities.
980
981    Args:
982        user_input: A string of the user input.
983        ld_range: An integer that range the searching scope. If the length of
984          user_input is 10, then Atest will calculate modules of which length is
985          between 8 and 12. 0 is equivalent to unlimited.
986
987    Returns:
988        A List of LDs and possible module names. If the user_input is "fax",
989        the output will be like:
990        [[2, "fog"], [2, "Fix"], [4, "duck"], [7, "Duckies"]]
991
992        Which means the most lilely names of "fax" are fog and Fix(LD=2),
993        while Dickies is the most unlikely one(LD=7).
994    """
995    atest_utils.colorful_print(
996        '\nSearching for similar module names using fuzzy search...',
997        constants.CYAN,
998    )
999    search_start = time.time()
1000    testable_modules = sorted(self.module_info.get_testable_modules(), key=len)
1001    lower_bound = len(user_input) - ld_range
1002    upper_bound = len(user_input) + ld_range
1003    testable_modules_with_ld = []
1004    for module_name in testable_modules:
1005      # Dispose those too short or too lengthy.
1006      if ld_range != 0:
1007        if len(module_name) < lower_bound:
1008          continue
1009        if len(module_name) > upper_bound:
1010          break
1011      testable_modules_with_ld.append([
1012          test_finder_utils.get_levenshtein_distance(user_input, module_name),
1013          module_name,
1014      ])
1015    search_duration = time.time() - search_start
1016    logging.debug('Fuzzy search took %ss', search_duration)
1017    metrics.LocalDetectEvent(
1018        detect_type=DetectType.FUZZY_SEARCH_TIME, result=round(search_duration)
1019    )
1020    return testable_modules_with_ld
1021
1022  def get_fuzzy_searching_results(self, user_input):
1023    """Give results which have no more than allowance of edit distances.
1024
1025    Args:
1026        user_input: the target module name for fuzzy searching.
1027
1028    Returns:
1029        A list of guessed modules.
1030    """
1031    modules_with_ld = self.get_testable_modules_with_ld(
1032        user_input, ld_range=constants.LD_RANGE
1033    )
1034    guessed_modules = []
1035    for distance_, module_ in modules_with_ld:
1036      if distance_ <= abs(constants.LD_RANGE):
1037        guessed_modules.append(module_)
1038    return guessed_modules
1039
1040  def find_test_by_config_name(self, config_name):
1041    """Find test for the given config name.
1042
1043    Args:
1044        config_name: A string of the test's config name.
1045
1046    Returns:
1047        A list that includes only 1 populated TestInfo namedtuple
1048        if found, otherwise None.
1049    """
1050    for module_name, mod_info in self.module_info.name_to_module_info.items():
1051      test_configs = mod_info.get(constants.MODULE_TEST_CONFIG, [])
1052      for test_config in test_configs:
1053        test_config_name = os.path.splitext(os.path.basename(test_config))[0]
1054        if test_config_name == config_name:
1055          tinfo = test_info.TestInfo(
1056              test_name=test_config_name,
1057              test_runner=self._TEST_RUNNER,
1058              build_targets=self._get_build_targets(module_name, test_config),
1059              data={
1060                  constants.TI_REL_CONFIG: test_config,
1061                  constants.TI_FILTER: frozenset(),
1062              },
1063              compatibility_suites=mod_info.get(
1064                  constants.MODULE_COMPATIBILITY_SUITES, []
1065              ),
1066          )
1067          test_config_path = os.path.join(self.root_dir, test_config)
1068          if test_finder_utils.need_aggregate_metrics_result(test_config_path):
1069            tinfo.aggregate_metrics_result = True
1070          if tinfo:
1071            # There should have only one test_config with the same
1072            # name in source tree.
1073            return [tinfo]
1074    return None
1075
1076  @staticmethod
1077  def _is_comparted_src(path):
1078    """Check if the input path need to match srcs information in module.
1079
1080    If path is a folder or android build file, we don't need to compart
1081    with module's srcs.
1082
1083    Args:
1084        path: A string of the test's path.
1085
1086    Returns:
1087        True if input path need to match with module's src info, else False.
1088    """
1089    if os.path.isdir(path):
1090      return False
1091    if atest_utils.is_build_file(path):
1092      return False
1093    return True
1094
1095
1096class MainlineModuleFinder(ModuleFinder):
1097  """Mainline Module finder class."""
1098
1099  NAME = 'MAINLINE_MODULE'
1100
1101  def __init__(self, module_info=None):
1102    super().__init__()
1103