blob: 62b2b0ed3d29a49f09f70d3da0ac3359b551dfc7 [file] [log] [blame]
#!/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.
"""Functional test for aidegen project files."""
from __future__ import absolute_import
from __future__ import print_function
import argparse
import functools
import itertools
import json
import logging
import os
import subprocess
import sys
import xml.etree.ElementTree
import xml.parsers.expat
from aidegen import aidegen_main
from aidegen import constant
from aidegen.lib import clion_project_file_gen
# pylint: disable=no-name-in-module
from aidegen.lib import common_util
from aidegen.lib import errors
from aidegen.lib import module_info_util
from aidegen.lib import project_config
from aidegen.lib import project_file_gen
from atest import module_info
_PRODUCT_DIR = '$PROJECT_DIR$'
_ROOT_DIR = os.path.join(common_util.get_android_root_dir(),
'tools/asuite/aidegen_functional_test')
_TEST_DATA_PATH = os.path.join(_ROOT_DIR, 'test_data')
_VERIFY_COMMANDS_JSON = os.path.join(_TEST_DATA_PATH, 'verify_commands.json')
_GOLDEN_SAMPLES_JSON = os.path.join(_TEST_DATA_PATH, 'golden_samples.json')
_VERIFY_BINARY_JSON = os.path.join(_TEST_DATA_PATH, 'verify_binary_upload.json')
_VERIFY_PRESUBMIT_JSON = os.path.join(_TEST_DATA_PATH, 'verify_presubmit.json')
_ANDROID_COMMON = 'android_common'
_LINUX_GLIBC_COMMON = 'linux_glibc_common'
_SRCS = 'srcs'
_JARS = 'jars'
_URL = 'url'
_TEST_ERROR = 'AIDEGen functional test error: {}-{} is different.'
_MSG_NOT_IN_PROJECT_FILE = ('{} is expected, but not found in the created '
'project file: {}')
_MSG_NOT_IN_SAMPLE_DATA = ('{} is unexpected, but found in the created project '
'file: {}')
_ALL_PASS = 'All tests passed!'
_GIT_COMMIT_ID_JSON = os.path.join(
_TEST_DATA_PATH, 'baseline_code_commit_id.json')
_GIT = 'git'
_CHECKOUT = 'checkout'
_BRANCH = 'branch'
_COMMIT = 'commit'
_LOG = 'log'
_ALL = '--all'
_COMMIT_ID_NOT_EXIST_ERROR = ('Commit ID: {} does not exist in path: {}. '
'Please use "git log" command to check if it '
'exists. If it does not, try to update your '
'source files to the latest version or do not '
'use "repo init --depth=1" command.')
_GIT_LOG_ERROR = 'Command "git log -n 1" failed.'
_BE_REPLACED = '${config.X86_64GccRoot}'
_TO_REPLACE = 'prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.9'
def _parse_args(args):
"""Parse command line arguments.
Args:
args: A list of arguments.
Returns:
An argparse.Namespace object holding parsed args.
"""
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
usage='aidegen_functional_test [-c | -u | -b | -a] -v -r')
group = parser.add_mutually_exclusive_group()
parser.required = False
parser.add_argument(
'targets',
type=str,
nargs='*',
default=[''],
help='Android module name or path.e.g. frameworks/base')
group.add_argument(
'-c',
'--create-sample',
action='store_true',
dest='create_sample',
help=('Create aidegen project files and write data to sample json file '
'for aidegen_functional_test to compare.'))
parser.add_argument(
'-v',
'--verbose',
action='store_true',
help='Show DEBUG level logging.')
parser.add_argument(
'-r',
'--remove_bp_json',
action='store_true',
help='Remove module_bp_java_deps.json for each use case test.')
parser.add_argument(
'-m',
'--make_clean',
action='store_true',
help=('Make clean before testing to create a clean environment, the '
'aidegen_functional_test can run only once if users command it.'))
group.add_argument(
'-u',
'--use_cases',
action='store_true',
dest='use_cases_verified',
help='Verify various use cases of executing aidegen.')
group.add_argument(
'-b',
action='store_true',
dest='binary_upload_verified',
help=('Verify aidegen\'s use cases by executing different aidegen '
'commands.'))
group.add_argument(
'-p',
action='store_true',
dest='binary_presubmit_verified',
help=('Verify aidegen\'s tool in presubmit test by executing'
'different aidegen commands.'))
group.add_argument(
'-a',
'--test-all',
action='store_true',
dest='test_all_samples',
help='Test all modules listed in module-info.json.')
group.add_argument(
'-n',
'--compare-sample-native',
action='store_true',
dest='compare_sample_native',
help=('Compare if sample native project file is the same as generated '
'by the build system.'))
return parser.parse_args(args)
def _import_project_file_xml_etree(filename):
"""Import iml project file and load its data into a dictionary.
Args:
filename: The input project file name.
Returns:
A dictionary contains dependent files' data of project file's contents.
The samples are like:
"srcs": [
...
"file://$PROJECT_DIR$/frameworks/base/cmds/am/src",
"file://$PROJECT_DIR$/frameworks/base/cmds/appwidget/src",
...
]
"jars": [
...
"jar://$PROJECT_DIR$/out/host/common/obj/**/classes-header.jar!/"
...
]
Raises:
EnvironmentError and xml parsing or format errors.
"""
data = {}
try:
tree = xml.etree.ElementTree.parse(filename)
data[_SRCS] = []
root = tree.getroot()
for element in root.iter('sourceFolder'):
src = element.get(_URL).replace(common_util.get_android_root_dir(),
_PRODUCT_DIR)
data[_SRCS].append(src)
data[_JARS] = []
for element in root.iter('root'):
jar = element.get(_URL).replace(common_util.get_android_root_dir(),
_PRODUCT_DIR)
data[_JARS].append(jar)
except (EnvironmentError, ValueError, LookupError,
xml.parsers.expat.ExpatError) as err:
print("{0}: import error: {1}".format(os.path.basename(filename), err))
raise
return data
def _get_project_file_names(abs_path):
"""Get project file name and depenencies name by relative path.
Args:
abs_path: an absolute module's path.
Returns:
file_name: a string of the project file name.
dep_name: a string of the merged project and dependencies file's name,
e.g., frameworks-dependencies.iml.
"""
# pylint: disable=maybe-no-member
code_name = project_file_gen.ProjectFileGenerator.get_unique_iml_name(
abs_path)
file_name = ''.join([code_name, '.iml'])
dep_name = ''.join([constant.KEY_DEPENDENCIES, '.iml'])
return file_name, dep_name
def _get_unique_module_name(rel_path, filename):
"""Get a unique project file name or dependencies name by relative path.
Args:
rel_path: a relative module's path to aosp root path.
filename: the file name to be generated in module_in type file name.
Returns:
A string, the unique file name for the whole module-info.json.
"""
code_names = rel_path.split(os.sep)
code_names[-1] = filename
return '-'.join(code_names)
def _get_git_current_commit_id(abs_path):
"""Get target's git checkout command list.
When we run command 'git log -n 1' and get the top first git log record, the
commit id is next to the specific word 'commit'.
Args:
abs_path: a string of the absolute path of the target branch.
Return:
The current git commit id.
Raises:
Call subprocess.check_output cause subprocess.CalledProcessError.
"""
cwd = os.getcwd()
os.chdir(abs_path)
git_log_cmds = [_GIT, _LOG, '-n', '1']
try:
out_put = subprocess.check_output(git_log_cmds).decode("utf-8")
except subprocess.CalledProcessError:
logging.error(_GIT_LOG_ERROR)
raise
finally:
os.chdir(cwd)
com_list = out_put.split()
return com_list[com_list.index(_COMMIT) + 1]
def _get_commit_id_dictionary():
"""Get commit id from dictionary of key, value 'module': 'commit id'."""
data_id_dict = {}
with open(_GIT_COMMIT_ID_JSON, 'r') as jsfile:
data_id_dict = json.load(jsfile)
return data_id_dict
def _git_checkout_commit_id(abs_path, commit_id):
"""Command to checkout specific commit id.
Change directory to the module's absolute path which users want to get its
specific commit id.
Args:
abs_path: the absolute path of the target branch. E.g., abs_path/.git
commit_id: the commit id users want to checkout.
Raises:
Call git checkout commit id failed, raise errors.CommitIDNotExistError.
"""
git_chekout_cmds = [_GIT, _CHECKOUT, commit_id]
cwd = os.getcwd()
try:
os.chdir(abs_path)
subprocess.check_output(git_chekout_cmds)
except subprocess.CalledProcessError:
err = _COMMIT_ID_NOT_EXIST_ERROR.format(commit_id, abs_path)
logging.error(err)
raise errors.CommitIDNotExistError(err)
finally:
os.chdir(cwd)
def _git_checkout_target_commit_id(target, commit_id):
"""Command to checkout target commit id.
Switch code base to specific commit id which is kept in data_id_dict with
target: commit_id as key: value. If the data is missing in data_id_dict, the
target isn't a selected golden sample raise error for it.
Args:
target: the string of target's module name or module path to checkout
the relevant git to its specific commit id.
commit_id: a string represent target's specific commit id.
Returns:
current_commit_id: the current commit id of the target which should be
switched back to.
"""
atest_module_info = module_info.ModuleInfo()
_, abs_path = common_util.get_related_paths(atest_module_info, target)
current_commit_id = _get_git_current_commit_id(abs_path)
_git_checkout_commit_id(abs_path, commit_id)
return current_commit_id
def _checkout_baseline_code_to_spec_commit_id():
"""Get a dict of target, commit id listed in baseline_code_commit_id.json.
To make sure all samples run on the same environment, we need to keep all
the baseline code in a specific commit id. For example, all samples should
be created in the same specific commit id of the baseline code
'frameworks/base' for comparison except 'frameworks/base' itself.
Returns:
A dictionary contains target, specific and current commit id.
"""
spec_and_cur_commit_id_dict = {}
data_id_dict = _get_commit_id_dictionary()
for target in data_id_dict:
commit_id = data_id_dict.get(target, '')
current_commit_id = _git_checkout_target_commit_id(target, commit_id)
spec_and_cur_commit_id_dict[target] = {}
spec_and_cur_commit_id_dict[target]['current'] = current_commit_id
return spec_and_cur_commit_id_dict
def _generate_target_real_iml_data(target):
"""Generate single target's real iml file content's data.
Args:
target: Android module name or path to be create iml data.
Returns:
data: A dictionary contains key: unique file name and value: iml
content.
"""
data = {}
try:
aidegen_main.main([target, '-s', '-n', '-v'])
except (errors.FakeModuleError,
errors.ProjectOutsideAndroidRootError,
errors.ProjectPathNotExistError,
errors.NoModuleDefinedInModuleInfoError) as err:
logging.error(str(err))
return data
atest_module_info = module_info.ModuleInfo()
rel_path, abs_path = common_util.get_related_paths(atest_module_info,
target)
for filename in iter(_get_project_file_names(abs_path)):
real_iml_file = os.path.join(abs_path, filename)
item_name = _get_unique_module_name(rel_path, filename)
data[item_name] = _import_project_file_xml_etree(real_iml_file)
return data
def _generate_sample_json(test_list):
"""Generate sample iml data.
We use all baseline code samples on the version of their own specific commit
id which is kept in _GIT_COMMIT_ID_JSON file. We need to switch back to
their current commit ids after generating golden samples' data.
Args:
test_list: a list of module name and module path.
Returns:
data: a dictionary contains dependent files' data of project file's
contents.
The sample is like:
"frameworks-base.iml": {
"srcs": [
....
"file://$PROJECT_DIR$/frameworks/base/cmds/am/src",
"file://$PROJECT_DIR$/frameworks/base/cmds/appwidget/src",
....
],
"jars": [
....
"jar://$PROJECT_DIR$/out/target/common/**/aapt2.srcjar!/",
....
]
}
"""
data = {}
spec_and_cur_commit_id_dict = _checkout_baseline_code_to_spec_commit_id()
for target in test_list:
data.update(_generate_target_real_iml_data(target))
atest_module_info = module_info.ModuleInfo()
for target in spec_and_cur_commit_id_dict:
_, abs_path = common_util.get_related_paths(atest_module_info, target)
_git_checkout_commit_id(
abs_path, spec_and_cur_commit_id_dict[target]['current'])
return data
def _create_some_sample_json_file(targets):
"""Write some samples' iml data into a json file.
Args:
targets: Android module name or path to be create iml data.
linked_function: _generate_sample_json()
"""
data = _generate_sample_json(targets)
data_sample = {}
with open(_GOLDEN_SAMPLES_JSON, 'r') as infile:
try:
data_sample = json.load(infile)
# pylint: disable=maybe-no-member
except json.JSONDecodeError as err:
print("Json decode error: {}".format(err))
data_sample = {}
data_sample.update(data)
with open(_GOLDEN_SAMPLES_JSON, 'w') as outfile:
json.dump(data_sample, outfile, indent=4, sort_keys=False)
def test_samples(func):
"""Decorate a function to deal with preparing and verifying staffs of it.
Args:
func: a function is to be compared its iml data with the json file's
data.
Returns:
The wrapper function.
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""A wrapper function."""
test_successful = True
with open(_GOLDEN_SAMPLES_JSON, 'r') as outfile:
data_sample = json.load(outfile)
data_real = func(*args, **kwargs)
for name in data_real:
for item in [_SRCS, _JARS]:
s_items = data_sample[name][item]
r_items = data_real[name][item]
if set(s_items) != set(r_items):
diff_iter = _compare_content(name, item, s_items, r_items)
if diff_iter:
print('\n{} {}'.format(
common_util.COLORED_INFO('Test error:'),
_TEST_ERROR.format(name, item)))
print('{} {} contents are different:'.format(
name, item))
for diff in diff_iter:
print(diff)
test_successful = False
if test_successful:
print(common_util.COLORED_PASS(_ALL_PASS))
return test_successful
return wrapper
@test_samples
def _test_some_sample_iml(targets=None):
"""Compare with sample iml's data to assure the project's contents is right.
Args:
targets: Android module name or path to be create iml data.
"""
if targets:
return _generate_sample_json(targets)
return _generate_sample_json(_get_commit_id_dictionary().keys())
@test_samples
def _test_all_samples_iml():
"""Compare all imls' data with all samples' data.
It's to make sure each iml's contents is right. The function is implemented
but hasn't been used yet.
"""
all_module_list = module_info.ModuleInfo().name_to_module_info.keys()
return _generate_sample_json(all_module_list)
def _compare_content(module_name, item_type, s_items, r_items):
"""Compare src or jar files' data of two dictionaries.
Args:
module_name: the test module name.
item_type: the type is src or jar.
s_items: sample jars' items.
r_items: real jars' items.
Returns:
An iterator of not equal sentences after comparison.
"""
if item_type == _SRCS:
cmp_iter1 = _compare_srcs_content(module_name, s_items, r_items,
_MSG_NOT_IN_PROJECT_FILE)
cmp_iter2 = _compare_srcs_content(module_name, r_items, s_items,
_MSG_NOT_IN_SAMPLE_DATA)
else:
cmp_iter1 = _compare_jars_content(module_name, s_items, r_items,
_MSG_NOT_IN_PROJECT_FILE)
cmp_iter2 = _compare_jars_content(module_name, r_items, s_items,
_MSG_NOT_IN_SAMPLE_DATA)
return itertools.chain(cmp_iter1, cmp_iter2)
def _compare_srcs_content(module_name, s_items, r_items, msg):
"""Compare src or jar files' data of two dictionaries.
Args:
module_name: the test module name.
s_items: sample jars' items.
r_items: real jars' items.
msg: the message will be written into log file.
Returns:
An iterator of not equal sentences after comparison.
"""
for sample in s_items:
if sample not in r_items:
yield msg.format(sample, module_name)
def _compare_jars_content(module_name, s_items, r_items, msg):
"""Compare src or jar files' data of two dictionaries.
AIDEGen treats the jars in folder names 'linux_glib_common' and
'android_common' as the same content. If the paths are different only
because of these two names, we ignore it.
Args:
module_name: the test module name.
s_items: sample jars' items.
r_items: real jars' items.
msg: the message will be written into log file.
Returns:
An iterator of not equal sentences after comparison.
"""
for sample in s_items:
if sample not in r_items:
lnew = sample
if constant.LINUX_GLIBC_COMMON in sample:
lnew = sample.replace(constant.LINUX_GLIBC_COMMON,
constant.ANDROID_COMMON)
else:
lnew = sample.replace(constant.ANDROID_COMMON,
constant.LINUX_GLIBC_COMMON)
if not lnew in r_items:
yield msg.format(sample, module_name)
# pylint: disable=broad-except
# pylint: disable=eval-used
@common_util.back_to_cwd
@common_util.time_logged
def _verify_aidegen(verified_file_path, forced_remove_bp_json,
is_presubmit=False):
"""Verify various use cases of executing aidegen.
There are two types of running commands:
1. Use 'eval' to run the commands for present codes in aidegen_main.py,
such as:
aidegen_main.main(['tradefed', '-n', '-v'])
2. Use 'subprocess.check_call' to run the commands for the binary codes of
aidegen such as:
aidegen tradefed -n -v
Remove module_bp_java_deps.json in the beginning of running use cases. If
users need to remove module_bp_java_deps.json between each use case they
can set forced_remove_bp_json true.
Args:
verified_file_path: The json file path to be verified.
forced_remove_bp_json: Remove module_bp_java_deps.json for each use case
test.
Raises:
There are two type of exceptions:
1. aidegen.lib.errors for projects' or modules' issues such as,
ProjectPathNotExistError.
2. Any exceptions other than aidegen.lib.errors such as,
subprocess.CalledProcessError.
"""
bp_json_path = common_util.get_blueprint_json_path(
constant.BLUEPRINT_JAVA_JSONFILE_NAME)
use_eval = (verified_file_path == _VERIFY_COMMANDS_JSON)
try:
with open(verified_file_path, 'r') as jsfile:
data = json.load(jsfile)
except IOError as err:
raise errors.JsonFileNotExistError(
'%s does not exist, error: %s.' % (verified_file_path, err))
if not is_presubmit:
_compare_sample_native_content()
os.chdir(common_util.get_android_root_dir())
for use_case in data:
print('Use case "{}" is running.'.format(use_case))
if forced_remove_bp_json and os.path.exists(bp_json_path):
os.remove(bp_json_path)
for cmd in data[use_case]:
print('Command "{}" is running.'.format(cmd))
try:
if use_eval:
eval(cmd)
else:
subprocess.check_call(cmd, shell=True)
except (errors.ProjectOutsideAndroidRootError,
errors.ProjectPathNotExistError,
errors.NoModuleDefinedInModuleInfoError,
errors.IDENotExistError) as err:
print('"{}" raises error: {}.'.format(use_case, err))
continue
except BaseException:
exc_type, _, _ = sys.exc_info()
print('"{}.{}" command {}.'.format(
use_case, cmd, common_util.COLORED_FAIL('executes failed')))
raise BaseException(
'Unexpected command "{}" exception: {}.'.format(
use_case, exc_type))
print('"{}" command {}!'.format(
use_case, common_util.COLORED_PASS('test passed')))
print(common_util.COLORED_PASS(_ALL_PASS))
@common_util.back_to_cwd
def _make_clean():
"""Make a command to clean out folder for a pure environment to test.
Raises:
Call subprocess.check_call to execute
'build/soong/soong_ui.bash --make-mode clean' and cause
subprocess.CalledProcessError.
"""
try:
os.chdir(common_util.get_android_root_dir())
subprocess.check_call(
['build/soong/soong_ui.bash --make-mode clean', '-j'],
shell=True)
except subprocess.CalledProcessError:
print('"build/soong/soong_ui.bash --make-mode clean" command failed.')
raise
def _read_file_content(path):
"""Read file's content.
Args:
path: Path of input file.
Returns:
A list of content strings.
"""
with open(path, encoding='utf8') as template:
contents = []
for cnt in template:
if cnt.startswith('#'):
continue
contents.append(cnt.rstrip())
return contents
# pylint: disable=protected-access
def _compare_sample_native_content():
"""Compares 'libui' sample module's project file.
Compares 'libui' sample module's project file generated by AIDEGen with that
generated by the soong build system. Check if their contents are the same.
There should be only one different:
${config.X86_64GccRoot} # in the soong build sytem
becomes
prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.9 # in AIDEGen
"""
target_arch_variant = 'x86_64'
env_on = {
'TARGET_PRODUCT': 'aosp_x86_64',
'TARGET_BUILD_VARIANT': 'eng',
'TARGET_ARCH_VARIANT': target_arch_variant,
'SOONG_COLLECT_JAVA_DEPS': 'true',
'SOONG_GEN_CMAKEFILES': '1',
'SOONG_COLLECT_CC_DEPS': '1'
}
try:
project_config.ProjectConfig(
aidegen_main._parse_args(['-n', '-v'])).init_environment()
module_info_util.generate_merged_module_info(env_on)
cc_path = os.path.join(common_util.get_soong_out_path(),
constant.BLUEPRINT_CC_JSONFILE_NAME)
mod_name = 'libui'
mod_info = common_util.get_json_dict(cc_path)['modules'][mod_name]
if mod_info:
clion_project_file_gen.CLionProjectFileGenerator(
mod_info).generate_cmakelists_file()
out_dir = os.path.join(common_util.get_android_root_dir(),
common_util.get_android_out_dir(),
constant.RELATIVE_NATIVE_PATH,
mod_info['path'][0])
content1 = _read_file_content(os.path.join(
out_dir, mod_name, constant.CLION_PROJECT_FILE_NAME))
cc_file_name = ''.join(
[mod_name, '-', target_arch_variant, '-android'])
cc_file_path = os.path.join(
out_dir, cc_file_name, constant.CLION_PROJECT_FILE_NAME)
content2 = _read_file_content(cc_file_path)
same = True
for lino, (cnt1, cnt2) in enumerate(
zip(content1, content2), start=1):
if _BE_REPLACED in cnt2:
cnt2 = cnt2.replace(_BE_REPLACED, _TO_REPLACE)
if cnt1 != cnt2:
print('Contents {} and {} are different in line:{}.'.format(
cnt1, cnt2, lino))
same = False
if same:
print('Files {} and {} are the same.'.format(
mod_name, cc_file_name))
except errors.BuildFailureError:
print('Compare native content failed.')
def main(argv):
"""Main entry.
1. Create the iml file data of each module in module-info.json and write it
into single_module.json.
2. Verify every use case of AIDEGen.
3. Compare all or some iml project files' data to the data recorded in
single_module.json.
Args:
argv: A list of system arguments.
"""
args = _parse_args(argv)
common_util.configure_logging(args.verbose)
os.environ[constant.AIDEGEN_TEST_MODE] = 'true'
if args.make_clean:
_make_clean()
if args.create_sample:
_create_some_sample_json_file(args.targets)
elif args.use_cases_verified:
_verify_aidegen(_VERIFY_COMMANDS_JSON, args.remove_bp_json)
elif args.binary_upload_verified:
_verify_aidegen(_VERIFY_BINARY_JSON, args.remove_bp_json)
elif args.binary_presubmit_verified:
_verify_aidegen(_VERIFY_PRESUBMIT_JSON, args.remove_bp_json, True)
elif args.test_all_samples:
_test_all_samples_iml()
elif args.compare_sample_native:
_compare_sample_native_content()
else:
if not args.targets[0]:
_test_some_sample_iml()
else:
_test_some_sample_iml(args.targets)
del os.environ[constant.AIDEGEN_TEST_MODE]
if __name__ == '__main__':
main(sys.argv[1:])