blob: ff297cce61c9903818f0dc1c7362bfea39f6c360 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2019, 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.
"""
Atest tool functions.
"""
from __future__ import print_function
import logging
import os
import pickle
import shutil
import subprocess
import sys
import constants
import module_info
from metrics import metrics_utils
MAC_UPDB_SRC = os.path.join(os.path.dirname(__file__), 'updatedb_darwin.sh')
MAC_UPDB_DST = os.path.join(os.getenv(constants.ANDROID_HOST_OUT, ''), 'bin')
UPDATEDB = 'updatedb'
LOCATE = 'locate'
SEARCH_TOP = os.getenv(constants.ANDROID_BUILD_TOP, '')
MACOSX = 'Darwin'
OSNAME = os.uname()[0]
# When adding new index, remember to append constants to below tuple.
INDEXES = (constants.CC_CLASS_INDEX,
constants.CLASS_INDEX,
constants.LOCATE_CACHE,
constants.MODULE_INDEX,
constants.PACKAGE_INDEX,
constants.QCLASS_INDEX)
# The list was generated by command:
# find `gettop` -type d -wholename `gettop`/out -prune -o -type d -name '.*'
# -print | awk -F/ '{{print $NF}}'| sort -u
PRUNENAMES = ['.abc', '.appveyor', '.azure-pipelines',
'.bazelci', '.buildscript',
'.ci', '.circleci', '.conan', '.config',
'.externalToolBuilders',
'.git', '.github', '.github-ci', '.google', '.gradle',
'.idea', '.intermediates',
'.jenkins',
'.kokoro',
'.libs_cffi_backend',
'.mvn',
'.prebuilt_info', '.private', '__pycache__',
'.repo',
'.semaphore', '.settings', '.static', '.svn',
'.test', '.travis', '.tx',
'.vscode']
def _mkdir_when_inexists(dirname):
if not os.path.isdir(dirname):
os.makedirs(dirname)
def _install_updatedb():
"""Install a customized updatedb for MacOS and ensure it is executable."""
_mkdir_when_inexists(MAC_UPDB_DST)
_mkdir_when_inexists(constants.INDEX_DIR)
if OSNAME == MACOSX:
shutil.copy2(MAC_UPDB_SRC, os.path.join(MAC_UPDB_DST, UPDATEDB))
os.chmod(os.path.join(MAC_UPDB_DST, UPDATEDB), 0755)
def _delete_indexes():
"""Delete all available index files."""
for index in INDEXES:
if os.path.isfile(index):
os.remove(index)
def has_command(cmd):
"""Detect if the command is available in PATH.
shutil.which('cmd') is only valid in Py3 so we need to customise it.
Args:
cmd: A string of the tested command.
Returns:
True if found, False otherwise."""
paths = os.getenv('PATH', '').split(':')
for path in paths:
if os.path.isfile(os.path.join(path, cmd)):
return True
return False
def run_updatedb(search_root=SEARCH_TOP, output_cache=constants.LOCATE_CACHE,
**kwargs):
"""Run updatedb and generate cache in $ANDROID_HOST_OUT/indexes/mlocate.db
Args:
search_root: The path of the search root(-U).
output_cache: The filename of the updatedb cache(-o).
kwargs: (optional)
prunepaths: A list of paths unwanted to be searched(-e).
prunenames: A list of dirname that won't be cached(-n).
"""
prunenames = kwargs.pop('prunenames', ' '.join(PRUNENAMES))
prunepaths = kwargs.pop('prunepaths', os.path.join(search_root, 'out'))
if kwargs:
raise TypeError('Unexpected **kwargs: %r' % kwargs)
updatedb_cmd = [UPDATEDB, '-l0']
updatedb_cmd.append('-U%s' % search_root)
updatedb_cmd.append('-e%s' % prunepaths)
updatedb_cmd.append('-n%s' % prunenames)
updatedb_cmd.append('-o%s' % output_cache)
try:
_install_updatedb()
except IOError as e:
logging.error('Error installing updatedb: %s', e)
if not has_command(UPDATEDB):
return
logging.debug('Running updatedb... ')
try:
full_env_vars = os.environ.copy()
logging.debug('Executing: %s', updatedb_cmd)
subprocess.check_call(updatedb_cmd, env=full_env_vars)
except (KeyboardInterrupt, SystemExit):
logging.error('Process interrupted or failure.')
def _dump_index(dump_file, output, output_re, key, value):
"""Dump indexed data with pickle.
Args:
dump_file: A string of absolute path of the index file.
output: A string generated by locate and grep.
output_re: An regex which is used for grouping patterns.
key: A string for dictionary key, e.g. classname, package, cc_class, etc.
value: A set of path.
The data structure will be like:
{
'Foo': {'/path/to/Foo.java', '/path2/to/Foo.kt'},
'Boo': {'/path3/to/Boo.java'}
}
"""
_dict = {}
with open(dump_file, 'wb') as cache_file:
for entry in output.splitlines():
match = output_re.match(entry)
if match:
_dict.setdefault(match.group(key), set()).add(match.group(value))
try:
pickle.dump(_dict, cache_file, protocol=2)
except IOError:
os.remove(dump_file)
logging.error('Failed in dumping %s', dump_file)
def _get_cc_result(locatedb=None):
"""Search all testable cc/cpp and grep TEST(), TEST_F() or TEST_P().
Returns:
A string object generated by subprocess.
"""
if not locatedb:
locatedb = constants.LOCATE_CACHE
cc_grep_re = r'^\s*TEST(_P|_F)?\s*\([[:alnum:]]+,'
if OSNAME == MACOSX:
find_cmd = (r"locate -d {0} '*.cpp' '*.cc' | grep -i test "
"| xargs egrep -sH '{1}' || true")
else:
find_cmd = (r"locate -d {0} / | egrep -i '/*.test.*\.(cc|cpp)$' "
"| xargs egrep -sH '{1}' || true")
find_cc_cmd = find_cmd.format(locatedb, cc_grep_re)
logging.debug('Probing CC classes:\n %s', find_cc_cmd)
return subprocess.check_output(find_cc_cmd, shell=True)
def _get_java_result(locatedb=None):
"""Search all testable java/kt and grep package.
Returns:
A string object generated by subprocess.
"""
if not locatedb:
locatedb = constants.LOCATE_CACHE
package_grep_re = r'^\s*package\s+[a-z][[:alnum:]]+[^{]'
if OSNAME == MACOSX:
find_cmd = r"locate -d%s '*.java' '*.kt'|grep -i test" % locatedb
else:
find_cmd = r"locate -d%s / | egrep -i '/*.test.*\.(java|kt)$'" % locatedb
find_java_cmd = find_cmd + '| xargs egrep -sH \'%s\' || true' % package_grep_re
logging.debug('Probing Java classes:\n %s', find_java_cmd)
return subprocess.check_output(find_java_cmd, shell=True)
def _index_testable_modules(index):
"""Dump testable modules read by tab completion.
Args:
index: A string path of the index file.
"""
logging.debug('indexing testable modules.')
testable_modules = module_info.ModuleInfo().get_testable_modules()
with open(index, 'wb') as cache:
try:
pickle.dump(testable_modules, cache, protocol=2)
except IOError:
os.remove(cache)
logging.error('Failed in dumping %s', cache)
def _index_cc_classes(output, index):
"""Index Java classes.
The data structure is like:
{
'FooTestCase': {'/path1/to/the/FooTestCase.java',
'/path2/to/the/FooTestCase.kt'}
}
Args:
output: A string object generated by _get_cc_result().
index: A string path of the index file.
"""
logging.debug('indexing CC classes.')
_dump_index(dump_file=index, output=output,
output_re=constants.CC_OUTPUT_RE,
key='test_name', value='file_path')
def _index_java_classes(output, index):
"""Index Java classes.
The data structure is like:
{
'FooTestCase': {'/path1/to/the/FooTestCase.java',
'/path2/to/the/FooTestCase.kt'}
}
Args:
output: A string object generated by _get_java_result().
index: A string path of the index file.
"""
logging.debug('indexing Java classes.')
_dump_index(dump_file=index, output=output,
output_re=constants.CLASS_OUTPUT_RE,
key='class', value='java_path')
def _index_packages(output, index):
"""Index Java packages.
The data structure is like:
{
'a.b.c.d': {'/path1/to/a/b/c/d/',
'/path2/to/a/b/c/d/'
}
Args:
output: A string object generated by _get_java_result().
index: A string path of the index file.
"""
logging.debug('indexing packages.')
_dump_index(dump_file=index,
output=output, output_re=constants.PACKAGE_OUTPUT_RE,
key='package', value='java_dir')
def _index_qualified_classes(output, index):
"""Index Fully Qualified Java Classes(FQCN).
The data structure is like:
{
'a.b.c.d.FooTestCase': {'/path1/to/a/b/c/d/FooTestCase.java',
'/path2/to/a/b/c/d/FooTestCase.kt'}
}
Args:
output: A string object generated by _get_java_result().
index: A string path of the index file.
"""
logging.debug('indexing qualified classes.')
_dict = {}
with open(index, 'wb') as cache_file:
for entry in output.split('\n'):
match = constants.QCLASS_OUTPUT_RE.match(entry)
if match:
fqcn = match.group('package') + '.' + match.group('class')
_dict.setdefault(fqcn, set()).add(match.group('java_path'))
try:
pickle.dump(_dict, cache_file, protocol=2)
except (KeyboardInterrupt, SystemExit):
logging.error('Process interrupted or failure.')
os.remove(index)
except IOError:
logging.error('Failed in dumping %s', index)
def index_targets(output_cache=constants.LOCATE_CACHE, **kwargs):
"""The entrypoint of indexing targets.
Utilise mlocate database to index reference types of CLASS, CC_CLASS,
PACKAGE and QUALIFIED_CLASS. Testable module for tab completion is also
generated in this method.
Args:
output_cache: A file path of the updatedb cache(e.g. /path/to/mlocate.db).
kwargs: (optional)
class_index: A path string of the Java class index.
qclass_index: A path string of the qualified class index.
package_index: A path string of the package index.
cc_class_index: A path string of the CC class index.
module_index: A path string of the testable module index.
integration_index: A path string of the integration index.
"""
class_index = kwargs.pop('class_index', constants.CLASS_INDEX)
qclass_index = kwargs.pop('qclass_index', constants.QCLASS_INDEX)
package_index = kwargs.pop('package_index', constants.PACKAGE_INDEX)
cc_class_index = kwargs.pop('cc_class_index', constants.CC_CLASS_INDEX)
module_index = kwargs.pop('module_index', constants.MODULE_INDEX)
# Uncomment below if we decide to support INTEGRATION.
#integration_index = kwargs.pop('integration_index', constants.INT_INDEX)
if kwargs:
raise TypeError('Unexpected **kwargs: %r' % kwargs)
try:
# Step 0: generate mlocate database prior to indexing targets.
run_updatedb(SEARCH_TOP, constants.LOCATE_CACHE)
if not has_command(LOCATE):
return
# Step 1: generate output string for indexing targets.
logging.debug('Indexing targets... ')
cc_result = _get_cc_result(output_cache)
java_result = _get_java_result(output_cache)
# Step 2: index Java and CC classes.
_index_cc_classes(cc_result, cc_class_index)
_index_java_classes(java_result, class_index)
_index_qualified_classes(java_result, qclass_index)
_index_packages(java_result, package_index)
# Step 3: index testable mods and TEST_MAPPING files.
_index_testable_modules(module_index)
# Delete indexes when mlocate.db is locked() or other CalledProcessError.
# (b/141588997)
except subprocess.CalledProcessError as err:
logging.error('Executing %s error.', UPDATEDB)
metrics_utils.handle_exc_and_send_exit_event(
constants.MLOCATEDB_LOCKED)
if err.output:
logging.error(err.output)
_delete_indexes()
if __name__ == '__main__':
if not os.getenv(constants.ANDROID_HOST_OUT, ''):
sys.exit()
index_targets()