1# Copyright 2017, The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15#pylint: disable=too-many-lines 16""" 17Command Line Translator for atest. 18""" 19 20import json 21import logging 22import os 23import sys 24import time 25 26import atest_error 27import constants 28import test_finder_handler 29import test_mapping 30 31TEST_MAPPING = 'TEST_MAPPING' 32 33 34#pylint: disable=no-self-use 35class CLITranslator(object): 36 """ 37 CLITranslator class contains public method translate() and some private 38 helper methods. The atest tool can call the translate() method with a list 39 of strings, each string referencing a test to run. Translate() will 40 "translate" this list of test strings into a list of build targets and a 41 list of TradeFederation run commands. 42 43 Translation steps for a test string reference: 44 1. Narrow down the type of reference the test string could be, i.e. 45 whether it could be referencing a Module, Class, Package, etc. 46 2. Try to find the test files assuming the test string is one of these 47 types of reference. 48 3. If test files found, generate Build Targets and the Run Command. 49 """ 50 51 def __init__(self, module_info=None): 52 """CLITranslator constructor 53 54 Args: 55 module_info: ModuleInfo class that has cached module-info.json. 56 """ 57 self.mod_info = module_info 58 59 def _get_test_infos(self, tests, test_mapping_test_details=None): 60 """Return set of TestInfos based on passed in tests. 61 62 Args: 63 tests: List of strings representing test references. 64 test_mapping_test_details: List of TestDetail for tests configured 65 in TEST_MAPPING files. 66 67 Returns: 68 Set of TestInfos based on the passed in tests. 69 """ 70 test_infos = set() 71 if not test_mapping_test_details: 72 test_mapping_test_details = [None] * len(tests) 73 for test, tm_test_detail in zip(tests, test_mapping_test_details): 74 test_found = False 75 for finder in test_finder_handler.get_find_methods_for_test( 76 self.mod_info, test): 77 # For tests in TEST_MAPPING, find method is only related to 78 # test name, so the details can be set after test_info object 79 # is created. 80 test_info = finder.find_method(finder.test_finder_instance, 81 test) 82 if test_info: 83 if tm_test_detail: 84 test_info.data[constants.TI_MODULE_ARG] = ( 85 tm_test_detail.options) 86 test_infos.add(test_info) 87 test_found = True 88 break 89 if not test_found: 90 raise atest_error.NoTestFoundError('No test found for: %s' % 91 test) 92 return test_infos 93 94 def _find_tests_by_test_mapping( 95 self, path='', test_group=constants.TEST_GROUP_PRESUBMIT, 96 file_name=TEST_MAPPING): 97 """Find tests defined in TEST_MAPPING in the given path. 98 99 Args: 100 path: A string of path in source. Default is set to '', i.e., CWD. 101 test_group: Group of tests to run. Default is set to `presubmit`. 102 file_name: Name of TEST_MAPPING file. Default is set to 103 `TEST_MAPPING`. The argument is added for testing purpose. 104 105 Returns: 106 A tuple of (tests, all_tests), where, 107 tests is a set of tests (test_mapping.TestDetail) defined in 108 TEST_MAPPING file of the given path, and its parent directories, 109 with matching test_group. 110 all_tests is a dictionary of all tests in TEST_MAPPING files, 111 grouped by test group. 112 """ 113 path = os.path.realpath(path) 114 if path == constants.ANDROID_BUILD_TOP or path == os.sep: 115 return None, None 116 tests = set() 117 all_tests = {} 118 test_mapping_dict = None 119 test_mapping_file = os.path.join(path, file_name) 120 if os.path.exists(test_mapping_file): 121 with open(test_mapping_file) as json_file: 122 test_mapping_dict = json.load(json_file) 123 for test_group_name, test_list in test_mapping_dict.items(): 124 grouped_tests = all_tests.setdefault(test_group_name, set()) 125 grouped_tests.update( 126 [test_mapping.TestDetail(test) for test in test_list]) 127 for test in test_mapping_dict.get(test_group, []): 128 tests.add(test_mapping.TestDetail(test)) 129 parent_dir_tests, parent_dir_all_tests = ( 130 self._find_tests_by_test_mapping( 131 os.path.dirname(path), test_group, file_name)) 132 if parent_dir_tests: 133 tests.update(parent_dir_tests) 134 if parent_dir_all_tests: 135 for test_group_name, test_list in parent_dir_all_tests.items(): 136 grouped_tests = all_tests.setdefault(test_group_name, set()) 137 grouped_tests.update(test_list) 138 if test_group == constants.TEST_GROUP_POSTSUBMIT: 139 tests.update(all_tests.get( 140 constants.TEST_GROUP_PRESUBMIT, set())) 141 return tests, all_tests 142 143 def _gather_build_targets(self, test_infos): 144 targets = set() 145 for test_info in test_infos: 146 targets |= test_info.build_targets 147 return targets 148 149 def translate(self, tests): 150 """Translate atest command line into build targets and run commands. 151 152 Args: 153 tests: A list of strings referencing the tests to run. 154 155 Returns: 156 A tuple with set of build_target strings and list of TestInfos. 157 """ 158 # Test details from TEST_MAPPING files 159 test_details_list = None 160 if not tests: 161 # Pull out tests from test mapping 162 # TODO(dshi): Support other groups of tests in TEST_MAPPING files, 163 # e.g., postsubmit. 164 test_details, all_test_details = self._find_tests_by_test_mapping() 165 test_details_list = list(test_details) 166 if test_details_list: 167 tests = [detail.name for detail in test_details_list] 168 else: 169 logging.warn( 170 'No tests of group %s found in TEST_MAPPING at %s or its ' 171 'parent directories.\nYou might be missing atest arguments,' 172 ' try `atest --help` for more information', 173 constants.TEST_GROUP_PRESUBMIT, os.path.realpath('')) 174 if all_test_details: 175 tests = '' 176 for test_group, test_list in all_test_details.items(): 177 tests += '%s:\n' % test_group 178 for test_detail in sorted(test_list): 179 tests += '\t%s\n' % test_detail 180 logging.warn( 181 'All available tests in TEST_MAPPING files are:\n%s', 182 tests) 183 sys.exit(constants.EXIT_CODE_TEST_NOT_FOUND) 184 logging.info('Finding tests: %s', tests) 185 if test_details_list: 186 details = '\n'.join([str(detail) for detail in test_details_list]) 187 logging.info('Test details:\n%s', details) 188 start = time.time() 189 test_infos = self._get_test_infos(tests, test_details_list) 190 end = time.time() 191 logging.debug('Found tests in %ss', end - start) 192 for test_info in test_infos: 193 logging.debug('%s\n', test_info) 194 build_targets = self._gather_build_targets(test_infos) 195 return build_targets, test_infos 196