blob: 7d5a669f2649d25d09b87de889734c5dff815af4 [file] [log] [blame]
mikehoran63d61b42017-07-28 15:28:50 -07001# 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
mikehoran106d74e2018-02-02 14:09:27 -080015#pylint: disable=too-many-lines
mikehoran63d61b42017-07-28 15:28:50 -070016"""
17Command Line Translator for atest.
18"""
19
nelsonli34997d52018-08-17 09:43:28 +080020from __future__ import print_function
21
Dan Shie4e267f2018-06-01 11:31:57 -070022import fnmatch
mikehoran43ed32d2017-08-18 17:13:36 -070023import json
mikehoranbe9102f2017-08-04 16:04:03 -070024import logging
mikehoran43ed32d2017-08-18 17:13:36 -070025import os
Dan Shifa016d12018-02-02 00:37:19 -080026import sys
mikehoran43ed32d2017-08-18 17:13:36 -070027import time
mikehoran63d61b42017-07-28 15:28:50 -070028
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080029import atest_error
Dan Shi0ddd3e42018-05-30 11:24:30 -070030import atest_utils
Dan Shifa016d12018-02-02 00:37:19 -080031import constants
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080032import test_finder_handler
Dan Shicdbda552018-05-18 23:31:33 -070033import test_mapping
Simran Basi3f29a942017-12-21 15:19:31 -080034
Dan Shi8441cba2018-01-30 15:52:29 -080035TEST_MAPPING = 'TEST_MAPPING'
36
mikehoran38b98792017-10-18 16:22:55 -070037
mikehoranbe9102f2017-08-04 16:04:03 -070038#pylint: disable=no-self-use
mikehoran63d61b42017-07-28 15:28:50 -070039class CLITranslator(object):
40 """
41 CLITranslator class contains public method translate() and some private
42 helper methods. The atest tool can call the translate() method with a list
43 of strings, each string referencing a test to run. Translate() will
44 "translate" this list of test strings into a list of build targets and a
45 list of TradeFederation run commands.
mikehoran3d553632017-08-03 14:44:41 -070046
47 Translation steps for a test string reference:
48 1. Narrow down the type of reference the test string could be, i.e.
49 whether it could be referencing a Module, Class, Package, etc.
50 2. Try to find the test files assuming the test string is one of these
51 types of reference.
52 3. If test files found, generate Build Targets and the Run Command.
mikehoran63d61b42017-07-28 15:28:50 -070053 """
54
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080055 def __init__(self, module_info=None):
Simran Basi05384b82018-01-03 16:19:30 -080056 """CLITranslator constructor
57
58 Args:
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080059 module_info: ModuleInfo class that has cached module-info.json.
Simran Basi05384b82018-01-03 16:19:30 -080060 """
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080061 self.mod_info = module_info
mikehoran43ed32d2017-08-18 17:13:36 -070062
Dan Shicdbda552018-05-18 23:31:33 -070063 def _get_test_infos(self, tests, test_mapping_test_details=None):
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080064 """Return set of TestInfos based on passed in tests.
mikehoran43ed32d2017-08-18 17:13:36 -070065
Simran Basi05384b82018-01-03 16:19:30 -080066 Args:
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080067 tests: List of strings representing test references.
Dan Shicdbda552018-05-18 23:31:33 -070068 test_mapping_test_details: List of TestDetail for tests configured
69 in TEST_MAPPING files.
mikehoran3622d9a2017-09-20 15:43:54 -070070
71 Returns:
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080072 Set of TestInfos based on the passed in tests.
mikehoran3622d9a2017-09-20 15:43:54 -070073 """
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080074 test_infos = set()
Dan Shicdbda552018-05-18 23:31:33 -070075 if not test_mapping_test_details:
76 test_mapping_test_details = [None] * len(tests)
77 for test, tm_test_detail in zip(tests, test_mapping_test_details):
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080078 test_found = False
nelsonlic4a71452018-09-13 14:10:30 +080079 find_test_err_msg = None
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080080 for finder in test_finder_handler.get_find_methods_for_test(
81 self.mod_info, test):
Dan Shicdbda552018-05-18 23:31:33 -070082 # For tests in TEST_MAPPING, find method is only related to
83 # test name, so the details can be set after test_info object
84 # is created.
nelsonlic4a71452018-09-13 14:10:30 +080085 try:
86 test_info = finder.find_method(finder.test_finder_instance,
87 test)
88 except atest_error.TestDiscoveryException as e:
89 find_test_err_msg = e
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080090 if test_info:
Dan Shicdbda552018-05-18 23:31:33 -070091 if tm_test_detail:
92 test_info.data[constants.TI_MODULE_ARG] = (
93 tm_test_detail.options)
Dan Shi6daad632018-08-30 10:58:03 -070094 test_info.from_test_mapping = True
Dan Shi08c7b722018-11-29 10:25:59 -080095 test_info.host = tm_test_detail.host
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080096 test_infos.add(test_info)
97 test_found = True
nelsonli34997d52018-08-17 09:43:28 +080098 finder_info = finder.finder_info
nelsonlic4a71452018-09-13 14:10:30 +080099 print("Found '%s' as %s" % (
100 atest_utils.colorize(test, constants.GREEN),
101 finder_info))
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800102 break
103 if not test_found:
nelsonlic4a71452018-09-13 14:10:30 +0800104 print('No test found for: %s' %
105 atest_utils.colorize(test, constants.RED))
106 if find_test_err_msg:
107 print('%s\n' % (atest_utils.colorize(
108 find_test_err_msg, constants.MAGENTA)))
109 else:
110 print('(This can happen after a repo sync or if the test'
111 ' is new. Running: with "%s" may resolve the issue.)'
112 '\n' % (atest_utils.colorize(
113 constants.REBUILD_MODULE_INFO_FLAG,
114 constants.RED)))
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800115 return test_infos
mikehoran3d553632017-08-03 14:44:41 -0700116
Dan Shie4e267f2018-06-01 11:31:57 -0700117 def _read_tests_in_test_mapping(self, test_mapping_file):
118 """Read tests from a TEST_MAPPING file.
119
120 Args:
121 test_mapping_file: Path to a TEST_MAPPING file.
122
123 Returns:
Dan Shi350e7472018-06-19 12:25:32 -0700124 A tuple of (all_tests, imports), where
125 all_tests is a dictionary of all tests in the TEST_MAPPING file,
126 grouped by test group.
127 imports is a list of test_mapping.Import to include other test
128 mapping files.
Dan Shie4e267f2018-06-01 11:31:57 -0700129 """
130 all_tests = {}
Dan Shi350e7472018-06-19 12:25:32 -0700131 imports = []
Dan Shie4e267f2018-06-01 11:31:57 -0700132 test_mapping_dict = None
133 with open(test_mapping_file) as json_file:
134 test_mapping_dict = json.load(json_file)
135 for test_group_name, test_list in test_mapping_dict.items():
Dan Shi350e7472018-06-19 12:25:32 -0700136 if test_group_name == constants.TEST_MAPPING_IMPORTS:
137 for import_detail in test_list:
138 imports.append(
139 test_mapping.Import(test_mapping_file, import_detail))
140 else:
141 grouped_tests = all_tests.setdefault(test_group_name, set())
Simran Basi2cc996e2018-10-09 12:19:25 -0700142 tests = []
143 for test in test_list:
144 test_mod_info = self.mod_info.name_to_module_info.get(
145 test['name'])
146 if not test_mod_info:
147 print('WARNING: %s is not a valid build target and '
148 'may not be discoverable by TreeHugger. If you '
149 'want to specify a class or test-package, '
150 'please set \'name\' to the test module and use '
151 '\'options\' to specify the right tests via '
152 '\'include-filter\'.\nNote: this can also occur '
153 'if the test module is not built for your '
154 'current lunch target.\n' %
155 atest_utils.colorize(test['name'], constants.RED))
156 elif not any(x in test_mod_info['compatibility_suites'] for
157 x in constants.TEST_MAPPING_SUITES):
158 print('WARNING: Please add %s to either suite: %s for '
159 'this TEST_MAPPING file to work with TreeHugger.' %
160 (atest_utils.colorize(test['name'],
161 constants.RED),
162 atest_utils.colorize(constants.TEST_MAPPING_SUITES,
163 constants.GREEN)))
164 tests.append(test_mapping.TestDetail(test))
165 grouped_tests.update(tests)
Dan Shi350e7472018-06-19 12:25:32 -0700166 return all_tests, imports
Dan Shie4e267f2018-06-01 11:31:57 -0700167
168 def _find_files(self, path, file_name=TEST_MAPPING):
169 """Find all files with given name under the given path.
170
171 Args:
172 path: A string of path in source.
173
174 Returns:
175 A list of paths of the files with the matching name under the given
176 path.
177 """
178 test_mapping_files = []
179 for root, _, filenames in os.walk(path):
180 for filename in fnmatch.filter(filenames, file_name):
181 test_mapping_files.append(os.path.join(root, filename))
182 return test_mapping_files
183
184 def _get_tests_from_test_mapping_files(
185 self, test_group, test_mapping_files):
186 """Get tests in the given test mapping files with the match group.
187
188 Args:
189 test_group: Group of tests to run. Default is set to `presubmit`.
190 test_mapping_files: A list of path of TEST_MAPPING files.
191
192 Returns:
Dan Shi350e7472018-06-19 12:25:32 -0700193 A tuple of (tests, all_tests, imports), where,
Dan Shie4e267f2018-06-01 11:31:57 -0700194 tests is a set of tests (test_mapping.TestDetail) defined in
195 TEST_MAPPING file of the given path, and its parent directories,
196 with matching test_group.
197 all_tests is a dictionary of all tests in TEST_MAPPING files,
198 grouped by test group.
Dan Shi350e7472018-06-19 12:25:32 -0700199 imports is a list of test_mapping.Import objects that contains the
200 details of where to import a TEST_MAPPING file.
Dan Shie4e267f2018-06-01 11:31:57 -0700201 """
Dan Shi350e7472018-06-19 12:25:32 -0700202 all_imports = []
Dan Shie4e267f2018-06-01 11:31:57 -0700203 # Read and merge the tests in all TEST_MAPPING files.
204 merged_all_tests = {}
205 for test_mapping_file in test_mapping_files:
Dan Shi350e7472018-06-19 12:25:32 -0700206 all_tests, imports = self._read_tests_in_test_mapping(
207 test_mapping_file)
208 all_imports.extend(imports)
Dan Shie4e267f2018-06-01 11:31:57 -0700209 for test_group_name, test_list in all_tests.items():
210 grouped_tests = merged_all_tests.setdefault(
211 test_group_name, set())
212 grouped_tests.update(test_list)
213
214 tests = set(merged_all_tests.get(test_group, []))
215 # Postsubmit tests shall include all presubmit tests as well.
216 if test_group == constants.TEST_GROUP_POSTSUBMIT:
217 tests.update(merged_all_tests.get(
218 constants.TEST_GROUP_PRESUBMIT, set()))
219 elif test_group == constants.TEST_GROUP_ALL:
220 for grouped_tests in merged_all_tests.values():
221 tests.update(grouped_tests)
Dan Shi350e7472018-06-19 12:25:32 -0700222 return tests, merged_all_tests, all_imports
Dan Shie4e267f2018-06-01 11:31:57 -0700223
Dan Shi350e7472018-06-19 12:25:32 -0700224 # pylint: disable=too-many-arguments
225 # pylint: disable=too-many-locals
Dan Shi7a8e1ad2018-04-12 11:01:07 -0700226 def _find_tests_by_test_mapping(
Dan Shicdbda552018-05-18 23:31:33 -0700227 self, path='', test_group=constants.TEST_GROUP_PRESUBMIT,
Dan Shi350e7472018-06-19 12:25:32 -0700228 file_name=TEST_MAPPING, include_subdirs=False, checked_files=None):
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800229 """Find tests defined in TEST_MAPPING in the given path.
Dan Shid6881bb2017-10-12 15:13:25 -0700230
231 Args:
232 path: A string of path in source. Default is set to '', i.e., CWD.
Dan Shicdbda552018-05-18 23:31:33 -0700233 test_group: Group of tests to run. Default is set to `presubmit`.
Dan Shi8441cba2018-01-30 15:52:29 -0800234 file_name: Name of TEST_MAPPING file. Default is set to
Dan Shi0ddd3e42018-05-30 11:24:30 -0700235 `TEST_MAPPING`. The argument is added for testing purpose.
Dan Shie4e267f2018-06-01 11:31:57 -0700236 include_subdirs: True to include tests in TEST_MAPPING files in sub
237 directories.
Dan Shi350e7472018-06-19 12:25:32 -0700238 checked_files: Paths of TEST_MAPPING files that have been checked.
Dan Shid6881bb2017-10-12 15:13:25 -0700239
240 Returns:
Dan Shi7a8e1ad2018-04-12 11:01:07 -0700241 A tuple of (tests, all_tests), where,
Dan Shicdbda552018-05-18 23:31:33 -0700242 tests is a set of tests (test_mapping.TestDetail) defined in
243 TEST_MAPPING file of the given path, and its parent directories,
244 with matching test_group.
Dan Shi7a8e1ad2018-04-12 11:01:07 -0700245 all_tests is a dictionary of all tests in TEST_MAPPING files,
Dan Shicdbda552018-05-18 23:31:33 -0700246 grouped by test group.
Dan Shid6881bb2017-10-12 15:13:25 -0700247 """
Dan Shi7a8e1ad2018-04-12 11:01:07 -0700248 path = os.path.realpath(path)
Dan Shie4e267f2018-06-01 11:31:57 -0700249 test_mapping_files = set()
Dan Shi350e7472018-06-19 12:25:32 -0700250 all_tests = {}
Dan Shi7a8e1ad2018-04-12 11:01:07 -0700251 test_mapping_file = os.path.join(path, file_name)
Dan Shid6881bb2017-10-12 15:13:25 -0700252 if os.path.exists(test_mapping_file):
Dan Shie4e267f2018-06-01 11:31:57 -0700253 test_mapping_files.add(test_mapping_file)
Dan Shi350e7472018-06-19 12:25:32 -0700254 # Include all TEST_MAPPING files in sub-directories if `include_subdirs`
255 # is set to True.
Dan Shie4e267f2018-06-01 11:31:57 -0700256 if include_subdirs:
257 test_mapping_files.update(self._find_files(path, file_name))
258 # Include all possible TEST_MAPPING files in parent directories.
Dan Shi350e7472018-06-19 12:25:32 -0700259 root_dir = os.environ.get(constants.ANDROID_BUILD_TOP, os.sep)
260 while path != root_dir and path != os.sep:
Dan Shie4e267f2018-06-01 11:31:57 -0700261 path = os.path.dirname(path)
262 test_mapping_file = os.path.join(path, file_name)
263 if os.path.exists(test_mapping_file):
264 test_mapping_files.add(test_mapping_file)
Dan Shi0ddd3e42018-05-30 11:24:30 -0700265
Dan Shi350e7472018-06-19 12:25:32 -0700266 if checked_files is None:
267 checked_files = set()
268 test_mapping_files.difference_update(checked_files)
269 checked_files.update(test_mapping_files)
270 if not test_mapping_files:
271 return test_mapping_files, all_tests
272
273 tests, all_tests, imports = self._get_tests_from_test_mapping_files(
Dan Shie4e267f2018-06-01 11:31:57 -0700274 test_group, test_mapping_files)
Dan Shid6881bb2017-10-12 15:13:25 -0700275
Dan Shi350e7472018-06-19 12:25:32 -0700276 # Load TEST_MAPPING files from imports recursively.
277 if imports:
278 for import_detail in imports:
279 path = import_detail.get_path()
280 # (b/110166535 #19) Import path might not exist if a project is
281 # located in different directory in different branches.
282 if path is None:
283 logging.warn(
284 'Failed to import TEST_MAPPING at %s', import_detail)
285 continue
286 # Search for tests based on the imported search path.
287 import_tests, import_all_tests = (
288 self._find_tests_by_test_mapping(
289 path, test_group, file_name, include_subdirs,
290 checked_files))
291 # Merge the collections
292 tests.update(import_tests)
293 for group, grouped_tests in import_all_tests.items():
294 all_tests.setdefault(group, set()).update(grouped_tests)
295
296 return tests, all_tests
297
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800298 def _gather_build_targets(self, test_infos):
299 targets = set()
mikehoran38b98792017-10-18 16:22:55 -0700300 for test_info in test_infos:
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800301 targets |= test_info.build_targets
302 return targets
mikehoranbe9102f2017-08-04 16:04:03 -0700303
Dan Shie4e267f2018-06-01 11:31:57 -0700304 def _get_test_mapping_tests(self, args):
305 """Find the tests in TEST_MAPPING files.
306
307 Args:
308 args: arg parsed object.
309
310 Returns:
311 A tuple of (test_names, test_details_list), where
312 test_names: a list of test name
313 test_details_list: a list of test_mapping.TestDetail objects for
314 the tests in TEST_MAPPING files with matching test group.
315 """
316 # Pull out tests from test mapping
317 src_path = ''
318 test_group = constants.TEST_GROUP_PRESUBMIT
319 if args.tests:
320 if ':' in args.tests[0]:
321 src_path, test_group = args.tests[0].split(':')
322 else:
323 src_path = args.tests[0]
324
325 test_details, all_test_details = self._find_tests_by_test_mapping(
326 path=src_path, test_group=test_group,
Dan Shi350e7472018-06-19 12:25:32 -0700327 include_subdirs=args.include_subdirs, checked_files=set())
Dan Shie4e267f2018-06-01 11:31:57 -0700328 test_details_list = list(test_details)
329 if not test_details_list:
330 logging.warn(
331 'No tests of group `%s` found in TEST_MAPPING at %s or its '
332 'parent directories.\nYou might be missing atest arguments,'
333 ' try `atest --help` for more information',
334 test_group, os.path.realpath(''))
335 if all_test_details:
336 tests = ''
337 for test_group, test_list in all_test_details.items():
338 tests += '%s:\n' % test_group
339 for test_detail in sorted(test_list):
340 tests += '\t%s\n' % test_detail
341 logging.warn(
342 'All available tests in TEST_MAPPING files are:\n%s',
343 tests)
344 sys.exit(constants.EXIT_CODE_TEST_NOT_FOUND)
345
nelsonli34997d52018-08-17 09:43:28 +0800346 logging.debug(
Dan Shie4e267f2018-06-01 11:31:57 -0700347 'Test details:\n%s',
348 '\n'.join([str(detail) for detail in test_details_list]))
349 test_names = [detail.name for detail in test_details_list]
350 return test_names, test_details_list
351
352
Dan Shi0ddd3e42018-05-30 11:24:30 -0700353 def translate(self, args):
mikehoran63d61b42017-07-28 15:28:50 -0700354 """Translate atest command line into build targets and run commands.
355
356 Args:
Dan Shi0ddd3e42018-05-30 11:24:30 -0700357 args: arg parsed object.
mikehoran63d61b42017-07-28 15:28:50 -0700358
359 Returns:
Kevin Cheng7edb0b92017-12-14 15:00:25 -0800360 A tuple with set of build_target strings and list of TestInfos.
mikehoran63d61b42017-07-28 15:28:50 -0700361 """
Dan Shi0ddd3e42018-05-30 11:24:30 -0700362 tests = args.tests
Dan Shicdbda552018-05-18 23:31:33 -0700363 # Test details from TEST_MAPPING files
364 test_details_list = None
Dan Shi0ddd3e42018-05-30 11:24:30 -0700365 if atest_utils.is_test_mapping(args):
Dan Shie4e267f2018-06-01 11:31:57 -0700366 tests, test_details_list = self._get_test_mapping_tests(args)
mikehoran1609f8b2018-09-04 12:46:27 -0700367 atest_utils.colorful_print("\nFinding Tests...", constants.CYAN)
nelsonli34997d52018-08-17 09:43:28 +0800368 logging.debug('Finding Tests: %s', tests)
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800369 start = time.time()
Dan Shicdbda552018-05-18 23:31:33 -0700370 test_infos = self._get_test_infos(tests, test_details_list)
Dan Shi0ddd3e42018-05-30 11:24:30 -0700371 logging.debug('Found tests in %ss', time.time() - start)
mikehoran8bf6d082018-02-26 16:22:06 -0800372 for test_info in test_infos:
373 logging.debug('%s\n', test_info)
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800374 build_targets = self._gather_build_targets(test_infos)
Kevin Cheng7edb0b92017-12-14 15:00:25 -0800375 return build_targets, test_infos