普通文本  |  350行  |  12.57 KB

#!/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