blob: a23c8aa3e13d8528e080748dbdd0addee222dd73 [file] [log] [blame]
# 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.
"""
# pylint: disable=line-too-long
import copy
import logging
import os
import re
import xml.etree.ElementTree as ElementTree
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<int_name>.*).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 = list(filter(None, [d for x in _TF_TARGETS for d in self._get_mod_paths(x)]))
gtf_dirs = list(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 <include> 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 <include> tags in a given xml tree.
Python xml libraries don't support our type of <include> 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.warning('skipping <include> 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.warning('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.warning('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