# Copyright 2018, The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Integration Finder class. """ import copy import logging import os import re import xml.etree.ElementTree as ElementTree # pylint: disable=import-error import atest_error import constants from test_finders import test_info from test_finders import test_finder_base from test_finders import test_finder_utils from test_runners import atest_tf_test_runner # Find integration name based on file path of integration config xml file. # Group matches "foo/bar" given "blah/res/config/blah/res/config/foo/bar.xml _INT_NAME_RE = re.compile(r'^.*\/res\/config\/(?P.*).xml$') _TF_TARGETS = frozenset(['tradefed', 'tradefed-contrib']) _GTF_TARGETS = frozenset(['google-tradefed', 'google-tradefed-contrib']) _CONTRIB_TARGETS = frozenset(['google-tradefed-contrib']) _TF_RES_DIR = '../res/config' class TFIntegrationFinder(test_finder_base.TestFinderBase): """Integration Finder class.""" NAME = 'INTEGRATION' _TEST_RUNNER = atest_tf_test_runner.AtestTradefedTestRunner.NAME def __init__(self, module_info=None): super(TFIntegrationFinder, self).__init__() self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP) self.module_info = module_info # TODO: Break this up into AOSP/google_tf integration finders. self.tf_dirs, self.gtf_dirs = self._get_integration_dirs() self.integration_dirs = self.tf_dirs + self.gtf_dirs def _get_mod_paths(self, module_name): """Return the paths of the given module name.""" if self.module_info: # Since aosp/801774 merged, the path of test configs have been # changed to ../res/config. if module_name in _CONTRIB_TARGETS: mod_paths = self.module_info.get_paths(module_name) return [os.path.join(path, _TF_RES_DIR) for path in mod_paths] return self.module_info.get_paths(module_name) return [] def _get_integration_dirs(self): """Get integration dirs from MODULE_INFO based on targets. Returns: A tuple of lists of strings of integration dir rel to repo root. """ tf_dirs = filter(None, [d for x in _TF_TARGETS for d in self._get_mod_paths(x)]) gtf_dirs = filter(None, [d for x in _GTF_TARGETS for d in self._get_mod_paths(x)]) return tf_dirs, gtf_dirs def _get_build_targets(self, rel_config): config_file = os.path.join(self.root_dir, rel_config) xml_root = self._load_xml_file(config_file) targets = test_finder_utils.get_targets_from_xml_root(xml_root, self.module_info) if self.gtf_dirs: targets.add(constants.GTF_TARGET) return frozenset(targets) def _load_xml_file(self, path): """Load an xml file with option to expand tags Args: path: A string of path to xml file. Returns: An xml.etree.ElementTree.Element instance of the root of the tree. """ tree = ElementTree.parse(path) root = tree.getroot() self._load_include_tags(root) return root #pylint: disable=invalid-name def _load_include_tags(self, root): """Recursively expand in-place the tags in a given xml tree. Python xml libraries don't support our type of tags. Logic used below is modified version of the built-in ElementInclude logic found here: https://github.com/python/cpython/blob/2.7/Lib/xml/etree/ElementInclude.py Args: root: The root xml.etree.ElementTree.Element. Returns: An xml.etree.ElementTree.Element instance with include tags expanded """ i = 0 while i < len(root): elem = root[i] if elem.tag == 'include': # expand included xml file integration_name = elem.get('name') if not integration_name: logging.warn('skipping tag with no "name" value') continue full_paths = self._search_integration_dirs(integration_name) node = None if full_paths: node = self._load_xml_file(full_paths[0]) if node is None: raise atest_error.FatalIncludeError("can't load %r" % integration_name) node = copy.copy(node) if elem.tail: node.tail = (node.tail or "") + elem.tail root[i] = node i = i + 1 def _search_integration_dirs(self, name): """Search integration dirs for name and return full path. Args: name: A string of integration name as seen in tf's list configs. Returns: A list of test path. """ test_files = [] for integration_dir in self.integration_dirs: abs_path = os.path.join(self.root_dir, integration_dir) found_test_files = test_finder_utils.run_find_cmd( test_finder_utils.FIND_REFERENCE_TYPE.INTEGRATION, abs_path, name) if found_test_files: test_files.extend(found_test_files) return test_files def find_test_by_integration_name(self, name): """Find the test info matching the given integration name. Args: name: A string of integration name as seen in tf's list configs. Returns: A populated TestInfo namedtuple if test found, else None """ class_name = None if ':' in name: name, class_name = name.split(':') test_files = self._search_integration_dirs(name) if test_files is None: return None # Don't use names that simply match the path, # must be the actual name used by TF to run the test. t_infos = [] for test_file in test_files: t_info = self._get_test_info(name, test_file, class_name) if t_info: t_infos.append(t_info) return t_infos def _get_test_info(self, name, test_file, class_name): """Find the test info matching the given test_file and class_name. Args: name: A string of integration name as seen in tf's list configs. test_file: A string of test_file full path. class_name: A string of user's input. Returns: A populated TestInfo namedtuple if test found, else None. """ match = _INT_NAME_RE.match(test_file) if not match: logging.error('Integration test outside config dir: %s', test_file) return None int_name = match.group('int_name') if int_name != name: logging.warn('Input (%s) not valid integration name, ' 'did you mean: %s?', name, int_name) return None rel_config = os.path.relpath(test_file, self.root_dir) filters = frozenset() if class_name: class_name, methods = test_finder_utils.split_methods(class_name) test_filters = [] if '.' in class_name: test_filters.append(test_info.TestFilter(class_name, methods)) else: logging.warn('Looking up fully qualified class name for: %s.' 'Improve speed by using fully qualified names.', class_name) paths = test_finder_utils.find_class_file(self.root_dir, class_name) if not paths: return None for path in paths: class_name = ( test_finder_utils.get_fully_qualified_class_name( path)) test_filters.append(test_info.TestFilter( class_name, methods)) filters = frozenset(test_filters) return test_info.TestInfo( test_name=name, test_runner=self._TEST_RUNNER, build_targets=self._get_build_targets(rel_config), data={constants.TI_REL_CONFIG: rel_config, constants.TI_FILTER: filters}) def find_int_test_by_path(self, path): """Find the first test info matching the given path. Strategy: path_to_integration_file --> Resolve to INTEGRATION # If the path is a dir, we return nothing. path_to_dir_with_integration_files --> Return None Args: path: A string of the test's path. Returns: A list of populated TestInfo namedtuple if test found, else None """ path, _ = test_finder_utils.split_methods(path) # Make sure we're looking for a config. if not path.endswith('.xml'): return None # TODO: See if this can be generalized and shared with methods above # create absolute path from cwd and remove symbolic links path = os.path.realpath(path) if not os.path.exists(path): return None int_dir = test_finder_utils.get_int_dir_from_path(path, self.integration_dirs) if int_dir: rel_config = os.path.relpath(path, self.root_dir) match = _INT_NAME_RE.match(rel_config) if not match: logging.error('Integration test outside config dir: %s', rel_config) return None int_name = match.group('int_name') return [test_info.TestInfo( test_name=int_name, test_runner=self._TEST_RUNNER, build_targets=self._get_build_targets(rel_config), data={constants.TI_REL_CONFIG: rel_config, constants.TI_FILTER: frozenset()})] return None