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