• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2018, 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"""
16Integration Finder class.
17"""
18
19import copy
20import logging
21import os
22import re
23import xml.etree.ElementTree as ElementTree
24
25# pylint: disable=import-error
26import atest_error
27import constants
28from test_finders import test_info
29from test_finders import test_finder_base
30from test_finders import test_finder_utils
31from test_runners import atest_tf_test_runner
32
33# Find integration name based on file path of integration config xml file.
34# Group matches "foo/bar" given "blah/res/config/blah/res/config/foo/bar.xml
35_INT_NAME_RE = re.compile(r'^.*\/res\/config\/(?P<int_name>.*).xml$')
36_TF_TARGETS = frozenset(['tradefed', 'tradefed-contrib'])
37_GTF_TARGETS = frozenset(['google-tradefed', 'google-tradefed-contrib'])
38_CONTRIB_TARGETS = frozenset(['google-tradefed-contrib'])
39_TF_RES_DIR = '../res/config'
40
41
42class TFIntegrationFinder(test_finder_base.TestFinderBase):
43    """Integration Finder class."""
44    NAME = 'INTEGRATION'
45    _TEST_RUNNER = atest_tf_test_runner.AtestTradefedTestRunner.NAME
46
47
48    def __init__(self, module_info=None):
49        super(TFIntegrationFinder, self).__init__()
50        self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
51        self.module_info = module_info
52        # TODO: Break this up into AOSP/google_tf integration finders.
53        self.tf_dirs, self.gtf_dirs = self._get_integration_dirs()
54        self.integration_dirs = self.tf_dirs + self.gtf_dirs
55
56    def _get_mod_paths(self, module_name):
57        """Return the paths of the given module name."""
58        if self.module_info:
59            # Since aosp/801774 merged, the path of test configs have been
60            # changed to ../res/config.
61            if module_name in _CONTRIB_TARGETS:
62                mod_paths = self.module_info.get_paths(module_name)
63                return [os.path.join(path, _TF_RES_DIR) for path in mod_paths]
64            return self.module_info.get_paths(module_name)
65        return []
66
67    def _get_integration_dirs(self):
68        """Get integration dirs from MODULE_INFO based on targets.
69
70        Returns:
71            A tuple of lists of strings of integration dir rel to repo root.
72        """
73        tf_dirs = filter(None, [d for x in _TF_TARGETS for d in self._get_mod_paths(x)])
74        gtf_dirs = filter(None, [d for x in _GTF_TARGETS for d in self._get_mod_paths(x)])
75        return tf_dirs, gtf_dirs
76
77    def _get_build_targets(self, rel_config):
78        config_file = os.path.join(self.root_dir, rel_config)
79        xml_root = self._load_xml_file(config_file)
80        targets = test_finder_utils.get_targets_from_xml_root(xml_root,
81                                                              self.module_info)
82        if self.gtf_dirs:
83            targets.add(constants.GTF_TARGET)
84        return frozenset(targets)
85
86    def _load_xml_file(self, path):
87        """Load an xml file with option to expand <include> tags
88
89        Args:
90            path: A string of path to xml file.
91
92        Returns:
93            An xml.etree.ElementTree.Element instance of the root of the tree.
94        """
95        tree = ElementTree.parse(path)
96        root = tree.getroot()
97        self._load_include_tags(root)
98        return root
99
100    #pylint: disable=invalid-name
101    def _load_include_tags(self, root):
102        """Recursively expand in-place the <include> tags in a given xml tree.
103
104        Python xml libraries don't support our type of <include> tags. Logic used
105        below is modified version of the built-in ElementInclude logic found here:
106        https://github.com/python/cpython/blob/2.7/Lib/xml/etree/ElementInclude.py
107
108        Args:
109            root: The root xml.etree.ElementTree.Element.
110
111        Returns:
112            An xml.etree.ElementTree.Element instance with include tags expanded
113        """
114        i = 0
115        while i < len(root):
116            elem = root[i]
117            if elem.tag == 'include':
118                # expand included xml file
119                integration_name = elem.get('name')
120                if not integration_name:
121                    logging.warn('skipping <include> tag with no "name" value')
122                    continue
123                full_paths = self._search_integration_dirs(integration_name)
124                node = None
125                if full_paths:
126                    node = self._load_xml_file(full_paths[0])
127                if node is None:
128                    raise atest_error.FatalIncludeError("can't load %r" %
129                                                        integration_name)
130                node = copy.copy(node)
131                if elem.tail:
132                    node.tail = (node.tail or "") + elem.tail
133                root[i] = node
134            i = i + 1
135
136    def _search_integration_dirs(self, name):
137        """Search integration dirs for name and return full path.
138        Args:
139            name: A string of integration name as seen in tf's list configs.
140
141        Returns:
142            A list of test path.
143        """
144        test_files = []
145        for integration_dir in self.integration_dirs:
146            abs_path = os.path.join(self.root_dir, integration_dir)
147            found_test_files = test_finder_utils.run_find_cmd(
148                test_finder_utils.FIND_REFERENCE_TYPE.INTEGRATION,
149                abs_path, name)
150            if found_test_files:
151                test_files.extend(found_test_files)
152        return test_files
153
154    def find_test_by_integration_name(self, name):
155        """Find the test info matching the given integration name.
156
157        Args:
158            name: A string of integration name as seen in tf's list configs.
159
160        Returns:
161            A populated TestInfo namedtuple if test found, else None
162        """
163        class_name = None
164        if ':' in name:
165            name, class_name = name.split(':')
166        test_files = self._search_integration_dirs(name)
167        if test_files is None:
168            return None
169        # Don't use names that simply match the path,
170        # must be the actual name used by TF to run the test.
171        t_infos = []
172        for test_file in test_files:
173            t_info = self._get_test_info(name, test_file, class_name)
174            if t_info:
175                t_infos.append(t_info)
176        return t_infos
177
178    def _get_test_info(self, name, test_file, class_name):
179        """Find the test info matching the given test_file and class_name.
180
181        Args:
182            name: A string of integration name as seen in tf's list configs.
183            test_file: A string of test_file full path.
184            class_name: A string of user's input.
185
186        Returns:
187            A populated TestInfo namedtuple if test found, else None.
188        """
189        match = _INT_NAME_RE.match(test_file)
190        if not match:
191            logging.error('Integration test outside config dir: %s',
192                          test_file)
193            return None
194        int_name = match.group('int_name')
195        if int_name != name:
196            logging.warn('Input (%s) not valid integration name, '
197                         'did you mean: %s?', name, int_name)
198            return None
199        rel_config = os.path.relpath(test_file, self.root_dir)
200        filters = frozenset()
201        if class_name:
202            class_name, methods = test_finder_utils.split_methods(class_name)
203            test_filters = []
204            if '.' in class_name:
205                test_filters.append(test_info.TestFilter(class_name, methods))
206            else:
207                logging.warn('Looking up fully qualified class name for: %s.'
208                             'Improve speed by using fully qualified names.',
209                             class_name)
210                paths = test_finder_utils.find_class_file(self.root_dir,
211                                                          class_name)
212                if not paths:
213                    return None
214                for path in paths:
215                    class_name = (
216                        test_finder_utils.get_fully_qualified_class_name(
217                            path))
218                    test_filters.append(test_info.TestFilter(
219                        class_name, methods))
220            filters = frozenset(test_filters)
221        return test_info.TestInfo(
222            test_name=name,
223            test_runner=self._TEST_RUNNER,
224            build_targets=self._get_build_targets(rel_config),
225            data={constants.TI_REL_CONFIG: rel_config,
226                  constants.TI_FILTER: filters})
227
228    def find_int_test_by_path(self, path):
229        """Find the first test info matching the given path.
230
231        Strategy:
232            path_to_integration_file --> Resolve to INTEGRATION
233            # If the path is a dir, we return nothing.
234            path_to_dir_with_integration_files --> Return None
235
236        Args:
237            path: A string of the test's path.
238
239        Returns:
240            A list of populated TestInfo namedtuple if test found, else None
241        """
242        path, _ = test_finder_utils.split_methods(path)
243
244        # Make sure we're looking for a config.
245        if not path.endswith('.xml'):
246            return None
247
248        # TODO: See if this can be generalized and shared with methods above
249        # create absolute path from cwd and remove symbolic links
250        path = os.path.realpath(path)
251        if not os.path.exists(path):
252            return None
253        int_dir = test_finder_utils.get_int_dir_from_path(path,
254                                                          self.integration_dirs)
255        if int_dir:
256            rel_config = os.path.relpath(path, self.root_dir)
257            match = _INT_NAME_RE.match(rel_config)
258            if not match:
259                logging.error('Integration test outside config dir: %s',
260                              rel_config)
261                return None
262            int_name = match.group('int_name')
263            return [test_info.TestInfo(
264                test_name=int_name,
265                test_runner=self._TEST_RUNNER,
266                build_targets=self._get_build_targets(rel_config),
267                data={constants.TI_REL_CONFIG: rel_config,
268                      constants.TI_FILTER: frozenset()})]
269        return None
270