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