Home | History | Annotate | Download | only in test_finders
      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 """
     16 Module Finder class.
     17 """
     18 
     19 import logging
     20 import os
     21 import re
     22 
     23 # pylint: disable=import-error
     24 import atest_error
     25 import constants
     26 import test_info
     27 import test_finder_base
     28 import test_finder_utils
     29 from test_runners import atest_tf_test_runner
     30 from test_runners import robolectric_test_runner
     31 from test_runners import vts_tf_test_runner
     32 
     33 _JAVA_EXT = '.java'
     34 
     35 # Parse package name from the package declaration line of a java file.
     36 # Group matches "foo.bar" of line "package foo.bar;"
     37 _PACKAGE_RE = re.compile(r'\s*package\s+(?P<package>[^;]+)\s*;\s*', re.I)
     38 
     39 _MODULES_IN = 'MODULES-IN-%s'
     40 _ANDROID_MK = 'Android.mk'
     41 
     42 # These are suites in LOCAL_COMPATIBILITY_SUITE that aren't really suites so
     43 # we can ignore them.
     44 _SUITES_TO_IGNORE = frozenset({'general-tests', 'device-tests', 'tests'})
     45 
     46 
     47 class ModuleFinder(test_finder_base.TestFinderBase):
     48     """Module finder class."""
     49     NAME = 'MODULE'
     50     _TEST_RUNNER = atest_tf_test_runner.AtestTradefedTestRunner.NAME
     51     _ROBOLECTRIC_RUNNER = robolectric_test_runner.RobolectricTestRunner.NAME
     52     _VTS_TEST_RUNNER = vts_tf_test_runner.VtsTradefedTestRunner.NAME
     53 
     54     def __init__(self, module_info=None):
     55         super(ModuleFinder, self).__init__()
     56         self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
     57         self.module_info = module_info
     58 
     59     def _has_test_config(self, mod_info):
     60         """Validate if this module has a test config.
     61 
     62         A module can have a test config in the following manner:
     63           - The module name is not for 2nd architecture.
     64           - AndroidTest.xml at the module path.
     65           - Auto-generated config via the auto_test_config key in module-info.json.
     66 
     67         Args:
     68             mod_info: Dict of module info to check.
     69 
     70         Returns:
     71             True if this module has a test config, False otherwise.
     72         """
     73         # Check if the module is for 2nd architecture.
     74         if test_finder_utils.is_2nd_arch_module(mod_info):
     75             return False
     76 
     77         # Check for AndroidTest.xml at the module path.
     78         for path in mod_info.get(constants.MODULE_PATH, []):
     79             if os.path.isfile(os.path.join(self.root_dir, path,
     80                                            constants.MODULE_CONFIG)):
     81                 return True
     82 
     83         # Check if the module has an auto-generated config.
     84         return self._is_auto_gen_test_config(mod_info.get(constants.MODULE_NAME))
     85 
     86     def _is_testable_module(self, mod_info):
     87         """Check if module is something we can test.
     88 
     89         A module is testable if:
     90           - it's installed.
     91           - it's a robolectric module (or shares path with one).
     92 
     93         Args:
     94             mod_info: Dict of module info to check.
     95 
     96         Returns:
     97             True if we can test this module, False otherwise.
     98         """
     99         if not mod_info:
    100             return False
    101         if mod_info.get(constants.MODULE_INSTALLED) and self._has_test_config(mod_info):
    102             return True
    103         if self._is_robolectric_test(mod_info.get(constants.MODULE_NAME)):
    104             return True
    105         return False
    106 
    107     def _get_first_testable_module(self, path):
    108         """Returns first testable module given module path.
    109 
    110         Args:
    111             path: String path of module to look for.
    112 
    113         Returns:
    114             String of first installed module name.
    115         """
    116         for mod in self.module_info.get_module_names(path):
    117             mod_info = self.module_info.get_module_info(mod)
    118             if self._is_testable_module(mod_info):
    119                 return mod_info.get(constants.MODULE_NAME)
    120         return None
    121 
    122     def _is_vts_module(self, module_name):
    123         """Returns True if the module is a vts module, else False."""
    124         mod_info = self.module_info.get_module_info(module_name)
    125         suites = []
    126         if mod_info:
    127             suites = mod_info.get('compatibility_suites', [])
    128         # Pull out all *ts (cts, tvts, etc) suites.
    129         suites = [suite for suite in suites if suite not in _SUITES_TO_IGNORE]
    130         return len(suites) == 1 and 'vts' in suites
    131 
    132     def _update_to_vts_test_info(self, test):
    133         """Fill in the fields with vts specific info.
    134 
    135         We need to update the runner to use the vts runner and also find the
    136         test specific depedencies
    137 
    138         Args:
    139             test: TestInfo to update with vts specific details.
    140 
    141         Return:
    142             TestInfo that is ready for the vts test runner.
    143         """
    144         test.test_runner = self._VTS_TEST_RUNNER
    145         config_file = os.path.join(self.root_dir,
    146                                    test.data[constants.TI_REL_CONFIG])
    147         # Need to get out dir (special logic is to account for custom out dirs).
    148         # The out dir is used to construct the build targets for the test deps.
    149         out_dir = os.environ.get(constants.ANDROID_HOST_OUT)
    150         custom_out_dir = os.environ.get(constants.ANDROID_OUT_DIR)
    151         # If we're not an absolute custom out dir, get relative out dir path.
    152         if custom_out_dir is None or not os.path.isabs(custom_out_dir):
    153             out_dir = os.path.relpath(out_dir, self.root_dir)
    154         vts_out_dir = os.path.join(out_dir, 'vts', 'android-vts', 'testcases')
    155 
    156         # Add in vts test build targets.
    157         test.build_targets = test_finder_utils.get_targets_from_vts_xml(
    158             config_file, vts_out_dir, self.module_info)
    159         test.build_targets.add('vts-test-core')
    160         test.build_targets.add(test.test_name)
    161         return test
    162 
    163     def _get_robolectric_test_name(self, module_name):
    164         """Returns run robolectric module.
    165 
    166         There are at least 2 modules in every robolectric module path, return
    167         the module that we can run as a build target.
    168 
    169         Arg:
    170             module_name: String of module.
    171 
    172         Returns:
    173             String of module that is the run robolectric module, None if none
    174             could be found.
    175         """
    176         module_name_info = self.module_info.get_module_info(module_name)
    177         if not module_name_info:
    178             return None
    179         for mod in self.module_info.get_module_names(
    180                 module_name_info.get(constants.MODULE_PATH, [])[0]):
    181             mod_info = self.module_info.get_module_info(mod)
    182             if test_finder_utils.is_robolectric_module(mod_info):
    183                 return mod
    184         return None
    185 
    186     def _is_robolectric_test(self, module_name):
    187         """Check if module is a robolectric test.
    188 
    189         A module can be a robolectric test if the specified module has their
    190         class set as ROBOLECTRIC (or shares their path with a module that does).
    191 
    192         Args:
    193             module_name: String of module to check.
    194 
    195         Returns:
    196             True if the module is a robolectric module, else False.
    197         """
    198         # Check 1, module class is ROBOLECTRIC
    199         mod_info = self.module_info.get_module_info(module_name)
    200         if mod_info and test_finder_utils.is_robolectric_module(mod_info):
    201             return True
    202         # Check 2, shared modules in the path have class ROBOLECTRIC_CLASS.
    203         if self._get_robolectric_test_name(module_name):
    204             return True
    205         return False
    206 
    207     def _update_to_robolectric_test_info(self, test):
    208         """Update the fields for a robolectric test.
    209 
    210         Args:
    211           test: TestInfo to be updated with robolectric fields.
    212 
    213         Returns:
    214           TestInfo with robolectric fields.
    215         """
    216         test.test_runner = self._ROBOLECTRIC_RUNNER
    217         test.test_name = self._get_robolectric_test_name(test.test_name)
    218         return test
    219 
    220     def _process_test_info(self, test):
    221         """Process the test info and return some fields updated/changed.
    222 
    223         We need to check if the test found is a special module (like vts) and
    224         update the test_info fields (like test_runner) appropriately.
    225 
    226         Args:
    227             test: TestInfo that has been filled out by a find method.
    228 
    229         Return:
    230             TestInfo that has been modified as needed.
    231         """
    232         # Check if this is only a vts module.
    233         if self._is_vts_module(test.test_name):
    234             return self._update_to_vts_test_info(test)
    235         elif self._is_robolectric_test(test.test_name):
    236             return self._update_to_robolectric_test_info(test)
    237         module_name = test.test_name
    238         rel_config = test.data[constants.TI_REL_CONFIG]
    239         test.build_targets = self._get_build_targets(module_name, rel_config)
    240         return test
    241 
    242     def _is_auto_gen_test_config(self, module_name):
    243         """Check if the test config file will be generated automatically.
    244 
    245         Args:
    246             module_name: A string of the module name.
    247 
    248         Returns:
    249             True if the test config file will be generated automatically.
    250         """
    251         if self.module_info.is_module(module_name):
    252             mod_info = self.module_info.get_module_info(module_name)
    253             auto_test_config = mod_info.get('auto_test_config', [])
    254             return auto_test_config and auto_test_config[0]
    255         return False
    256 
    257     def _get_build_targets(self, module_name, rel_config):
    258         """Get the test deps.
    259 
    260         Args:
    261             module_name: name of the test.
    262             rel_config: XML for the given test.
    263 
    264         Returns:
    265             Set of build targets.
    266         """
    267         targets = set()
    268         if not self._is_auto_gen_test_config(module_name):
    269             config_file = os.path.join(self.root_dir, rel_config)
    270             targets = test_finder_utils.get_targets_from_xml(config_file,
    271                                                              self.module_info)
    272         mod_dir = os.path.dirname(rel_config).replace('/', '-')
    273         targets.add(_MODULES_IN % mod_dir)
    274         return targets
    275 
    276     def find_test_by_module_name(self, module_name):
    277         """Find test for the given module name.
    278 
    279         Args:
    280             module_name: A string of the test's module name.
    281 
    282         Returns:
    283             A populated TestInfo namedtuple if found, else None.
    284         """
    285         mod_info = self.module_info.get_module_info(module_name)
    286         if self._is_testable_module(mod_info):
    287             # path is a list with only 1 element.
    288             rel_config = os.path.join(mod_info['path'][0],
    289                                       constants.MODULE_CONFIG)
    290             return self._process_test_info(test_info.TestInfo(
    291                 test_name=module_name,
    292                 test_runner=self._TEST_RUNNER,
    293                 build_targets=set(),
    294                 data={constants.TI_REL_CONFIG: rel_config,
    295                       constants.TI_FILTER: frozenset()}))
    296         return None
    297 
    298     def find_test_by_class_name(self, class_name, module_name=None,
    299                                 rel_config=None):
    300         """Find test files given a class name.
    301 
    302         If module_name and rel_config not given it will calculate it determine
    303         it by looking up the tree from the class file.
    304 
    305         Args:
    306             class_name: A string of the test's class name.
    307             module_name: Optional. A string of the module name to use.
    308             rel_config: Optional. A string of module dir relative to repo root.
    309 
    310         Returns:
    311             A populated TestInfo namedtuple if test found, else None.
    312         """
    313         class_name, methods = test_finder_utils.split_methods(class_name)
    314         if rel_config:
    315             search_dir = os.path.join(self.root_dir,
    316                                       os.path.dirname(rel_config))
    317         else:
    318             search_dir = self.root_dir
    319         test_path = test_finder_utils.find_class_file(search_dir, class_name)
    320         if not test_path and rel_config:
    321             logging.info('Did not find class (%s) under module path (%s), '
    322                          'researching from repo root.', class_name, rel_config)
    323             test_path = test_finder_utils.find_class_file(self.root_dir,
    324                                                           class_name)
    325         if not test_path:
    326             return None
    327         full_class_name = test_finder_utils.get_fully_qualified_class_name(
    328             test_path)
    329         test_filter = frozenset([test_info.TestFilter(full_class_name,
    330                                                       methods)])
    331         if not rel_config:
    332             test_dir = os.path.dirname(test_path)
    333             rel_module_dir = test_finder_utils.find_parent_module_dir(
    334                 self.root_dir, test_dir, self.module_info)
    335             rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG)
    336         if not module_name:
    337             module_name = self._get_first_testable_module(os.path.dirname(
    338                 rel_config))
    339         return self._process_test_info(test_info.TestInfo(
    340             test_name=module_name,
    341             test_runner=self._TEST_RUNNER,
    342             build_targets=set(),
    343             data={constants.TI_FILTER: test_filter,
    344                   constants.TI_REL_CONFIG: rel_config}))
    345 
    346     def find_test_by_module_and_class(self, module_class):
    347         """Find the test info given a MODULE:CLASS string.
    348 
    349         Args:
    350             module_class: A string of form MODULE:CLASS or MODULE:CLASS#METHOD.
    351 
    352         Returns:
    353             A populated TestInfo namedtuple if found, else None.
    354         """
    355         if ':' not in module_class:
    356             return None
    357         module_name, class_name = module_class.split(':')
    358         module_info = self.find_test_by_module_name(module_name)
    359         if not module_info:
    360             return None
    361         return self.find_test_by_class_name(
    362             class_name, module_info.test_name,
    363             module_info.data.get(constants.TI_REL_CONFIG))
    364 
    365     def find_test_by_package_name(self, package, module_name=None,
    366                                   rel_config=None):
    367         """Find the test info given a PACKAGE string.
    368 
    369         Args:
    370             package: A string of the package name.
    371             module_name: Optional. A string of the module name.
    372             ref_config: Optional. A string of rel path of config.
    373 
    374         Returns:
    375             A populated TestInfo namedtuple if found, else None.
    376         """
    377         _, methods = test_finder_utils.split_methods(package)
    378         if methods:
    379             raise atest_error.MethodWithoutClassError('Method filtering '
    380                                                       'requires class')
    381         # Confirm that packages exists and get user input for multiples.
    382         if rel_config:
    383             search_dir = os.path.join(self.root_dir,
    384                                       os.path.dirname(rel_config))
    385         else:
    386             search_dir = self.root_dir
    387         package_path = test_finder_utils.run_find_cmd(
    388             test_finder_utils.FIND_REFERENCE_TYPE.PACKAGE, search_dir,
    389             package.replace('.', '/'))
    390         # package path will be the full path to the dir represented by package
    391         if not package_path:
    392             return None
    393         test_filter = frozenset([test_info.TestFilter(package, frozenset())])
    394         if not rel_config:
    395             rel_module_dir = test_finder_utils.find_parent_module_dir(
    396                 self.root_dir, package_path, self.module_info)
    397             rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG)
    398         if not module_name:
    399             module_name = self._get_first_testable_module(
    400                 os.path.dirname(rel_config))
    401         return self._process_test_info(test_info.TestInfo(
    402             test_name=module_name,
    403             test_runner=self._TEST_RUNNER,
    404             build_targets=set(),
    405             data={constants.TI_FILTER: test_filter,
    406                   constants.TI_REL_CONFIG: rel_config}))
    407 
    408     def find_test_by_module_and_package(self, module_package):
    409         """Find the test info given a MODULE:PACKAGE string.
    410 
    411         Args:
    412             module_package: A string of form MODULE:PACKAGE
    413 
    414         Returns:
    415             A populated TestInfo namedtuple if found, else None.
    416         """
    417         module_name, package = module_package.split(':')
    418         module_info = self.find_test_by_module_name(module_name)
    419         if not module_info:
    420             return None
    421         return self.find_test_by_package_name(
    422             package, module_info.test_name,
    423             module_info.data.get(constants.TI_REL_CONFIG))
    424 
    425     def find_test_by_path(self, path):
    426         """Find the first test info matching the given path.
    427 
    428         Strategy:
    429             path_to_java_file --> Resolve to CLASS
    430             path_to_module_file -> Resolve to MODULE
    431             path_to_module_dir -> Resolve to MODULE
    432             path_to_dir_with_class_files--> Resolve to PACKAGE
    433             path_to_any_other_dir --> Resolve as MODULE
    434 
    435         Args:
    436             path: A string of the test's path.
    437 
    438         Returns:
    439             A populated TestInfo namedtuple if test found, else None
    440         """
    441         logging.debug('Finding test by path: %s', path)
    442         path, methods = test_finder_utils.split_methods(path)
    443         # TODO: See if this can be generalized and shared with methods above
    444         # create absolute path from cwd and remove symbolic links
    445         path = os.path.realpath(path)
    446         if not os.path.exists(path):
    447             return None
    448         dir_path, file_name = test_finder_utils.get_dir_path_and_filename(path)
    449         # Module/Class
    450         rel_module_dir = test_finder_utils.find_parent_module_dir(
    451             self.root_dir, dir_path, self.module_info)
    452         if not rel_module_dir:
    453             return None
    454         module_name = self._get_first_testable_module(rel_module_dir)
    455         rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG)
    456         data = {constants.TI_REL_CONFIG: rel_config,
    457                 constants.TI_FILTER: frozenset()}
    458         # Path is to java file
    459         if file_name and file_name.endswith(_JAVA_EXT):
    460             full_class_name = test_finder_utils.get_fully_qualified_class_name(
    461                 path)
    462             data[constants.TI_FILTER] = frozenset(
    463                 [test_info.TestFilter(full_class_name, methods)])
    464         # path to non-module dir, treat as package
    465         elif (not file_name and not self._is_auto_gen_test_config(module_name)
    466               and rel_module_dir != os.path.relpath(path, self.root_dir)):
    467             dir_items = [os.path.join(path, f) for f in os.listdir(path)]
    468             for dir_item in dir_items:
    469                 if dir_item.endswith(_JAVA_EXT):
    470                     package_name = test_finder_utils.get_package_name(dir_item)
    471                     if package_name:
    472                         # methods should be empty frozenset for package.
    473                         if methods:
    474                             raise atest_error.MethodWithoutClassError()
    475                         data[constants.TI_FILTER] = frozenset(
    476                             [test_info.TestFilter(package_name, methods)])
    477                         break
    478         return self._process_test_info(test_info.TestInfo(
    479             test_name=module_name,
    480             test_runner=self._TEST_RUNNER,
    481             build_targets=set(),
    482             data=data))
    483