• 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(['tradefed-contrib', '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_path = self._search_integration_dirs(integration_name)
124                node = self._load_xml_file(full_path)
125                if node is None:
126                    raise atest_error.FatalIncludeError("can't load %r" %
127                                                        integration_name)
128                node = copy.copy(node)
129                if elem.tail:
130                    node.tail = (node.tail or "") + elem.tail
131                root[i] = node
132            i = i + 1
133
134    def _search_integration_dirs(self, name):
135        """Search integration dirs for name and return full path.
136        Args:
137            name: A string of integration name as seen in tf's list configs.
138
139        Returns:
140            A string of test path if test found, else None.
141        """
142        for integration_dir in self.integration_dirs:
143            abs_path = os.path.join(self.root_dir, integration_dir)
144            test_file = test_finder_utils.run_find_cmd(
145                test_finder_utils.FIND_REFERENCE_TYPE.INTEGRATION,
146                abs_path, name)
147            if test_file:
148                return test_file
149        return None
150
151    def find_test_by_integration_name(self, name):
152        """Find the test info matching the given integration name.
153
154        Args:
155            name: A string of integration name as seen in tf's list configs.
156
157        Returns:
158            A populated TestInfo namedtuple if test found, else None
159        """
160        class_name = None
161        if ':' in name:
162            name, class_name = name.split(':')
163        test_file = self._search_integration_dirs(name)
164        if test_file is None:
165            return None
166        # Don't use names that simply match the path,
167        # must be the actual name used by TF to run the test.
168        t_info = self._get_test_info(name, test_file, class_name)
169        return t_info
170
171    def _get_test_info(self, name, test_file, class_name):
172        """Find the test info matching the given test_file and class_name.
173
174        Args:
175            name: A string of integration name as seen in tf's list configs.
176            test_file: A string of test_file full path.
177            class_name: A string of user's input.
178
179        Returns:
180            A populated TestInfo namedtuple if test found, else None.
181        """
182        match = _INT_NAME_RE.match(test_file)
183        if not match:
184            logging.error('Integration test outside config dir: %s',
185                          test_file)
186            return None
187        int_name = match.group('int_name')
188        if int_name != name:
189            logging.warn('Input (%s) not valid integration name, '
190                         'did you mean: %s?', name, int_name)
191            return None
192        rel_config = os.path.relpath(test_file, self.root_dir)
193        filters = frozenset()
194        if class_name:
195            class_name, methods = test_finder_utils.split_methods(class_name)
196            if '.' not in class_name:
197                logging.warn('Looking up fully qualified class name for: %s.'
198                             'Improve speed by using fully qualified names.',
199                             class_name)
200                path = test_finder_utils.find_class_file(self.root_dir,
201                                                         class_name)
202                if not path:
203                    return None
204                class_name = test_finder_utils.get_fully_qualified_class_name(
205                    path)
206            filters = frozenset([test_info.TestFilter(class_name, methods)])
207        return test_info.TestInfo(
208            test_name=name,
209            test_runner=self._TEST_RUNNER,
210            build_targets=self._get_build_targets(rel_config),
211            data={constants.TI_REL_CONFIG: rel_config,
212                  constants.TI_FILTER: filters})
213
214    def find_int_test_by_path(self, path):
215        """Find the first test info matching the given path.
216
217        Strategy:
218            path_to_integration_file --> Resolve to INTEGRATION
219            # If the path is a dir, we return nothing.
220            path_to_dir_with_integration_files --> Return None
221
222        Args:
223            path: A string of the test's path.
224
225        Returns:
226            A populated TestInfo namedtuple if test found, else None
227        """
228        path, _ = test_finder_utils.split_methods(path)
229
230        # Make sure we're looking for a config.
231        if not path.endswith('.xml'):
232            return None
233
234        # TODO: See if this can be generalized and shared with methods above
235        # create absolute path from cwd and remove symbolic links
236        path = os.path.realpath(path)
237        if not os.path.exists(path):
238            return None
239        int_dir = test_finder_utils.get_int_dir_from_path(path,
240                                                          self.integration_dirs)
241        if int_dir:
242            rel_config = os.path.relpath(path, self.root_dir)
243            match = _INT_NAME_RE.match(rel_config)
244            if not match:
245                logging.error('Integration test outside config dir: %s',
246                              rel_config)
247                return None
248            int_name = match.group('int_name')
249            return test_info.TestInfo(
250                test_name=int_name,
251                test_runner=self._TEST_RUNNER,
252                build_targets=self._get_build_targets(rel_config),
253                data={constants.TI_REL_CONFIG: rel_config,
254                      constants.TI_FILTER: frozenset()})
255        return None
256