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