#!/usr/bin/env python3
#
# 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.
"""common_util
This module has a collection of functions that provide helper functions to
other modules.
"""
import logging
import os
import time
from functools import partial
from functools import wraps
from aidegen import constant
from aidegen.lib.errors import FakeModuleError
from aidegen.lib.errors import NoModuleDefinedInModuleInfoError
from aidegen.lib.errors import ProjectOutsideAndroidRootError
from aidegen.lib.errors import ProjectPathNotExistError
from atest import constants
from atest import module_info
from atest.atest_utils import colorize
COLORED_INFO = partial(colorize, color=constants.MAGENTA, highlight=False)
COLORED_PASS = partial(colorize, color=constants.GREEN, highlight=False)
COLORED_FAIL = partial(colorize, color=constants.RED, highlight=False)
FAKE_MODULE_ERROR = '{} is a fake module.'
OUTSIDE_ROOT_ERROR = '{} is outside android root.'
PATH_NOT_EXISTS_ERROR = 'The path {} doesn\'t exist.'
NO_MODULE_DEFINED_ERROR = 'No modules defined at {}.'
# Java related classes.
JAVA_TARGET_CLASSES = ['APPS', 'JAVA_LIBRARIES', 'ROBOLECTRIC']
# C, C++ related classes.
NATIVE_TARGET_CLASSES = [
'HEADER_LIBRARIES', 'NATIVE_TESTS', 'STATIC_LIBRARIES', 'SHARED_LIBRARIES'
]
TARGET_CLASSES = JAVA_TARGET_CLASSES
TARGET_CLASSES.extend(NATIVE_TARGET_CLASSES)
_REBUILD_MODULE_INFO = '%s We should rebuild module-info.json file for it.'
def time_logged(func=None, *, message='', maximum=1):
"""Decorate a function to find out how much time it spends.
Args:
func: a function is to be calculated its spending time.
message: the message the decorated function wants to show.
maximum: a interger, minutes. If time exceeds the maximum time show
message, otherwise doesn't.
Returns:
The wrapper function.
"""
if func is None:
return partial(time_logged, message=message, maximum=maximum)
@wraps(func)
def wrapper(*args, **kwargs):
"""A wrapper function."""
start = time.time()
try:
return func(*args, **kwargs)
finally:
timestamp = time.time() - start
logging.debug('{}.{} takes: {:.2f}s'.format(
func.__module__, func.__name__, timestamp))
if message and timestamp > maximum * 60:
print(message)
return wrapper
def get_related_paths(atest_module_info, target=None):
"""Get the relative and absolute paths of target from module-info.
Args:
atest_module_info: A ModuleInfo instance.
target: A string user input from command line. It could be several cases
such as:
1. Module name, e.g. Settings
2. Module path, e.g. packages/apps/Settings
3. Relative path, e.g. ../../packages/apps/Settings
4. Current directory, e.g. . or no argument
Return:
rel_path: The relative path of a module, return None if no matching
module found.
abs_path: The absolute path of a module, return None if no matching
module found.
"""
rel_path = None
abs_path = None
if target:
# User inputs a module name.
if atest_module_info.is_module(target):
paths = atest_module_info.get_paths(target)
if paths:
rel_path = paths[0]
abs_path = os.path.join(constant.ANDROID_ROOT_PATH, rel_path)
# User inputs a module path or a relative path of android root folder.
elif (atest_module_info.get_module_names(target) or os.path.isdir(
os.path.join(constant.ANDROID_ROOT_PATH, target))):
rel_path = target.strip(os.sep)
abs_path = os.path.join(constant.ANDROID_ROOT_PATH, rel_path)
# User inputs a relative path of current directory.
else:
abs_path = os.path.abspath(os.path.join(os.getcwd(), target))
rel_path = os.path.relpath(abs_path, constant.ANDROID_ROOT_PATH)
else:
# User doesn't input.
abs_path = os.getcwd()
if is_android_root(abs_path):
rel_path = ''
else:
rel_path = os.path.relpath(abs_path, constant.ANDROID_ROOT_PATH)
return rel_path, abs_path
def is_target_android_root(atest_module_info, targets):
"""Check if any target is the Android root path.
Args:
atest_module_info: A ModuleInfo instance contains data of
module-info.json.
targets: A list of target modules or project paths from user input.
Returns:
True if target is Android root, otherwise False.
"""
for target in targets:
_, abs_path = get_related_paths(atest_module_info, target)
if is_android_root(abs_path):
return True
return False
def is_android_root(abs_path):
"""Check if an absolute path is the Android root path.
Args:
abs_path: The absolute path of a module.
Returns:
True if abs_path is Android root, otherwise False.
"""
return abs_path == constant.ANDROID_ROOT_PATH
def has_build_target(atest_module_info, rel_path):
"""Determine if a relative path contains buildable module.
Args:
atest_module_info: A ModuleInfo instance contains data of
module-info.json.
rel_path: The module path relative to android root.
Returns:
True if the relative path contains a build target, otherwise false.
"""
return any(
mod_path.startswith(rel_path)
for mod_path in atest_module_info.path_to_module_info)
def _check_modules(atest_module_info, targets, raise_on_lost_module=True):
"""Check if all targets are valid build targets.
Args:
atest_module_info: A ModuleInfo instance contains data of
module-info.json.
targets: A list of target modules or project paths from user input.
When locating the path of the target, given a matched module
name has priority over path. Below is the priority of locating a
target:
1. Module name, e.g. Settings
2. Module path, e.g. packages/apps/Settings
3. Relative path, e.g. ../../packages/apps/Settings
4. Current directory, e.g. . or no argument
raise_on_lost_module: A boolean, pass to _check_module to determine if
ProjectPathNotExistError or NoModuleDefinedInModuleInfoError
should be raised.
Returns:
True if any _check_module return flip the True/False.
"""
for target in targets:
if not _check_module(atest_module_info, target, raise_on_lost_module):
return False
return True
def _check_module(atest_module_info, target, raise_on_lost_module=True):
"""Check if a target is valid or it's a path containing build target.
Args:
atest_module_info: A ModuleInfo instance contains the data of
module-info.json.
target: A target module or project path from user input.
When locating the path of the target, given a matched module
name has priority over path. Below is the priority of locating a
target:
1. Module name, e.g. Settings
2. Module path, e.g. packages/apps/Settings
3. Relative path, e.g. ../../packages/apps/Settings
4. Current directory, e.g. . or no argument
raise_on_lost_module: A boolean, handles if ProjectPathNotExistError or
NoModuleDefinedInModuleInfoError should be raised.
Returns:
1. If there is no error _check_module always return True.
2. If there is a error,
a. When raise_on_lost_module is False, _check_module will raise the
error.
b. When raise_on_lost_module is True, _check_module will return
False if module's error is ProjectPathNotExistError or
NoModuleDefinedInModuleInfoError else raise the error.
Raises:
Raise ProjectPathNotExistError and NoModuleDefinedInModuleInfoError only
when raise_on_lost_module is True, others don't subject to the limit.
The rules for raising exceptions:
1. Absolute path of a module is None -> FakeModuleError
2. Module doesn't exist in repo root -> ProjectOutsideAndroidRootError
3. The given absolute path is not a dir -> ProjectPathNotExistError
4. If the given abs path doesn't contain any target and not repo root
-> NoModuleDefinedInModuleInfoError
"""
rel_path, abs_path = get_related_paths(atest_module_info, target)
if not abs_path:
err = FAKE_MODULE_ERROR.format(target)
logging.error(err)
raise FakeModuleError(err)
if not abs_path.startswith(constant.ANDROID_ROOT_PATH):
err = OUTSIDE_ROOT_ERROR.format(abs_path)
logging.error(err)
raise ProjectOutsideAndroidRootError(err)
if not os.path.isdir(abs_path):
err = PATH_NOT_EXISTS_ERROR.format(rel_path)
if raise_on_lost_module:
logging.error(err)
raise ProjectPathNotExistError(err)
logging.debug(_REBUILD_MODULE_INFO, err)
return False
if (not has_build_target(atest_module_info, rel_path)
and not is_android_root(abs_path)):
err = NO_MODULE_DEFINED_ERROR.format(rel_path)
if raise_on_lost_module:
logging.error(err)
raise NoModuleDefinedInModuleInfoError(err)
logging.debug(_REBUILD_MODULE_INFO, err)
return False
return True
def get_abs_path(rel_path):
"""Get absolute path from a relative path.
Args:
rel_path: A string, a relative path to constant.ANDROID_ROOT_PATH.
Returns:
abs_path: A string, an absolute path starts with
constant.ANDROID_ROOT_PATH.
"""
if not rel_path:
return constant.ANDROID_ROOT_PATH
if rel_path.startswith(constant.ANDROID_ROOT_PATH):
return rel_path
return os.path.join(constant.ANDROID_ROOT_PATH, rel_path)
def is_project_path_relative_module(data, project_relative_path):
"""Determine if the given project path is relative to the module.
The rules:
1. If project_relative_path is empty, it's under Android root, return
True.
2. If module's path equals or starts with project_relative_path return
True, otherwise return False.
Args:
data: the module-info dictionary of the checked module.
project_relative_path: project's relative path
Returns:
True if it's the given project path is relative to the module, otherwise
False.
"""
if 'path' not in data:
return False
path = data['path'][0]
if project_relative_path == '':
return True
if ('class' in data
and (path == project_relative_path
or path.startswith(project_relative_path + os.sep))):
return True
return False
def is_target(src_file, src_file_extensions):
"""Check if src_file is a type of src_file_extensions.
Args:
src_file: A string of the file path to be checked.
src_file_extensions: A list of file types to be checked
Returns:
True if src_file is one of the types of src_file_extensions, otherwise
False.
"""
return any(src_file.endswith(x) for x in src_file_extensions)
def get_atest_module_info(targets):
"""Get the right version of atest ModuleInfo instance.
The rules:
Check if the targets don't exist in ModuleInfo, we'll regain a new atest
ModleInfo instance by setting force_build=True and call _check_modules
again. If targets still don't exist, raise exceptions.
Args:
targets: A list of targets to be built.
Returns:
An atest ModuleInfo instance.
"""
amodule_info = module_info.ModuleInfo()
if not _check_modules(amodule_info, targets, raise_on_lost_module=False):
amodule_info = module_info.ModuleInfo(force_build=True)
_check_modules(amodule_info, targets)
return amodule_info