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