• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""
16Utils for finder classes.
17"""
18
19# pylint: disable=line-too-long
20# pylint: disable=too-many-lines
21
22from __future__ import print_function
23
24import logging
25import os
26import pickle
27import re
28import shutil
29import subprocess
30import tempfile
31import time
32import xml.etree.ElementTree as ET
33
34from contextlib import contextmanager
35from enum import unique, Enum
36from pathlib import Path
37from typing import Any, Dict
38
39from atest import atest_error
40from atest import atest_utils
41from atest import constants
42
43from atest.atest_enum import ExitCode, DetectType
44from atest.metrics import metrics, metrics_utils
45
46# Helps find apk files listed in a test config (AndroidTest.xml) file.
47# Matches "filename.apk" in <option name="foo", value="filename.apk" />
48# We want to make sure we don't grab apks with paths in their name since we
49# assume the apk name is the build target.
50_APK_RE = re.compile(r'^[^/]+\.apk$', re.I)
51
52# Macros that used in GTest. Detailed explanation can be found in
53# $ANDROID_BUILD_TOP/external/googletest/googletest/samples/sample*_unittest.cc
54# 1. Traditional Tests:
55#   TEST(class, method)
56#   TEST_F(class, method)
57# 2. Type Tests:
58#   TYPED_TEST_SUITE(class, types)
59#     TYPED_TEST(class, method)
60# 3. Value-parameterized Tests:
61#   TEST_P(class, method)
62#     INSTANTIATE_TEST_SUITE_P(Prefix, class, param_generator, name_generator)
63# 4. Type-parameterized Tests:
64#   TYPED_TEST_SUITE_P(class)
65#     TYPED_TEST_P(class, method)
66#       REGISTER_TYPED_TEST_SUITE_P(class, method)
67#         INSTANTIATE_TYPED_TEST_SUITE_P(Prefix, class, Types)
68# Macros with (class, method) pattern.
69_CC_CLASS_METHOD_RE = re.compile(
70    r'^\s*(TYPED_TEST(?:|_P)|TEST(?:|_F|_P))\s*\(\s*'
71    r'(?P<class_name>\w+),\s*(?P<method_name>\w+)\)\s*\{', re.M)
72# Macros with (prefix, class, ...) pattern.
73# Note: Since v1.08, the INSTANTIATE_TEST_CASE_P was replaced with
74#   INSTANTIATE_TEST_SUITE_P. However, Atest does not intend to change the
75#   behavior of a test, so we still search *_CASE_* macros.
76_CC_PARAM_CLASS_RE = re.compile(
77    r'^\s*INSTANTIATE_(?:|TYPED_)TEST_(?:SUITE|CASE)_P\s*\(\s*'
78    r'(?P<instantiate>\w+),\s*(?P<class>\w+)\s*,', re.M)
79# Type/Type-parameterized Test macros:
80_TYPE_CC_CLASS_RE = re.compile(
81    r'^\s*TYPED_TEST_SUITE(?:|_P)\(\s*(?P<class_name>\w+)', re.M)
82
83# Group that matches java/kt method.
84_JAVA_METHODS_RE = r'.*\s+(fun|void)\s+(?P<method>\w+)\('
85# Parse package name from the package declaration line of a java or
86# a kotlin file.
87# Group matches "foo.bar" of line "package foo.bar;" or "package foo.bar"
88_PACKAGE_RE = re.compile(r'\s*package\s+(?P<package>[^(;|\s)]+)\s*', re.I)
89# Matches install paths in module_info to install location(host or device).
90_HOST_PATH_RE = re.compile(r'.*\/host\/.*', re.I)
91_DEVICE_PATH_RE = re.compile(r'.*\/target\/.*', re.I)
92# RE for checking if parameterized java class.
93_PARAMET_JAVA_CLASS_RE = re.compile(
94    r'^\s*@RunWith\s*\(\s*(Parameterized|TestParameterInjector|'
95    r'JUnitParamsRunner|DataProviderRunner|JukitoRunner|Theories|BedsteadJUnit4'
96    r').class\s*\)', re.I)
97# RE for Java/Kt parent classes:
98# Java:   class A extends B {...}
99# Kotlin: class A : B (...)
100_PARENT_CLS_RE = re.compile(r'.*class\s+\w+\s+(?:extends|:)\s+'
101                            r'(?P<parent>[\w\.]+)\s*(?:\{|\()')
102_CC_GREP_RE = r'^\s*(TYPED_TEST(_P)*|TEST(_F|_P)*)\s*\({1},'
103
104@unique
105class TestReferenceType(Enum):
106    """An Enum class that stores the ways of finding a reference."""
107    # Name of a java/kotlin class, usually file is named the same
108    # (HostTest lives in HostTest.java or HostTest.kt)
109    CLASS = (
110        constants.CLASS_INDEX,
111        r"find {0} -type f| egrep '.*/{1}\.(kt|java)$' || true")
112    # Like CLASS but also contains the package in front like
113    # com.android.tradefed.testtype.HostTest.
114    QUALIFIED_CLASS = (
115        constants.QCLASS_INDEX,
116        r"find {0} -type f | egrep '.*{1}\.(kt|java)$' || true")
117    # Name of a Java package.
118    PACKAGE = (
119        constants.PACKAGE_INDEX,
120        r"find {0} -wholename '*{1}' -type d -print")
121    # XML file name in one of the 4 integration config directories.
122    INTEGRATION = (
123        constants.INT_INDEX,
124        r"find {0} -wholename '*/{1}\.xml' -print")
125    # Name of a cc/cpp class.
126    CC_CLASS = (
127        constants.CC_CLASS_INDEX,
128        (r"find {0} -type f -print | egrep -i '/*test.*\.(cc|cpp)$'"
129         f"| xargs -P0 egrep -sH '{_CC_GREP_RE}' || true"))
130
131    def __init__(self, index_file, find_command):
132        self.index_file = index_file
133        self.find_command = find_command
134
135# XML parsing related constants.
136_COMPATIBILITY_PACKAGE_PREFIX = "com.android.compatibility"
137_XML_PUSH_DELIM = '->'
138_APK_SUFFIX = '.apk'
139DALVIK_TEST_RUNNER_CLASS = 'com.android.compatibility.testtype.DalvikTest'
140LIBCORE_TEST_RUNNER_CLASS = 'com.android.compatibility.testtype.LibcoreTest'
141DALVIK_TESTRUNNER_JAR_CLASSES = [DALVIK_TEST_RUNNER_CLASS,
142                                 LIBCORE_TEST_RUNNER_CLASS]
143DALVIK_DEVICE_RUNNER_JAR = 'cts-dalvik-device-test-runner'
144DALVIK_HOST_RUNNER_JAR = 'cts-dalvik-host-test-runner'
145DALVIK_TEST_DEPS = {DALVIK_DEVICE_RUNNER_JAR,
146                    DALVIK_HOST_RUNNER_JAR,
147                    constants.CTS_JAR}
148# Setup script for device perf tests.
149_PERF_SETUP_LABEL = 'perf-setup.sh'
150_PERF_SETUP_TARGET = 'perf-setup'
151
152# XML tags.
153_XML_NAME = 'name'
154_XML_VALUE = 'value'
155
156# VTS xml parsing constants.
157_VTS_TEST_MODULE = 'test-module-name'
158_VTS_MODULE = 'module-name'
159_VTS_BINARY_SRC = 'binary-test-source'
160_VTS_PUSH_GROUP = 'push-group'
161_VTS_PUSH = 'push'
162_VTS_BINARY_SRC_DELIM = '::'
163_VTS_PUSH_DIR = os.path.join(os.environ.get(constants.ANDROID_BUILD_TOP, ''),
164                             'test', 'vts', 'tools', 'vts-tradefed', 'res',
165                             'push_groups')
166_VTS_PUSH_SUFFIX = '.push'
167_VTS_BITNESS = 'append-bitness'
168_VTS_BITNESS_TRUE = 'true'
169_VTS_BITNESS_32 = '32'
170_VTS_BITNESS_64 = '64'
171_VTS_TEST_FILE = 'test-file-name'
172_VTS_APK = 'apk'
173# Matches 'DATA/target' in '_32bit::DATA/target'
174_VTS_BINARY_SRC_DELIM_RE = re.compile(r'.*::(?P<target>.*)$')
175_VTS_OUT_DATA_APP_PATH = 'DATA/app'
176
177def split_methods(user_input):
178    """Split user input string into test reference and list of methods.
179
180    Args:
181        user_input: A string of the user's input.
182                    Examples:
183                        class_name
184                        class_name#method1,method2
185                        path
186                        path#method1,method2
187    Returns:
188        A tuple. First element is String of test ref and second element is
189        a set of method name strings or empty list if no methods included.
190    Exception:
191        atest_error.TooManyMethodsError raised when input string is trying to
192        specify too many methods in a single positional argument.
193
194        Examples of unsupported input strings:
195            module:class#method,class#method
196            class1#method,class2#method
197            path1#method,path2#method
198    """
199    error_msg = (
200        'Too many "{}" characters in user input:\n\t{}\n'
201        'Multiple classes should be separated by space, and methods belong to '
202        'the same class should be separated by comma. Example syntaxes are:\n'
203        '\tclass1 class2#method1 class3#method2,method3\n'
204        '\tclass1#method class2#method')
205    if not '#' in user_input:
206        if ',' in user_input:
207            raise atest_error.MoreThanOneClassError(
208                error_msg.format(',', user_input))
209        return user_input, frozenset()
210    parts = user_input.split('#')
211    if len(parts) > 2:
212        raise atest_error.TooManyMethodsError(
213            error_msg.format('#', user_input))
214    # (b/260183137) Support parsing multiple parameters.
215    parsed_methods = []
216    brackets = ('[', ']')
217    for part in parts[1].split(','):
218        count = {part.count(p) for p in brackets}
219        # If brackets are in pair, the length of count should be 1.
220        if len(count) == 1:
221            parsed_methods.append(part)
222        else:
223            # The front part of the pair, e.g. 'method[1'
224            if re.compile(r'^[a-zA-Z0-9]+\[').match(part):
225                parsed_methods.append(part)
226                continue
227            # The rear part of the pair, e.g. '5]]', accumulate this part to
228            # the last index of parsed_method.
229            parsed_methods[-1] += f',{part}'
230    return parts[0], frozenset(parsed_methods)
231
232
233# pylint: disable=inconsistent-return-statements
234def get_fully_qualified_class_name(test_path):
235    """Parse the fully qualified name from the class java file.
236
237    Args:
238        test_path: A string of absolute path to the java class file.
239
240    Returns:
241        A string of the fully qualified class name.
242
243    Raises:
244        atest_error.MissingPackageName if no class name can be found.
245    """
246    with open(test_path) as class_file:
247        for line in class_file:
248            match = _PACKAGE_RE.match(line)
249            if match:
250                package = match.group('package')
251                cls = os.path.splitext(os.path.split(test_path)[1])[0]
252                return '%s.%s' % (package, cls)
253    raise atest_error.MissingPackageNameError('%s: Test class java file'
254                                              'does not contain a package'
255                                              'name.'% test_path)
256
257
258def has_cc_class(test_path):
259    """Find out if there is any test case in the cc file.
260
261    Args:
262        test_path: A string of absolute path to the cc file.
263
264    Returns:
265        Boolean: has cc class in test_path or not.
266    """
267    with open_cc(test_path) as class_file:
268        content = class_file.read()
269        if re.findall(_CC_CLASS_METHOD_RE, content):
270            return True
271        if re.findall(_CC_PARAM_CLASS_RE, content):
272            return True
273        if re.findall(_TYPE_CC_CLASS_RE, content):
274            return True
275    return False
276
277
278def get_package_name(file_name):
279    """Parse the package name from a java file.
280
281    Args:
282        file_name: A string of the absolute path to the java file.
283
284    Returns:
285        A string of the package name or None
286    """
287    with open(file_name) as data:
288        for line in data:
289            match = _PACKAGE_RE.match(line)
290            if match:
291                return match.group('package')
292
293
294def get_parent_cls_name(file_name):
295    """Parse the parent class name from a java/kt file.
296
297    Args:
298        file_name: A string of the absolute path to the javai/kt file.
299
300    Returns:
301        A string of the parent class name or None
302    """
303    with open(file_name) as data:
304        for line in data:
305            match = _PARENT_CLS_RE.match(line)
306            if match:
307                return match.group('parent')
308
309
310def get_java_parent_paths(test_path):
311    """Find out the paths of parent classes, including itself.
312
313    Args:
314        test_path: A string of absolute path to the test file.
315
316    Returns:
317        A set of test paths.
318    """
319    all_parent_test_paths = set([test_path])
320    parent = get_parent_cls_name(test_path)
321    if not parent:
322        return all_parent_test_paths
323    # Remove <Generics> if any.
324    parent_cls = re.sub(r'\<\w+\>', '', parent)
325    package = get_package_name(test_path)
326    # Use Fully Qualified Class Name for searching precisely.
327    # package org.gnome;
328    # public class Foo extends com.android.Boo -> com.android.Boo
329    # public class Foo extends Boo -> org.gnome.Boo
330    if '.' in parent_cls:
331        parent_fqcn = parent_cls
332    else:
333        parent_fqcn = package + '.' + parent_cls
334    parent_test_paths = run_find_cmd(
335        TestReferenceType.QUALIFIED_CLASS,
336        os.environ.get(constants.ANDROID_BUILD_TOP),
337        parent_fqcn)
338    # Recursively search parent classes until the class is not found.
339    if parent_test_paths:
340        for parent_test_path in parent_test_paths:
341            all_parent_test_paths |= get_java_parent_paths(parent_test_path)
342    return all_parent_test_paths
343
344
345def has_method_in_file(test_path, methods):
346    """Find out if every method can be found in the file.
347
348    Note: This method doesn't handle if method is in comment sections.
349
350    Args:
351        test_path: A string of absolute path to the test file.
352        methods: A set of method names.
353
354    Returns:
355        Boolean: there is at least one method in test_path.
356    """
357    if not os.path.isfile(test_path):
358        return False
359    all_methods = set()
360    if constants.JAVA_EXT_RE.match(test_path):
361        # omit parameterized pattern: method[0]
362        _methods = set(re.sub(r'\[\S+\]', '', x) for x in methods)
363        # Return True when every method is in the same Java file.
364        if _methods.issubset(get_java_methods(test_path)):
365            return True
366        # Otherwise, search itself and all the parent classes respectively
367        # to get all test names.
368        parent_test_paths = get_java_parent_paths(test_path)
369        logging.debug('Will search methods %s in %s\n',
370                      _methods, parent_test_paths)
371        for path in parent_test_paths:
372            all_methods |= get_java_methods(path)
373        if _methods.issubset(all_methods):
374            return True
375        # If cannot find all methods, override the test_path for debugging.
376        test_path = parent_test_paths
377    elif constants.CC_EXT_RE.match(test_path):
378        # omit parameterized pattern: method/argument
379        _methods = set(re.sub(r'\/.*', '', x) for x in methods)
380        class_info = get_cc_class_info(test_path)
381        for info in class_info.values():
382            all_methods |= info.get('methods')
383        if _methods.issubset(all_methods):
384            return True
385    missing_methods = _methods - all_methods
386    logging.debug('Cannot find methods %s in %s',
387        atest_utils.colorize(','.join(missing_methods), constants.RED),
388        test_path)
389    return False
390
391
392def extract_test_path(output, methods=None):
393    """Extract the test path from the output of a unix 'find' command.
394
395    Example of find output for CLASS find cmd:
396    /<some_root>/cts/tests/jank/src/android/jank/cts/ui/CtsDeviceJankUi.java
397
398    Args:
399        output: A string or list output of a unix 'find' command.
400        methods: A set of method names.
401
402    Returns:
403        A list of the test paths or None if output is '' or None.
404    """
405    if not output:
406        return None
407    verified_tests = set()
408    if isinstance(output, str):
409        output = output.splitlines()
410    for test in output:
411        match_obj = constants.CC_OUTPUT_RE.match(test)
412        # Legacy "find" cc output (with TEST_P() syntax):
413        if match_obj:
414            fpath = match_obj.group('file_path')
415            if not methods or match_obj.group('method_name') in methods:
416                verified_tests.add(fpath)
417        # "locate" output path for both java/cc.
418        elif not methods or has_method_in_file(test, methods):
419            verified_tests.add(test)
420    return extract_test_from_tests(sorted(list(verified_tests)))
421
422
423def extract_test_from_tests(tests, default_all=False):
424    """Extract the test path from the tests.
425
426    Return the test to run from tests. If more than one option, prompt the user
427    to select multiple ones. Supporting formats:
428    - An integer. E.g. 0
429    - Comma-separated integers. E.g. 1,3,5
430    - A range of integers denoted by the starting integer separated from
431      the end integer by a dash, '-'. E.g. 1-3
432
433    Args:
434        tests: A string list which contains multiple test paths.
435
436    Returns:
437        A string list of paths.
438    """
439    count = len(tests)
440    if default_all or count <= 1:
441        return tests if count else None
442    mtests = set()
443    try:
444        numbered_list = ['%s: %s' % (i, t) for i, t in enumerate(tests)]
445        numbered_list.append('%s: All' % count)
446        print('Multiple tests found:\n{0}'.format('\n'.join(numbered_list)))
447        test_indices = input("Please enter numbers of test to use. If none of "
448                             "above option matched, keep searching for other "
449                             "possible tests.\n(multiple selection is supported, "
450                             "e.g. '1' or '0,1' or '0-2'): ")
451        for idx in re.sub(r'(\s)', '', test_indices).split(','):
452            indices = idx.split('-')
453            len_indices = len(indices)
454            if len_indices > 0:
455                start_index = min(int(indices[0]), int(indices[len_indices-1]))
456                end_index = max(int(indices[0]), int(indices[len_indices-1]))
457                # One of input is 'All', return all options.
458                if count in (start_index, end_index):
459                    return tests
460                mtests.update(tests[start_index:(end_index+1)])
461    except (ValueError, IndexError, AttributeError, TypeError) as err:
462        logging.debug('%s', err)
463        print('None of above option matched, keep searching for other'
464              ' possible tests...')
465    return list(mtests)
466
467
468def run_find_cmd(ref_type, search_dir, target, methods=None):
469    """Find a path to a target given a search dir and a target name.
470
471    Args:
472        ref_type: An Enum of the reference type.
473        search_dir: A string of the dirpath to search in.
474        target: A string of what you're trying to find.
475        methods: A set of method names.
476
477    Return:
478        A list of the path to the target.
479        If the search_dir is inexistent, None will be returned.
480    """
481    if not os.path.isdir(search_dir):
482        logging.debug('\'%s\' does not exist!', search_dir)
483        return None
484    ref_name = ref_type.name
485    index_file = ref_type.index_file
486    start = time.time()
487    if os.path.isfile(index_file):
488        _dict, out = {}, None
489        with open(index_file, 'rb') as index:
490            try:
491                _dict = pickle.load(index, encoding='utf-8')
492            except (TypeError, IOError, EOFError, pickle.UnpicklingError) as err:
493                logging.debug('Exception raised: %s', err)
494                metrics_utils.handle_exc_and_send_exit_event(
495                    constants.ACCESS_CACHE_FAILURE)
496                os.remove(index_file)
497        if _dict.get(target):
498            out = [path for path in _dict.get(target) if search_dir in path]
499            logging.debug('Found %s in %s', target, out)
500    else:
501        if '.' in target:
502            target = target.replace('.', '/')
503        find_cmd = ref_type.find_command.format(search_dir, target)
504        logging.debug('Executing %s find cmd: %s', ref_name, find_cmd)
505        out = subprocess.check_output(find_cmd, shell=True)
506        if isinstance(out, bytes):
507            out = out.decode()
508        logging.debug('%s find cmd out: %s', ref_name, out)
509    logging.debug('%s find completed in %ss', ref_name, time.time() - start)
510    return extract_test_path(out, methods)
511
512
513def find_class_file(search_dir, class_name, is_native_test=False, methods=None):
514    """Find a path to a class file given a search dir and a class name.
515
516    Args:
517        search_dir: A string of the dirpath to search in.
518        class_name: A string of the class to search for.
519        is_native_test: A boolean variable of whether to search for a native
520        test or not.
521        methods: A set of method names.
522
523    Return:
524        A list of the path to the java/cc file.
525    """
526    if is_native_test:
527        ref_type = TestReferenceType.CC_CLASS
528    elif '.' in class_name:
529        ref_type = TestReferenceType.QUALIFIED_CLASS
530    else:
531        ref_type = TestReferenceType.CLASS
532    return run_find_cmd(ref_type, search_dir, class_name, methods)
533
534
535def is_equal_or_sub_dir(sub_dir, parent_dir):
536    """Return True sub_dir is sub dir or equal to parent_dir.
537
538    Args:
539      sub_dir: A string of the sub directory path.
540      parent_dir: A string of the parent directory path.
541
542    Returns:
543        A boolean of whether both are dirs and sub_dir is sub of parent_dir
544        or is equal to parent_dir.
545    """
546    # avoid symlink issues with real path
547    parent_dir = os.path.realpath(parent_dir)
548    sub_dir = os.path.realpath(sub_dir)
549    if not os.path.isdir(sub_dir) or not os.path.isdir(parent_dir):
550        return False
551    return os.path.commonprefix([sub_dir, parent_dir]) == parent_dir
552
553
554def find_parent_module_dir(root_dir, start_dir, module_info):
555    """From current dir search up file tree until root dir for module dir.
556
557    Args:
558        root_dir: A string  of the dir that is the parent of the start dir.
559        start_dir: A string of the dir to start searching up from.
560        module_info: ModuleInfo object containing module information from the
561                     build system.
562
563    Returns:
564        A string of the module dir relative to root, None if no Module Dir
565        found. There may be multiple testable modules at this level.
566
567    Exceptions:
568        ValueError: Raised if cur_dir not dir or not subdir of root dir.
569    """
570    if not is_equal_or_sub_dir(start_dir, root_dir):
571        raise ValueError('%s not in repo %s' % (start_dir, root_dir))
572    auto_gen_dir = None
573    current_dir = start_dir
574    while current_dir != root_dir:
575        # TODO (b/112904944) - migrate module_finder functions to here and
576        # reuse them.
577        rel_dir = os.path.relpath(current_dir, root_dir)
578        # Check if actual config file here but need to make sure that there
579        # exist module in module-info with the parent dir.
580        if (os.path.isfile(os.path.join(current_dir, constants.MODULE_CONFIG))
581                and module_info.get_module_names(current_dir)):
582            return rel_dir
583        # Check module_info if auto_gen config or robo (non-config) here
584        for mod in module_info.path_to_module_info.get(rel_dir, []):
585            if module_info.is_legacy_robolectric_class(mod):
586                return rel_dir
587            for test_config in mod.get(constants.MODULE_TEST_CONFIG, []):
588                # If the test config doesn's exist until it was auto-generated
589                # in the build time(under <android_root>/out), atest still
590                # recognizes it testable.
591                if test_config:
592                    return rel_dir
593            if mod.get('auto_test_config'):
594                auto_gen_dir = rel_dir
595                # Don't return for auto_gen, keep checking for real config,
596                # because common in cts for class in apk that's in hostside
597                # test setup.
598        current_dir = os.path.dirname(current_dir)
599    return auto_gen_dir
600
601
602def get_targets_from_xml(xml_file, module_info):
603    """Retrieve build targets from the given xml.
604
605    Just a helper func on top of get_targets_from_xml_root.
606
607    Args:
608        xml_file: abs path to xml file.
609        module_info: ModuleInfo class used to verify targets are valid modules.
610
611    Returns:
612        A set of build targets based on the signals found in the xml file.
613    """
614    if not os.path.isfile(xml_file):
615        return set()
616    xml_root = ET.parse(xml_file).getroot()
617    return get_targets_from_xml_root(xml_root, module_info)
618
619
620def _get_apk_target(apk_target):
621    """Return the sanitized apk_target string from the xml.
622
623    The apk_target string can be of 2 forms:
624      - apk_target.apk
625      - apk_target.apk->/path/to/install/apk_target.apk
626
627    We want to return apk_target in both cases.
628
629    Args:
630        apk_target: String of target name to clean.
631
632    Returns:
633        String of apk_target to build.
634    """
635    apk = apk_target.split(_XML_PUSH_DELIM, 1)[0].strip()
636    return apk[:-len(_APK_SUFFIX)]
637
638
639def _is_apk_target(name, value):
640    """Return True if XML option is an apk target.
641
642    We have some scenarios where an XML option can be an apk target:
643      - value is an apk file.
644      - name is a 'push' option where value holds the apk_file + other stuff.
645
646    Args:
647        name: String name of XML option.
648        value: String value of the XML option.
649
650    Returns:
651        True if it's an apk target we should build, False otherwise.
652    """
653    if _APK_RE.match(value):
654        return True
655    if name == 'push' and value.endswith(_APK_SUFFIX):
656        return True
657    return False
658
659
660def get_targets_from_xml_root(xml_root, module_info):
661    """Retrieve build targets from the given xml root.
662
663    We're going to pull the following bits of info:
664      - Parse any .apk files listed in the config file.
665      - Parse option value for "test-module-name" (for vts10 tests).
666      - Look for the perf script.
667
668    Args:
669        module_info: ModuleInfo class used to verify targets are valid modules.
670        xml_root: ElementTree xml_root for us to look through.
671
672    Returns:
673        A set of build targets based on the signals found in the xml file.
674    """
675    targets = set()
676    option_tags = xml_root.findall('.//option')
677    for tag in option_tags:
678        target_to_add = None
679        name = tag.attrib[_XML_NAME].strip()
680        value = tag.attrib[_XML_VALUE].strip()
681        if _is_apk_target(name, value):
682            target_to_add = _get_apk_target(value)
683        elif _PERF_SETUP_LABEL in value:
684            target_to_add = _PERF_SETUP_TARGET
685
686        # Let's make sure we can actually build the target.
687        if target_to_add and module_info.is_module(target_to_add):
688            targets.add(target_to_add)
689        elif target_to_add:
690            logging.debug('Build target (%s) not present in module info, '
691                          'skipping build', target_to_add)
692
693    # TODO (b/70813166): Remove this lookup once all runtime dependencies
694    # can be listed as a build dependencies or are in the base test harness.
695    nodes_with_class = xml_root.findall(".//*[@class]")
696    for class_attr in nodes_with_class:
697        fqcn = class_attr.attrib['class'].strip()
698        if fqcn.startswith(_COMPATIBILITY_PACKAGE_PREFIX):
699            targets.add(constants.CTS_JAR)
700        if fqcn in DALVIK_TESTRUNNER_JAR_CLASSES:
701            for dalvik_dep in DALVIK_TEST_DEPS:
702                if module_info.is_module(dalvik_dep):
703                    targets.add(dalvik_dep)
704    logging.debug('Targets found in config file: %s', targets)
705    return targets
706
707
708def _get_vts_push_group_targets(push_file, rel_out_dir):
709    """Retrieve vts10 push group build targets.
710
711    A push group file is a file that list out test dependencies and other push
712    group files. Go through the push file and gather all the test deps we need.
713
714    Args:
715        push_file: Name of the push file in the VTS
716        rel_out_dir: Abs path to the out dir to help create vts10 build targets.
717
718    Returns:
719        Set of string which represent build targets.
720    """
721    targets = set()
722    full_push_file_path = os.path.join(_VTS_PUSH_DIR, push_file)
723    # pylint: disable=invalid-name
724    with open(full_push_file_path) as f:
725        for line in f:
726            target = line.strip()
727            # Skip empty lines.
728            if not target:
729                continue
730
731            # This is a push file, get the targets from it.
732            if target.endswith(_VTS_PUSH_SUFFIX):
733                targets |= _get_vts_push_group_targets(line.strip(),
734                                                       rel_out_dir)
735                continue
736            sanitized_target = target.split(_XML_PUSH_DELIM, 1)[0].strip()
737            targets.add(os.path.join(rel_out_dir, sanitized_target))
738    return targets
739
740
741def _specified_bitness(xml_root):
742    """Check if the xml file contains the option append-bitness.
743
744    Args:
745        xml_root: abs path to xml file.
746
747    Returns:
748        True if xml specifies to append-bitness, False otherwise.
749    """
750    option_tags = xml_root.findall('.//option')
751    for tag in option_tags:
752        value = tag.attrib[_XML_VALUE].strip()
753        name = tag.attrib[_XML_NAME].strip()
754        if name == _VTS_BITNESS and value == _VTS_BITNESS_TRUE:
755            return True
756    return False
757
758
759def _get_vts_binary_src_target(value, rel_out_dir):
760    """Parse out the vts10 binary src target.
761
762    The value can be in the following pattern:
763      - {_32bit,_64bit,_IPC32_32bit}::DATA/target (DATA/target)
764      - DATA/target->/data/target (DATA/target)
765      - out/host/linx-x86/bin/VtsSecuritySelinuxPolicyHostTest (the string as
766        is)
767
768    Args:
769        value: String of the XML option value to parse.
770        rel_out_dir: String path of out dir to prepend to target when required.
771
772    Returns:
773        String of the target to build.
774    """
775    # We'll assume right off the bat we can use the value as is and modify it if
776    # necessary, e.g. out/host/linux-x86/bin...
777    target = value
778    # _32bit::DATA/target
779    match = _VTS_BINARY_SRC_DELIM_RE.match(value)
780    if match:
781        target = os.path.join(rel_out_dir, match.group('target'))
782    # DATA/target->/data/target
783    elif _XML_PUSH_DELIM in value:
784        target = value.split(_XML_PUSH_DELIM, 1)[0].strip()
785        target = os.path.join(rel_out_dir, target)
786    return target
787
788
789def get_plans_from_vts_xml(xml_file):
790    """Get configs which are included by xml_file.
791
792    We're looking for option(include) to get all dependency plan configs.
793
794    Args:
795        xml_file: Absolute path to xml file.
796
797    Returns:
798        A set of plan config paths which are depended by xml_file.
799    """
800    if not os.path.exists(xml_file):
801        raise atest_error.XmlNotExistError('%s: The xml file does'
802                                           'not exist' % xml_file)
803    plans = set()
804    xml_root = ET.parse(xml_file).getroot()
805    plans.add(xml_file)
806    option_tags = xml_root.findall('.//include')
807    if not option_tags:
808        return plans
809    # Currently, all vts10 xmls live in the same dir :
810    # https://android.googlesource.com/platform/test/vts/+/master/tools/vts-tradefed/res/config/
811    # If the vts10 plans start using folders to organize the plans, the logic here
812    # should be changed.
813    xml_dir = os.path.dirname(xml_file)
814    for tag in option_tags:
815        name = tag.attrib[_XML_NAME].strip()
816        plans |= get_plans_from_vts_xml(os.path.join(xml_dir, name + ".xml"))
817    return plans
818
819
820def get_targets_from_vts_xml(xml_file, rel_out_dir, module_info):
821    """Parse a vts10 xml for test dependencies we need to build.
822
823    We have a separate vts10 parsing function because we make a big assumption
824    on the targets (the way they're formatted and what they represent) and we
825    also create these build targets in a very special manner as well.
826    The 6 options we're looking for are:
827      - binary-test-source
828      - push-group
829      - push
830      - test-module-name
831      - test-file-name
832      - apk
833
834    Args:
835        module_info: ModuleInfo class used to verify targets are valid modules.
836        rel_out_dir: Abs path to the out dir to help create vts10 build targets.
837        xml_file: abs path to xml file.
838
839    Returns:
840        A set of build targets based on the signals found in the xml file.
841    """
842    xml_root = ET.parse(xml_file).getroot()
843    targets = set()
844    option_tags = xml_root.findall('.//option')
845    for tag in option_tags:
846        value = tag.attrib[_XML_VALUE].strip()
847        name = tag.attrib[_XML_NAME].strip()
848        if name in [_VTS_TEST_MODULE, _VTS_MODULE]:
849            if module_info.is_module(value):
850                targets.add(value)
851            else:
852                logging.debug('vts10 test module (%s) not present in module '
853                              'info, skipping build', value)
854        elif name == _VTS_BINARY_SRC:
855            targets.add(_get_vts_binary_src_target(value, rel_out_dir))
856        elif name == _VTS_PUSH_GROUP:
857            # Look up the push file and parse out build artifacts (as well as
858            # other push group files to parse).
859            targets |= _get_vts_push_group_targets(value, rel_out_dir)
860        elif name == _VTS_PUSH:
861            # Parse out the build artifact directly.
862            push_target = value.split(_XML_PUSH_DELIM, 1)[0].strip()
863            # If the config specified append-bitness, append the bits suffixes
864            # to the target.
865            if _specified_bitness(xml_root):
866                targets.add(os.path.join(
867                    rel_out_dir, push_target + _VTS_BITNESS_32))
868                targets.add(os.path.join(
869                    rel_out_dir, push_target + _VTS_BITNESS_64))
870            else:
871                targets.add(os.path.join(rel_out_dir, push_target))
872        elif name == _VTS_TEST_FILE:
873            # The _VTS_TEST_FILE values can be set in 2 possible ways:
874            #   1. test_file.apk
875            #   2. DATA/app/test_file/test_file.apk
876            # We'll assume that test_file.apk (#1) is in an expected path (but
877            # that is not true, see b/76158619) and create the full path for it
878            # and then append the _VTS_TEST_FILE value to targets to build.
879            target = os.path.join(rel_out_dir, value)
880            # If value is just an APK, specify the path that we expect it to be in
881            # e.g. out/host/linux-x86/vts10/android-vts10/testcases/DATA/app/test_file/test_file.apk
882            head, _ = os.path.split(value)
883            if not head:
884                target = os.path.join(rel_out_dir, _VTS_OUT_DATA_APP_PATH,
885                                      _get_apk_target(value), value)
886            targets.add(target)
887        elif name == _VTS_APK:
888            targets.add(os.path.join(rel_out_dir, value))
889    logging.debug('Targets found in config file: %s', targets)
890    return targets
891
892
893def get_dir_path_and_filename(path):
894    """Return tuple of dir and file name from given path.
895
896    Args:
897        path: String of path to break up.
898
899    Returns:
900        Tuple of (dir, file) paths.
901    """
902    if os.path.isfile(path):
903        dir_path, file_path = os.path.split(path)
904    else:
905        dir_path, file_path = path, None
906    return dir_path, file_path
907
908
909def get_cc_filter(class_info, class_name, methods):
910    """Get the cc filter.
911
912    Args:
913        class_info: a dict of class info.
914        class_name: class name of the cc test.
915        methods: a list of method names.
916
917    Returns:
918        A formatted string for cc filter.
919        For a Type/Typed-parameterized test, it will be:
920          "class1/*.method1:class1/*.method2" or "class1/*.*"
921        For a parameterized test, it will be:
922          "*/class1.*" or "prefix/class1.*"
923        For the rest the pattern will be:
924          "class1.method1:class1.method2" or "class1.*"
925    """
926    #Strip prefix from class_name.
927    _class_name = class_name
928    if '/' in class_name:
929        _class_name = str(class_name).split('/')[-1]
930    type_str = get_cc_class_type(class_info, _class_name)
931    logging.debug('%s is a "%s".', _class_name, type_str)
932    # When found parameterized tests, recompose the class name
933    # in */$(ClassName) if the prefix is not given.
934    if type_str in (constants.GTEST_TYPED_PARAM, constants.GTEST_PARAM):
935        if not '/' in class_name:
936            class_name = '*/%s' % class_name
937    if type_str in (constants.GTEST_TYPED, constants.GTEST_TYPED_PARAM):
938        if methods:
939            sorted_methods = sorted(list(methods))
940            return ":".join(["%s/*.%s" % (class_name, x) for x in sorted_methods])
941        return "%s/*.*" % class_name
942    if methods:
943        sorted_methods = sorted(list(methods))
944        return ":".join(["%s.%s" % (class_name, x) for x in sorted_methods])
945    return "%s.*" % class_name
946
947
948def search_integration_dirs(name, int_dirs):
949    """Search integration dirs for name and return full path.
950
951    Args:
952        name: A string of plan name needed to be found.
953        int_dirs: A list of path needed to be searched.
954
955    Returns:
956        A list of the test path.
957        Ask user to select if multiple tests are found.
958        None if no matched test found.
959    """
960    root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
961    test_files = []
962    for integration_dir in int_dirs:
963        abs_path = os.path.join(root_dir, integration_dir)
964        test_paths = run_find_cmd(TestReferenceType.INTEGRATION, abs_path,
965                                  name)
966        if test_paths:
967            test_files.extend(test_paths)
968    return extract_test_from_tests(test_files)
969
970
971def get_int_dir_from_path(path, int_dirs):
972    """Search integration dirs for the given path and return path of dir.
973
974    Args:
975        path: A string of path needed to be found.
976        int_dirs: A list of path needed to be searched.
977
978    Returns:
979        A string of the test dir. None if no matched path found.
980    """
981    root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
982    if not os.path.exists(path):
983        return None
984    dir_path, file_name = get_dir_path_and_filename(path)
985    int_dir = None
986    for possible_dir in int_dirs:
987        abs_int_dir = os.path.join(root_dir, possible_dir)
988        if is_equal_or_sub_dir(dir_path, abs_int_dir):
989            int_dir = abs_int_dir
990            break
991    if not file_name:
992        logging.debug('Found dir (%s) matching input (%s).'
993                      ' Referencing an entire Integration/Suite dir'
994                      ' is not supported. If you are trying to reference'
995                      ' a test by its path, please input the path to'
996                      ' the integration/suite config file itself.',
997                      int_dir, path)
998        return None
999    return int_dir
1000
1001
1002def get_install_locations(installed_paths):
1003    """Get install locations from installed paths.
1004
1005    Args:
1006        installed_paths: List of installed_paths from module_info.
1007
1008    Returns:
1009        Set of install locations from module_info installed_paths. e.g.
1010        set(['host', 'device'])
1011    """
1012    install_locations = set()
1013    for path in installed_paths:
1014        if _HOST_PATH_RE.match(path):
1015            install_locations.add(constants.DEVICELESS_TEST)
1016        elif _DEVICE_PATH_RE.match(path):
1017            install_locations.add(constants.DEVICE_TEST)
1018    return install_locations
1019
1020
1021def get_levenshtein_distance(test_name, module_name,
1022                             dir_costs=constants.COST_TYPO):
1023    """Return an edit distance between test_name and module_name.
1024
1025    Levenshtein Distance has 3 actions: delete, insert and replace.
1026    dis_costs makes each action weigh differently.
1027
1028    Args:
1029        test_name: A keyword from the users.
1030        module_name: A testable module name.
1031        dir_costs: A tuple which contains 3 integer, where dir represents
1032                   Deletion, Insertion and Replacement respectively.
1033                   For guessing typos: (1, 1, 1) gives the best result.
1034                   For searching keywords, (8, 1, 5) gives the best result.
1035
1036    Returns:
1037        An edit distance integer between test_name and module_name.
1038    """
1039    rows = len(test_name) + 1
1040    cols = len(module_name) + 1
1041    deletion, insertion, replacement = dir_costs
1042
1043    # Creating a Dynamic Programming Matrix and weighting accordingly.
1044    dp_matrix = [[0 for _ in range(cols)] for _ in range(rows)]
1045    # Weigh rows/deletion
1046    for row in range(1, rows):
1047        dp_matrix[row][0] = row * deletion
1048    # Weigh cols/insertion
1049    for col in range(1, cols):
1050        dp_matrix[0][col] = col * insertion
1051    # The core logic of LD
1052    for col in range(1, cols):
1053        for row in range(1, rows):
1054            if test_name[row-1] == module_name[col-1]:
1055                cost = 0
1056            else:
1057                cost = replacement
1058            dp_matrix[row][col] = min(dp_matrix[row-1][col] + deletion,
1059                                      dp_matrix[row][col-1] + insertion,
1060                                      dp_matrix[row-1][col-1] + cost)
1061
1062    return dp_matrix[row][col]
1063
1064
1065def is_test_from_kernel_xml(xml_file, test_name):
1066    """Check if test defined in xml_file.
1067
1068    A kernel test can be defined like:
1069    <option name="test-command-line" key="test_class_1" value="command 1" />
1070    where key is the name of test class and method of the runner. This method
1071    returns True if the test_name was defined in the given xml_file.
1072
1073    Args:
1074        xml_file: Absolute path to xml file.
1075        test_name: test_name want to find.
1076
1077    Returns:
1078        True if test_name in xml_file, False otherwise.
1079    """
1080    if not os.path.exists(xml_file):
1081        return False
1082    xml_root = ET.parse(xml_file).getroot()
1083    option_tags = xml_root.findall('.//option')
1084    for option_tag in option_tags:
1085        if option_tag.attrib['name'] == 'test-command-line':
1086            if option_tag.attrib['key'] == test_name:
1087                return True
1088    return False
1089
1090
1091def is_parameterized_java_class(test_path):
1092    """Find out if input test path is a parameterized java class.
1093
1094    Args:
1095        test_path: A string of absolute path to the java file.
1096
1097    Returns:
1098        Boolean: Is parameterized class or not.
1099    """
1100    with open(test_path) as class_file:
1101        for line in class_file:
1102            match = _PARAMET_JAVA_CLASS_RE.match(line)
1103            if match:
1104                return True
1105    return False
1106
1107
1108def get_java_methods(test_path):
1109    """Find out the java test class of input test_path.
1110
1111    Args:
1112        test_path: A string of absolute path to the java file.
1113
1114    Returns:
1115        A set of methods.
1116    """
1117    logging.debug('Probing %s:', test_path)
1118    with open(test_path) as class_file:
1119        content = class_file.read()
1120    matches = re.findall(_JAVA_METHODS_RE, content)
1121    if matches:
1122        methods = {match[1] for match in matches}
1123        logging.debug('Available methods: %s\n', methods)
1124        return methods
1125    return set()
1126
1127
1128@contextmanager
1129def open_cc(filename: str):
1130    """Open a cc/cpp file with comments trimmed."""
1131    target_cc = filename
1132    if shutil.which('gcc'):
1133        tmp = tempfile.NamedTemporaryFile()
1134        cmd = (f'gcc -fpreprocessed -dD -E {filename} > {tmp.name}')
1135        strip_proc = subprocess.run(cmd, shell=True, check=True)
1136        if strip_proc.returncode == ExitCode.SUCCESS:
1137            target_cc = tmp.name
1138        else:
1139            logging.debug('Failed to strip comments in %s. Parsing '
1140                          'class/method name may not be accurate.',
1141                          target_cc)
1142    else:
1143        logging.debug('Cannot find "gcc" and unable to trim comments.')
1144    try:
1145        cc_obj = open(target_cc, 'r')
1146        yield cc_obj
1147    finally:
1148        cc_obj.close()
1149
1150
1151# pylint: disable=too-many-branches
1152def get_cc_class_info(test_path):
1153    """Get the class info of the given cc input test_path.
1154
1155    The class info dict will be like:
1156        {'classA': {
1157            'methods': {'m1', 'm2'}, 'prefixes': {'pfx1'}, 'typed': True},
1158         'classB': {
1159            'methods': {'m3', 'm4'}, 'prefixes': set(), 'typed': False},
1160         'classC': {
1161            'methods': {'m5', 'm6'}, 'prefixes': set(), 'typed': True},
1162         'classD': {
1163            'methods': {'m7', 'm8'}, 'prefixes': {'pfx3'}, 'typed': False}}
1164    According to the class info, we can tell that:
1165        classA is a typed-parameterized test. (TYPED_TEST_SUITE_P)
1166        classB is a regular gtest.            (TEST_F|TEST)
1167        classC is a typed test.               (TYPED_TEST_SUITE)
1168        classD is a value-parameterized test. (TEST_P)
1169
1170    Args:
1171        test_path: A string of absolute path to the cc file.
1172
1173    Returns:
1174        A dict of class info.
1175    """
1176    logging.debug('Parsing: %s', test_path)
1177    with open_cc(test_path) as class_file:
1178        content = class_file.read()
1179        # ('TYPED_TEST', 'PrimeTableTest', 'ReturnsTrueForPrimes')
1180        method_matches = re.findall(_CC_CLASS_METHOD_RE, content)
1181        # ('OnTheFlyAndPreCalculated', 'PrimeTableTest2')
1182        prefix_matches = re.findall(_CC_PARAM_CLASS_RE, content)
1183        # 'PrimeTableTest'
1184        typed_matches = re.findall(_TYPE_CC_CLASS_RE, content)
1185
1186    classes = {cls[1] for cls in method_matches}
1187    class_info = {}
1188    test_not_found = False
1189    for cls in classes:
1190        class_info.setdefault(cls, {'methods': set(),
1191                                    'prefixes': set(),
1192                                    'typed': False})
1193    logging.debug('Probing TestCase.TestName pattern:')
1194    for match in method_matches:
1195        if class_info.get(match[1]):
1196            logging.debug('  Found %s.%s', match[1], match[2])
1197            class_info[match[1]]['methods'].add(match[2])
1198        else:
1199            test_not_found = True
1200    # Parameterized test.
1201    logging.debug('Probing InstantiationName/TestCase pattern:')
1202    for match in prefix_matches:
1203        if class_info.get(match[1]):
1204            logging.debug('  Found %s/%s', match[0], match[1])
1205            class_info[match[1]]['prefixes'].add(match[0])
1206        else:
1207            test_not_found = True
1208    # Typed test
1209    logging.debug('Probing typed test names:')
1210    for match in typed_matches:
1211        if class_info.get(match):
1212            logging.debug('  Found %s', match)
1213            class_info[match]['typed'] = True
1214        else:
1215            test_not_found = True
1216    if test_not_found:
1217        metrics.LocalDetectEvent(
1218            detect_type=DetectType.NATIVE_TEST_NOT_FOUND,
1219            result=DetectType.NATIVE_TEST_NOT_FOUND)
1220    return class_info
1221
1222def get_cc_class_type(class_info, classname):
1223    """Tell the type of the given class.
1224
1225    Args:
1226        class_info: A dict of class info.
1227        classname: A string of class name.
1228
1229    Returns:
1230        String of the gtest type to prompt. The output will be one of:
1231        1. 'regular test'             (GTEST_REGULAR)
1232        2. 'typed test'               (GTEST_TYPED)
1233        3. 'value-parameterized test' (GTEST_PARAM)
1234        4. 'typed-parameterized test' (GTEST_TYPED_PARAM)
1235    """
1236    if class_info.get(classname).get('prefixes'):
1237        if class_info.get(classname).get('typed'):
1238            return constants.GTEST_TYPED_PARAM
1239        return constants.GTEST_PARAM
1240    if class_info.get(classname).get('typed'):
1241        return constants.GTEST_TYPED
1242    return constants.GTEST_REGULAR
1243
1244def find_host_unit_tests(module_info, path):
1245    """Find host unit tests for the input path.
1246
1247    Args:
1248        module_info: ModuleInfo obj.
1249        path: A string of the relative path from $ANDROID_BUILD_TOP that we want
1250              to search.
1251
1252    Returns:
1253        A list that includes the module name of host unit tests, otherwise an empty
1254        list.
1255    """
1256    logging.debug('finding host unit tests under %s', path)
1257    host_unit_test_names = module_info.get_all_host_unit_tests()
1258    logging.debug('All the host unit tests: %s', host_unit_test_names)
1259
1260    # Return all tests if the path relative to ${ANDROID_BUILD_TOP} is '.'.
1261    if path == '.':
1262        return host_unit_test_names
1263
1264    tests = []
1265    for name in host_unit_test_names:
1266        for test_path in module_info.get_paths(name):
1267            if test_path.find(path) == 0:
1268                tests.append(name)
1269    return tests
1270
1271def get_annotated_methods(annotation, file_path):
1272    """Find all the methods annotated by the input annotation in the file_path.
1273
1274    Args:
1275        annotation: A string of the annotation class.
1276        file_path: A string of the file path.
1277
1278    Returns:
1279        A set of all the methods annotated.
1280    """
1281    methods = set()
1282    annotation_name = '@' + str(annotation).split('.')[-1]
1283    with open(file_path) as class_file:
1284        enter_annotation_block = False
1285        for line in class_file:
1286            if str(line).strip().startswith(annotation_name):
1287                enter_annotation_block = True
1288                continue
1289            if enter_annotation_block:
1290                matches = re.findall(_JAVA_METHODS_RE, line)
1291                if matches:
1292                    methods.update({match[1] for match in matches})
1293                    enter_annotation_block = False
1294                    continue
1295    return methods
1296
1297def get_test_config_and_srcs(test_info, module_info):
1298    """Get the test config path for the input test_info.
1299
1300    The search rule will be:
1301    Check if test name in test_info could be found in module_info
1302      1. AndroidTest.xml under module path if no test config be set.
1303      2. The first test config defined in Android.bp if test config be set.
1304    If test name could not found matched module in module_info, search all the
1305    test config name if match.
1306
1307    Args:
1308        test_info: TestInfo obj.
1309        module_info: ModuleInfo obj.
1310
1311    Returns:
1312        A string of the config path and list of srcs, None if test config not
1313        exist.
1314    """
1315    android_root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
1316    test_name = test_info.test_name
1317    mod_info = module_info.get_module_info(test_name)
1318    if mod_info:
1319        test_configs = mod_info.get(constants.MODULE_TEST_CONFIG, [])
1320        if len(test_configs) == 0:
1321            # Check for AndroidTest.xml at the module path.
1322            for path in mod_info.get(constants.MODULE_PATH, []):
1323                config_path = os.path.join(
1324                    android_root_dir, path, constants.MODULE_CONFIG)
1325                if os.path.isfile(config_path):
1326                    return config_path, mod_info.get(constants.MODULE_SRCS, [])
1327        if len(test_configs) >= 1:
1328            test_config = test_configs[0]
1329            config_path = os.path.join(android_root_dir, test_config)
1330            if os.path.isfile(config_path):
1331                return config_path, mod_info.get(constants.MODULE_SRCS, [])
1332    else:
1333        for _, info in module_info.name_to_module_info.items():
1334            test_configs = info.get(constants.MODULE_TEST_CONFIG, [])
1335            for test_config in test_configs:
1336                config_path = os.path.join(android_root_dir, test_config)
1337                config_name = os.path.splitext(os.path.basename(config_path))[0]
1338                if config_name == test_name and os.path.isfile(config_path):
1339                    return config_path, info.get(constants.MODULE_SRCS, [])
1340    return None, None
1341
1342
1343def need_aggregate_metrics_result(test_xml: str) -> bool:
1344    """Check if input test config need aggregate metrics.
1345
1346    If the input test define metrics_collector, which means there's a need for
1347    atest to have the aggregate metrics result.
1348
1349    Args:
1350        test_xml: A string of the path for the test xml.
1351
1352    Returns:
1353        True if input test need to enable aggregate metrics result.
1354    """
1355    # Due to (b/211640060) it may replace .xml with .config in the xml as
1356    # workaround.
1357    if not Path(test_xml).is_file():
1358        if Path(test_xml).suffix == '.config':
1359            test_xml = test_xml.rsplit('.', 1)[0] + '.xml'
1360
1361    if Path(test_xml).is_file():
1362        xml_root = ET.parse(test_xml).getroot()
1363        if xml_root.findall('.//metrics_collector'):
1364            return True
1365        # Recursively check included configs in the same git repository.
1366        git_dir = get_git_path(test_xml)
1367        include_configs = xml_root.findall('.//include')
1368        for include_config in include_configs:
1369            name = include_config.attrib[_XML_NAME].strip()
1370            # Get the absolute path for the included configs.
1371            include_paths = search_integration_dirs(
1372                os.path.splitext(name)[0], [git_dir])
1373            for include_path in include_paths:
1374                if need_aggregate_metrics_result(include_path):
1375                    return True
1376    return False
1377
1378
1379def get_git_path(file_path: str) -> str:
1380    """Get the path of the git repository for the input file.
1381
1382    Args:
1383        file_path: A string of the path to find the git path it belongs.
1384
1385    Returns:
1386        The path of the git repository for the input file, return the path of
1387        $ANDROID_BUILD_TOP if nothing find.
1388    """
1389    build_top = os.environ.get(constants.ANDROID_BUILD_TOP)
1390    parent = Path(file_path).absolute().parent
1391    while not parent.samefile('/') and not parent.samefile(build_top):
1392        if parent.joinpath('.git').is_dir():
1393            return parent.absolute()
1394        parent = parent.parent
1395    return build_top
1396
1397
1398def parse_test_reference(test_ref: str) -> Dict[str, str]:
1399    """Parse module, class/pkg, and method name from the given test reference.
1400
1401    The result will be a none empty dictionary only if input test reference
1402    match $module:$pkg_class or $module:$pkg_class:$method.
1403
1404    Args:
1405        test_ref: A string of the input test reference from command line.
1406
1407    Returns:
1408        Dict includes module_name, pkg_class_name and method_name.
1409    """
1410    ref_match = re.match(
1411        r'^(?P<module_name>[^:#]+):(?P<pkg_class_name>[^#]+)'
1412        r'#?(?P<method_name>.*)$', test_ref)
1413
1414    return ref_match.groupdict(default=dict()) if ref_match else dict()
1415