You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
985 lines
36 KiB
985 lines
36 KiB
# Copyright 2018, The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""
|
|
Utils for finder classes.
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
import logging
|
|
import multiprocessing
|
|
import os
|
|
import pickle
|
|
import re
|
|
import subprocess
|
|
import time
|
|
import xml.etree.ElementTree as ET
|
|
|
|
# pylint: disable=import-error
|
|
import atest_decorator
|
|
import atest_error
|
|
import atest_enum
|
|
import constants
|
|
|
|
from metrics import metrics_utils
|
|
|
|
# Helps find apk files listed in a test config (AndroidTest.xml) file.
|
|
# Matches "filename.apk" in <option name="foo", value="filename.apk" />
|
|
# We want to make sure we don't grab apks with paths in their name since we
|
|
# assume the apk name is the build target.
|
|
_APK_RE = re.compile(r'^[^/]+\.apk$', re.I)
|
|
# RE for checking if TEST or TEST_F is in a cc file or not.
|
|
_CC_CLASS_RE = re.compile(r'^[ ]*TEST(_F|_P)?[ ]*\(', re.I)
|
|
# RE for checking if there exists one of the methods in java file.
|
|
_JAVA_METHODS_PATTERN = r'.*[ ]+({0})\(.*'
|
|
# RE for checking if there exists one of the methods in cc file.
|
|
_CC_METHODS_PATTERN = r'^[ ]*TEST(_F|_P)?[ ]*\(.*,[ ]*({0})\).*'
|
|
# Parse package name from the package declaration line of a java or a kotlin file.
|
|
# Group matches "foo.bar" of line "package foo.bar;" or "package foo.bar"
|
|
_PACKAGE_RE = re.compile(r'\s*package\s+(?P<package>[^(;|\s)]+)\s*', re.I)
|
|
# Matches install paths in module_info to install location(host or device).
|
|
_HOST_PATH_RE = re.compile(r'.*\/host\/.*', re.I)
|
|
_DEVICE_PATH_RE = re.compile(r'.*\/target\/.*', re.I)
|
|
|
|
# Explanation of FIND_REFERENCE_TYPEs:
|
|
# ----------------------------------
|
|
# 0. CLASS: Name of a java/kotlin class, usually file is named the same
|
|
# (HostTest lives in HostTest.java or HostTest.kt)
|
|
# 1. QUALIFIED_CLASS: Like CLASS but also contains the package in front like
|
|
# com.android.tradefed.testtype.HostTest.
|
|
# 2. PACKAGE: Name of a java package.
|
|
# 3. INTEGRATION: XML file name in one of the 4 integration config directories.
|
|
# 4. CC_CLASS: Name of a cc class.
|
|
|
|
FIND_REFERENCE_TYPE = atest_enum.AtestEnum(['CLASS', 'QUALIFIED_CLASS',
|
|
'PACKAGE', 'INTEGRATION', 'CC_CLASS'])
|
|
# Get cpu count.
|
|
_CPU_COUNT = 0 if os.uname()[0] == 'Linux' else multiprocessing.cpu_count()
|
|
|
|
# Unix find commands for searching for test files based on test type input.
|
|
# Note: Find (unlike grep) exits with status 0 if nothing found.
|
|
FIND_CMDS = {
|
|
FIND_REFERENCE_TYPE.CLASS: r"find {0} {1} -type f"
|
|
r"| egrep '.*/{2}\.(kt|java)$' || true",
|
|
FIND_REFERENCE_TYPE.QUALIFIED_CLASS: r"find {0} {1} -type f"
|
|
r"| egrep '.*{2}\.(kt|java)$' || true",
|
|
FIND_REFERENCE_TYPE.PACKAGE: r"find {0} {1} -wholename "
|
|
r"'*{2}' -type d -print",
|
|
FIND_REFERENCE_TYPE.INTEGRATION: r"find {0} {1} -wholename "
|
|
r"'*{2}.xml' -print",
|
|
# Searching a test among files where the absolute paths contain *test*.
|
|
# If users complain atest couldn't find a CC_CLASS, ask them to follow the
|
|
# convention that the filename or dirname must contain *test*, where *test*
|
|
# is case-insensitive.
|
|
FIND_REFERENCE_TYPE.CC_CLASS: r"find {0} {1} -type f -print"
|
|
r"| egrep -i '/*test.*\.(cc|cpp)$'"
|
|
r"| xargs -P" + str(_CPU_COUNT) +
|
|
r" egrep -sH '^[ ]*TEST(_F|_P)?[ ]*\({2}' || true"
|
|
}
|
|
|
|
# Map ref_type with its index file.
|
|
FIND_INDEXES = {
|
|
FIND_REFERENCE_TYPE.CLASS: constants.CLASS_INDEX,
|
|
FIND_REFERENCE_TYPE.QUALIFIED_CLASS: constants.QCLASS_INDEX,
|
|
FIND_REFERENCE_TYPE.PACKAGE: constants.PACKAGE_INDEX,
|
|
FIND_REFERENCE_TYPE.INTEGRATION: constants.INT_INDEX,
|
|
FIND_REFERENCE_TYPE.CC_CLASS: constants.CC_CLASS_INDEX
|
|
}
|
|
|
|
# XML parsing related constants.
|
|
_COMPATIBILITY_PACKAGE_PREFIX = "com.android.compatibility"
|
|
_CTS_JAR = "cts-tradefed"
|
|
_XML_PUSH_DELIM = '->'
|
|
_APK_SUFFIX = '.apk'
|
|
# Setup script for device perf tests.
|
|
_PERF_SETUP_LABEL = 'perf-setup.sh'
|
|
|
|
# XML tags.
|
|
_XML_NAME = 'name'
|
|
_XML_VALUE = 'value'
|
|
|
|
# VTS xml parsing constants.
|
|
_VTS_TEST_MODULE = 'test-module-name'
|
|
_VTS_MODULE = 'module-name'
|
|
_VTS_BINARY_SRC = 'binary-test-source'
|
|
_VTS_PUSH_GROUP = 'push-group'
|
|
_VTS_PUSH = 'push'
|
|
_VTS_BINARY_SRC_DELIM = '::'
|
|
_VTS_PUSH_DIR = os.path.join(os.environ.get(constants.ANDROID_BUILD_TOP, ''),
|
|
'test', 'vts', 'tools', 'vts-tradefed', 'res',
|
|
'push_groups')
|
|
_VTS_PUSH_SUFFIX = '.push'
|
|
_VTS_BITNESS = 'append-bitness'
|
|
_VTS_BITNESS_TRUE = 'true'
|
|
_VTS_BITNESS_32 = '32'
|
|
_VTS_BITNESS_64 = '64'
|
|
_VTS_TEST_FILE = 'test-file-name'
|
|
_VTS_APK = 'apk'
|
|
# Matches 'DATA/target' in '_32bit::DATA/target'
|
|
_VTS_BINARY_SRC_DELIM_RE = re.compile(r'.*::(?P<target>.*)$')
|
|
_VTS_OUT_DATA_APP_PATH = 'DATA/app'
|
|
|
|
# pylint: disable=inconsistent-return-statements
|
|
def split_methods(user_input):
|
|
"""Split user input string into test reference and list of methods.
|
|
|
|
Args:
|
|
user_input: A string of the user's input.
|
|
Examples:
|
|
class_name
|
|
class_name#method1,method2
|
|
path
|
|
path#method1,method2
|
|
Returns:
|
|
A tuple. First element is String of test ref and second element is
|
|
a set of method name strings or empty list if no methods included.
|
|
Exception:
|
|
atest_error.TooManyMethodsError raised when input string is trying to
|
|
specify too many methods in a single positional argument.
|
|
|
|
Examples of unsupported input strings:
|
|
module:class#method,class#method
|
|
class1#method,class2#method
|
|
path1#method,path2#method
|
|
"""
|
|
parts = user_input.split('#')
|
|
if len(parts) == 1:
|
|
return parts[0], frozenset()
|
|
elif len(parts) == 2:
|
|
return parts[0], frozenset(parts[1].split(','))
|
|
raise atest_error.TooManyMethodsError(
|
|
'Too many methods specified with # character in user input: %s.'
|
|
'\n\nOnly one class#method combination supported per positional'
|
|
' argument. Multiple classes should be separated by spaces: '
|
|
'class#method class#method')
|
|
|
|
|
|
# pylint: disable=inconsistent-return-statements
|
|
def get_fully_qualified_class_name(test_path):
|
|
"""Parse the fully qualified name from the class java file.
|
|
|
|
Args:
|
|
test_path: A string of absolute path to the java class file.
|
|
|
|
Returns:
|
|
A string of the fully qualified class name.
|
|
|
|
Raises:
|
|
atest_error.MissingPackageName if no class name can be found.
|
|
"""
|
|
with open(test_path) as class_file:
|
|
for line in class_file:
|
|
match = _PACKAGE_RE.match(line)
|
|
if match:
|
|
package = match.group('package')
|
|
cls = os.path.splitext(os.path.split(test_path)[1])[0]
|
|
return '%s.%s' % (package, cls)
|
|
raise atest_error.MissingPackageNameError('%s: Test class java file'
|
|
'does not contain a package'
|
|
'name.'% test_path)
|
|
|
|
|
|
def has_cc_class(test_path):
|
|
"""Find out if there is any test case in the cc file.
|
|
|
|
Args:
|
|
test_path: A string of absolute path to the cc file.
|
|
|
|
Returns:
|
|
Boolean: has cc class in test_path or not.
|
|
"""
|
|
with open(test_path) as class_file:
|
|
for line in class_file:
|
|
match = _CC_CLASS_RE.match(line)
|
|
if match:
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_package_name(file_name):
|
|
"""Parse the package name from a java file.
|
|
|
|
Args:
|
|
file_name: A string of the absolute path to the java file.
|
|
|
|
Returns:
|
|
A string of the package name or None
|
|
"""
|
|
with open(file_name) as data:
|
|
for line in data:
|
|
match = _PACKAGE_RE.match(line)
|
|
if match:
|
|
return match.group('package')
|
|
|
|
|
|
def has_method_in_file(test_path, methods):
|
|
"""Find out if there is at least one method in the file.
|
|
|
|
Note: This method doesn't handle if method is in comment sections or not.
|
|
If the file has any method(even in comment sections), it will return True.
|
|
|
|
Args:
|
|
test_path: A string of absolute path to the test file.
|
|
methods: A set of method names.
|
|
|
|
Returns:
|
|
Boolean: there is at least one method in test_path.
|
|
"""
|
|
if not os.path.isfile(test_path):
|
|
return False
|
|
methods_re = None
|
|
if constants.JAVA_EXT_RE.match(test_path):
|
|
methods_re = re.compile(_JAVA_METHODS_PATTERN.format(
|
|
'|'.join([r'%s' % x for x in methods])))
|
|
elif constants.CC_EXT_RE.match(test_path):
|
|
methods_re = re.compile(_CC_METHODS_PATTERN.format(
|
|
'|'.join([r'%s' % x for x in methods])))
|
|
if methods_re:
|
|
with open(test_path) as test_file:
|
|
for line in test_file:
|
|
match = re.match(methods_re, line)
|
|
if match:
|
|
return True
|
|
return False
|
|
|
|
|
|
def extract_test_path(output, methods=None):
|
|
"""Extract the test path from the output of a unix 'find' command.
|
|
|
|
Example of find output for CLASS find cmd:
|
|
/<some_root>/cts/tests/jank/src/android/jank/cts/ui/CtsDeviceJankUi.java
|
|
|
|
Args:
|
|
output: A string or list output of a unix 'find' command.
|
|
methods: A set of method names.
|
|
|
|
Returns:
|
|
A list of the test paths or None if output is '' or None.
|
|
"""
|
|
if not output:
|
|
return None
|
|
verified_tests = set()
|
|
if isinstance(output, str):
|
|
output = output.splitlines()
|
|
for test in output:
|
|
# compare CC_OUTPUT_RE with output
|
|
match_obj = constants.CC_OUTPUT_RE.match(test)
|
|
if match_obj:
|
|
# cc/cpp
|
|
fpath = match_obj.group('file_path')
|
|
if not methods or match_obj.group('method_name') in methods:
|
|
verified_tests.add(fpath)
|
|
else:
|
|
# TODO (b/138997521) - Atest checks has_method_in_file of a class
|
|
# without traversing its parent classes. A workaround for this is
|
|
# do not check has_method_in_file. Uncomment below when a solution
|
|
# to it is applied.
|
|
# java/kt
|
|
#if not methods or has_method_in_file(test, methods):
|
|
verified_tests.add(test)
|
|
return extract_test_from_tests(list(verified_tests))
|
|
|
|
|
|
def extract_test_from_tests(tests):
|
|
"""Extract the test path from the tests.
|
|
|
|
Return the test to run from tests. If more than one option, prompt the user
|
|
to select multiple ones. Supporting formats:
|
|
- An integer. E.g. 0
|
|
- Comma-separated integers. E.g. 1,3,5
|
|
- A range of integers denoted by the starting integer separated from
|
|
the end integer by a dash, '-'. E.g. 1-3
|
|
|
|
Args:
|
|
tests: A string list which contains multiple test paths.
|
|
|
|
Returns:
|
|
A string list of paths.
|
|
"""
|
|
count = len(tests)
|
|
if count <= 1:
|
|
return tests if count else None
|
|
mtests = set()
|
|
try:
|
|
numbered_list = ['%s: %s' % (i, t) for i, t in enumerate(tests)]
|
|
numbered_list.append('%s: All' % count)
|
|
print('Multiple tests found:\n{0}'.format('\n'.join(numbered_list)))
|
|
test_indices = raw_input("Please enter numbers of test to use. "
|
|
"If none of above option matched, keep "
|
|
"searching for other possible tests."
|
|
"\n(multiple selection is supported,"
|
|
" e.g. '1' or '0,1' or '0-2'): ")
|
|
for idx in re.sub(r'(\s)', '', test_indices).split(','):
|
|
indices = idx.split('-')
|
|
len_indices = len(indices)
|
|
if len_indices > 0:
|
|
start_index = min(int(indices[0]), int(indices[len_indices-1]))
|
|
end_index = max(int(indices[0]), int(indices[len_indices-1]))
|
|
# One of input is 'All', return all options.
|
|
if start_index == count or end_index == count:
|
|
return tests
|
|
mtests.update(tests[start_index:(end_index+1)])
|
|
except (ValueError, IndexError, AttributeError, TypeError) as err:
|
|
logging.debug('%s', err)
|
|
print('None of above option matched, keep searching for other'
|
|
' possible tests...')
|
|
return list(mtests)
|
|
|
|
|
|
@atest_decorator.static_var("cached_ignore_dirs", [])
|
|
def _get_ignored_dirs():
|
|
"""Get ignore dirs in find command.
|
|
|
|
Since we can't construct a single find cmd to find the target and
|
|
filter-out the dir with .out-dir, .find-ignore and $OUT-DIR. We have
|
|
to run the 1st find cmd to find these dirs. Then, we can use these
|
|
results to generate the real find cmd.
|
|
|
|
Return:
|
|
A list of the ignore dirs.
|
|
"""
|
|
out_dirs = _get_ignored_dirs.cached_ignore_dirs
|
|
if not out_dirs:
|
|
build_top = os.environ.get(constants.ANDROID_BUILD_TOP)
|
|
find_out_dir_cmd = (r'find %s -maxdepth 2 '
|
|
r'-type f \( -name ".out-dir" -o -name '
|
|
r'".find-ignore" \)') % build_top
|
|
out_files = subprocess.check_output(find_out_dir_cmd, shell=True)
|
|
# Get all dirs with .out-dir or .find-ignore
|
|
if out_files:
|
|
out_files = out_files.splitlines()
|
|
for out_file in out_files:
|
|
if out_file:
|
|
out_dirs.append(os.path.dirname(out_file.strip()))
|
|
# Get the out folder if user specified $OUT_DIR
|
|
custom_out_dir = os.environ.get(constants.ANDROID_OUT_DIR)
|
|
if custom_out_dir:
|
|
user_out_dir = None
|
|
if os.path.isabs(custom_out_dir):
|
|
user_out_dir = custom_out_dir
|
|
else:
|
|
user_out_dir = os.path.join(build_top, custom_out_dir)
|
|
# only ignore the out_dir when it under $ANDROID_BUILD_TOP
|
|
if build_top in user_out_dir:
|
|
if user_out_dir not in out_dirs:
|
|
out_dirs.append(user_out_dir)
|
|
_get_ignored_dirs.cached_ignore_dirs = out_dirs
|
|
return out_dirs
|
|
|
|
|
|
def _get_prune_cond_of_ignored_dirs():
|
|
"""Get the prune condition of ignore dirs.
|
|
|
|
Generation a string of the prune condition in the find command.
|
|
It will filter-out the dir with .out-dir, .find-ignore and $OUT-DIR.
|
|
Because they are the out dirs, we don't have to find them.
|
|
|
|
Return:
|
|
A string of the prune condition of the ignore dirs.
|
|
"""
|
|
out_dirs = _get_ignored_dirs()
|
|
prune_cond = r'-type d \( -name ".*"'
|
|
for out_dir in out_dirs:
|
|
prune_cond += r' -o -path %s' % out_dir
|
|
prune_cond += r' \) -prune -o'
|
|
return prune_cond
|
|
|
|
|
|
def run_find_cmd(ref_type, search_dir, target, methods=None):
|
|
"""Find a path to a target given a search dir and a target name.
|
|
|
|
Args:
|
|
ref_type: An AtestEnum of the reference type.
|
|
search_dir: A string of the dirpath to search in.
|
|
target: A string of what you're trying to find.
|
|
methods: A set of method names.
|
|
|
|
Return:
|
|
A list of the path to the target.
|
|
If the search_dir is inexistent, None will be returned.
|
|
"""
|
|
# If module_info.json is outdated, finding in the search_dir can result in
|
|
# raising exception. Return null immediately can guild users to run
|
|
# --rebuild-module-info to resolve the problem.
|
|
if not os.path.isdir(search_dir):
|
|
logging.debug('\'%s\' does not exist!', search_dir)
|
|
return None
|
|
ref_name = FIND_REFERENCE_TYPE[ref_type]
|
|
start = time.time()
|
|
if os.path.isfile(FIND_INDEXES[ref_type]):
|
|
_dict, out = {}, None
|
|
with open(FIND_INDEXES[ref_type], 'rb') as index:
|
|
try:
|
|
_dict = pickle.load(index)
|
|
except (IOError, EOFError, pickle.UnpicklingError) as err:
|
|
logging.debug('Exception raised: %s', err)
|
|
metrics_utils.handle_exc_and_send_exit_event(
|
|
constants.ACCESS_CACHE_FAILURE)
|
|
os.remove(FIND_INDEXES[ref_type])
|
|
if _dict.get(target):
|
|
logging.debug('Found %s in %s', target, FIND_INDEXES[ref_type])
|
|
out = [path for path in _dict.get(target) if search_dir in path]
|
|
else:
|
|
prune_cond = _get_prune_cond_of_ignored_dirs()
|
|
if '.' in target:
|
|
target = target.replace('.', '/')
|
|
find_cmd = FIND_CMDS[ref_type].format(search_dir, prune_cond, target)
|
|
logging.debug('Executing %s find cmd: %s', ref_name, find_cmd)
|
|
out = subprocess.check_output(find_cmd, shell=True)
|
|
logging.debug('%s find cmd out: %s', ref_name, out)
|
|
logging.debug('%s find completed in %ss', ref_name, time.time() - start)
|
|
return extract_test_path(out, methods)
|
|
|
|
|
|
def find_class_file(search_dir, class_name, is_native_test=False, methods=None):
|
|
"""Find a path to a class file given a search dir and a class name.
|
|
|
|
Args:
|
|
search_dir: A string of the dirpath to search in.
|
|
class_name: A string of the class to search for.
|
|
is_native_test: A boolean variable of whether to search for a native
|
|
test or not.
|
|
methods: A set of method names.
|
|
|
|
Return:
|
|
A list of the path to the java/cc file.
|
|
"""
|
|
if is_native_test:
|
|
ref_type = FIND_REFERENCE_TYPE.CC_CLASS
|
|
elif '.' in class_name:
|
|
ref_type = FIND_REFERENCE_TYPE.QUALIFIED_CLASS
|
|
else:
|
|
ref_type = FIND_REFERENCE_TYPE.CLASS
|
|
return run_find_cmd(ref_type, search_dir, class_name, methods)
|
|
|
|
|
|
def is_equal_or_sub_dir(sub_dir, parent_dir):
|
|
"""Return True sub_dir is sub dir or equal to parent_dir.
|
|
|
|
Args:
|
|
sub_dir: A string of the sub directory path.
|
|
parent_dir: A string of the parent directory path.
|
|
|
|
Returns:
|
|
A boolean of whether both are dirs and sub_dir is sub of parent_dir
|
|
or is equal to parent_dir.
|
|
"""
|
|
# avoid symlink issues with real path
|
|
parent_dir = os.path.realpath(parent_dir)
|
|
sub_dir = os.path.realpath(sub_dir)
|
|
if not os.path.isdir(sub_dir) or not os.path.isdir(parent_dir):
|
|
return False
|
|
return os.path.commonprefix([sub_dir, parent_dir]) == parent_dir
|
|
|
|
|
|
def find_parent_module_dir(root_dir, start_dir, module_info):
|
|
"""From current dir search up file tree until root dir for module dir.
|
|
|
|
Args:
|
|
root_dir: A string of the dir that is the parent of the start dir.
|
|
start_dir: A string of the dir to start searching up from.
|
|
module_info: ModuleInfo object containing module information from the
|
|
build system.
|
|
|
|
Returns:
|
|
A string of the module dir relative to root, None if no Module Dir
|
|
found. There may be multiple testable modules at this level.
|
|
|
|
Exceptions:
|
|
ValueError: Raised if cur_dir not dir or not subdir of root dir.
|
|
"""
|
|
if not is_equal_or_sub_dir(start_dir, root_dir):
|
|
raise ValueError('%s not in repo %s' % (start_dir, root_dir))
|
|
auto_gen_dir = None
|
|
current_dir = start_dir
|
|
while current_dir != root_dir:
|
|
# TODO (b/112904944) - migrate module_finder functions to here and
|
|
# reuse them.
|
|
rel_dir = os.path.relpath(current_dir, root_dir)
|
|
# Check if actual config file here
|
|
if os.path.isfile(os.path.join(current_dir, constants.MODULE_CONFIG)):
|
|
return rel_dir
|
|
# Check module_info if auto_gen config or robo (non-config) here
|
|
for mod in module_info.path_to_module_info.get(rel_dir, []):
|
|
if module_info.is_robolectric_module(mod):
|
|
return rel_dir
|
|
for test_config in mod.get(constants.MODULE_TEST_CONFIG, []):
|
|
if os.path.isfile(os.path.join(root_dir, test_config)):
|
|
return rel_dir
|
|
if mod.get('auto_test_config'):
|
|
auto_gen_dir = rel_dir
|
|
# Don't return for auto_gen, keep checking for real config, because
|
|
# common in cts for class in apk that's in hostside test setup.
|
|
current_dir = os.path.dirname(current_dir)
|
|
return auto_gen_dir
|
|
|
|
|
|
def get_targets_from_xml(xml_file, module_info):
|
|
"""Retrieve build targets from the given xml.
|
|
|
|
Just a helper func on top of get_targets_from_xml_root.
|
|
|
|
Args:
|
|
xml_file: abs path to xml file.
|
|
module_info: ModuleInfo class used to verify targets are valid modules.
|
|
|
|
Returns:
|
|
A set of build targets based on the signals found in the xml file.
|
|
"""
|
|
xml_root = ET.parse(xml_file).getroot()
|
|
return get_targets_from_xml_root(xml_root, module_info)
|
|
|
|
|
|
def _get_apk_target(apk_target):
|
|
"""Return the sanitized apk_target string from the xml.
|
|
|
|
The apk_target string can be of 2 forms:
|
|
- apk_target.apk
|
|
- apk_target.apk->/path/to/install/apk_target.apk
|
|
|
|
We want to return apk_target in both cases.
|
|
|
|
Args:
|
|
apk_target: String of target name to clean.
|
|
|
|
Returns:
|
|
String of apk_target to build.
|
|
"""
|
|
apk = apk_target.split(_XML_PUSH_DELIM, 1)[0].strip()
|
|
return apk[:-len(_APK_SUFFIX)]
|
|
|
|
|
|
def _is_apk_target(name, value):
|
|
"""Return True if XML option is an apk target.
|
|
|
|
We have some scenarios where an XML option can be an apk target:
|
|
- value is an apk file.
|
|
- name is a 'push' option where value holds the apk_file + other stuff.
|
|
|
|
Args:
|
|
name: String name of XML option.
|
|
value: String value of the XML option.
|
|
|
|
Returns:
|
|
True if it's an apk target we should build, False otherwise.
|
|
"""
|
|
if _APK_RE.match(value):
|
|
return True
|
|
if name == 'push' and value.endswith(_APK_SUFFIX):
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_targets_from_xml_root(xml_root, module_info):
|
|
"""Retrieve build targets from the given xml root.
|
|
|
|
We're going to pull the following bits of info:
|
|
- Parse any .apk files listed in the config file.
|
|
- Parse option value for "test-module-name" (for vts10 tests).
|
|
- Look for the perf script.
|
|
|
|
Args:
|
|
module_info: ModuleInfo class used to verify targets are valid modules.
|
|
xml_root: ElementTree xml_root for us to look through.
|
|
|
|
Returns:
|
|
A set of build targets based on the signals found in the xml file.
|
|
"""
|
|
targets = set()
|
|
option_tags = xml_root.findall('.//option')
|
|
for tag in option_tags:
|
|
target_to_add = None
|
|
name = tag.attrib[_XML_NAME].strip()
|
|
value = tag.attrib[_XML_VALUE].strip()
|
|
if _is_apk_target(name, value):
|
|
target_to_add = _get_apk_target(value)
|
|
elif _PERF_SETUP_LABEL in value:
|
|
targets.add(_PERF_SETUP_LABEL)
|
|
continue
|
|
|
|
# Let's make sure we can actually build the target.
|
|
if target_to_add and module_info.is_module(target_to_add):
|
|
targets.add(target_to_add)
|
|
elif target_to_add:
|
|
logging.warning('Build target (%s) not present in module info, '
|
|
'skipping build', target_to_add)
|
|
|
|
# TODO (b/70813166): Remove this lookup once all runtime dependencies
|
|
# can be listed as a build dependencies or are in the base test harness.
|
|
nodes_with_class = xml_root.findall(".//*[@class]")
|
|
for class_attr in nodes_with_class:
|
|
fqcn = class_attr.attrib['class'].strip()
|
|
if fqcn.startswith(_COMPATIBILITY_PACKAGE_PREFIX):
|
|
targets.add(_CTS_JAR)
|
|
logging.debug('Targets found in config file: %s', targets)
|
|
return targets
|
|
|
|
|
|
def _get_vts_push_group_targets(push_file, rel_out_dir):
|
|
"""Retrieve vts10 push group build targets.
|
|
|
|
A push group file is a file that list out test dependencies and other push
|
|
group files. Go through the push file and gather all the test deps we need.
|
|
|
|
Args:
|
|
push_file: Name of the push file in the VTS
|
|
rel_out_dir: Abs path to the out dir to help create vts10 build targets.
|
|
|
|
Returns:
|
|
Set of string which represent build targets.
|
|
"""
|
|
targets = set()
|
|
full_push_file_path = os.path.join(_VTS_PUSH_DIR, push_file)
|
|
# pylint: disable=invalid-name
|
|
with open(full_push_file_path) as f:
|
|
for line in f:
|
|
target = line.strip()
|
|
# Skip empty lines.
|
|
if not target:
|
|
continue
|
|
|
|
# This is a push file, get the targets from it.
|
|
if target.endswith(_VTS_PUSH_SUFFIX):
|
|
targets |= _get_vts_push_group_targets(line.strip(),
|
|
rel_out_dir)
|
|
continue
|
|
sanitized_target = target.split(_XML_PUSH_DELIM, 1)[0].strip()
|
|
targets.add(os.path.join(rel_out_dir, sanitized_target))
|
|
return targets
|
|
|
|
|
|
def _specified_bitness(xml_root):
|
|
"""Check if the xml file contains the option append-bitness.
|
|
|
|
Args:
|
|
xml_root: abs path to xml file.
|
|
|
|
Returns:
|
|
True if xml specifies to append-bitness, False otherwise.
|
|
"""
|
|
option_tags = xml_root.findall('.//option')
|
|
for tag in option_tags:
|
|
value = tag.attrib[_XML_VALUE].strip()
|
|
name = tag.attrib[_XML_NAME].strip()
|
|
if name == _VTS_BITNESS and value == _VTS_BITNESS_TRUE:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _get_vts_binary_src_target(value, rel_out_dir):
|
|
"""Parse out the vts10 binary src target.
|
|
|
|
The value can be in the following pattern:
|
|
- {_32bit,_64bit,_IPC32_32bit}::DATA/target (DATA/target)
|
|
- DATA/target->/data/target (DATA/target)
|
|
- out/host/linx-x86/bin/VtsSecuritySelinuxPolicyHostTest (the string as
|
|
is)
|
|
|
|
Args:
|
|
value: String of the XML option value to parse.
|
|
rel_out_dir: String path of out dir to prepend to target when required.
|
|
|
|
Returns:
|
|
String of the target to build.
|
|
"""
|
|
# We'll assume right off the bat we can use the value as is and modify it if
|
|
# necessary, e.g. out/host/linux-x86/bin...
|
|
target = value
|
|
# _32bit::DATA/target
|
|
match = _VTS_BINARY_SRC_DELIM_RE.match(value)
|
|
if match:
|
|
target = os.path.join(rel_out_dir, match.group('target'))
|
|
# DATA/target->/data/target
|
|
elif _XML_PUSH_DELIM in value:
|
|
target = value.split(_XML_PUSH_DELIM, 1)[0].strip()
|
|
target = os.path.join(rel_out_dir, target)
|
|
return target
|
|
|
|
|
|
def get_plans_from_vts_xml(xml_file):
|
|
"""Get configs which are included by xml_file.
|
|
|
|
We're looking for option(include) to get all dependency plan configs.
|
|
|
|
Args:
|
|
xml_file: Absolute path to xml file.
|
|
|
|
Returns:
|
|
A set of plan config paths which are depended by xml_file.
|
|
"""
|
|
if not os.path.exists(xml_file):
|
|
raise atest_error.XmlNotExistError('%s: The xml file does'
|
|
'not exist' % xml_file)
|
|
plans = set()
|
|
xml_root = ET.parse(xml_file).getroot()
|
|
plans.add(xml_file)
|
|
option_tags = xml_root.findall('.//include')
|
|
if not option_tags:
|
|
return plans
|
|
# Currently, all vts10 xmls live in the same dir :
|
|
# https://android.googlesource.com/platform/test/vts/+/master/tools/vts-tradefed/res/config/
|
|
# If the vts10 plans start using folders to organize the plans, the logic here
|
|
# should be changed.
|
|
xml_dir = os.path.dirname(xml_file)
|
|
for tag in option_tags:
|
|
name = tag.attrib[_XML_NAME].strip()
|
|
plans |= get_plans_from_vts_xml(os.path.join(xml_dir, name + ".xml"))
|
|
return plans
|
|
|
|
|
|
def get_targets_from_vts_xml(xml_file, rel_out_dir, module_info):
|
|
"""Parse a vts10 xml for test dependencies we need to build.
|
|
|
|
We have a separate vts10 parsing function because we make a big assumption
|
|
on the targets (the way they're formatted and what they represent) and we
|
|
also create these build targets in a very special manner as well.
|
|
The 6 options we're looking for are:
|
|
- binary-test-source
|
|
- push-group
|
|
- push
|
|
- test-module-name
|
|
- test-file-name
|
|
- apk
|
|
|
|
Args:
|
|
module_info: ModuleInfo class used to verify targets are valid modules.
|
|
rel_out_dir: Abs path to the out dir to help create vts10 build targets.
|
|
xml_file: abs path to xml file.
|
|
|
|
Returns:
|
|
A set of build targets based on the signals found in the xml file.
|
|
"""
|
|
xml_root = ET.parse(xml_file).getroot()
|
|
targets = set()
|
|
option_tags = xml_root.findall('.//option')
|
|
for tag in option_tags:
|
|
value = tag.attrib[_XML_VALUE].strip()
|
|
name = tag.attrib[_XML_NAME].strip()
|
|
if name in [_VTS_TEST_MODULE, _VTS_MODULE]:
|
|
if module_info.is_module(value):
|
|
targets.add(value)
|
|
else:
|
|
logging.warning('vts10 test module (%s) not present in module '
|
|
'info, skipping build', value)
|
|
elif name == _VTS_BINARY_SRC:
|
|
targets.add(_get_vts_binary_src_target(value, rel_out_dir))
|
|
elif name == _VTS_PUSH_GROUP:
|
|
# Look up the push file and parse out build artifacts (as well as
|
|
# other push group files to parse).
|
|
targets |= _get_vts_push_group_targets(value, rel_out_dir)
|
|
elif name == _VTS_PUSH:
|
|
# Parse out the build artifact directly.
|
|
push_target = value.split(_XML_PUSH_DELIM, 1)[0].strip()
|
|
# If the config specified append-bitness, append the bits suffixes
|
|
# to the target.
|
|
if _specified_bitness(xml_root):
|
|
targets.add(os.path.join(rel_out_dir, push_target + _VTS_BITNESS_32))
|
|
targets.add(os.path.join(rel_out_dir, push_target + _VTS_BITNESS_64))
|
|
else:
|
|
targets.add(os.path.join(rel_out_dir, push_target))
|
|
elif name == _VTS_TEST_FILE:
|
|
# The _VTS_TEST_FILE values can be set in 2 possible ways:
|
|
# 1. test_file.apk
|
|
# 2. DATA/app/test_file/test_file.apk
|
|
# We'll assume that test_file.apk (#1) is in an expected path (but
|
|
# that is not true, see b/76158619) and create the full path for it
|
|
# and then append the _VTS_TEST_FILE value to targets to build.
|
|
target = os.path.join(rel_out_dir, value)
|
|
# If value is just an APK, specify the path that we expect it to be in
|
|
# e.g. out/host/linux-x86/vts10/android-vts10/testcases/DATA/app/test_file/test_file.apk
|
|
head, _ = os.path.split(value)
|
|
if not head:
|
|
target = os.path.join(rel_out_dir, _VTS_OUT_DATA_APP_PATH,
|
|
_get_apk_target(value), value)
|
|
targets.add(target)
|
|
elif name == _VTS_APK:
|
|
targets.add(os.path.join(rel_out_dir, value))
|
|
logging.debug('Targets found in config file: %s', targets)
|
|
return targets
|
|
|
|
|
|
def get_dir_path_and_filename(path):
|
|
"""Return tuple of dir and file name from given path.
|
|
|
|
Args:
|
|
path: String of path to break up.
|
|
|
|
Returns:
|
|
Tuple of (dir, file) paths.
|
|
"""
|
|
if os.path.isfile(path):
|
|
dir_path, file_path = os.path.split(path)
|
|
else:
|
|
dir_path, file_path = path, None
|
|
return dir_path, file_path
|
|
|
|
|
|
def get_cc_filter(class_name, methods):
|
|
"""Get the cc filter.
|
|
|
|
Args:
|
|
class_name: class name of the cc test.
|
|
methods: a list of method names.
|
|
|
|
Returns:
|
|
A formatted string for cc filter.
|
|
Ex: "class1.method1:class1.method2" or "class1.*"
|
|
"""
|
|
if methods:
|
|
return ":".join(["%s.%s" % (class_name, x) for x in methods])
|
|
return "%s.*" % class_name
|
|
|
|
|
|
def search_integration_dirs(name, int_dirs):
|
|
"""Search integration dirs for name and return full path.
|
|
|
|
Args:
|
|
name: A string of plan name needed to be found.
|
|
int_dirs: A list of path needed to be searched.
|
|
|
|
Returns:
|
|
A list of the test path.
|
|
Ask user to select if multiple tests are found.
|
|
None if no matched test found.
|
|
"""
|
|
root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
|
|
test_files = []
|
|
for integration_dir in int_dirs:
|
|
abs_path = os.path.join(root_dir, integration_dir)
|
|
test_paths = run_find_cmd(FIND_REFERENCE_TYPE.INTEGRATION, abs_path,
|
|
name)
|
|
if test_paths:
|
|
test_files.extend(test_paths)
|
|
return extract_test_from_tests(test_files)
|
|
|
|
|
|
def get_int_dir_from_path(path, int_dirs):
|
|
"""Search integration dirs for the given path and return path of dir.
|
|
|
|
Args:
|
|
path: A string of path needed to be found.
|
|
int_dirs: A list of path needed to be searched.
|
|
|
|
Returns:
|
|
A string of the test dir. None if no matched path found.
|
|
"""
|
|
root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
|
|
if not os.path.exists(path):
|
|
return None
|
|
dir_path, file_name = get_dir_path_and_filename(path)
|
|
int_dir = None
|
|
for possible_dir in int_dirs:
|
|
abs_int_dir = os.path.join(root_dir, possible_dir)
|
|
if is_equal_or_sub_dir(dir_path, abs_int_dir):
|
|
int_dir = abs_int_dir
|
|
break
|
|
if not file_name:
|
|
logging.warn('Found dir (%s) matching input (%s).'
|
|
' Referencing an entire Integration/Suite dir'
|
|
' is not supported. If you are trying to reference'
|
|
' a test by its path, please input the path to'
|
|
' the integration/suite config file itself.',
|
|
int_dir, path)
|
|
return None
|
|
return int_dir
|
|
|
|
|
|
def get_install_locations(installed_paths):
|
|
"""Get install locations from installed paths.
|
|
|
|
Args:
|
|
installed_paths: List of installed_paths from module_info.
|
|
|
|
Returns:
|
|
Set of install locations from module_info installed_paths. e.g.
|
|
set(['host', 'device'])
|
|
"""
|
|
install_locations = set()
|
|
for path in installed_paths:
|
|
if _HOST_PATH_RE.match(path):
|
|
install_locations.add(constants.DEVICELESS_TEST)
|
|
elif _DEVICE_PATH_RE.match(path):
|
|
install_locations.add(constants.DEVICE_TEST)
|
|
return install_locations
|
|
|
|
|
|
def get_levenshtein_distance(test_name, module_name, dir_costs=constants.COST_TYPO):
|
|
"""Return an edit distance between test_name and module_name.
|
|
|
|
Levenshtein Distance has 3 actions: delete, insert and replace.
|
|
dis_costs makes each action weigh differently.
|
|
|
|
Args:
|
|
test_name: A keyword from the users.
|
|
module_name: A testable module name.
|
|
dir_costs: A tuple which contains 3 integer, where dir represents
|
|
Deletion, Insertion and Replacement respectively.
|
|
For guessing typos: (1, 1, 1) gives the best result.
|
|
For searching keywords, (8, 1, 5) gives the best result.
|
|
|
|
Returns:
|
|
An edit distance integer between test_name and module_name.
|
|
"""
|
|
rows = len(test_name) + 1
|
|
cols = len(module_name) + 1
|
|
deletion, insertion, replacement = dir_costs
|
|
|
|
# Creating a Dynamic Programming Matrix and weighting accordingly.
|
|
dp_matrix = [[0 for _ in range(cols)] for _ in range(rows)]
|
|
# Weigh rows/deletion
|
|
for row in range(1, rows):
|
|
dp_matrix[row][0] = row * deletion
|
|
# Weigh cols/insertion
|
|
for col in range(1, cols):
|
|
dp_matrix[0][col] = col * insertion
|
|
# The core logic of LD
|
|
for col in range(1, cols):
|
|
for row in range(1, rows):
|
|
if test_name[row-1] == module_name[col-1]:
|
|
cost = 0
|
|
else:
|
|
cost = replacement
|
|
dp_matrix[row][col] = min(dp_matrix[row-1][col] + deletion,
|
|
dp_matrix[row][col-1] + insertion,
|
|
dp_matrix[row-1][col-1] + cost)
|
|
|
|
return dp_matrix[row][col]
|
|
|
|
|
|
def is_test_from_kernel_xml(xml_file, test_name):
|
|
"""Check if test defined in xml_file.
|
|
|
|
A kernel test can be defined like:
|
|
<option name="test-command-line" key="test_class_1" value="command 1" />
|
|
where key is the name of test class and method of the runner. This method
|
|
returns True if the test_name was defined in the given xml_file.
|
|
|
|
Args:
|
|
xml_file: Absolute path to xml file.
|
|
test_name: test_name want to find.
|
|
|
|
Returns:
|
|
True if test_name in xml_file, False otherwise.
|
|
"""
|
|
if not os.path.exists(xml_file):
|
|
raise atest_error.XmlNotExistError('%s: The xml file does'
|
|
'not exist' % xml_file)
|
|
xml_root = ET.parse(xml_file).getroot()
|
|
option_tags = xml_root.findall('.//option')
|
|
for option_tag in option_tags:
|
|
if option_tag.attrib['name'] == 'test-command-line':
|
|
if option_tag.attrib['key'] == test_name:
|
|
return True
|
|
return False
|