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